【Unity】棒倒し法で迷路を自動生成する方法

はじめに

どうも! 高杉 皆為(@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の設定

Landの色は、白色:Color = (255, 255, 255, 255) です。

Startの設定

Startの色は、黄色:Color = (255, 255, 0, 255) です。

Wallの設定

Wallの色は、灰色:Color = (149, 149, 149, 255) です。

光の設定

シーン(Hierarchy)から「Directional Light」を削除します。

Spotlightの追加

Hierarchyで右クリックし、「Light>Spotlight」を選択します。そして、Spot Lightの設定を次のように変更します。

Spot Lightの設定
  • Type = Spot
  • Range = 10000
  • Spot Angle = 179
  • Color = (134, 188, 255, 255)
  • Mode = Baked
  • Intensity = 1
  • Indirect Multiplier = 1

地面の作成

迷路で遊ぶためには地面が必要ですので、地面を作ります。

Planeの作成

Hierarchyで右クリックし、「3D Object>Plane」を選択します。

Landの設定

わかりやすいように、Planeの名前を「Land」に変更します。そして、Landの設定を次のように変更します。さらに、マテリアル「Land」をMaterialsフォルダーからドラッグ&ドロップします。

  • Position = (0, 0, 0)
  • Scale = (1000, 1, 1000)
  • Mesh RendererのElement 0 = Land

人の作成

迷路で遊ぶためには、自分の代わりに迷路で行動してくれる人が必要です。その人を作成します。

Cylinderの作成

Hierarchyで右クリックし、「3D Object>Cylinder」を選択します。

Human

わかりやすいように、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の子オブジェクト化

次に、Main CameraをHumanにドラッグ&ドロップして、Humanの子オブジェクトにします。上の画像のようにしてください。

これで、人(Human)の動きにカメラがついていくようになります。

Main Cameraの設定

そして、Main Cameraの設定を次のように変更します。

  • Position = (0, 0.5, 0)
  • Scale = (1, 0.5, 1)

これで、人の視界を表現することができます。

スタート地点の置物の作成

迷路のスタート地点がわかるように、置物を作成しておきます。

Cubeの作成

Hierarchyで右クリックし、「3D Object>Cube」を選択します。

Startの設定

わかりやすいように、Cubeの名前を「Start」に変更します。そして、Startの設定を次のように変更します。さらに、マテリアル「Cube」をMaterialsフォルダーからドラッグ&ドロップします。

  • Position = (5, 3, -3)
  • Scale = (5, 3, 1)
  • Mesh RendererのElement 0 = Start

壁の作成とリソース化

迷路には壁が必要ですので、(後で自動生成される)壁の作成をおこないます。

Cubeの作成

Hierarchyで右クリックし、「3D Object>Cube」を選択します。

Wallの設定

わかりやすいように、Cubeの名前を「Wall」に変更します。そして、Startの設定を次のように変更します。さらに、マテリアル「Wall」をMaterialsフォルダーからドラッグ&ドロップします。

  • Position = (0, 5, 0)
  • Scale = (5, 10, 5)
  • Mesh RendererのElement 0 = Wall
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は「回転しない」という意味です。したがって、壁は回転されずにそのまま生成されます。

スクリプトのアタッチ

スクリプトを作っても、アタッチ(装着)しなければ発動しません。そのため、スクリプトをアタッチしていきます。

Main Cameraへのアタッチ

上の図のように、「CameraManager.cs」をドラッグ&ドロップでMain Cameraにアタッチします。

Landへのアタッチ

上の図のように、「MazeStick.cs」をドラッグ&ドロップでLandにアタッチします。

Max_zとMax_xの値は、5から99ほどの奇数にしてください。数値が大きいほど、自動生成される迷路のサイズも大きくなります。

テストプレイ

これで、迷路で遊ぶ準備が完了しました。

プレイボタン

Playボタンをクリックすると、自動生成された迷路で遊べます!

次の動画は、私が自動生成された迷路で実際に遊んでみたものです。音は出ません。

いかがでしょうか? しっかりと迷路になっていることがわかります。

ぜひ、各自で作成してみてください。

長くなりましたが、今回はここまでです。お疲れさまでした。


 

みなためじゃんけん

 

このコーナーは、私と擬似的にじゃんけんできるコーナーです。

 

みなためじゃんけん、じゃんけんぽん!

 

私が出したのは……





 

パーでした! チョキの勝利です!


この記事をSNSでシェアする


プログラミングカテゴリーの最新記事(5件)

最新記事(10件)

管理人のTwitter

内部リンク集