アマゾンバナーリンク

迷路ゲームの作り方

こんにちは!ジェイです。今回は迷路ゲームを作っていきますが、C#3.0以降の書き方をとり入れてがんがん新しい技術を使っていきます。

プロジェクトの作成後以下のオブジェクトを追加します

  • Floor
  • Block
  • Goal
  • Player

オブジェクトの作成

Floor

3DObject→Cubeで作成して、Transformを以下の様に設定しマテリアルを適当に色をつけて設定する。

Block

3DObject→Cubeで作成して、Transformを以下の様に設定しマテリアルを適当に色をつけて設定する。

Goal

3DObject→Cylinderで作成して、Transformを以下の様に設定しマテリアルを適当に色をつけて設定する。(Zは0.3)

プレイヤーの作成

  1. GameObjetct→CreateEmptyで空のオブジェクトを作成して、Playerという名前をつける。
  2. 3DObjetct→Sphereを作成しBodyと名前を付けてPlayerの子にする。
  3. CreateEmptyで空のオブジェクトを作成して、Eyesと名前をつけてPlayerの子にする。
  4. Eyesの下にSphereを二つ作成し、RightとLeftと名前をつける。
  5. それぞれポジションを合わせる

パーツのそれぞれの構造は以下のようになってます。

Right
Left
Eyes
Player

P1ayerのPositionに関してはなんでもいいです。Scaleに気を付けましょう。

body

カメラの追加

ヒエラルキーからPlayerを右クリックして、Cameraを選びます。これでPlayerの子としてCameraが作成されます。この時にAudio Listenerは削除しておきましょう。

Camera

このカメラはすぐには使わないのでチェックボタンをはずして無効かしといてください。

プレイヤーの透過率をマテリアルから変更する

プレイヤーのマテリアルを選択し、RenderRingModeからTransparentを選んで、色のところをクリックして、RGBAのAの値を200にしましょう。そうすると色が透過できます。

次にRigidbodyを追加して、重力が加算されるようにします。この時にConstrantsのFreezeRotationのXとZにチェックを入れましょう。(チェックした軸が制限されて回転しなくなります)

最後に、CapsuleCollderを追加しておきましょう。

セルを作って迷路を作る

