自動生成のダンジョンを作る④

質感、小物を追加していく

前回からの続きです。 ringogames.hatenablog.com

自動生成ダンジョンに質感や小物を追加して、それっぽく見えるようにしていきました。 今回のところまでのイメージ

使用した素材など

- 壁

以下のテクスチャを使用させていただきました。 assetstore.unity.com

- 地面

以下のテクスチャの8番を使用させていただきました。 assetstore.unity.com ワールド座標で投影するように簡単なシェーダーをShaderGraphで作成しています。

- 松明モデル

下記モデルを使用させていただきました。

sketchfab.com

- 炎スプライト素材

適度な素材が見つからず、EmberGenで簡易に作成し、Flipbookで再現。

- ゾンビちゃん

過去に作ったものをスケール確認用に仮配置

今後

ライトなどを追加して暗い感じのダンジョンンにしていく予定です。 ここまでのプロジェクトは以下にアップしています。

https://gitlab.com/ringogames2022/kruskalalgorithmdungeon/-/tree/blog-20230201

自動生成されたマップで動くAIキャラクターの作成②

自動生成ダンジョンで自動的に動くAIキャラクターの生成

自動生成ダンジョンにランタイム時におけるNavMesh生成プログラムを適用してみます。 自動生成ダンジョンプログラムは以前のものになります。

        public void Awake()
        {
            GameObject rootObject = new GameObject("Root");
            rootObject.transform.parent = transform;

            GenerateDungeon(rootObject, dungeonColumnSize, dungeonRowSize);

            // generate navmesh
            NavMeshSurface surface = null;
            TryGetComponent(out surface);
            if (surface)
            {
                surface.BuildNavMesh();
            }
        }

Awake時にNavMeshSurfaceコンポーネントで、NavMeshの自動生成を行っているだけの非常に簡単なコードになります。 結果がこちらです。簡単にNavMeshが生成できました。

== AIで動くキャラクターの作成 ダンジョンに開始位置、終了位置を設定し、開始位置から終了位置まで動いていく簡単なキャラクターを作成します。 Capsuleコンポーネントを作成し、そこにNavMeshAgentのコンポーネントと、制御用スクリプトを追加します。 制御用スクリプトは以下のような記述になります。

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

public class DummyAICharacter : MonoBehaviour
{
    public Vector3 Destination = Vector3.zero;
    private NavMeshAgent m_navMeshAgent = null;
    

    // Start is called before the first frame update
    void Start()
    {
        TryGetComponent(out m_navMeshAgent);
    }

    // Update is called once per frame
    void Update()
    {
        if (m_navMeshAgent)
        {
            m_navMeshAgent.destination = Destination;
        }
    }
}

GameManagerオブジェクトも作成し、以下のようなコードで、ダンジョンの開始位置をAICharacterに与えるようにしました。

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

public class DummyAICharacter : MonoBehaviour
{
    public Vector3 Destination = Vector3.zero;
    private NavMeshAgent m_navMeshAgent = null;
    

    // Start is called before the first frame update
    void Start()
    {
        TryGetComponent(out m_navMeshAgent);
    }

    // Update is called once per frame
    void Update()
    {
        if (m_navMeshAgent)
        {
            m_navMeshAgent.destination = Destination;
        }
    }
}

実行結果はこのようになります。 スタート地点(S)からゴール地点(G)までNavMeshAgentによりChacacterは迷うことなく迷路を進んでいくことができています。

今回のプロジェクトは以下からダウンロードできます。

https://gitlab.com/ringogames2022/kruskalalgorithmdungeon/-/tree/main

ringogames.hatenablog.com

自動生成されたマップで動くAIキャラクターの作成①

Runtime時のNavMeshのアップデート

AIで動くキャラクターは、マップ形状に応じて動く必要があります。UnityではNavMeshを使用してキャラクターの動く経路を計算するのが一般的ですが、マップがランタイム時の自動生成の場合、NavMeshもランタイム時に更新してやる必要があります。Unityの2022から正式に導入されたUnity.AI.Navigationを使用するとランタイム時のNavMesh更新が可能になっていました。

