Prog-G

岐阜大学プログラミングサークル

信長の野望風のゲームを行き当たりばったりで作ってみた話

岐大祭2018 Unity

この記事は、岐大祭に向けてアドベントカレンダー的な記事を書く企画の 3 日目です。

私が作ったのは、信長の野望風のゲームを東方のキャラを使用して同人ゲームとして仕上げてみたものです。 Unity の UI 周りの勉強がてらに作ったものなのでとてもごちゃごちゃしていますが、少しでも参考・転用できる技術があれば、という記事になります。

信長の野望とは

コーエーテクモゲームス の人気シリーズである歴史シミュレーションゲームです。

UI について

このゲームでは、基本的にあらかじめ UI を作成しておき、必要のないときは gameObject.SetActive(false) で表示されないようにしています。

Button クリック時のスクリプト呼び出しを共通化する

スクリプト呼び出し方は 2 種類あり、今回は両方とも使ってみました。

  1. こういった Button の多くなるゲームでは スクリプト呼び出しを共通化 することによって Script の管理を簡潔にできます。

  2. まず、各 Scene の UI の処理を行う シングルトン に Button を押した際に呼びたいメソッドを記述します。 そして Unity の Inspecter 上からそのメソッドをクリックイベントに登録すると、これを呼ぶことが出来ます。 Scene を跨ぐシングルトンを参照したい場合は、以下の手順で行わないと NullReferenceException を起こすので注意しましょう。

Button
 ↓参照
その Scene の UI を統括するシングルトン
 ↓参照
全ての Scene で使用されるシングルトン(今回の場合、 BGMController など)

Layout

UI の Layout は主に、 Vertical Layout Group, Horizontal Layout Group, Content Size Fitter を使用しています。 VLG と HLG は子オブジェクトの配置を良い感じにしてくれる Conponent です。 CSF は VLG や HLG で配置した子オブジェクトの大きさに応じて、親オブジェクトの大きさを良い感じにしてくれる Conponent です。

使用方法は簡単で、並べたい方向に応じて VLG または HLG をアタッチして UI 群を整頓し、そのあと CSF をアタッチして有効にします。

Image

UI の Image をスクリプトから設定したいときは、 Resource フォルダにあらかじめ保存しておき、 以下のように呼び出します。

image.AddComponent<Image>().sprite = Resources.Load<Sprite>("ImageName");

Scroll View

UI の中でも Scroll View は少し複雑で、このような構成になっています。 この Content に VLG や HLG, CSF をアタッチすることで、中身を自動的に整頓・整形してくれるようになります。

ScrollView
├── Viewport  <=  実際に表示されるViewの大きさ(Maskになっている)
│   └── Content  <=  実際に表示したいGameObjectを入れる親オブジェクト
├── Scrollbar Horizontal
│   └── Sliding Area
│       └── Handle
└── Scrollbar Vertical
    └── Sliding Area
        └── Handle

Viewport の大きさを画面サイズに、 Content のサイズを画面サイズの 2 倍にします。 そして Content の Image の Conponent を削除し、 Content に子オブジェクトを設定すると、画面内を移動する UI を作ることもできます。

スクリーン座標とワールド座標

ワールド座標からスクリーン座標を取得 したい場合は、以下のようにします。 今回は 「カーソルが target に hit している場合にその詳細を UI 上に表示する」 という風に使っています。

UI.position = RectTransformUtility.WorldToScreenPoint (Camera.main, target.transform);

スクリーン座標からワールド座標を取得 したい場合は、以下のようにします。

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, Mathf.Infinity))
{
    // Raycast が GameObject に hit した場合の処理
    // hit.transform.position で最初に hit した Position を、
    // hit.transform.gameObject で hit した GameObject を取得できる
}

ただしこれでは、一番最初に hit した GameObject しか取得できないため、複数の GameObject を取得したい場合は以下を使います。

PointerEventData pointer = new PointerEventData(EventSystem.current);
pointer.position = Input.mousePosition;

// カーソルの位置にある UI をすべて返す
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointer, results);

// カーソルの位置にある GameObject をすべて返す
List<RaycastHit> raycastHits = Physics.RaycastAll(ray).ToList();

Terrain

このゲームでは Terrain を スクリプト上から設定 し、ゲームごとにマップをランダム生成しています。 基本的には 「川, 山, 海岸線, 森, 橋, 拠点 の生成」 と 「Texture の設定」 を行っています。

Height Map

Height Map は Terrain の高さを設定する値であり、 0.0 〜 1.0 の値を持つ float[,] 型の2次元配列です。 これを設定することで 川, 山, 海岸線 の生成を行っています。 設定した2次元配列は Terrain.TerrainData.SetHeights(int x, int y, float[,] heights) で Terrain にセット出来ます。 xy はオフセットです。

Alpha Map

Alpha Map は Terrain の Texture の濃度を設定する値であり、 0.0 〜 1.0 の値を持つ float[,,] 型の3次元配列です。 3次元目はあらかじめ Inspecter 上で設定されている Texture 番号を int 型で指定します。 設定した3次元配列は Terrain.TerrainData.SetAlphamaps(int x, int y, float[,,] map) で Terrain にセット出来ます。 xy はオフセットです。

また、 Alpha Map は Texture が加算されてしまうので、不要な箇所の値は 0 にしておく必要があります。

マスタデータのインポート

このゲームでは、キャラクターの基本情報やアイテムの基本情報などを Excel からインポート しています。 こうすることで、多くのデータを1つの Excel ファイルに整理出来て便利だと思います。

セーブ機能

このゲームでは、 Json 形式を用いたセーブ方法 を採用しています。 以下のようなことを行っています。

public void Save()
{
    // このクラスにセーブしたい要素を一度保存する
    _jsonText = JsonUtility.ToJson(this);  // このクラスをJson化する
    File.WriteAllText(GetSaveFilePath(), _jsonText);  // GetSaveFilePath で保存する Path を設定する
}

public void Load()
{
    JsonUtility.FromJsonOverwrite(GetJson(), this);  // GetJson で保存した Json を取得し、このクラスを上書きする
    // 各 GameObject にロードした Json を反映する
}

このゲームでは Unit の移動に NavMeshAgent を使用しています。 NavMeshAgent は NavMesh 上を自動で移動してくれる便利な Conponent ですが、マップを毎回ランダムに生成するため 「NavMesh の静的 Bake」 が出来ません。 そこで下記のように 動的 Bake を行いました。

NavMeshData = NavMeshSurface.BuildNavMesh();  // 空の NavMeshData を作成する
NavMeshSurface.UpdateNavMesh(NavMeshData));  // こうすると非同期で Bake が出来る!すごい!

NavMeshSurfaceNavMeshComponents という追加アセットの中にあります。 動的 Bake を非同期で行うことにより、 NowLoading 画面を表示することも可能になりました(この Bake がとても長い……)。