もくじ
はじめに
どうも! みなため(@MinatameT)です。
この記事では、Unityで迷路を自動生成する方法を説明しています。
なお、迷路で実際に遊べる段階まで説明していきますので、説明はやや長くなりますことをご了承ください。
迷路生成のアルゴリズムは「棒倒し法」というものを使います。詳しくは、次の記事で説明しています。棒倒し法をまだ理解できていない方は、ご覧ください。
動作環境
- Windows 7
- Unity 2019
それでは、実際に作っていきましょう!
棒倒し法での迷路の作り方
フォルダーの準備
まず、Assetsフォルダーを右クリックし、「Create>Folder」から、Assetsフォルダー内に4つのフォルダーを作成します。
フォルダー名は、それぞれ次のようにしてください。
- Materials
- Resources
- Scenes
- Scripts
マテリアルの作成
Materialsフォルダーを右クリックし、「Create>Material」から、Materialsフォルダー内に3つのマテリアルを作成します。
マテリアルの名前は、それぞれ次のように変更してください。
- Land
- Start
- Wall
Landは地面の色で、Startはスタート地点の置物の色で、Wallは壁の色です。
マテリアルの色は個人の自由なのですが、私は次のように設定しました。
Landの色は、白色:Color = (255, 255, 255, 255) です。
Startの色は、黄色:Color = (255, 255, 0, 255) です。
Wallの色は、灰色:Color = (149, 149, 149, 255) です。
光の設定
シーン(Hierarchy)から「Directional Light」を削除します。
Hierarchyで右クリックし、「Light>Spotlight」を選択します。そして、Spot Lightの設定を次のように変更します。
- Type = Spot
- Range = 10000
- Spot Angle = 179
- Color = (134, 188, 255, 255)
- Mode = Baked
- Intensity = 1
- Indirect Multiplier = 1
地面の作成
迷路で遊ぶためには地面が必要ですので、地面を作ります。
Hierarchyで右クリックし、「3D Object>Plane」を選択します。
わかりやすいように、Planeの名前を「Land」に変更します。そして、Landの設定を次のように変更します。さらに、マテリアル「Land」をMaterialsフォルダーからドラッグ&ドロップします。
- Position = (0, 0, 0)
- Scale = (1000, 1, 1000)
- Mesh RendererのElement 0 = Land
人の作成
迷路で遊ぶためには、自分の代わりに迷路で行動してくれる人が必要です。その人を作成します。
Hierarchyで右クリックし、「3D Object>Cylinder」を選択します。
わかりやすいように、Cylinderの名前を「Human」に変更します。そして、Humanの設定を次のように変更します。
- Position = (5, 2, 0)
- Scale = (1, 2, 1)
ちなみに、Scaleの高さを2に設定しているのは、私が2 (m) の人を想定しているからです。アメリカンサイズですね。
次に、Humanに「Rigidbody」をアタッチ(装着)し、設定を次のように変更します。上の画像も参考にしてください。
- Mass = 1
- Drag = 0
- Angular Drag = 0
- Interpolate = Interpolate
- Collision Detection = Continuous
- Freeze Position Y = ON
- Freeze Rotation X, Y, Z = ON
次に、Main CameraをHumanにドラッグ&ドロップして、Humanの子オブジェクトにします。上の画像のようにしてください。
これで、人(Human)の動きにカメラがついていくようになります。
そして、Main Cameraの設定を次のように変更します。
- Position = (0, 0.5, 0)
- Scale = (1, 0.5, 1)
これで、人の視界を表現することができます。
スタート地点の置物の作成
迷路のスタート地点がわかるように、置物を作成しておきます。
Hierarchyで右クリックし、「3D Object>Cube」を選択します。
わかりやすいように、Cubeの名前を「Start」に変更します。そして、Startの設定を次のように変更します。さらに、マテリアル「Cube」をMaterialsフォルダーからドラッグ&ドロップします。
- Position = (5, 3, -3)
- Scale = (5, 3, 1)
- Mesh RendererのElement 0 = Start
壁の作成とリソース化
迷路には壁が必要ですので、(後で自動生成される)壁の作成をおこないます。
Hierarchyで右クリックし、「3D Object>Cube」を選択します。
わかりやすいように、Cubeの名前を「Wall」に変更します。そして、Startの設定を次のように変更します。さらに、マテリアル「Wall」をMaterialsフォルダーからドラッグ&ドロップします。
- Position = (0, 5, 0)
- Scale = (5, 10, 5)
- Mesh RendererのElement 0 = Wall
WallをResourcesフォルダーにドラッグ&ドロップします。これで、Wallをリソース化することができます。リソース化したゲームオブジェクトは、スクリプトから生成したりすることができます。便利ですね。
そして、シーン(Hierarchy)にあるWallを削除します。
スクリプトの作成
これから、人を動かすためのスクリプトと、迷路を自動生成するためのスクリプトを用意します。
Scriptsフォルダーを右クリックし、「Create>C# Script」から、Scriptsフォルダー内に2つのスクリプトを作成します。
スクリプトの名前は、それぞれ次のようにしてください。
- CameraManager
- MazeStick
それぞれのスクリプトの内容は、次のとおりです。コピペして使ってください。
まずは、「CameraManager.cs」のソースコードです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraManager : MonoBehaviour
{
Transform tf; //Main CameraのTransform
Camera cam; //Main CameraのCamera
Rigidbody hrb; //Human(親オブジェクト)のRigidbody
void Start()
{
tf = this.gameObject.GetComponent<Transform>(); //Main CameraのTransformを取得する。
cam = this.gameObject.GetComponent<Camera>(); //Main CameraのCameraを取得する。
hrb = transform.parent.gameObject.GetComponent<Rigidbody>(); //Human(親オブジェクト)のRigidbodyを取得する。
}
void FixedUpdate()
{
if(!(Input.GetKey(KeyCode.LeftShift)) && Input.GetKey(KeyCode.UpArrow)) //上キーが押されていれば
{
hrb.position = hrb.position + (transform.forward*Time.deltaTime*7.0f); //人を前進させる。
}
else if(!(Input.GetKey(KeyCode.LeftShift)) && Input.GetKey(KeyCode.DownArrow)) //下キーが押されていれば
{
hrb.position = hrb.position - (transform.forward*Time.deltaTime*7.0f); //人を後ずさりさせる。
}
if(!(Input.GetKey(KeyCode.LeftShift)) && Input.GetKey(KeyCode.LeftArrow)) //左キーが押されていれば
{
hrb.position = hrb.position - (transform.right*Time.deltaTime*7.0f); //人を左へカニ歩きさせる。
}
else if(!(Input.GetKey(KeyCode.LeftShift)) && Input.GetKey(KeyCode.RightArrow)) //右キーが押されていれば
{
hrb.position = hrb.position + (transform.right*Time.deltaTime*7.0f); //人を右へカニ歩きさせる。
}
if(Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.UpArrow)) //左側のShiftと上キーが押されていれば
{
transform.Rotate(new Vector3(-2.0f,0.0f,0.0f)); //カメラを上へ回転。
}
else if(Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.DownArrow)) //左側のShiftと下キーが押されていれば
{
transform.Rotate(new Vector3(2.0f,0.0f,0.0f)); //カメラを下へ回転。
}
if(Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.LeftArrow)) //左側のShiftと左キーが押されていれば
{
transform.Rotate(new Vector3(0.0f,-2.0f,0.0f)); //カメラを左へ回転。
}
else if(Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.RightArrow)) //左側のShiftと右キーが押されていれば
{
transform.Rotate(new Vector3(0.0f,2.0f,0.0f)); //カメラを右へ回転。
}
if(Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.R)) //左側のShiftとRキーが押されていれば
{
tf.rotation = new Quaternion(0.0f,0.0f,0.0f,0.0f); //カメラの回転をリセットする。
}
}
}
操作方法は次の表のとおりです。
押すキー | 動作 |
上キー | 前に進む。 |
下キー | 後ろに進む。 |
左キー | 左に進む。 |
右キー | 右に進む。 |
左側のShift + 上キー | カメラを上に回転させる。 |
左側のShift + 下キー | カメラを下に回転させる。 |
左側のShift + 左キー | カメラを左に回転させる。 |
左側のShift + 右キー | カメラを右に回転させる。 |
左側のShift + R | カメラの回転をリセットする。 |
次に、「MazeStick.cs」のソースコードです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class MazeStick : MonoBehaviour
{
public int max_z; //フィールドの縦幅。5以上の奇数にすること。
public int max_x; //フィールドの横幅。5以上の奇数にすること。
int z; //フィールド配列の縦の要素番号
int x; //フィールド配列の横の要素番号
int r; //乱数の値
Object wall; //壁オブジェクト
GameObject wallgo; //壁のゲームオブジェクト
void Start()
{
int[,] field = new int[max_z,max_x]; //フィールド(0が通路で、1が壁。)
wall = Resources.Load("Wall"); //壁オブジェクトを読み込む。
//通路(0)の生成
for(z=0; z<max_z; z=z+1) //フィールドの縦幅の分だけループする。
{
for(x=0; x<max_x; x=x+1) //フィールドの横幅の分だけループする。
{
field[z,x] = 0;
}
}
//上下の外壁(1)の生成
for(x=0; x<max_x; x=x+1) //フィールドの横幅の分だけループする。
{
field[0,x] = 1;
field[max_z-1,x] = 1;
}
//左右の外壁(1)の生成
for(z=0; z<max_z; z=z+1) //フィールドの縦幅の分だけループする。
{
field[z,0] = 1;
field[z,max_x-1] = 1;
}
//棒倒し法を使った壁(1)の生成(1行めのみ)
z = 2; //1行め
for(x=2; x<max_x-1; x=x+2) //要素番号xが2から要素番号max_x-1の値まで、1マス飛ばしで棒倒し。
{
r = Random.Range(1,13); //乱数生成(r = 1から12のランダムな値)
field[z,x] = 1; //中心から……
if(r<=3) //rが3以下のとき
{
if(field[z-1,x]==0) //上に棒(壁)がなければ
{
field[z-1,x] = 1; //上に棒を倒す。
}
else if(field[z-1,x]==1) //上に棒(壁)があれば
{
x = x - 2; //棒を倒さずに、乱数生成をやり直す。
}
}
if(r>=4 && r<=6) //rが4から6のとき
{
if(field[z+1,x]==0) //下に棒(壁)がなければ
{
field[z+1,x] = 1; //下に棒を倒す。
}
else if(field[z+1,x]==1) //下に棒(壁)があれば
{
x = x - 2; //棒を倒さずに、乱数生成をやり直す。
}
}
if(r>=7 && r<=9) //rが7から9のとき
{
if(field[z,x-1]==0) //左に棒(壁)がなければ
{
field[z,x-1] = 1; //左に棒を倒す。
}
else if(field[z,x-1]==1) //左に棒(壁)があれば
{
x = x - 2; //棒を倒さずに、乱数生成をやり直す。
}
}
if(r>=10) //rが10以上のとき
{
if(field[z,x+1]==0) //右に棒(壁)がなければ
{
field[z,x+1] = 1; //右に棒を倒す。
}
else if(field[z,x+1]==1) //右に棒(壁)があれば
{
x = x - 2; //棒を倒さずに、乱数生成をやり直す。
}
}
}
//棒倒し法を使った壁(1)の生成(2行め以降)
for(z=4; z<max_z-1; z=z+2) //zの要素番号4から要素番号max_z-1まで、1マス飛ばしで棒倒し。
{
for(x=2; x<max_x-1; x=x+2) //xの要素番号2から要素番号max_x-1まで、1マス飛ばしで棒倒し。
{
r = Random.Range(1,13); //乱数生成(r = 1から12のランダムな値)
field[z,x] = 1; //中心から……
if(r<=4) //rが4以下のとき
{
if(field[z+1,x]==0) //下に棒(壁)がなければ
{
field[z+1,x] = 1; //下に棒を倒す。
}
else if(field[z+1,x]==1) //下に棒(壁)があれば
{
x = x - 2; //棒を倒さずに、乱数生成をやり直す。
}
}
if(r>=5 && r<=8) //rが5から8のとき
{
if(field[z,x-1]==0) //左に棒(壁)がなければ
{
field[z,x-1] = 1; //左に棒を倒す。
}
else if(field[z,x-1]==1) //左に棒(壁)があれば
{
x = x - 2; //棒を倒さずに、乱数生成をやり直す。
}
}
if(r>=9) //rが9以上のとき
{
if(field[z,x+1]==0) //右に棒(壁)がなければ
{
field[z,x+1] = 1; //右に棒を倒す。
}
else if(field[z,x+1]==1) //右に棒(壁)があれば
{
x = x - 2; //棒を倒さずに、乱数生成をやり直す。
}
}
}
}
field[0,1] = 0; //スタート地点の壁を撤去する。
field[max_z-1,max_x-2] = 0; //ゴール地点の壁を撤去する。
//壁の配置
for(z=0; z<max_z; z=z+1) //フィールドの縦幅の分だけループする。
{
for(x=0; x<max_x; x=x+1) //フィールドの横幅の分だけループする。
{
if(field[z,x]==0) //通路なら
{
//何も配置しない。
}
else if(field[z,x]==1) //壁なら
{
wallgo = (GameObject)Instantiate(wall, new Vector3(5.0f*x,5.0f,5.0f*z), Quaternion.identity); //壁を配置する。
}
}
}
}
}
難しいコードに見えるかもしれませんが、やっていることは簡単です。
フィールドの横幅をx、奥行きをzとしており、棒倒し法で0(通路)と1(壁)のマスをランダムに決めていくものです。
0のマスには何も生成しませんが、1のマスにはリソース化した壁(Wall)を生成します。
なお、InstantiateのQuaternion.identityは「回転しない」という意味です。したがって、壁は回転されずにそのまま生成されます。
スクリプトのアタッチ
スクリプトを作っても、アタッチ(装着)しなければ発動しません。そのため、スクリプトをアタッチしていきます。
上の図のように、「CameraManager.cs」をドラッグ&ドロップでMain Cameraにアタッチします。
上の図のように、「MazeStick.cs」をドラッグ&ドロップでLandにアタッチします。
Max_zとMax_xの値は、5から99ほどの奇数にしてください。数値が大きいほど、自動生成される迷路のサイズも大きくなります。
テストプレイ
これで、迷路で遊ぶ準備が完了しました。
Playボタンをクリックすると、自動生成された迷路で遊べます!
次の動画は、私が自動生成された迷路で実際に遊んでみたものです。音は出ません。
いかがでしょうか? しっかりと迷路になっていることがわかります。
ぜひ、各自で作成してみてください。
長くなりましたが、今回はここまでです。お疲れさまでした。