ミニゲームを作ってUnityを学ぶ![ひつじコレクション編]
###第2回目: ステージの作成
前回はユニティちゃんアセットを使用してプレイヤーが操作するキャラクターを作成しました。
今回はゲームの舞台となるステージをスクリプトで動的に生成していきます。
#アセットのインポート
はじめに、ステージを構成する部品として以下のアセットをストアからインポートします。
続いて、下の画像をダウンロードして「FloorTexture」という名前でプロジェクトにインポートします。
#地面のプレハブを作成
まずはインポートしたFloorTextureを使ってステージの地面に当たる部分を作っていきます。
- 「FloorCheck」という名前で新しいマテリアルを作成
- マテリアルのAlbedoにFloorTextureを設定
- ゼロポジションに「Floor」という名前でPlaneを配置し、FloorCheckをアタッチ
- Floorオブジェクトをプレハブに書き出す
#ブロック(ブッシュ)のプレハブを作成
次はステージ上でプレイヤーの通行できないエリアを形成するブロックを作成します。
今回の舞台は森の中ですので、ブロックには木のオブジェクトを利用してみます。
- LowPoly_Foliage/Models/Bushes/Bush_b_a_01をゼロポジションに配置
- 配置されたオブジェクトの名前を「Bush」に変更
- Bushの子にキューブを配置してTransformを以下のように修正
- キューブにアタッチされたMeshRendererのチェックを外して非アクティブに設定
- Bushをプレハブに書き出す
Position: x=0, y=0.25, z=0
Rotation: x=0, y=0, z=0
Scale: x=0.5, y=0.5 z=0.5
ちなみに、今回利用したLowPoly:Foliageに含まれているプレハブにはLODという仕組みが導入されています。
【LOD(Level Of Detail)】
1つのオブジェクトに対して複数のメッシュを設定し、カメラとの距離によってそれを切り替える仕組み。
被写体が近い場合は頂点数の多い高精度のモデルを描画し、遠く離れている場合は頂点数の少ないモデルを描画することで
クオリティを保ちながらもパフォーマンスを維持することができる。
#ステージの構造をファイルにする
ステージを構成する部品が準備できましたので、次はステージの構造をまとめたテキストファイルを作成します。
- メモ帳など、テキストエディタに以下をコピペしてstage_1.txtという名前で保存
- Project内にResourcesという名前のフォルダを作成し、そこにstage_1.txtを配置
@size, 41, 34
@bush
0101010101010101010101010101010101010101010101010101010101010101010101010101010101
0100000000000000000000000001010000000000000000000000000000000000000000000000000001
0100000000000000000000000001010000000000000000000000000000000000000000000000000001
0100000101000001010101000001010000010100000101000001000001010101010000010101000001
0100000101000001010101000001010000010100000101000001000001010101010000010101000001
0100000000000000000000000000000000000000000000000001000000000000000000000000000001
0100000000000000000000000000000000000000000000000001000000000000000000000000000001
0100000101010101010101010101010100000101010101000001000001010101010101010101000001
0100000100000000000000000000000000000100000000000001000000000000000000000000000001
0100000100000000000000000000000000000100000000000001000000000000000000000000000001
0100000100000101010101010000010100000100000101010101000001010101000001010101000001
0100000100000101010101010000010100000100000000000001000000000000000000000001000001
0100000100000000000000000000000000000100000000000001000000000000000000000001000001
0100000100000000000000000000000000000101010101000001000001000001000001000001000001
0100000100000101010100000101010100000100000000000001000001000001000001000000000001
0100000100000101010100000101010100000100000000000001000001000001000001000000000001
0100000100000000000000000000000000000100000101000001000001000001000001010101000001
0100000100000000000000000000000000000100000101000001000001000001000000000000000001
0100000101010101000001010101010100000100000101000001000001000001000000000000000001
0100000000000000000000000000000000000000000101000001000001000001010101010101000001
0100000000000000000000000000000000000000000101000001000001000000000000000000000001
0100000101000001010000010100000101010100000101000001000001000000000000000000000001
0100000101000001010000010100000101010100000101000001000001010101010101010101000001
0100000000000000000000010100000000000000000000000000000000000000000000000000000001
0100000000000000000000010100000000000000000000000000000000000000000000000000000001
0100000101000001010101010101010101010100000001010101010101010101010100000101010101
0100000101000000000000000000000001000000000000000100000000000000000000000000000001
0100000101000000000000000000000001000000000000000100000000000000000000000000000001
0100000101010101010101010101000001000000000000000100000101010101010100000101000001
0100000101010101010101010101000001000000000000000100000101010101010100000101000001
0100000101010101010101010101000001000000000000000100000101010101010100000101000001
0100000000000000000000000000000000000000000000000000000000000000000000000000000001
0100000000000000000000000000000000000000000000000000000000000000000000000000000001
0101010101010101010101010101010101010101010101010101010101010101010101010101010101
@player, 17, 26
@popup, 5, 3
@popup, 19, 3
@popup, 33, 3
@popup, 5, 21
@popup, 19, 21
@popup, 33, 20
@chaser, 1, 2.0, 0.2, 1, 1
@chaser, 2, 2.4, 0.2, 20, 1
@chaser, 3, 3.0, 0.2, 38, 1
@goal, 20, 29, 1.5, 1.5
stage_1.txtは先頭にアットマークの付いた識別子と、それに続いて具体的な内容を数字とカンマで表したテキストファイルです。
@size: ステージのサイズ(x, y)
@player: プレイヤーの初期位置(x, y)
@bush: ステージ上のブッシュの配置を16進数で表したモノ(01がブッシュ, 00が通路)
@Popupより下は次回以降に対応しますので今は特に何の効果もないと考えてください。
###StageConstructor
先ほどのファイルを読み込んでステージを動的に生成するためのスクリプトを作成していきます。
- 「StageConstructor」という名前のスクリプトを作成
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System;
using UnityEngine;
public class StageConstructor : MonoBehaviour
{
private Transform mTrans;
void Awake()
{
mTrans = GetComponent<Transform>();
}
// ファイルデータ整形用
private const string WHITE_EXPRESSION = "[ \t]"; // ホワイトスペース除去用(半角・全角スペース、タブ文字)
private readonly Regex REGEX_WHITE = new Regex(WHITE_EXPRESSION);
private readonly char[] DELIMITER = { ',' };
// 規格
private const float BLOCK_SIZE = 0.5f;
private const float BLOCK_SIZE_HALF = 0.25f;
private const int ID_BUSH = 1;
// ステージサイズと構造の配列
private int mStageX;
private int mStageY;
private int[,] mStageMap;
/// <summary>
/// 指定IDのファイルを読み込んでステージを生成
/// </summary>
/// <param name="stageId"></param>
public void LoadStage(int stageId)
{
// 指定されたステージファイルを読み込んで1行ずつ処理
string filePath = "stage_" + stageId;
TextAsset textAsset = Resources.Load(filePath) as TextAsset;
string text = textAsset.text;
string line;
using (TextReader reader = new StringReader(text)) // usingとは、処理終わりにstreamの解放を自動で行う構文(finally句でDisposeを実行するコードと同じ)
{
while ((line = reader.ReadLine()) != null)
{
// ステージサイズ
if (line.StartsWith("@size"))
{
line = REGEX_WHITE.Replace(line, "");
string[] mapSize = line.Split(DELIMITER, System.StringSplitOptions.RemoveEmptyEntries);
mStageX = int.Parse(mapSize[1]);
mStageY = int.Parse(mapSize[2]);
mStageMap = new int[mStageY, mStageX];
InstantiateFloor();
continue;
}
// ステージ構造(ブッシュ)
if (line.StartsWith("@bush"))
{
// ステージ構造は16進数表記の文字列なため、10進数のint配列に変換
StringBuilder sbStage = new StringBuilder();
for (int y = 0; y < mStageY; y++)
{
sbStage.Append(reader.ReadLine());
}
int start = 0;
for (int y = 0; y < mStageY; y++)
{
for (int x = 0; x < mStageX; x++)
{
mStageMap[y, x] = Convert.ToInt32(sbStage.ToString(start, 2), 16);
start += 2;
}
}
// ステージ構造を元にブッシュを生成
InstantiateBushs();
continue;
}
// プレイヤー初期位置
if (line.StartsWith("@player"))
{
line = REGEX_WHITE.Replace(line, "");
string[] strPlayerPos = line.Split(DELIMITER, System.StringSplitOptions.RemoveEmptyEntries);
InstantiatePlayer(int.Parse(strPlayerPos[1]), int.Parse(strPlayerPos[2]));
continue;
}
}
}
}
//----------
// フロア //
//---------------------------------------------------------------------------------
[SerializeField]
private GameObject prefabFloor;
/// <summary>
/// ステージサイズに合わせて床を生成
/// </summary>
private void InstantiateFloor()
{
float sizeX = mStageX * BLOCK_SIZE;
float sizeZ = mStageY * BLOCK_SIZE - BLOCK_SIZE_HALF;
Vector3 pos = new Vector3(sizeX / 2.0f, 0.0f, sizeZ / -2.0f);
GameObject obj = Instantiate(prefabFloor, pos, Quaternion.identity);
float scaleX = sizeX / 10.0f;
float scaleZ = sizeZ / 10.0f;
obj.transform.localScale = new Vector3(scaleX, 1.0f, scaleZ);
obj.transform.parent = mTrans;
}
//-----------------------
// ブッシュ(ブロック) //
//---------------------------------------------------------------------------------
[SerializeField]
private GameObject prefabBush;
/// <summary>
/// ステージを構成するブッシュを生成
/// </summary>
private void InstantiateBushs()
{
for (int y = 0; y < mStageY; y++)
{
for (int x = 0; x < mStageX; x++)
{
switch (mStageMap[y, x])
{
case ID_BUSH:
InstantiateBush(x, y);
break;
}
}
}
}
/// <summary>
/// ブロックを指定されたステージ座標に生成
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
private void InstantiateBush(int x, int y)
{
Vector3 pos = new Vector3(x * BLOCK_SIZE + BLOCK_SIZE_HALF, 0.0f, y * -BLOCK_SIZE - BLOCK_SIZE_HALF);
GameObject obj = Instantiate(prefabBush, pos, Quaternion.identity);
obj.transform.parent = mTrans;
}
//-------------
// プレイヤー //
//---------------------------------------------------------------------------------
[SerializeField]
private GameObject prefabPlayer;
public GameObject Player { get; private set; }
/// <summary>
/// プレイヤーを指定されたステージ座標に生成
/// Player#GameObjectの参照を保持する
/// </summary>
/// <param name="posX"></param>
/// <param name="posY"></param>
private void InstantiatePlayer(int posX, int posY)
{
Vector3 pos = new Vector3(posX * BLOCK_SIZE + BLOCK_SIZE, 0.0f, posY * -BLOCK_SIZE - BLOCK_SIZE);
Player = Instantiate(prefabPlayer, pos, Quaternion.Euler(new Vector3(0.0f, 180.0f, 0.0f)));
}
}
StageConstructorはLoadStage()で指定されたステージのデータが入ったファイルを読み込み、その内容を1行ずつ処理していくことでステージを生成しています。
string filePath = "stage_" + stageId;
TextAsset textAsset = Resources.Load(filePath) as TextAsset;
上記がファイルを読み込んでいる部分です。
Resourcesフォルダに入っているアセットにアクセスするためにはResources.Load()を使用します。
参考: 特殊なフォルダについて - KAYAC enginner's blog
尚、Unity公式のResourcesフォルダのベストプラクティスによるとResourcesフォルダを使わずにAssetBundleを使用するように推奨されていますので、そちらについても参考となるサイトをご紹介させていただきます。
参考: AssetBundleとは - (:3[kanのメモ帳]
###StageManager
続けて、ステージを管理するマネージャーを作成します。
- 「StageManager」という名前の空オブジェクトをゼロポジションに配置
- 「StageManager」という名前のスクリプトを作成し、StageManagerオブジェクトにアタッチ
- 同じく、先ほどのStageConstructorをStageManagerオブジェクトにアタッチ
- インスペクタでStageConstructorのプロパティにそれぞれのプレハブを設定
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System;
using UnityEngine;
public class StageManager : MonoBehaviour
{
private StageConstructor mConstructor;
void Start()
{
mConstructor = GetComponent<StageConstructor>();
LoadStage(1);
}
public void LoadStage(int stageId)
{
mConstructor.LoadStage(stageId);
}
}
ゲーム開始時にStageManagerのStart()が呼ばれた際にStageConstructorのLoadStage()を呼び出すことで対応したファイルからステージを生成します。
#問題発生!
ステージを動的に生成することができましたが、ここで新しく2つの問題が発生しました。
- Floorオブジェクトがステージのサイズに合わせてスケーリングすることでテクスチャの表示が崩れてしまう
- そもそも、ゲーム画面にプレイヤーが映り込んでいない
###テクスチャの表示が崩れてしまう
1つ目の問題についてはオブジェクトの大きさに合わせてテクスチャのタイリングを自動的に調節することで対応します。
- 「AutoTiling」という名前のスクリプトを作成し、Floorプレハブにアタッチ
- インスペクタからAutoTilingのプロパティを以下のように設定
mTypeId: XZ
mTilePerScaleX: 20
mTilePerScaleY: 20
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AutoTiling : MonoBehaviour
{
private enum TILING_TYPE
{
XZ = 0
}
[SerializeField]
private TILING_TYPE mTypeId;
[SerializeField]
[Tooltip("対応するtransform.scaleが1のときのTiling.x")]
private int mTilePerScaleX;
[SerializeField]
[Tooltip("対応するtransform.scaleが1のときのTiling.y")]
private int mTilePerScaleY;
void Start()
{
float tilingX = 0;
float tilingY = 0;
Vector3 scale = GetComponent<Transform>().localScale;
Material mtl = GetComponent<Renderer>().material;
switch (mTypeId)
{
case TILING_TYPE.XZ:
tilingX = scale.x * mTilePerScaleX;
tilingY = scale.z * mTilePerScaleY;
break;
}
mtl.mainTextureScale = new Vector2(tilingX, tilingY);
}
}
###プレイヤーが行方不明
2つ目の問題についてはカメラをプレイヤーに追従させることで対応します。
- FollowCameraという名前のスクリプトを作成し、Main Cameraにアタッチ
- stageConstructorに対応するコードを追加
- インスペクタからmFollowCameraMainプロパティにMain Cameraを設定
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FollowCamera : MonoBehaviour
{
private Transform mTrans;
private Transform mTarget; // 追跡するターゲット
private Vector3 mDistance; // ターゲットとの距離
void Awake()
{
mTrans = GetComponent<Transform>();
}
/// <summary>
/// ターゲット追跡の初期化
/// </summary>
/// <param name="trans">ターゲット</param>
/// <param name="distance">ターゲットとの距離</param>
public void SetTarget(Transform trans, Vector3 distance, Quaternion rotation)
{
mTarget = trans;
mDistance = distance;
mTrans.position = mTarget.position + mDistance;
mTrans.rotation = rotation;
}
void LateUpdate()
{
if (mTarget == null) return;
mTrans.position = mTarget.position + mDistance;
}
}
private void InstantiatePlayer(int posX, int posY)
{
Vector3 pos = new Vector3(posX * BLOCK_SIZE + BLOCK_SIZE, 0.0f, posY * -BLOCK_SIZE - BLOCK_SIZE);
Player = Instantiate(prefabPlayer, pos, Quaternion.Euler(new Vector3(0.0f, 180.0f, 0.0f)));
追加 SetCameraTarget();
}
//-------------------
// カメラの追従設定 //
//---------------------------------------------------------------------------------
[SerializeField]
private FollowCamera mFollowCameraMain;
private void SetCameraTarget()
{
mFollowCameraMain.SetTarget(Player.transform, new Vector3(0.0f, 4.0f, -5.0f), Quaternion.Euler(new Vector3(37.0f, 0.0f, 0.0f)));
}
これで無事に問題が解決しました。
プロジェクトを実行してプレイヤーを自由に操作してみてください。
この作品はユニティちゃんライセンス条項の元に提供されています