環境

現状最新のAI Navigation 1.1.1 からRuntime時のNavMesh更新が正式にサポートされたようです。 Unity公式ドキュメントはこちらになります。 docs.unity3d.com

  • Unity 2022.2.2f
  • Unity.AI.Navigation 1.1.1

Unity2022で起動後、PackageManagerからUnity.AI.Navigationをインストールします。

テストコード

テストとして、広めのプレーンの上に3つのブロックをおいて動作を見ていきます。 NavMeshManagerはEmptyオブジェクトで、ここにNavMeshSurfaceのコンポーネントを割り当てます。

このオブジェクトの子供のオブジェクトのみ、NavMeshの対象にしたかったため、Collect ObjectsをCurrent Object Hierarchyに設定しました。

Start時にNavMeshを生成する簡単なコードを用意しました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.AI.Navigation;

public class NavMeshManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        NavMeshSurface surface = null;
        TryGetComponent(out surface);
        if (surface)
        {
            surface.BuildNavMesh();
        }
    }
}

スタートして実行してみます。 青色のメッシュがNavMeshになります。 非常に簡単な仕組みで、NavMeshをランタイム時に生成することができました。 次回以降、これを発展させていきます。

ringogames.hatenablog.com

自動生成のダンジョンを作る③

前回の続きです

 

ringogames.hatenablog.com

②で解説したアルゴリズムをもとに実際にUnity上で迷路を作成するプロジェクトを作成しました。

以下にプロジェクトをアップしています。

https://gitlab.com/ringogames2022/kruskalalgorithmdungeon

制作環境

  • Unity 2021.3.16f1
  • URP

で開発しています。

サンプルシーンを開いて実行すると以下のように迷路が作成されます。

この迷路は毎回ランダムで生成されるようになっています。

シーン上に用意されたDungeonGeneratorが迷路生成用アルゴリズムのメイン部分となっています。

  • WallObject
    • 迷路の壁となるオブジェクトのPrefabを指定
  • DungeonTile
    • 迷路の床となる面のPrefabを指定
  • Width、Height
    • タイルの横方向、縦方向の数
  • StartPoint
    • 迷路の左下の座標
  • TileLength
    • WallとTileの幅と一致させます。

今回のサンプルでは、WallとTileの幅は3mとなっています。

壁はX軸方向正に3mの幅としています。

 

基本的に、②で解説したアルゴリズムをDungeonGenerator.csのGenerateDungeon関数で実現してます。

