三日間で作成したゲームの技術的な話


ゲーム開発の技術的な話

はじめに

Qiita初投稿です。
昨年度末に二人で、三日間かけてUnityの2Dにてスマホゲームを開発しました。
作成したゲームは
横スクロールゲームで、プレイヤーが落ちないようにタップして浮かせつつ、横からくる障害物(ブロック)をかわしつつアイテムをとり、体力ゲージを意識しながらプレイするものです。

そのとき使った技術的な内容についてまとめてみます。
今回使用した技術・考え方としては以下の3つです。

  • シングルトン(GameManager)
  • ゲームマスタ(今回は操作説明シーンで使用)
  • ブロック自動生成

これらについてまとめていこうと思います。

シングルトン(GameManagerの作成)

シングルトンとは

今回Unityで作成したゲームで最も開発がしやすかった手法として、シングルトンという方法(考え方?)が挙げられます。
シングルトンとは
https://qiita.com/shoheiyokoyama/items/c16fd547a77773c0ccc1
にも記載されている通り、

  • 指定したクラスのインスタンスが1つしか存在しないことを保証する
  • インスタンスが1個しか存在しないことをプログラム上で表現したい

という利点があります。
作成したアプリが100%この手法を使えているかといわれると自信はないのですが、基本的にはこの考え方に従って開発を進めました。

Unityでシングルトンを利用する

Unityでシングルトンを実現するために、まずGameManagerを作成します。

GameManagerで変数を管理するため
ゲーム起動時にGameManagerでインスタンスを生成しておく必要があります。
ゲームを起動すると(基本的には)Startが最初に開かれるためStartSceneにGameManagerを設置します。

StartSceneに空のオブジェクトを配置し、GameManagerのスクリプトをアタッチします。

GameManagerの作り方

GameManagerの構成は以下の4つの項目で作りました。

  • Awake()でGameManagerのインスタンスを生成
  • 使用する変数系(スコアやタイマー、プレイヤーの状態など)を宣言
  • 変数を持ってくるためのgetter
  • GameManagerにある変数に値をセットするためのsetter

GameManagerが完成すれば、あとは開発を進めていくうちに必要な変数は増えていくので、基本全てGameManagerで宣言、getter・setterを追記していきます。
必要になればGameManagerのインスタンスを参照しgetterを呼ぶ、値を変えたければsetterで値をセットする、をするだけで変数を管理できます。

マスターデータの作成(操作説明用マスタの作成)

次は、操作説明に出力させる文言を管理する操作説明用マスターを作成しました。
開発当初はtxtファイルをResourcesフォルダからロードするだけでした。
しかし、txtファイルで読み込むと改行やページ番号の管理などが難しくなってしまったためにマスターを作ることにしました。(小規模のゲーム開発ならここまでしなくてもよかったかも)

マスターとは

マスターとは、文字列で管理されたデータ群のことです。(違っているかも)

例えば、RPGなどで出てくるPlayerについて考えてみます。
ある人がいて、その人は剣を持っていて、HPは300で、MPは600で、髪の毛は黒で、レベルは30で……
とたくさんのデータが詰まっていると思います。
このような登場人物がたくさん出てくると管理が大変になってきます。
これらをうまく管理させるために表を作成します。
上記の例で行くと、以下のような表が作れそうです。

Player

PlayerId HP Weapon Level
1 300 sord 30
2 500 - 50
3 100 stick 10

(長いので項目略)

このように、表で管理すると見やすく、管理がしやすくなります。
また、登場人物が増えても行を追加するだけで表現することができます。

マスターの作り方・処理の流れ

今回Unityで作成たマスターは次のような流れに沿って作成しました。

  1. Excelでシートを作成
  2. Excelに必要項目を記載
  3. Excelのシートにボタンを設置
  4. マクロを組み、JSONデータを出力させる
  5. Unity側でMaster.csを作成
  6. Master.csでJSONデータを読み込む
  7. JSONデータに従ってUnityが出力する

上記の流れを図にすると、以下のようになります。

マスタを作成してしまえば、行を追加してJSONを出力させればUnity側ですぐ反映させることができます。
また、登場人物すべてのデータの管理が一括で行うことができます。

今回のゲーム製作では、Playerなどの管理ではなく、操作説明で表示する文字列をマスタとして作成し
表示しているページ数や、表示するときの画像のパスなどをマスタで管理するようにしました。
(画像のパスはうまく使えていませんが)

ブロックの自動生成

今回作成したゲームは、
「横スクロールゲーム」で、Playerが落ちないようにタップして宙に浮かせつつ、
横から流れてくるブロックに触れたり、かわしたりを楽しむものです。
加えて、障害物とPlayerに属性を持たせ、それぞれの属性の相性によって体力ゲージの増減が決定するという要素もあります。

