土鍋で雑多煮

土鍋で雑多煮

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

【Unity ECS】ECSのEntityを画面クリックで選択できるようにする

はじめに

どうも、土鍋です。

Unity ECS (DOTS)では従来のようにカメラからRayを飛ばして、オブジェクトの取得をすることはできません。というのもMonoBehaviourであるカメラからのRaycastとECSで生成されたオブジェクトや移動したオブジェクトは直接参照することができないのです。

そこで今回は、Playerの画面クリック情報を座標データとしてECSのSystemに渡して、ECSのRaycastをしてあげることで解決しました。

実装していく

画面クリック情報を取得

using R3;
using UnityEngine;

public class PlayerClick : MonoBehaviour
{
    public Camera camera;
    private RaycastHit hit;

    public Observable<(Vector3, Vector3)> OnClick => clickSubject;
    private Subject<(Vector3,Vector3)> clickSubject = new Subject<(Vector3, Vector3)>();

    void Update () {
        if (Input.GetMouseButtonDown(0))
        {
            var ray = camera.ScreenPointToRay(Input.mousePosition);
            
            clickSubject.OnNext((ray.origin,ray.direction));
        }
    }
}

このコードでは画面クリック時にクリックした場所のカメラ座標と方向情報を3次元座標に変換して、値を渡しています。

※R3を使っていますので、R3を導入していない場合はEventやUniRxのもので代用してください。

ECSに渡す

using R3;
using Unity.Entities;
using UnityEngine;

public class WorldManager : MonoBehaviour
{
    [SerializeField]
    private PlayerClick _playerClick;
    private ECSPlayerInput ecsPlayerInput;
    
    void Start()
    {
        var world = World.DefaultGameObjectInjectionWorld;
        ecsPlayerInput = world.GetExistingSystemManaged<ECSPlayerInput>();

        _playerClick.OnClick.Subscribe(click =>
        {
            ecsPlayerInput.ClickRaycast(click.Item1,click.Item2);
        }).AddTo(this);
    }
}

World

WorldとはECSのあらゆる情報を保有するものです。

デフォルトで1つ目のWorldは作成され、World.DefaultGameObjectInjectionWorldで取得することが可能です。

※2つ目以降のWorldもMonoBehaviourから作成できます。

world.GetExistingSystemManagedでSystemBase継承クラスを取得できます。

クリックイベントの購読

PlayerClickのOnClickを購読しています。
画面クリックが発生すると次のECSPlayerInputにクリック情報を送ってRaycast用のメソッドを実行しています。

ECS PhysicsによるRaycast

using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Physics.Systems;
using UnityEngine;

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(PhysicsSystemGroup))]
[BurstCompile]
public partial class ECSPlayerInput : SystemBase
{
    RaycastInput input;
    PhysicsWorldSingleton physics;
    
    protected override void OnCreate()
    {
        var filter = new CollisionFilter()
        {
            BelongsTo = ~0u,
            CollidesWith =  ~0u
        };

        input = new RaycastInput
        {
            Start = new float3(0, 0, 0),
            Filter = filter,
            End = new float3(0, 0, 0)
        };
    }

    protected override void OnUpdate()
    {
    }

    public void ClickRaycast(Vector3 inputOrigin, Vector3 inputDirection)
    {
        physics = SystemAPI.GetSingleton<PhysicsWorldSingleton>();

        input.Start = inputOrigin;
        
        var distance = 100;
        var goal = inputDirection * distance;
        input.End = goal + inputOrigin;
        
        if (physics.CastRay(input, out var hit))
        {
            var name = this.EntityManager.GetName(hit.Entity);
            Debug.Log(name);
        }
    }
}

MonoBehaviourから実行する必要があるのでSystemBase継承クラスで作りました。

RaycastInput

Raycastのスタート地点とゴール地点を設定します。
通常のRaycastと違い、スタートと方向ではなくゴール地点を指定してあげる必要があります。

CollisionFilterはUInt32型で自分自身のレイヤーや衝突対象のレイヤーを指定することができます。 今回は全てに衝突する「~0u」を指定しています。