まずは上で作ったBlockをプレハブ化して、C#ScriptをCLoorとCBlocksという名前で作成します。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class CBlocks
{
    public class CBlockObj
    {
        public CBlockObj(int x, int z, GameObject b)
        {
            this.X = x;
            this.Z = z;
            this.Block = b;
        }
        public int X { get; private set; }
        public int Z { get; private set; }
        public GameObject Block { get; set; }
    }

    GameObject prefab;
    Transform floor;
    int width;
    int height;
    CBlockObj[] blocks;
    int[] map;
    bool remap;
    Vector3 blockSize;
    string prefsName;

    public CBlocks(GameObject prefab, Transform floor, int dx, int dz, string prefsName)
    {
        this.prefab = prefab;
        this.floor = floor;
        this.width = dx;
        this.height = dz;
        this.prefsName = prefsName;
        this.blockSize = prefab.GetComponent<Transform>().localScale;

        blocks = new CBlockObj[width * height];
        map = new int[blocks.Length];
        foreach (var item in blocks.Select((v, i) => new { v, i }))
        {
            blocks[item.i] = new CBlockObj(i2x(item.i), i2z(item.i), null);
        }
    }
    public void Init(Dictionary<string, int[]> objPositions)
    {
        int[] mapv = PlayerPrefs.GetString(prefsName).Split(',').Where(s => s.Length != 0).Select(s => int.Parse(s)).ToArray();
        foreach (var item in blocks.Select((v, i) => new { v, i }))
        {
            int x = i2x(item.i);
            int z = i2z(item.i);

            bool b0 = objPositions.Any(i => i.Value[0] == x && i.Value[1] == z) == false;
            bool b1 = b0 && (item.i < mapv.Length ? mapv[item.i] == -1 : false);
            if (b1)
            {
                CreateBlock(x, z);
            }
        }
    }

    public int i2x(int i)
    {
        return i % width;
    }
    public int i2z(int i)
    {
        return i / width;
    }
    public int[] i2xz(int i)
    {
        return new int[] { i2x(i), i2z(i) };
    }
    public int xz2i(int[] xz)
    {
        return xz2i(xz[0], xz[1]);
    }
    public int xz2i(int x, int z)
    {
        return x + z * width;
    }

    public CBlockObj Find(GameObject obj)
    {
        return Array.Find<CBlockObj>(blocks, x => x.Block == obj);
    }
    public int[] GetBlockIndexXZ(Vector3 pos)
    {
        int[] index = new int[] {
            Mathf.FloorToInt(( ( pos.x - (floor.position.x - floor.localScale.x/2f) ) * width / floor.localScale.x )),
            Mathf.FloorToInt( ( pos.z - (floor.position.z - floor.localScale.z/2f) ) * height / floor.localScale.z ),
        };
        return index;
    }
    public int GetBlockIndex(Vector3 pos)
    {
        return xz2i(GetBlockIndexXZ(pos));
    }

    public void CreateBlock(int x, int z, bool save = true)
    {
        blocks[xz2i(x, z)].Block = UnityEngine.Object.Instantiate(prefab, GetBlockPosition(x, z), Quaternion.identity);
        remap = true;
        if (save)
        {
            SavePrefs();
        }
    }

    public Vector3 GetBlockPosition(int index)
    {
        return GetBlockPosition(i2x(index), i2z(index));
    }
    public Vector3 GetBlockPosition(int iX, int iZ)
    {
        return new Vector3(
            iX * floor.localScale.x / width + (floor.position.x - floor.localScale.x / 2f) + blockSize.x / 2f,
            floor.position.y + floor.localScale.y / 2f + blockSize.y / 2f,
            iZ * floor.localScale.z / height + (floor.position.z - floor.localScale.z / 2f) + blockSize.z / 2f
            );
    }
    public void RemoveBlock(int x, int z, bool save = true)
    {
        RemoveBlock(blocks[xz2i(x, z)], save);
    }
    public void RemoveBlock(CBlockObj obj, bool save = true)
    {
        UnityEngine.Object.Destroy(obj.Block);
        obj.Block = null;
        remap = true;
        if (save)
        {
            SavePrefs();
        }
    }

    public void SavePrefs()
    {
        GetMap();
        PlayerPrefs.SetString(prefsName, string.Join(",", map.Select(x => x.ToString()).ToArray()));
        PlayerPrefs.Save();
    }
    public void DeletePrefs()
    {
        PlayerPrefs.DeleteKey(prefsName);
    }

    public int[] GetMap()
    {
        if (remap)
        {
            foreach (var item in blocks.Select((v, i) => new { v, i }))
            {
                map[xz2i(item.v.X, item.v.Z)] = (item.v.Block == null ? 1 : -1);
            }
            remap = false;
        }
        return map;
    }

    public bool All(System.Func<int, int, bool> f)
    {
        return blocks.Select((v, i) => new { v, i }).All(item => { return f(i2x(item.i), i2z(item.i)); });
    }
    public bool IsIn(int x, int z)
    {
        return x >= 0 && x < width && z >= 0 && z < height;
    }
    public bool IsWall(int x, int z)
    {
        GetMap();
        return IsIn(x, z) == false || map[xz2i(x, z)] == -1;
    }
}

CBlockクラスはセルを管理するクラスなのでMonoBehaviourの継承の必要はないです。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class CFloor : MonoBehaviour
{
    public GameObject BlockPrefab;
    public CBlocks _Blocks;
    int dx = 10;
    int dz = 10;
    Transform _Floor;

    string PlayerName = "Player";
    string EnemyName = "Enemy";
    string GoalName = "Goal";
    string StartName = "Start";
    Dictionary<string, int[]> ObjPositions = new Dictionary<string, int[]>();
    // Start is called before the first frame update
    void Start()
    {
        _Floor = GetComponent<Transform>();
        ObjPositions[PlayerName] = new int[] { 0, 0 };
        ObjPositions[StartName] = new int[] { 0, 0 };
        ObjPositions[GoalName] = new int[] { dx-1, dz-1 };
        ObjPositions[EnemyName] = new int[] { Mathf.RoundToInt(dx/2), Mathf.RoundToInt(dz/2) };

        BlockPrefab.GetComponent<Transform>().localScale = new Vector3(_Floor.localScale.x / dx, 1f, _Floor.localScale.z / dz);
        _Blocks = new CBlocks(BlockPrefab, _Floor, dx, dz, "Map");
        _Blocks.Init(ObjPositions);
    }

    // Update is called once per frame
    void Update()
    {
        // 押してない0 左1 右2が返ってくる
        int i = Enumerable.Range(1, 2).FirstOrDefault(v => Input.GetMouseButtonDown(v - 1));
        if(i != 0)
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            RaycastHit hit = new RaycastHit();
            if(Physics.Raycast(ray.origin, ray.direction, out hit, Mathf.Infinity))
            {
                CBlocks.CBlockObj target = _Blocks.Find(hit.collider.gameObject);
                if(i == 2 && target != null)
                {
                    _Blocks.RemoveBlock(target);
                } // 左クリックしてブロックにぶつからなかったら
                else if(i == 1 && gameObject == hit.collider.gameObject)
                {
                    int[] index = _Blocks.GetBlockIndexXZ(hit.point);
                    _Blocks.CreateBlock(index[0], index[1]);
                }
            }
        }
    }
}

