土鍋で雑多煮

UnityでXR・ゲーム開発をしています。学んだことや備忘録、趣味の記録などを書いていきます。

MENU

UnityECSで都市開発シミュレーションゲームを作る【その4】~建物を建築する~

はじめに

どうも、土鍋です。

これは土鍋ひとり Advent Calendar 2024の2日目の記事です。

前回は土鍋さんの【ECS&DI】Unity ECSでVContainerによるDIをやってみる - 土鍋で雑多煮でした。

さて、ECSで都市開発シミュレーションゲームを作るシリーズ4回目はいよいよ建築できるようにしようと思います。
といっても、建物選んでInstantiateするだけですが、まーーECSの仕様がむずくって手こずりました。

建築物選択メニューUI

ECSとUI Toolkit

今回のゲーム開発ではなるべくUnityの新しい要素に触れるようにしようという思想の元、開発しているので、UIに関してもUI Toolkitを導入しました。

UIを構築する

建築したい建物を選ぶメニューをどうしようかな~と思ったんですが、やっぱグリッドビューが一番視認性がいいし情報量が多くていいかなと思い実装しました。
ただここで発生した問題がUI Toolkitにグリッドビューがないという問題でした。(uGUIにはあるのに!)

ということで自力で実装して別記事にまとめたので、ご覧ください。

donabenabe.hatenablog.com

ClickEventの設定

上記の記事でグリッドビューの作成とそれにDataをBindingするとこまで実装したので、プラスアルファでどの建物を選択したかの情報をPlayerStatusHolderに渡しています。

public class ConstructMenu : MonoBehaviour
{
    [SerializeField] 
    private VisualTreeAsset buildingElement; // グリッド要素テンプレート
    
    [SerializeField] 
    private BuildingList buildingList; // 追加したいUI要素のList保持ScriptableObject
    
    private void Start()
    {
        var root = GetComponent<UIDocument>().rootVisualElement;

        foreach (var building in buildingList.buildings)
        {
            var buildingTemplate = buildingElement.Instantiate(); // テンプレートの生成
            buildingTemplate.Q<VisualElement>("thumbnail").dataSource = building; // データのバインド
            root.Q<VisualElement>("Grid").contentContainer.Add(buildingTemplate); // グリッドに要素追加
            buildingTemplate.RegisterCallback<ClickEvent, BuildingData>(Clicked, building); // クリックイベントの登録
        }
    }

    private void Clicked(ClickEvent evt, BuildingData data)
    {
        Debug.Log("Click: " + data.buildingName);
        PlayerStatusHolder.I.NowSelectConstructBuildingID = data.buildingID; // PlayerStatusHolderにIDを渡す
    }
}

Raycastによる建物配置場所の決定

Entity情報の取得

通常の画面クリックからのRaycastではECSのEntity情報は取得できません。
そのため、ECS用物理シミュレーションパッケージのPhysicsを使います。
実装方法は別記事にまとめたのでこちらをご覧ください。

donabenabe.hatenablog.com

UI上ではRaycastをブロック

このRaycastはUI上も構わず貫通してRayを飛ばすので対策が必要になってきます。
これも別記事に書いたので、そちらを参照してください。

donabenabe.hatenablog.com

Entityの生成

さてここまででようやく建物選択と配置のためのRaycastが実装できたので、最後は建物のEntityを生成する必要があります。

PrefabのBake

建物のPrefabを作ったのは良いのですが、これはGameObjectなのでそのままECSでは使えません。

そのためBakeを行う必要があるのですが、ランタイムで生成時にBakeは行えません。
(自分はその手法を発見できなかったのでご存じの方いらっしゃいましたらご教授いただきたいです)

建物の種類すべてにBakeを行うために、適当なMonoBehaviorクラスにListなどで全Prefabを保持させて、再生時に一括Bakeさせました。

普通にGameObjectのListからGetEntityでEntityをAddしてやればいいだけかと思いきや
ArgumentException: srcEntity is not a valid entity
InvalidOperationException: Baking error: Attempt to add duplicate component
のようなエラーが出てしまい、苦労しましたが、以下の記事を参考にさせていただき、書き直したところなんとか動きました。

qiita.com

[System.Serializable]
public struct PrefabEntityComponent : IComponentData // 中身なし → Tag的扱い
{
}

public struct PrefabElement : IBufferElementData
{
    public Entity prefabEntity;
}
public class PrefabBaker : MonoBehaviour
{
    [SerializeField]
    private List<GameObject> prefabs;

    class Baker : Baker<PrefabBaker>
    {
        public override void Bake(PrefabBaker authoring)
        {
            var entity = GetEntity(TransformUsageFlags.Dynamic);
            var sample = new PrefabEntityComponent();
            var buffer = AddBuffer<PrefabElement>(entity);
            foreach (var prefab in authoring.prefabs)
            {
                buffer.Add(new PrefabElement
                {
                    prefabEntity = GetEntity(prefab, TransformUsageFlags.Dynamic)
                });
            }
            AddComponent(entity, sample);
        }
    }
}

画像のようにインスペクターからBakeしたいPrefabを指定してあげることで、そのPrefab群を自動的にBakeするようになりました。

Entityの生成

あとは以下のようなコードをクリック時に実行してあげればクリックした場所に生成されます。

if (physics.CastRay(input, out var hit))
{
    var name = this.EntityManager.GetName(hit.Entity);

    if (name == "Plane") // ここ名前でやってるのよくないので変えます
    {
        foreach (var buffer in SystemAPI.Query<DynamicBuffer<PrefabElement>>().WithAll<PrefabEntityComponent>())
        {
            for (int i = 0; i < buffer.Length; i++)
            {
                var entity = buffer[i].prefabEntity;
                if (PlayerStatusHolder.I.NowSelectConstructBuildingID == BuildingLookup[entity].BuildingID) 
                {
                    var buildingTransform = SystemAPI.GetComponentRW<LocalTransform>(entity);
                    buildingTransform.ValueRW.Position = new float3(hit.Position.x, hit.Position.y + buildingTransform.ValueRW.Scale/2, hit.Position.z); // 生成場所の決定
                    EntityManager.Instantiate(entity); // Entityの生成
                }
            }
        }
    }
}

完成

まとめ

MonoBehaviorなら簡単なこともかなり脳みそ使いますね…。
ただ、今回の実装でだいぶECSの気持ちが分かってきました。

参考

qiita.com

wgn-obs.shop-pro.jp

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

qiita.com

www.f-sp.com

www.f-sp.com