physics.CastRay

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

inputの条件でRaycastして衝突したオブジェクトがあったらtrueを返します。

SystemBase継承クラスなのでthis.EntityManagerを使用してヒットしたEntityの名前を取得し、Debug.Logで出力しています。

完成

参考

qiita.com

qiita.com

qiita.com

qiita.com

docs.unity3d.com

https://discussions.unity.com/t/safely-using-entitymanager-in-systembase/805590

https://discussions.unity.com/t/safely-using-entitymanager-in-systembase/805590

【Unity6・URP】Unity6&AR FoundationでAR開発を始める

はじめに

どうも、土鍋です。

開発でAR Foundationを使うことになったのですが、Unity6で若干UIや設定が変わったのでメモ的に記事を書きました。

AR Foundationのセットアップ

AR Foundationのインポート

Package ManagerでUnity Registryで「AR Foundation」を検索し、インポート。

XR Plugin設定の変更

XR Plug-in Managementから自分のビルドしたいプラットフォームのProvider plug-inを選んでください。

※私はAndroidなのでAR Coreを選択。

Project Validationですべての項目をFixしてください。

URPの設定変更

このままビルドするとうまくARが機能しないので、AR用のRenderFeatureを追加する必要があります。

ProjectSettingsのQualityからどのRenderPipelineAssetを使用しているか確認してください。

※ビルドする対象の設定を確認してください。

使用しているRenderPipelineAssetのUniversal Render Dataを選択。

Add Render FeatureでAR Background Render Featureを追加。

これで問題なくARが機能します。

平面にキャラクターを配置するアプリ

シーンのセットアップ

ここからは実際にARアプリを作ってみます。

ヒエラルキーから

XR > AR Session
XR > XR Origin

この2つを追加してください。

平面検知

XR OriginにAR Plane Managerを追加。

検出結果を表示するには

XR > AR Default Plane

を出して、それをPrefab化し、
AR Plane ManagerのPlane Prefabにセット。

これで平面検知ができるようになった。

タップした場所にオブジェクト配置

XR OriginにAR Raycast Managerを追加。

タップした場所に配置して、スワイプで向きを調整できるようにします。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;

public class ARCharacterSpawn : MonoBehaviour
{
    [SerializeField]
    private ARRaycastManager arRaycastManager = null;
    
    [SerializeField]
    private Transform characterTransform;
    
    private Vector2 _beforePosition;
    private Vector2 _nowPosition;
    private float _rotateSpeed = 5f;
    
    private void Update()
    {
        if (Input.touchCount <= 0) return;
        
        var touch = Input.GetTouch(0);

        switch (touch.phase)
        {
            case TouchPhase.Began:
                _beforePosition = touch.position;
                break;
            case TouchPhase.Moved:
            {
                // スワイプでキャラクター回転
                _nowPosition = touch.position;

                if (_nowPosition.x - _beforePosition.x != 0)
                {
                    var horizontalAngle = (_nowPosition.x - _beforePosition.x) * _rotateSpeed * Time.deltaTime;

                    characterTransform.transform.Rotate(0, horizontalAngle, 0, Space.World);
                }
                break;
            }
            case TouchPhase.Ended:
            {
                // 指を離した場所に配置
                var hitResults = new List<ARRaycastHit>();
                if (arRaycastManager.Raycast(touch.position, hitResults))
                {
                    characterTransform.position = hitResults[0].pose.position;
                }
                break;
            }
        }
    }
}

完成

参考

docs.unity3d.com

bluebirdofoz.hatenablog.com

vend9520-lab.net

【Unity】UI Toolkitでランタイムに値を反映する

はじめに

どうも、土鍋です。

UI Toolkitでランタイム処理に向けてイベントの登録や値変更通知の方法を調べました。

コールバックを設定する

UI要素のマウス押下のイベントのコールバックを設定するには以下のようにできます。

VisualElement myElement = new VisualElement();

myElement.RegisterCallback<MouseDownEvent>()

値の変更を購読する