ここまででブロックを生成し削除することができるようになりました。次は周りの壁を作ります

周りの壁を作る

次にゴールの位置を設定し、周りの壁を作ります。

public class CFloor : MonoBehaviour
{
    ~中略~
    void Start()
    {
        _Floor = GetComponent<Transform>();
        ObjPositions[PlayerName] = new int[] { 0, 0 };
        ObjPositions[StartName] = new int[] { 0, 0 };
        ObjPositions[GoalName] = new int[] { dx-1, dz-1 };
        ObjPositions[EnemyName] = new int[] { Mathf.RoundToInt(dx/2), Mathf.RoundToInt(dz/2) };

        BlockPrefab.GetComponent<Transform>().localScale = new Vector3(_Floor.localScale.x / dx, 1f, _Floor.localScale.z / dz);
        _Blocks = new CBlocks(BlockPrefab, _Floor, dx, dz, "Map");
        _Blocks.Init(ObjPostions);

        // Goalの位置を変更
        GameObject goal = GameObject.Find(GoalName);
        goal.name = GoalName;
        goal.GetComponent<Transform>().position = _Blocks.GetBlockPosition(
            ObjPositions[GoalName][0], ObjPositions[GoalName][1]);

        // 周りに壁を作る
        Vector3 scale = BlockPrefab.GetComponent<Transform>().localScale;
        for (int angle = 0; angle < 360; angle += 90)
        {
            float x = Mathf.Cos(Mathf.Deg2Rad * angle);
            float z = Mathf.Sin(Mathf.Deg2Rad * angle);

            BlockPrefab.GetComponent<Transform>().localScale = new Vector3(
                Mathf.RoundToInt(z * 10) == 0 ? 0.01f : _Floor.localScale.x,
                scale.y,
                Mathf.RoundToInt(x * 10) == 0 ? 0.01f : _Floor.localScale.z
                );

            float px = x * _Floor.localScale.x / 2f;
            float pz = z * _Floor.localScale.z / 2f;
            float py = _Floor.localScale.y / 2f + _Floor.position.y + scale.y / 2f;
            Instantiate(BlockPrefab, new Vector3(px, py, pz), Quaternion.identity);
        }
        BlockPrefab.GetComponent<Transform>().localScale = scale;
    }
}

ここまでで実行してみると周りに壁ができます。

プレイヤーを動かす

次にプレイヤーを動かすスクリプトを書きます。

仕様

  • セル単位で移動する。
  • 移動は、時間とともに少しずつ、位置が変わるアニメーションを作成する。
  • プレイヤーは矢印キーで、敵は自動で移動する。

CLoor.csに以下のプログラムを追加。PlayerをPrefab化してCFoorSceneのPlayerPrefabにアタッチして、インスペクターのPlayerは削除する。

public class CFloor : MonoBehaviour
{
    ~中略~
    public GameObject PlayerPrefab;
    GameObject Player;

    Camera BirdEye;
    Camera PlayersEye;
    public bool StartBirdView;
    GameObject TimerText;
    float Timer = 0;
    void Start()
    {
        ~中略~
        // player, enemy
        new string[] { PlayerName, EnemyName }.Select((v, i) => new { v, i }).All(
            item =>
            {
                GameObject obj = Instantiate(PlayerPrefab);
                obj.name = item.v;
                Transform transform = obj.GetComponent<Transform>();
                Vector3 p = _Blocks.GetBlockPosition(ObjPositions[item.v][0], ObjPositions[item.v][1]);
                p.y = _Floor.localScale.y / 2f + _Floor.position.y + transform.localScale.y * transform.Find("Body").localScale.y / 2f;
                transform.position = p;
                CPlayerCellController ctrl = obj.GetComponent<CPlayerCellController>();

                if (item.v == PlayerName)
                {
                    Player = obj;
                }
                else if (item.v == EnemyName)
                {

                }
                return true;
            }
         );
    }
    ~中略~
    public void UpdateObjPosition(string name, Vector3 pos, Quaternion rot)
    {
        int[] index = _Blocks.GetBlockIndexXZ(pos);
        ObjPositions[name] = index;
    }
}