その時、障害物であるブロックを自動で、かつランダムで生成するアルゴリズムを考えたので以下にまとめてみます。

ブロックの生成

いきなりすべてのブロックを自動生成する方法を考えるのではなく、まずは1つのブロックをどのように生成するかを考えました。

まず、1×1の正方形のブロックを用意します。

このブロックを縦方向にランダムで伸ばしてあげることで、「壁」を表現できると考えました。
例えば、3×1と5×1と2×1のブロックをそれぞれ横においてみましょう。
すると、以下の図のようになると思います。

今はわかりやすくするために、外枠の線を黒にしていますが、ブロックと同一色にすれば、いい感じのブロックができそうです。
あとは「高さをランダムにして、1つ作ったら生成する場所を1つ右にずらす」をほしい長さの分だけループさせれば1まとまりのブロックが生成できそうです。
ただし、高さ0を含めてしまうと、最悪の場合、すべて高さ0となってしまう可能性があるので、ランダム関数を1~MaxHeightの中からランダムで数値を出す、という処理にすれば今回の問題は回避できそうです。
これをコードにしてみると、以下のようになりました。

createBlock.cs
// MAX_LENGTHはブロック群の横幅
// MAX_HEIGHTは各ブロックの最大の高さ
for (int i = 0; i < MAX_LENGTH; i++)
{
    GameObject block = (GameObject)Resources.Load("Block");
    blockHeight = randam.Next(1, MAX_HEIGHT);
    block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる
    Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity); // 作ったブロックを配置
}

アイテムの配置

ただ単にブロックが流れてくるだけでは面白くない、属性の変更ができないという意見から、Playerの色を変えるためのアイテムや体力回復アイテムの設置方法を追加で考える必要が出てきました。

純粋にブロックの上に置けばいいですが、ここもランダムにしました。
以下がアイテム設置のコードです。

createBlock.cs
// MAX_LENGTHはブロック群の横幅
// MAX_HEIGHTは各ブロックの最大の高さ
// setItemPathには、アイテムが保存されているパスが格納されている
for (int i = 0; i < MAX_LENGTH; i++)
{
    GameObject block = (GameObject)Resources.Load("Block");
    blockHeight = randam.Next(1, MAX_HEIGHT);
    block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる

    var setColorRandomNum = randam.Next(0, 10); // 出現アイテムをランダムで設定
    var setBlockAboveItem = (GameObject)Resources.Load(setItemPath[setColorRandomNum]);
    Instantiate(setBlockAboveItem , new Vector2((float)i, blockHeight + 1.0f), Quaternion.identity); // アイテムブロックの上に配置

    Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity); // 作ったブロックを配置
}

ブロックを横にスクロール

ここまでできれば、あとはブロックを横に動かすだけです。
今回のゲームでは、右から左にブロックが流れるという仕様のため、時間とともに横に動かす処理を書きます。

ただし、生成したブロックを各1つずつうごかしているととてもコードが長くなってしまう&冗長です。
そのため、空のparentBlockというオブジェクトを作り、作った各ブロック、アイテムなどを子にしてしまいます。
そして、この親オブジェクトを動かすことで、全体を動かすことができます。
イメージとしては以下の図のような状態です。

これを実際にコードにすると、このような形になります。

createBlock.cs
// MAX_LENGTHはブロック群の横幅
// MAX_HEIGHTは各ブロックの最大の高さ
// setItemPathには、アイテムが保存されているパスが格納されている

var parentBlock = (GameObject)Resources.Load("parentBlock");
GameObject parentBlockObj = Instantiate(parentBlock, new Vector2(0.0f, 0.0f), Quaternion.identity) as GameObject;
for (int i = 0; i < MAX_LENGTH; i++)
{
    GameObject block = (GameObject)Resources.Load("Block");
    blockHeight = randam.Next(1, MAX_HEIGHT);
    block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる

    var setColorRandomNum = randam.Next(0, 10); // 出現アイテムをランダムで設定
    var setBlockAboveItem = (GameObject)Resources.Load(setItemPath[setColorRandomNum]);
    Instantiate(setBlockAboveItem , new Vector2((float)i, blockHeight + 1.0f), Quaternion.identity); // アイテムブロックの上に配置

    var blockObj = Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity) as GameObject;
    blockObj.transform.parent = parentBlockObj.transform; // 生成したブロックの親が誰かを教える
}

あとはこの親ブロックをTime.deltaTimeなどで右から左に動かす処理を書けばそれっぽい動きをしてくれました。

おわりに

今回まとめた内容はゲームを作るうえではとても基本的な内容だとは思います。
しかし、実際にプログラムを書き、それを自分の言葉でまとめることでより理解が深まったと思っています。
また、いろんな人に見てもらえる環境下で記事を書くことで自分の成長にもつながるかなと考えています。
三日間で成長できたと感じれたハッカソンでした。