オブジェクトの位置が変わったらUIのText変更して、
Textを変更してもオブジェクトの位置が移動するようなコードを書きました。

RegisterValueChangedCallback()を使用することで変更された値を流し込むことができます。

public class UITest2 : MonoBehaviour
{
    [SerializeField] private GameObject moveObject;
    
    private Vector3Field Vector3Field;
    
    void Start()
    {
        Vector3Field = GetComponent<UIDocument>().rootVisualElement.Q<Vector3Field>("test-vector3");
        
        Vector3Field.RegisterValueChangedCallback(x => moveObject.transform.position = x.newValue);
    }

    void Update()
    {
        Vector3Field.value = moveObject.transform.position;
    }
}

参考

docs.unity.cn

light11.hatenadiary.com

【Unity】UI ToolkitでUIを構築してみる

はじめに

どうも、土鍋です。

今まで気になってたけど触ってなかったUI Toolkitを触ってみました。

Unity6になって以前より使いやすくなり、機能も今後更に増えるようです。

内容は以下の動画とほぼ同じですが、プラスアルファでやってみたことを書いてます。

www.youtube.com

UI Toolkitとは

UnityのUIは「Unity UI (uGUI)」「IMGUI」 、そして最新の「UI Toolkit」の3種類が提供されています。

uGUIはいわゆるCanvasを作ってUIを作っていく一番メジャーな方法で、
IMGUIはUnity Editor拡張を作るときに利用しているものです。

UI Toolkitはそこに加わった新しい手法で、Web開発の概念が組み込まれ、かなり近代的手法で合理的にUIを構築できるようになります。

UI Toolkitをやってみる

UI ToolkitでのUI構築に必要なものを作ります。

Create > UI Toolkit > UI Document

Create > UI Toolkit > Panel Settings Asset

この2つが最低限UI ToolkitでのUI構築に必要です。

適当に作った空のオブジェクトにUI Documentコンポーネントに作ったUI DocumentとPanel Settings Assetを設定。

UI Builder

UI DocumentをダブルクリックするとUI Builderが開かれます。

Libraryから置きたいUIコンポーネントを選択し、ヒエラルキーorViewportにドラッグ&ドロップしてUIをデザインしていく。

これだけで自動的にxmlが生成される。

親子構造やAline、Size、Spacingをいい感じに調整して画面を作る。

Position Mode

Relative: 同階層のVisualElementは自動的に並ぶ。
Absolute: 階層に関係なく配置できる。重ねて配置する必要がある場合はこれ。

UIに実際の処理を追加する

コンポーネントに名前をつけることでスクリプトから指定できるようになる。

ボタンを押したときの処理は以下のように書ける。

using UnityEngine;
using UnityEngine.UIElements;

public class UITest : MonoBehaviour
{
    void Start()
    {
        var root = GetComponent<UIDocument>().rootVisualElement;

        root.Q<Button>("button-test1").clicked += () => Debug.Log("test1");
        root.Q<Button>("button-test2").clicked += () => Debug.Log("test2");
        root.Q<Button>("button-test3").clicked += () => Debug.Log("test3");

        root.Q<Button>("send").clicked += () => Debug.Log(root.Q<TextField>("textfield-test1").value);
    }
}

StyleSheets

WebでいうCSSにあたるものとしてUI ToolkitではUSSというものがあります。 よく使うコンポーネントをクラスとして保存して変更を容易にするものとして使われています。

左上の+ボタンから新しいUSSを作ります。

適当な名前をつけてクラスを作ります。

作ったクラスをボタンにドラッグ&ドロップで適用できる。
ボタンのインスペクターのStyleSheetを見ると適用されているのが確認できる。

作ったクラスを選択し、値を変更するとそのクラスを持つコンポーネントも変更される。

アニメーション

左上の+からホバー時のアニメーションのためのセレクターを追加する。

TransformのScaleを少し大きくする。

Previewを押して、確認するとホバー時にボタンの大きさが変わっているのが確認できる。

アニメーションを付けるにはTransitionAnimationsから何秒でどのプロパティの変更をどんなEasingでアニメーションするかを設定できる。