ここまでで実行するとプレイヤーと敵が現れますが敵にマテリアルを設定していないのでプレイヤーと同じ色になっています。

次にCPlayerCellController.csとCPlayerMotion.csを作成します。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CPlayerMotion : MonoBehaviour
{
    public class CAnimation
    {
        public float Duration { get; set; }
        public Action Complete { get; set; }
        Action<float> _Animate;
        public AudioClip Sound { get; set; }
        public float Volume { get; set; }

        public CAnimation(Action<float> a, float d, Action c = null, AudioClip sound = null, float volume = 0)
        {
            this._Animate = a;
            this.Duration = d;
            this.Complete = c;
            this.Sound = sound;
            this.Volume = volume;
        }
        public void Animate(float p)
        {
            _Animate(p);
            if (p >= 1.0f && Complete != null)
            {
                Complete();
            }
        }
    }
    List<CAnimation> Animations = new List<CAnimation>();
    float StartedTime = 0f;

    public void Add(Action<float> animate, float duration, Action complete = null, AudioClip sound = null, float volume = 0)
    {
        Add(new CAnimation(animate, duration, complete, sound, volume));
    }
    public void Add(CAnimation[] anis)
    {
        foreach (CAnimation ani in anis)
        {
            Add(ani);
        }
    }
    public void Add(CAnimation ani)
    {
        this.Animations.Add(ani);
    }
    public void Unset()
    {
        Animations.ForEach(a => a.Animate(1f));
        Animations.Clear();
        StartedTime = 0f;
    }
    public void Cancel()
    {
        Animations.Clear();
        StartedTime = 0f;
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (Animations.Count > 0)
        {
            if (StartedTime == 0f)
            {
                StartedTime = Time.realtimeSinceStartup;
            }
            float progress = (Time.realtimeSinceStartup - StartedTime) / Animations[0].Duration;
            Animations[0].Animate(Mathf.Min(1f, progress));
            if (progress >= 1.0f)
            {
                Animations.RemoveAt(0);
                StartedTime = 0f;
            }
        }
    }
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CPlayerCellController : MonoBehaviour
{
    Dictionary<string, int[]> NextPosition = new Dictionary<string, int[]>()
    {
        {"up",      new int[]{ 0, 1, 0, 1 } },//グローバル座標におけるx,zの変化量
        {"down",    new int[]{ 0,-1, 0, 1 } },
        {"left",    new int[]{-1, 0, 0, 1 } },
        {"right",   new int[]{ 1, 0, 0, 1 } },
    };
    // moving direction, rotating axis
    Dictionary<string, int[]> NextAction = new Dictionary<string, int[]>()
    {
        {"up",      new int[]{0, 1, 0, 0 } },//ローカル座標におけるx,z軸の変化量
        {"down",    new int[]{0,-1, 0, 0 } },
        {"left",    new int[]{0, 0,-90, 0 } },
        {"right",   new int[]{0, 0, 90, 0 } },
    };
    Dictionary<string, int[]> Actions;
    public int ActionType
    {
        set { Actions = value == 0 ? NextPosition : NextAction; }
    }
    CFloor _Floor;
    CPlayerMotion PMotion;

    public float AutoMovingSpan { get; set; }
    float AutoMovedTime = 0f;
    float AutoMovingSpeed = 1.0f;
    // Start is called before the first frame update
    void Start()
    {
        ActionType = 0;
        _Floor = GameObject.Find("Floor").GetComponent<CFloor>();
        PMotion = GetComponent<CPlayerMotion>();
    }

    // Update is called once per frame
    void Update()
    {
        if (AutoMovingSpan == 0)
        {
            foreach (var elem in Actions)
            {
                if (Input.GetKeyDown(elem.Key))
                {
                    Move(elem.Value);
                }
            }
        }
        else if (Time.realtimeSinceStartup > AutoMovedTime + AutoMovingSpan / AutoMovingSpeed)
        {
            AutoMovedTime = Time.realtimeSinceStartup;
            PMotion.Unset(); // 最後のモーションにもっていって終了させる

            int[] pos = _Floor._Blocks.GetBlockIndexXZ(GetComponent<Transform>().position);
            List<string> avail = new List<string>();
            foreach (var d in NextPosition)
            {
                if (_Floor._Blocks.IsWall(pos[0] + d.Value[0], pos[1] + d.Value[1]) == false)
                {
                    avail.Add(d.Key);
                }
            }
            if (avail.Count != 0)
            {
                Move(NextPosition[avail[UnityEngine.Random.Range(0, avail.Count)]]);
            }
        }
        _Floor.UpdateObjPosition(gameObject.name, GetComponent<Transform>().position, GetComponent<Transform>().rotation);
    }
    public void SetColor(Color32 col)
    {
        GetComponent<Transform>().Find("Body").GetComponent<Renderer>().material.color = col;
    }
    public void Move(int[] pos, Action aniComplete = null)
    {
        PMotion.Unset();
        if (pos[0] != 0 || pos[1] != 0)
        {
            Vector3 d = new Vector3(pos[0], 0, pos[1]);
            if (pos[3] == 1)
            {
                Quaternion q = new Quaternion();
                q.SetFromToRotation(Vector3.forward, new Vector3(pos[0], 0, pos[1]));
                int y = Mathf.RoundToInt((q.eulerAngles.y - GetComponent<Transform>().eulerAngles.y)) % 360;
                if (y != 0)
                {
                    Turn(NormalizedDegree(y), null);
                }
            }
            else
            {
                d = GetComponent<Transform>().localRotation * d;
            }
            int[] index = _Floor._Blocks.GetBlockIndexXZ(GetComponent<Transform>().position);
            Forward(index[0] + Mathf.RoundToInt(d.x), index[1] + Mathf.RoundToInt(d.z), aniComplete);
        }
        if (pos[2] != 0)
        {
            Turn(pos[2], aniComplete);
        }
    }
    // 180より大きい時に補正して無駄をなくす
    float NormalizedDegree(float deg)
    {
        while (deg >= 180)
        {
            deg -= 360;
        }
        while (deg < -180)
        {
            deg += 360;
        }
        return deg;
    }
    void Forward(int x, int z, Action aniComplete)
    {
        if (_Floor._Blocks.IsWall(x, z) == false)
        {
            Vector3 pos0 = GetComponent<Transform>().position;
            Vector3 pos1 = _Floor._Blocks.GetBlockPosition(x, z);
            pos1.y = pos0.y;
            PMotion.Add(p =>
            {
                GetComponent<Transform>().position = (pos1 - pos0) * p + pos0;
            }, 0.5f, aniComplete);
        }
    }
    void Turn(float deg, Action aniComplete)
    {
        float deg0 = GetComponent<Transform>().eulerAngles.y;
        float deg1 = RoundDegree(deg0 + deg);
        PMotion.Add(p =>
        {
            GetComponent<Transform>().rotation = Quaternion.Euler(0f, (deg1 - deg0) * p + deg0, 0f);
        }, 0.5f, aniComplete);
    }
    // 90の倍数に値を補正する
    float RoundDegree(float deg)
    {
        return Mathf.FloorToInt((deg + 45) / 90) * 90;
    }
    public void CancelMotions()
    {
        PMotion.Cancel();
    }
}

プレイヤ視点を作成する

次にプレイヤの視点を上から見るモードとプレイヤー視点を切り替えられる様にします。以下のコードをFloor.csに追加しましょう。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class CFloor : MonoBehaviour
{
    Camera BirdEye;
    Camera PlayersEye;
    public bool StartBirdView;
    ~中略~
    void Start()
    {
        ~中略~
        BirdEye = GameObject.FindWithTag("MainCamera").GetComponent<Camera>();
        BirdEye.enabled = false;
        PlayersEye = Player.GetComponent<Transform>().Find("Camera").GetComponent<Camera>();
        PlayersEye.enabled = true;
        SetPlayerActionType();
        if (StartBirdView == true)
        {
            ChangeCamera();
        }
    }
    void SetPlayerActionType()
    {
        Player.GetComponent<CPlayerCellController>().ActionType = BirdEye.enabled ? 0 : 1;
    }
    void ChangeCamera()
    {
        BirdEye.enabled = !BirdEye.enabled;
        PlayersEye.enabled = !PlayersEye.enabled;
        SetPlayerActionType();
    }
    ~中略~
}

上記の点を追加したら、CPlayerCellController.csのStart関数で設定しているActionTypeをコメントアウトしましょう。

実行結果