以下の部分はまず、すべてのエッジ部分に壁を置いている操作になるため、あまり難しい部分はありません。

 

        public void GenerateDungeon(int columnCount, int rowCount)
        {
            //generate tiles
            m_tiles = new List<DungeonTile>();
            for(int i= 0; i < rowCount; ++i)
            {
                for(int j=0; j < columnCount; ++j)
                {
                    Vector3 pos = new Vector3(i * m_tileLength, 0, j * m_tileLength);
                    Quaternion quat = Quaternion.identity;
                    m_tiles.Add(new DungeonTile(pos, quat));
                }
            }

            foreach(DungeonTile tile in m_tiles)
            {
                Instantiate(m_tileObject, tile.Pos, tile.Quat);
            }

            List<DungeonEdge> bounderyEdges = new List<DungeonEdge>();
            //1. generate outer bounderies
            //1-1. bottom
            for(int i = 0; i < columnCount; ++i)
            {
                Vector3 pos = m_startPoint + new Vector3(i * m_tileLength, 0, 0);
                Quaternion quat = Quaternion.Euler(0, 0, 0);
                bounderyEdges.Add(new DungeonEdge(pos, quat));
            }
            //1-2. top
            for(int i = 0; i < columnCount; ++i)
            {
                Vector3 pos = m_startPoint + new Vector3(i * m_tileLength, 0, rowCount * m_tileLength);
                Quaternion quat = Quaternion.Euler(0, 0, 0);
                bounderyEdges.Add(new DungeonEdge(pos, quat));
            }
            //1-3. left
            for(int i = 0; i < rowCount; ++i)
            {
                Vector3 pos = m_startPoint + new Vector3(0, 0, i * m_tileLength);
                Quaternion quat = Quaternion.Euler(0, -90, 0);
                bounderyEdges.Add(new DungeonEdge(pos, quat));
            }
            //1-4. right
            for(int i = 0; i < rowCount; ++i)
            {
                Vector3 pos = m_startPoint + new Vector3(columnCount * m_tileLength, 0, i * m_tileLength);
                Quaternion quat = Quaternion.Euler(0, -90, 0);
                bounderyEdges.Add(new DungeonEdge(pos, quat));
            }

            //instantiate
            foreach(DungeonEdge edge in bounderyEdges)
            {
                Instantiate(m_wallObject, edge.Pos, edge.Quat);
            }

            //2. generate inner edges
            List<DungeonEdge> innerEdges = new List<DungeonEdge>();
            //2-1. left to right edges
            int tileIndex = 0;
            for (int i = 0; i < rowCount; ++i)
            {
                for (int j = 1; j < columnCount; ++j)
                {
                    Vector3 pos = m_startPoint + new Vector3(j * m_tileLength, 0, i * m_tileLength);
                    Quaternion quat = Quaternion.Euler(0, -90, 0);
                    DungeonEdge edge = new DungeonEdge(pos, quat);
                    edge.NeigboringTileLeft  = m_tiles[tileIndex];
                    edge.NeigboringTileRight = m_tiles[tileIndex + 1];
                    innerEdges.Add(edge);
                    tileIndex += 1;
                }
                tileIndex += 1;
            }

            //2-2. bottom to top edges
            tileIndex = 0;
            for (int i = 1; i < rowCount; ++i)
            {
                for (int j = 0; j < columnCount; ++j)
                {
                    Vector3 pos = m_startPoint + new Vector3(j * m_tileLength, 0, i * m_tileLength);
                    Quaternion quat = Quaternion.Euler(0, 0, 0);
                    DungeonEdge edge = new DungeonEdge(pos, quat);
                    edge.NeigboringTileLeft  = m_tiles[tileIndex];
                    edge.NeigboringTileRight = m_tiles[tileIndex + columnCount];
                    innerEdges.Add(edge);
                    tileIndex += 1;
                }
            }

 

それに続く以下の部分で、削除対象のエッジをはさむタイルのグループを調べ、

  1. 同一グループであれば、エッジは削除しない
  2. 異なるグループであれば、エッジを削除したのち、そのタイルを同一グループにする

ということを行っています。

 

            //remove edges
            //generate edge
            m_edges = new List<DungeonEdge>();
            while (true)
            {
                if(innerEdges.Count == 0)
                {
                    break;
                }
                int edgeIndex = Random.Range(0, innerEdges.Count);
                var targetEdge = innerEdges[edgeIndex];
                innerEdges.RemoveAt(edgeIndex);

                var leftTile  = targetEdge.NeigboringTileLeft;
                var rightTile = targetEdge.NeigboringTileRight;
                if(leftTile.TopParent() == rightTile.TopParent())
                {
                    //keep this edge
                    m_edges.Add(targetEdge);
                }
                else
                {
                    //dose not keep this edge
                    //set parent right to left
                    rightTile.TopParent().Parent = leftTile;
                }
            }


            //instantiate
            foreach(DungeonEdge edge in m_edges)
            {
                Instantiate(m_wallObject, edge.Pos, edge.Quat);
            }

非常に簡単なアルゴリズムで、プロシージャルな迷路を作成することができました。

今後、このアルゴリズムを使用して、ゲーム作成を行っていきます。

自動生成のダンジョンを作る②

Kruskal's Algorithm

前回の続きです。

ringogames.hatenablog.com

 

Kruskal's Algorithmアルゴリズムを例を使って解説していきます。

もっとも簡単な例として、3x3マスで構成される図のような迷路を考えていきます。

A~Iをタイル、それぞれを隔てる境界部分をエッジとし、以下説明を行います。

Kruskalのアルゴリズムでは、タイルを隔てるすべてのエッジに壁を立てた状態から、それを削除していくことによって、迷路を完成させます。そのため、まずすべてのエッジに壁を立てます。
上記例では、12個壁が作られました。この12個のエッジからまずひとつランダムにエッジ選択をし、処理を行います。

まず、最初にランダムな選択の結果、BとEの間のエッジが選択されたとします。この場合、このエッジに存在する壁を削除します。

壁がなくなり、BとEのタイルはつながった状態になっています。この状態になった場合、タイルのグループを変更し、BとEを同一グループのタイルとみなします。ここでは、どちらもBとしました。

次に残りの11個のエッジから1個選択します。

GとHの間のエッジが選択されたとします。先ほどと同様にそこに存在する壁を削除します。HとGも同一のグループとなります。

残りのエッジは10本になりました。この操作を残り10本のエッジについても繰り返していきます。

ただし、毎回壁を削除するわけではありません。

次に図の赤エッジが選択されたとします。

このエッジに隣接するタイルはどちらもAです。エッジに隣接するタイルが同一のグループに所属する場合は、壁を削除する操作をスキップします。

これがこのアルゴリズムのポイントとなります。

処理を続けていきます。


これで、すべてのタイルが同じグループに所属しました。

すべてのタイルが同じグループに所属したら、迷路の完成となります。

まとめると

  • NxNのタイルに存在するすべてのエッジに関して1個ずつランダムに選択し、タイルを隔てている壁の削除を行っていく。
  • エッジに隣接する2つのタイルが異なるグループの際は、壁を削除、同一グループの際は、壁をキープする。
  • この処理をすべてのエッジに関して行い、すべてのタイルが同一グループに所属すれば、迷路は完成。

ということになります。

アルゴリズムとしては非常に簡単なものとなっています。

次回は、これを理解したうえで、実際にコーディングをしていきます。

 

自動生成のダンジョンを作る①

迷路アルゴリズム

自動生成のダンジョンが作れれば、繰り返し遊べるローグライクなゲームを開発することが可能になります。

そこでまず、迷路を自動生成するためのアルゴリズムを調べました。

有名なところでは以下のものがあるようです。

上から順にメジャーなアルゴリズムというわけではありませんが、KruskalのアルゴリズムでUnityで実装した事例を調べたところ、以下の動画が見つかりました。

 

www.youtube.com

 

プロジェクトもGithubにアップしてくれています。

github.com

 

こちらのコードを参考にしながら、次回以降自動生成ダンジョンを作っていきます。

 

 

ringogames.hatenablog.com

 

Unity URPでライティングを開始する初期状態を真っ暗にする方法

ライトを消しただけだと真っ暗にならない

Unity URPでライティングをゼロから実施するために、まず、真っ黒の画面から調整していきたい場合があります。

ただ、Unity URPプロジェクトで新規シーンを作成し、DirectionalLightを無効にしても、真っ暗にはなりません。これを真っ暗の初期状態にする方法のメモです。

環境光の影響で真っ暗になっていない

ライティングの設定を開きます。

Window->Rendering->Lightingにライティングメニューがあります。

URPのライティングはそこで制御する必要があります。

 

ライティング設定の上記タブでEnvironmentを選択します。

URPではゲーム内に配置されているライト以外の環境光の設定がここに存在しています。

以下を変更します。

  • Realtime Shadow Color を黒に
  • Environment Lighting
    • Sourceを「Color」に
    • Ambient Color を黒に

まだ完全に黒にできていません。何かが影響しています。

 

  • Environment Reflectionも黒に
  • FogのチェックをOffに

ここまですると完全に黒になります。

最後にカメラの塗りつぶし色を黒にします。

  • Background Type を Solid Colorに
  • Backgroundの色を黒に

 

これで完全に真っ暗になりました。

自分は、この状態からライティングをスタートしていき、ライティングをある程度固めたのちに、こうした要素を再度戻すことでライティング調整をしていきます。