戻るときにもアニメーションを付けたいのでホバーではない方のセレクターを選択し、同様にTransitionAnimationsを設定してあげる。

今の設定を保存する

data binding

Listの要素のためのUIを作る。

それぞれの要素に紐付いたScriptableObjectを作る。

using UnityEngine;

[CreateAssetMenu(fileName = "MovieCard", menuName = "Scriptable Objects/MovieCard")]
public class MovieCard : ScriptableObject
{
    public Texture2D thumbnail;
    public string movieTitle;
    public string channelName;
}

ScriptableObjectと紐づける

一番上の階層のVisualElementを選択し、Data SouceにScriptableObjectをセットしてあげる。

BindしたいVisualElementの項目を選択し、3点ボタンor右クリックでメニューを開いてAdd Bindingを押す。

Data Source PathにScriptableObjectのプロパティを選択することができる。

正しく設定できると画像のようにScriptableObjectの内容に沿ったものが反映される。

別のScriptableObjectを設定すれば内容も変わる。

ListView

ListViewを追加したら、
ItemTemplateに先ほど作ったUI Documentを設定する。
BindingSourceSelectionModeをAuto Assignにする。

リストに追加するデータを指定するためのScriptableObjectを作る。

using UnityEngine;

[CreateAssetMenu(fileName = "MovieList", menuName = "Scriptable Objects/MovieList")]
public class MovieList : ScriptableObject
{
    public MovieCard[] movieCards;
}

リストビューのitemsSourceに作ったScriptableObjectからデータを流し込む。

public class UITest : MonoBehaviour
{
    public MovieList movieList1;

    void Start()
    {
        var root = GetComponent<UIDocument>().rootVisualElement;

        root.Q<ListView>("movie-card-list1").itemsSource = movieList1.movieCards;
    }
}

色々調整して、試しにY◯utube風のUIを作ってみました。

※実はGridLayoutのテンプレートがないので、リストを2つ並べている。

まとめ

uGUIはUI作るには若干使いづらい節があったんですが、UI ToolkitはかなりUIを構築しやすくなりました。

特にWebなどで使われる概念を取り入れたことでFigma等との連携が取りやすくなったのがとても嬉しい点かなと思います。(UnityだとFigma通りに作るのが一苦労だった。)

ですが、uGUIではできたShaderの設定や複雑なアニメーションの設定、world spaceでの利用ができないので、これらを使う必要がある場合はまだuGUIを使う方が良いと思います。

参考

youtu.be

youtu.be

youtu.be

UnityECSで都市開発シミュレーションゲームを作る【その3】~経済を回す~

はじめに

どうも、土鍋です。

都市開発シミュレーションゲームを作るシリーズ三回目は住民がお金を稼いでそのお金を使うという流れを生み出します。

経済を回す

これを知ったので、コードを書き換えました。

donabenabe.hatenablog.com

住民がものを買う

青が店、緑が住民の家。

住民がお金を稼ぐ

黄色い建物はOfficeに設定した建物。

黄色い建物でお金を稼いで、青い建物でお金を使っている。

現状、市民に全部のコードを書いてるのでどうにかしたいなー

System間の参照の仕方とか、建物の種類ごとの処理の切り分けの部分がECSのにおいてよく分からない。

コードは長いので下に

void Execute(ref CitizenBase citizen, ref LocalTransform transform) // Queryの代わり。クエリ条件から合致するEntityを探して実行
{
    float minDistanceSq = float.MaxValue;

    citizen.IncreaseRandomDesire();

    if (!citizen.isNowMove)
    {
        if (IsAlreadyArrived(transform.Position,citizen.home))
        {
            if (citizen.appetite<500 || citizen.pocketMoney<500)
            {
                foreach (var buildingEntity in BuildingEntities)
                {
                    if (BuildingLookup[buildingEntity].buildingType == BuildingType.Office)
                    {
                        float distanceSq = math.distancesq(BuildingPosLookup[buildingEntity].Position, transform.Position);

                        if (distanceSq < minDistanceSq)
                        {
                            minDistanceSq = distanceSq;
                            closestBuildingEntity = buildingEntity;
                            citizen.destinationEntity = closestBuildingEntity;
                        }
                    }
                }
                citizen.destination = BuildingPosLookup[closestBuildingEntity].Position;
            }
            else
            {
                foreach (var buildingEntity in BuildingEntities)
                {
                    if (BuildingLookup[buildingEntity].buildingType == BuildingType.Food)
                    {
                        float distanceSq = math.distancesq(BuildingPosLookup[buildingEntity].Position, transform.Position);

                        if (distanceSq < minDistanceSq)
                        {
                            minDistanceSq = distanceSq;
                            closestBuildingEntity = buildingEntity;
                            citizen.destinationEntity = closestBuildingEntity;
                        }
                    }
                }

                citizen.destination = BuildingPosLookup[closestBuildingEntity].Position;
            }
        }
        else if (IsAlreadyArrived(transform.Position,citizen.destination))
        {
            if (citizen.destinationEntity==Entity.Null)
            {
                citizen.destination = citizen.home;
            }
            else
            {
                if (BuildingLookup[citizen.destinationEntity].buildingType == BuildingType.Food)
                {
                    citizen.destination = citizen.home;
                    citizen.appetite = 0;
                    citizen.pocketMoney -= 500;
                }
                else if (BuildingLookup[citizen.destinationEntity].buildingType == BuildingType.Office)
                {
                    citizen.destination = citizen.home;
                    citizen.pocketMoney += 500;
                }
            }
        }
    }
    
    Move(ref citizen, ref transform);
}

【UnityECS】EntityがComponentを持つか取得できるComponentLookup

どうも、土鍋です。

ECSでEntityは取得できてもコンポーネントにどうやったらアクセスできるんだ?
という問題に直面した際にComponentLookupが使えます。
自分はこれを知らなくて苦労しました。

まずはコードをご覧ください。

public partial struct CitizenMoveSystem : ISystem
{
    private EntityQuery _buildingQuery;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        _buildingQuery = state.GetEntityQuery(typeof(LocalTransform), typeof(BuildingBase));
    }
    
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // Jobの発行処理
        var job = new CitizenMoveUpdateJob()
        {
            Elapsed = (float)SystemAPI.Time.ElapsedTime,
            BuildingEntities = _buildingQuery.ToEntityArray(Allocator.TempJob),
            BuildingLookup = SystemAPI.GetComponentLookup<BuildingBase>(),
            BuildingPosLookup = SystemAPI.GetComponentLookup<LocalToWorld>()
        }; // 初期化
        job.ScheduleParallel(); // Jobの予約
    }
}
partial struct CitizenMoveUpdateJob : IJobEntity
{

~一部省略~

public NativeArray<Entity> BuildingEntities;
[ReadOnly] public ComponentLookup<BuildingBase> BuildingLookup;
[ReadOnly] public ComponentLookup<LocalToWorld> BuildingPosLookup;

void Execute(ref CitizenBase citizen, ref LocalTransform transform)
{

~一部省略~

foreach (var buildingEntity in BuildingEntities)
{
    if (BuildingLookup[buildingEntity].buildingType == BuildingType.Food)
    {
        float distanceSq = math.distancesq(BuildingPosLookup[buildingEntity].Position, transform.Position);

        if (distanceSq < minDistanceSq)
        {
            minDistanceSq = distanceSq;
            closestBuildingPosition = BuildingPosLookup[buildingEntity].Position;
        }
    }
}

~一部省略~

このコードではCitizenがBuildingEntitiyを全取得し、buildingTypeがFoodのものを選択し、一番近いものに訪問するということをやっています。

ComponentLookupについて

使い方

BuildingLookup[buildingEntity]

のようにEntityをIDとして配列の添え字のように記述することで、そのEntityのコンポーネントを取得することができます。

また、

BuildingLookup.HasComponent(buildingEntity)

のようにそのコンポーネントを持つかどうかのboolの取得もこのようにできます。

取得方法

SystemAPI.GetComponentLookup<BuildingBase>()

SystemAPI.GetComponentLookup<>()を使用することでほしいコンポーネントのComponentLookupを取得できます。

ちなみにEntityの取得はEntityQueryからToEntityArrayでNativeArrayを取得しています。

参考

wgn-obs.shop-pro.jp

qiita.com

土鍋的ハッカソンチーム開発ルーティン【Unity】

はじめに

どうも、土鍋です。

私は現在までいくつかのハッカソンに出場し、賞を頂いたりもしました。
つい先日もHackUに参加し、最優秀賞をいただくことができました。

そこで、今までのチーム開発経験をもとにいつもやっているルーティン的なことをまとめてみようと思い、この記事を書きました。

前提として、多くの人の影響を受けて現在のスタイルになったので、Unityでの開発経験がたくさんある人には当たり前のことが多いかもしれません。

ですのでこの記事のターゲットは
今後ハッカソンでUnityを使いたい方々やUnityでのチーム開発経験が少ない方々です。

また、チームリーダーがやるべきことも随所にあるので初めてエンジニアチームリーダーになるという人にも参考になると思います。

注意点として、Gitの使い方などの話は省きます。

チーム構成

まず初めに考えたいのはチームメンバーの開発経験です。
当たり前ですが、全員がUnity開発経験が豊富な場合は安心して開発やコミュニケーションを行うことができます。

が、初心者・チーム開発経験が少ないメンバーがいる場合は、経験のある人がある程度気を使ってあげる必要があります。

ドキュメント化できるところはドキュメント化し、慣れた者同士なら伝わるコミュニケーションやコーディングの仕方は避け、誰でも理解できる形であると良いと思います。 そういった心理的安全性の高いチームであれば、初心者の人が開発を嫌いにならずに、今後も楽しんでくれるようになると思います。

もちろん誰もが最初は初心者です。初心者は存分に開発経験ある人に甘えちゃいましょう。

プロジェクト構成

ここでは主にハッカソン期間が始まったら真っ先にやるであろうリポジトリやUnityプロジェクトの作成周りに関して話します。

フォルダ階層

自分はチームの開発経験と開発期間を踏まえて二通りを使い分けています。

開発経験がそこそこある or 開発期間が長い

Assets直下に自分たちのProject名のフォルダを作り、その下にScriptsやScenesといった各種要素フォルダを作る形式です。

これは次に紹介する方法と比べると長期運用向けです。
必要なものがどこにあるか明確になり、各スクリプトやPrefabの責任の範囲も分かりやすくなります。namespaceと一致するようフォルダを作ることでさらにそれを向上できます。

ただそれなりに気を使う必要があるので、開発経験がそこそこある場合か、開発期間が二ヶ月以上あるような場合のときにこのようにするべきかなーと思います。

開発経験があまりない or 開発経験が短い

Assets直下に自分たちのProject名のフォルダを作るとこまでは同じですが、その下にメンバーそれぞれのフォルダを作ります。これは一番の目的はコンフリクトを避けるためです。

開発経験があまりない場合、やはりスクリプトやシーン、3Dモデルなどががちゃがちゃになってしまうことがあります。もしフォルダを共有して使っていると余計なファイルまで変更してコンフリクトしてしまったり、ほしいものがどこにあるか見つけにくくなってしまいます。

そのため、メンバーそれぞれが好き勝手に使っていいフォルダを作ることで、開発初心者でもコンフリクトなどのことを意識せずに自由に気持ちよく開発することができます。

余計なことを考えすぎて迷惑かけたらどうしようとなったしまうと開発が楽しいということを経験できずに終わってしまいます。

これとは別で開発経験に関わらず、開発期間が短い場合も名前ごとにすると良いと思います。
これは短い期間でガッと開発するとなると他の人の作業がマージされるのを待っている時間はありません。そのため、それぞれがそれぞれのタスクを各自の責任下で開発を進め、統合が必要になったときに統合するという形が、開発者体験的に自分はいいなと思っています。

Githubリポジトリ作成

Unityは自動生成のものが多いからGit管理むずくないですか?という話をいただきますが、.gitignoreをちゃんと書けば問題なく利用できます。

Gitクライアント

Unityで開発する場合はGitクライアントアプリを使用したほうが良いと思います。(もちろんRider, VisualStudio, VSCodeのGit機能でも良い)
と、いいますのもUnityは自動的に変更されるファイルが多すぎて、CLIで管理するのが、かなり手間です。もちろん細かな状況に対応するときはCLIを使用しますが、基本的にはGUIで自動変更された差分をパッパと確認していったほうがいいです。

Github desktopは公式のものなのでWebから直接クライアントアプリに飛ぶことができます。

github.com

SourceTreeやForkではブランチをきれいに図示してくれるので俯瞰的にプロジェクトの状況を確認しやすいです。

www.sourcetreeapp.com

git-fork.com

ちなみに自分はFork派です。(SourceTreeはアカウント周りでめんどかった記憶)

ローカル

Unityのプロジェクトを作成したら、プロジェクトの最上位階層で「git init」を行ってGitを使用できるようにしてください。
この時点では数万ファイルの変更が生まれてしまいますが、.gitignoreを適用することで60ファイル程度に減ります。

github.com

リモート

Githubのサイトから新規でリポジトリを作り、リモートリポジトリとして設定し、ローカルのUnityプロジェクトをPushします。

Unityは外部アセットを使用するケースが多いので、むやみやたらにpublicリポジトリにすることができません。 そのため、publicにする可能性がある場合はExternalAssetsフォルダを作りその中に外部アセットを格納しておくことで、それを除いて公開することが容易になります。

プロジェクトマネジメント

これに関してはプロジェクトの規模と期間よると思いますが、今回はハッカソンなどの短期間向けの話に特化します。

タスク管理・スケジュール管理

これに関してはUnity固有の話はそんなにないですね。ここらへんは好みの問題な気がします。

私はNotionかGithubのProjectsを使ってました。

未着手、進行中、完了の3つのステータスによるカンバンや、それらをタイムライン形式で見れるようにしたり、Viewを切り替えられるタスク管理ツールを使うと便利です。

Gitに関して

Github Flowで進めることが一番多いと思います。リリースや運用を考える場合はGit Flowが良いと思います。

qiita.com

Git経験が少ない人がいる場合はここらへんのGitを使った作業の流れをドキュメント化しておくと良いと思います。

コーディング・設計関連

Scene

シーンは一番コンフリクトが懸念されるものです。ゲームで実際に使用するシーンはチームメンバー全員触りたくなるでしょうが、なるべく避けなければいけません。

ですので、それぞれのタスク用のシーンを作るのがベストです。そうすれば他人と同じシーンを触ることはめったに無いです。

どうしてもメインのビルドするシーンを触る場合は誰が触っているか明確化しておくと、コンフリクトが発生しません。
方法的には普段やり取りするチャットで宣言したり、スプシなどで管理するなどです。

Prefab

シーンの話にも繋がりますが、Prefab化するゲームオブジェクトは
複数シーンにまたがって使用することが想定されるものインスタンス化(スクリプトからオブジェクトを生成すること)するものが一般的です。

ですので、タスク用シーンと実際にビルドシーンにまたがる時点で多くのものがPrefab化した方が自分はいいかなーと思っています。 Prefab化しないものはゲームのワールド環境オブジェクトやそれに関連するロジックのスクリプト、ライティングなど、そのシーンでしか存在しないものくらいです。

SceneとPrefabのコミット

SceneやPrefabをコミットする場合は差分を必ず確認した方が良いと思います。

Prefabに対してScene内で変更を行うと以下のように青くなります。これはそのシーンだけではそのような変更がされているということなのでPrefabには直接影響しません。そのためSceneファイルに変更が生じます。
ですが、その変更がそのPrefabを使うときはいつもそのようにしたいという場合は、ApplyしてあげるとPrefabにその変更が適用され、Sceneには変更は発生しません。

シーンとPrefabのコンフリクトはUnityYAMLMergeを使用することで解消できるケースがありますが、意図しない統合のされ方をされてしまう可能性があるので、なるべくならコンフリクトしないように開発するのがベストです。

docs.unity3d.com

(もちろんUnityの開発になれるまでは深いことは考えずにコミットしちゃっていいです。)

(コンフリクトはチームのUnity得意な人にぶん投げちゃってください)

Game Manager

Unityで開発しているとまずぶち当たるのがスクリプト間の参照かなと思います。
いちばん簡単なのがインスペクターからその参照したいスクリプトを設定するという方法でしょう。(むしろこれがあるからUnityはプログラミング初心者がとっつきやすい)

ただ、ゲームの進行状況、今どのワールドにいるのか、ユーザーのステータスはどのような状態かなどのゲームの始まりから最後まで存在し続けるものに関してはSingletonパターンを適用したGame Managerクラスなどを用意しておくと、かなり開発が楽になります。

public static GameManager instance;

private void Awake()
{
    if (instance == null)
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
    } 
    else 
    {
        Destroy(gameObject);
    }
}

参照するときはGameManager.instance.〇〇 といった感じ。 DontDestroyOnLoadのSingletonクラスなので、どのシーンに移動しても存在し続け、複数存在することもありません。

また、ゲーム進行状況ステータスなどは変更が発生した瞬間に処理を発火したいケースが多いと思います。

その場合、EventかObserverパターンを使うと思うので一応書き方を書いておきます。(今回の主題からは外れるけども)

Event

    public GameStatus NowGameStatus
    {
        get => _nowGameStatus;
        set
        {
            OnGameStatusChanged?.Invoke(value);
            _nowGameStatus = value;
            Debug.Log("now GameStatus: " + value);
        }
    }
    private GameStatus _nowGameStatus;

    public event Action<GameStatus> OnGameStatusChanged;

OnGameStatusChangedに実行したいメソッドを追加。

    private void Start()
    {
        GameManager.instance.OnGameStatusChanged += 実行したいメソッド;
    }

    private void OnDisable()
    {
        GameManager.instance.OnGameStatusChanged -= 実行したいメソッド;
    }

Observerパターン

    public GameStatus NowGameStatus
    {
        get => _nowGameStatus;
        set
        {
            statusSubject .OnNext(value);
            _nowGameStatus = value;
            Debug.Log("now GameStatus: " + value);
        }
    }
    private GameStatus _nowGameStatus;

    public IObservable<GameStatus> OnGameStatusChanged => statusSubject;
    private Subject<GameStatus> statusSubject = new Subject<GameStatus>();

OnGameStatusChangedを購読(UniRx使用)

GameManager.instance.OnGameStatusChanged.Subscribe(x =>
{
    実行したいこと
}).AddTo(this);

ReactivePropertyを使用すれば、もっと楽にかけますが、個人的には処理を切り分けたいので上記のような実装をしています。

設計に関して

ハッカソンにおいては正直がっちり設計固めている時間はないので、ざっくり脳内でできるレベルの設計しかしません。 ただ、循環参照やSOLID原則に配慮したり、SingletonやObserverパターンなどそこまで重くないデザインパターンは適度に使っています。

ここらへんもチーム構成によって変わります。
初心者がいるときに変にMVPでUI設計しちゃったり、DI導入しちゃったりすると初心者の人が全くもって理解できずにあまり開発に関われなくなってしまいます。

なので、そういうがっちりとした設計はせずに、「GameManagerのこれを参照したら勝手にステータスが送られてくるから使ってみてね」的な伝え方をして、ある程度のコードの循環参照をしちゃわないようにこちらで配慮するなどといった感じです。

まとめ

と、ここまで語ってきましたが、ハッカソンにおいて重要なのは"とりあえず動けばいい"なので後半の設計の話などは正直気にしなくていい気はします。
ただ、自分が制御できる範囲は制御することで、バグの原因究明が容易だったり、自分のレビューの手間が減ったり、コンフリクトが起きなかったり、etc... メリットはたくさんあると思います。

ハッカソンは別に怖くないので、バンバン出て、作品を作るきっかけにしてください!

この記事がどなたかの参考になったら幸いです!