20200206のUnityに関する記事は9件です。

UnityでiOS実機とmacエディタでBLEを使う

UnityでiOS実機とmacエディタでBLEを使う

UnityでiOS実機とmacエディタで、CoreBluetoothを使って、Bluetooth Low Energyのデバイスと通信するnative pluginを作成しました。
Unity Packageを配布しています。
リポジトリはこちら→https://github.com/fuziki/UnityCoreBluetooth

目的

UnityにはBluetoothで通信する機能がないです。
iOSでdaydreamのコントローラを使いたかったので、プラグインを自作しました。
Unity Editorにも対応しており、editorで実際に接続して実装して、実機は動作確認だけという開発が可能です。

導入

組み込む

Daydreamのコントローラの生データを取得します

Daydreamコントローラについて

下記の条件のcharacteristicに接続すると、コントローラの生データを取得可能です。
BLEデバイスは複数のserviceを持っており、serviceは複数のcharacteristicを持っています。characteristicごとに決められた機能が提供されており、今回はcharacteristic uuidは同名のidが複数存在するので、notifyのusageのcharacteristicを使って生データを受け取ります。

| 項目 | 接続する端末の条件 |
|--|--|
| デバイス名 | Daydream controller |
| service uuid | FE55 |
| characteristic usage | notify |

UnityCoreBluetoothを使う

1. シングルトンインスタンスの生成

UnityCoreBluetoothはシングルトンで使用します。
bluetooth機能がpowerOnになったら、BLEデバイスのスキャンを開始させます。
コールバックの設定が全て終了したら、StartCoreBluetoothを呼び出して開始します。

        UnityCoreBluetooth.CreateSharedInstance();
        UnityCoreBluetooth.Shared.OnUpdateState((string state) =>
        {
            Debug.Log("state: " + state);
            if (state != "poweredOn") return;
            UnityCoreBluetooth.Shared.StartScan();
        });
        //~~中略~~
        UnityCoreBluetooth.Shared.StartCoreBluetooth();

2. 接続したいデバイス名のデバイスを見つけたら、接続する

        UnityCoreBluetooth.Shared.OnDiscoverPeripheral((UnityCBPeripheral peripheral) =>
        {
            if (peripheral.name != "")
                Debug.Log("discover peripheral name: " + peripheral.name);
            if (peripheral.name != "Daydream controller") return;

            UnityCoreBluetooth.Shared.StopScan();
            UnityCoreBluetooth.Shared.Connect(peripheral);
        });

3. デバイスに接続したら、サービスを探す。

        UnityCoreBluetooth.Shared.OnConnectPeripheral((UnityCBPeripheral peripheral) =>
        {
            Debug.Log("connected peripheral name: " + peripheral.name);
            peripheral.discoverServices();
        });

4. 対象のuuidのサービスが見つかったら、characteristicを探す。

        UnityCoreBluetooth.Shared.OnDiscoverService((UnityCBService service) =>
        {
            Debug.Log("discover service uuid: " + service.uuid);
            if (service.uuid != "FE55") return;
            service.discoverCharacteristics();
        });

5. usage がnotifyのcharacteristicが見つかったら、通知を有効にする

通知を有効にすることで、daydreamコントローラから連続して生データを受け取ることが可能になります。

        UnityCoreBluetooth.Shared.OnDiscoverCharacteristic((UnityCBCharacteristic characteristic) =>
        {
            string uuid = characteristic.uuid;
            string usage = characteristic.propertis[0];
            Debug.Log("discover characteristic uuid: " + uuid + ", usage: " + usage);
            if (usage != "notify") return;
            characteristic.setNotifyValue(true);
        });

6. characteristicから通知があったら、データを受け取る

※ リアルタイムで受け取れるのですが、メインスレッド保証ではないです。

        UnityCoreBluetooth.Shared.OnUpdateValue((UnityCBCharacteristic characteristic, byte[] data) =>
        {
            this.value = data;
            this.flag = true;
        });

7. シングルトンインスタンスの破棄

    void OnDestroy()
    {
        UnityCoreBluetooth.ReleaseSharedInstance();
    }

終わりに

unityエディタとiOS実機で動くBLEのプラグインを作成しました。
動作確認のたびにiOSの実機を起動するのは手間がかかるので、エディタで開発可能になることで開発が数倍楽になった気がします。
このプラグインの構造などはこちらの記事に書いてあります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自作パッケージで自作パッケージを使用したい【UnityPackageManager】

package.jsonのdependenciesに自作パッケージは適用できない

まず前提条件として、自作パッケージをパッケージするにあたって必要となるpackage.jsonだが、このjson内のdependenciesではなぜか、自作パッケージのgitリポジトリURLで依存関係を適用することができない。(Unity起動時に依存解決する際にエラーが発生する)したがって、自作パッケージで自作パッケージを適用したい場合は、別の方法を取る必要がある。

適用方法

1. AssemblyDefineに適用するパッケージのguid参照を登録する

パッケージするにあたって、プロジェクトにAssemblyDefineファイルを配置する必要があるが、自作パッケージ通しで別のAssemblyDefineを使用するため、片方が依存関係を持つ場合、もう片方のguidの参照を登録する必要がある。

Screen Shot 2020-02-06 at 21.56.50.png

2. 自作パッケージを使用する先のPackage/manifest.jsonで依存関係のあるパッケージもdependenciesに登録する

例えば、unity-master-dataという自作パッケージでunity-excelという自作パッケージを使用していて、別のプロジェクトでunity-master-dataを使用したい場合、Package/manifest.jsonに両方を追加する必要がある。

{
  "dependencies": {
    "com.tani-shi.unity-excel": "https://github.com/tani-shi/unity-excel.git#1.0.0",
    "com.tani-shi.unity-master-data": "https://github.com/tani-shi/unity-master-data.git#1.0.0",
    ...
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SteamVR2.0環境でHMDのコントローラを用いた移動・回転の実装

SteamVR2.0 でのコントローラを用いた移動・回転方法

後輩たちへのメモ程度にSteamVR2.0 での移動・回転方法を記述

1.下記のスクリプトを[CameraRig]にアタッチ
2.InspectorのVR_cameraに[CameraRig]の子オブジェクトCameraを設定
3.ActionManifestで設定した項目を各SerializeFieldに設定
※ここではActionManifestの説明を省くので,詳しくはググってください
気が向けば追記するかも

Unityで実行すると,左手で移動,右手で回転ができる
移動・回転スピードは値を変更してください

VR_Move.cs
using UnityEngine;
using Valve.VR;

public class VR_Move : MonoBehaviour
{
    [SerializeField] SteamVR_Input_Sources HandRight;
    [SerializeField] SteamVR_Input_Sources HandLeft;
    [SerializeField] SteamVR_Action_Boolean up;
    [SerializeField] SteamVR_Action_Boolean down;
    [SerializeField] SteamVR_Action_Boolean right;
    [SerializeField] SteamVR_Action_Boolean left;

    [SerializeField] private float speed = 6.0f;
    [SerializeField] private int Rotspeed = 2;

    [SerializeField] private Camera VR_camera;

    private float First_axis_Y = 0;

    void Start ()
    {
        //Y軸の初期値を取得
        First_axis_Y = transform.localPosition.y;
    }


    void Update ()
    {
        //左手トラックパッドで移動
        if (up.GetState(HandLeft) == true)
        {
            transform.localPosition += VR_camera.transform.forward * speed * Time.deltaTime;
        }

        if (down.GetState(HandLeft) == true)
        {
            transform.localPosition -= VR_camera.transform.forward * speed * Time.deltaTime;
        }

        if (right.GetState(HandLeft) == true)
        {
            transform.localPosition += VR_camera.transform.right * speed * Time.deltaTime;
        }

        if (left.GetState(HandLeft) == true)
        {
            transform.localPosition -= VR_camera.transform.right * speed * Time.deltaTime;
        }

        //Y軸を初期位置に固定.空中に浮きたいor重力概念を追加する場合は以下をコメントアウトする.
        transform.localPosition = new Vector3(transform.localPosition.x, First_axis_Y, transform.localPosition.z);

        //右手トラックパッドで回転
        if (right.GetState(HandRight) == true)
        {
            transform.Rotate(0, Rotspeed, 0);
        }

        if (left.GetState(HandRight) == true)
        {
            transform.Rotate(0, -Rotspeed, 0);
        }

    }

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

入れ替え、ソート可能な配列「Reorderable-List」を使ってみる

はじめに

最近、順次実行のイベント作成していたときに気づいたのですが
配列で管理していると入れ替えが大変なんですよね。

なので今回は入れ替え可能な「ReorderableList」なるものを組み込むことにしました。
これで後々変更があってもラックラクですよ!

ダウンロード

まずはダウンロードです。
こちらに最新のパッケージがあります。
https://github.com/cfoulston/Unity-Reorderable-List

そして下記赤丸のダウンロードします。ダウンロード.png

無事ダウンロードできました。

プロジェクトにインストール

Packagesウィンドウを開く

まずPackagesウィンドウを開きます。
ウィンドウは画面上部のメニュー「Window」→「Package Manager」から開くことが出来ます。

package.jsonを読み込む

  1. ダウンロードしたzipファイルを解凍。

  2. そしてPackagesウィンドウの左上にある+マークを押して「Add package from disk...」を押す。

  3. 「Unity-Reorderable-List-master」フォルダにある「package.json」を追加します。

インストール.png

これでインストール完了です。
では早速スクリプトで使ってみましょう。

2020/02/06追記

差分を見るとこちらのようにローカルを指しているので
プロジェクトにオールインワンとして入れ込みたい場合はAsset直下にフォルダを入れてください。その際、「Package Manager」での追加は不要なので削除しておいてください。
depencies.png

利用方法

使い方はこんな感じです。
継承で専用リストを作成してそれを公開する感じです。

sampleList.cs
using System.Collections.Generic;

public class SampleList : MonoBehaviour
{
    // リストを定義
    [System.Serializable]
    public class ExampleChildList : Malee.ReorderableArray<EventData>{};

    // リスト
    [SerializeField]
    [Malee.Reorderable] ExampleChildList EventReorderableList;
}

 使用画面

プロパティを編集するとこんな感じになりました。
めちゃくちゃ便利です。

reorderablelist.gif

最後に

いかがでしたでしょうか?
これまでのリストは自分で削除したり追加したりかなり手間が掛かっていたのでこの機能は凄いありがたいですね。

リストを使っていて不便だな~と感じる方はぜひ組み込んでみてくださいね。
ではここまでお読みいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】自前のHDRPシーンでDXRによるリアルタイムレイトレーシングを活用した機能(反射・AO・GI etc...)を利用する

本記事では、Unityにおいて、自前のHDRPシーンでリアルタイムレイトレを活用してリアルタイムな反射やGIなどを反映させるための設定方法を解説します。

レイトレについて(Unity公式から引用)

レイトレーシングは、リアルなライトの挙動と物理マテリアルとの相互作用をシミュレートすることで、写実的なものから様式化されたものまで、あらゆるプロジェクトに本物のグローバルイルミネーションやその他のエフェクトを作成できます。
(Unity のリアルタイムレイトレーシング)

Unityのリアルタイムレイトレでできること

Unityでは、DXR(DirectX Raytracing)を利用することで、以下のような機能を利用することができます。

Ray Traced Ambient Occlusion

既存のSSAOを置き換える形で適用。画面外のデータも活用した正確な環境遮蔽を実現。

Ray Traced Global Illumination

ライトプローブなどで実現されていたリアルタイムGIを置き換える形で適用。正確。

Ray Traced Reflections

既存のSSRを置き換える形で適用。画面外のデータも活用した正確な反射表現を実現。

Recursive Rendering

屈折光線と反射光線を再帰的にキャストする(よく分からん)

Light Clusterの利用

ラスタライズの場合のClustered Renderingみたいなことをレイトレでもやるっぽい?
セルごとに影響するライトのリストを保持し、効率的なレンダリングを行う。

Path Tracing

影や反射、物体のシェーディングを確率的計算によるレイのシミュレーションで求めるアルゴリズム。映画などのプリレンダな現場で使われている。
現状だと計算量が多い・ノイズが多いのでリアルタイムで使うのはまだ厳しそう。

HDRPプロジェクトでレイトレの機能を有効化する方法

以下、本題

環境

以下の環境で動作を確認しています。

  • Unity 2019.3 以降
  • Windows 10 v1803 以降
  • DXR対応のグラボ(NVIDIA Volta (Titan X)・NVIDIA Turing (2060, 2070, 2080, and their TI variants))
  • HDRP v7.1.8 以降 (それ以前でもDXRの利用はできますが、設定方法が変わったりしています)

HDRPシーンを用意する

HDRPを使うシーンを用意します。今回はHDRPテンプレートを使って作成したデフォルトのシーンを使います。
image.png
自前でお好きなHDRPシーンをご用意ください。

DXRを有効にする

Unityでレイトレ機能を使うには、DXRを有効にする必要があります。

現在のUnity Editor上では、DXRを有効にするには、

  • グラフィクスAPIをDirectX 12に変更
  • 静的バッチングの無効化
  • 各アセットのレイトレーシング機能の有効化
  • Screen Space ShadowsとScreen Space Reflectionsの有効化

以上の手順を踏む必要がありますが、HD Render Pipeline Wizardを利用することで、これらの手順を簡単に実行することが出来ます。

HD Render Pipeline WizardはWindow > Render Pipeline > HD Render Pipeline Wizard からアクセスできます。

image.png

HD Render Pipeline WizardのConfigulation Checkingから、HDRP+DXRタブを選択すると、どの項目を設定すべきかの一覧が表示されます。
「Fix All」をクリックすることで、DXRの有効化に必要な設定の手順を自動で実行してくれます。(※エディタの再起動を要求されます)image.png

DXR有効化時に出てくるエラーについて

DXRを有効にすると、以下のエラーが発生します
d3d12: generating mipmaps for array textures is not yet supported.
これは、DX12とDXRがまだプレビュー機能なため、いくつかの機能が欠落しているため生じるエラーだそうです。レイトレ機能の利用上は問題ないので今回は無視します。

Volumeを作成してEffectのオーバーライドを行う

DXRを有効にしただけではレイトレの各種機能を有効にすることはできません。HDRPのVolumeで各種エフェクトをレイトレの機能でオーバーライドすることでレイトレの機能を利用することが出来ます。

image.png

生成されたオブジェクトの名前は今回はRayTracing Volumeとしておきました。

Volume Profileを作成・編集する

インスペクタのVolumeコンポーネント上の「New」ボタンを押して、新規にVolume Profileを作成します(既存のプロファイルを書き換えて設定する方法でも構いません)。

Volume Profileを作成出来たら、「Add Override」から、上書きしたいエフェクトを選択し、「Ray Tracing」という項目のチェックボックスをOnにします。

今回は以下のエフェクトを追加します。

  • Screen Space Reflection
  • Ambient Occlusion
  • Global Illumination

image.png
これで上記の表現をレイトレで行うことが出来ました。
image.png
GIのノイズが目立っているので、デノイズの設定をすると以下のようになります。
image.png

image.png
ノイズが目立たなくなりましたね。
他にもVolume Profileの各値を設定することで、レイトレの結果を調整することができます。

マテリアルにScreen Space Reflectionが適用されるようにする

レイトレ反射をさせたいオブジェクトのマテリアルは、Receive SSRのチェックボックスをONにすることで、レイトレによる反射を反映させることができます。
image.png
image.png
レイトレによって物理的に正確な反射を表現(もちろんリアルタイム)

Ray-traced shadows・Ray-traced contanct shadowsの利用

影をレイトレ機能を用いて表現するためには、ライトの設定を変更する必要があります。

LightコンポーネントのShadowsの項目から、 Shadow Map > Ray Traced Shadows をOnにすることでRay-traced shadowsを利用することが出来ます。また、Contact Shadows > Ray TracingをOnにすることで、Ray-traced contanct shadowsを利用することが出来ます。image.png
Ray-traced shadowsを適用した結果

おまけ

Path Tracingを試す

映画などで活用されているレンダリングアルゴリズムであるPath TracingをDXRを活用することでUnityでも利用することができるので、試してみました。

Path Tracingを有効にする

Path Tracingを有効にするには、先ほどのVolume profileに「Path Tracing」をAdd OverrideしてEnableをOnにして有効化します。

image.png

今回は、以下のようなコーネルボックスを使って通常のレンダリングとパストレーシングの場合を比較してみました。

image.png
通常のレンダリング(反射・AO・GI・影生成にDXRを使用)

image.png

Path Tracingのレンダリング結果

パストレーシングでは、反射やAOのような用途のみでレイトレを使うわけではなく、画面全体をレイの計算を使って描画するため、描画処理に非常に時間がかかります。フルHDで1フレームのレイトレを完了するのに50秒くらいかかりました。まだリアルタイムパストレーシングは難しそうですね:sweat:
画としては、GIや物質の光沢表現が非常にきれいになっています。一方で透過オブジェクトを透過処理できていないことが分かります。また、アルゴリズムの都合上、ノイズが多く、特に輪郭の正確さが失われてしまっています。

さいごに

HDRPではDXRを有効化することで簡単にレイトレを使った機能を活用できることが分かりました。
正確な反射やGIを表現したいという方は使ってみてはいかがでしょうか?

参考ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

爆速でオブジェクトを動かすスクリプト

standardAssetsを入れる暇がない場合、
こんな感じのスクリプトを書けば、「とりあえず前後に移動して、左右に旋回する」ことができる。

マイクラとかみたいに、ジャンプなどは、とりあえずstandardAssetsを入れるか、
キャラクターコントローラーを使ったスクリプトが望ましい。

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

public class Move_player: MonoBehaviour
{

    public float MoveSpeed;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.position += transform.forward * MoveSpeed;
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            transform.position += transform.forward * -1 * MoveSpeed;
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            transform.Rotate(0, 5, 0);
        }
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            transform.Rotate(0, -5, 0);
        }
    }
}

gitHubに置いたMove_playerのリンクはここから

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

描画負荷がかからない透明なuGUI Imageを作る【Unity】

はじめに

uGUIの操作を制限するため手前に透明なImageを重ねることがあると思いますが、そのままでは1枚分の描画負荷がかかってしまいます。その負荷を軽減する方法を紹介します。

方法

スクリーンショット 2020-02-06 10.05.10.png

Imageと同じGameObjectについているCanvasRendererのCull Transparent Meshをオンにしてください。

スクリーンショット 2020-02-06 10.33.17.png

SceneビューのOverdrawモードで確認すると、設定したImage部分の描画がされていないことが確認できます。

ダウンロード (1).png

StatsでもBatchesが増えていないことが確認できます。

ちなみにCanvasRendererのCull Transparent Meshで全ての描画がなくなるのは全ての画像領域が完全に透明な場合のみです。アルファ値が0.1でも残っていると描画されてしまう点に注意してください。

おまけ: 最初から描画しないスクリプトを自作する

上記の方法とほぼほぼ同じことができるスクリプトです。途中から意味ないと気がついたのですが、折角なので紹介します。raycastも当たるやつです。

スクリーンショット 2020-02-06 10.20.18.png
^先ほどと同様にいくつ置いても描画されない

スクリプト

Gist: NoRenderImage.cs

NoRenderImage.cs
using UnityEditor;

namespace UnityEngine.UI
{
    /// <summary>
    /// 描画しない透明Imageコンポーネント
    /// </summary>
    [AddComponentMenu("UI/NonRenderImage", 2004)]
    public class NoRenderImage : Graphic
    {
        protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); }

#if UNITY_EDITOR
        [UnityEditor.CustomEditor(typeof(NoRenderImage))]
        class NonRenderImageEditor : UnityEditor.Editor
        {
            public override void OnInspectorGUI() { }

            private static Vector2 s_ImageElementSize = new Vector2(100f, 100f);

            [MenuItem("GameObject/UI/NoRender Image", false, 2004)]
            public static void CreateNoRenderImage()
            {
                var parent = Selection.activeGameObject?.transform;
                if (parent == null || parent.GetComponentInParent<Canvas>() == null)
                {
                    var canvas = new GameObject("Canvas");
                    canvas.transform.SetParent(parent);
                    canvas.AddComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
                    canvas.AddComponent<CanvasScaler>();
                    canvas.AddComponent<GraphicRaycaster>();
                    parent = canvas.transform;
                }
                var go = new GameObject("NoRenderImage");
                var rectTransform = go.AddComponent<RectTransform>();
                rectTransform.SetParent(parent);
                rectTransform.sizeDelta = s_ImageElementSize;
                rectTransform.anchoredPosition = Vector2.zero;
                Selection.activeGameObject = go;
                go.AddComponent<NoRenderImage>();
            }
        }
#endif
    }
}

メインはこの部分だけです。メッシュ描画のコールバック関数であるOnPopulateMeshをoverrideして、頂点をクリアしています。これで描画だけがされないGraphicコンポーネントが実現できました。
参考: ボタンの当たり判定(タッチ範囲)だけ広げる【Unity】【uGUI】 - (:3[kanのメモ帳]

public class NoRenderImage : Graphic
{
    protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); }
}

他の部分は便利に使うための記述です。

スクリーンショット 2020-02-06 10.25.39.png
^[Add Component]メニューのUIカテゴリから追加できる

スクリーンショット 2020-02-06 10.26.43.png
^Createメニューからも生成できる

スクリーンショット 2020-02-06 10.28.07.png
^Canvas以下でない階層で生成すると、自動的にCanvasを作ってくれる

この辺りは別の記事でまとめておこうと思います。

余談

Textコンポーネントも描画するテキストがない場合に描画がされないので、それで良い説もありますね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

グローバルゲームジャムでクラス設計をやった話2020

はじめに

毎年恒例のグローバルゲームジャム。今回も参加してきたのでそのまとめを書きます。

グローバルゲームジャムとは

GGJとは全世界同時に行われるゲームジャムのことです。ようするに、世界規模のゲーム開発ハッカソンです。
プログラマ、デザイナ、プランナ、グラフィッカなどさまざまな役職の人をごちゃまぜに、3~8人程度のチームを組み、48時間でゲームを作ろうというイベントです。(前回のコピペ)(前回のコピペ)(前回のコピペ)

今回も「ヒューマンアカデミー秋葉原会場」に参加しました。

ゲームの概要

今年のテーマ

今年のテーマは「REPAIR」でした。シンプルにわかりやすいテーマでいいですね。
これをお題としてゲームを1つ作っていきます。

作ったゲーム

image.png

今年作ったゲームは「R.R.R. (Remove Reuse Repair)」というタイトルです。

ゲーム解説

  • 最大4人まで遊べるco-opゲーム
  • 真ん中におもちゃのパーツが流れてくる
  • パーツを回収し、レシピ表のおもちゃを作り直して出荷する
  • レシピのおもちゃのパーツがすべて揃っていたら点数アップ
  • どれだけ多くスコアを稼げるか

という内容のゲームです。
自分が作ると毎回4人対戦になるのはお決まりのパターンです。

このゲームを作ることになった経緯

  • パーツを組み立てて、変なオブジェクトが作られるのをみたい
  • オーバークックが名作なのでそれっぽい感じのシステムだと面白そう
  • 妖精のおもちゃ工場っていいんじゃない
  • パーツを組み立てて、レシピとおりのおもちゃを作って出荷させよう
  • 対戦もいいけど、協力も面白そう

といった話を2時間くらいした結果、この形になりました。

使ったもの

  • Unity 2019.3.0f6
  • UniRx
  • UniTask
  • Extenject

プロジェクトファイルの管理は「Unity Collaborate」を使いました。
Unity Cloud Build も裏で実行していましたが、結局使いませんでした。

ソースコード

メンバ

  • プログラマ 4人(日本人3人、外国人1人)
  • デザイナ 2人
  • プランナ 2人(1人デザイナと兼任)

の計7人でした。

実際の開発スケジュール

金曜日

  • チームビルディング
  • 企画会議
  • 要件定義

ここまでを金曜日の24時ごろまでに完了させました。
そのあと、いったん解散して帰れる人は帰って寝るように指示を出しました。

この隙に自分は一度帰宅し、深夜にクラス設計をします。

土曜日

家でクラス設計を行い、プリントアウトしてから寝ます。
だいたい寝たのは3時くらいでした。

そして朝からひたすら実装をしていきます。
いろいろ割愛しますが、22時くらいにはほとんどの機能は実装が終わっていました。
あとはデザイナーのリソースが上がってくるのを待つ、といったところで解散しました。

日曜日

日曜日はプログラマの手が若干空き気味でした。
上がってきたリソースをどんどん組み込みつつ、バグっぽいところを潰していく作業を繰り返していました。

全体を通して

作るゲームの規模も小さく、また非常に優秀なプランナさんがいたおかげでかなりスムーズに開発が進みました。
徹夜をする必要もなく、かなり余裕をもって開発することができ精神的な余裕も大きかったです。

クラス設計2020

では、今年もどのようなクラス設計をしたのかを解説します。

1.要件定義

1.jpg

まず金曜日の24時くらいまで何を作るかをFixします。
この時点で決まった仕様は次のとおりです。

  • 目的「パーツを集めてレシピのおもちゃを作って点数を稼ぐ」
  • ゲームシステム
    • 制限時間制(ゲーム中に変更不可)
    • プレイ人数は1-4人(1人でも遊べる)
    • スコアを多く稼ぐ
    • ベルトコンベアでパーツが運ばれてくる
    • 中央にトレイがあり、パーツを入れる
    • プレイヤーが「出荷ボタン」を押すと出荷される
    • 出荷が完了した時点で点数が加算される
  • プレイヤー
    • 移動
    • 掴んで運ぶ/離して置く
    • 出荷ボタンを押す
    • 敵からダメージを受けて一定時間動けなくなる
    • プレイヤーに嫌がらせをする
      • プレイヤーは敵をもつこともできる
    • ロボット掃除機
      • 地面に置かれたパーツを消去する
      • プレイヤーが触れると一定時間動けなくする
  • パーツ
    • 種類がいくつかある
    • それぞれに素点がある(マイナス値は無し)
    • 金のパーツや壊れたパーツなどがある(クオリティ)
    • パーツは地面に置いておくことができる
    • 出荷されると消える
  • オーダー
    • パーツの組み合わせ(レシピ)の表
    • オーダーを満たすとコンボボーナスがもらえる
    • パーツのクオリティでさらにボーナスが増える
  • 全体のフロー
    • メニューシーン
      • プレイ人数を選ぶ
    • バトルシーン
      • 結果表示はオーバーレイするだけ
    • メニュー → バトル → 結果表示 → メニュー で1サイクル

正直、毎年似たようなゲームを作っているのでそれと同じ感じになりました。
この時点でたぶん土曜日中にほぼ実装は終わるだろうなという感想でした。
(実際、土曜日中に実装が終わった)

仕様書

今回はプランナーの方がとても優秀な方で、かなり詳細なリソースリストを作ってくれました。
そのため設計や実装が非常にやりやすくとても助かりました。

image.png
image.png
image.png
image.png

(作ってくれたリスト。めっちゃ助かった。)

追記:上記の資料のすべてを金曜日の段階で作ったわけではありません。3日間通して最終的に作られた資料一覧が↑です。

2.設計指針

去年同様、設計の方針を立てます。

  • 分業を意識して1コンポーネント1機能に抑える
  • 実装が必須なクラスのみをクラス図に書く
    • ただしUI周りなどパターン化している部分は省略
  • クラス間の関係性が維持されているなら、実装はどれだけ汚くても許容する
  • 「妥協するべき点」と「設計上守るべき部分」をハッキリ区分する
    • 後述
  • DIを使う
  • UniRxやUniTaskはどんどん使う

今年はメンバーの1人が設計についてかなり質問を投げてきました。
どのような内容の質問をしてきたかは後にまとめます。

3. 完成したクラス図

作ったクラス図がこちらです。

class.png

去年と比べると、登場人物が少ないので結構コンパクトに収まりました。

4.設計の順序

では、どのように設計していったかを解説します。

I.要素を洗い出す

毎回やっていますが、登場人物をまず洗い出します。
概念を列挙して、それぞれに名前空間を定義します。

まず、わかりきっているものを挙げて名前空間を切っていきます。

  • プレイヤー Players
  • Enemies
  • おもちゃのパーツ Parts
  • ステージ Stages
  • ゲームのマネージャ群 Managers

1.png

これでも十分ですが、もうちょっと概念を切り分けます。

  • 持って運ぶの概念を扱う Holders
  • ダメージの概念を扱う Damages

これらも追加します。

1_.png

「レシピ」をどこで扱うかを迷ったのですが、とりあえずこのままいくことにしました。

II. わかりやすいところから埋める

まずは予想がすぐできる「Damages」と「Holders」を先に埋めました。

2.png

まずHoldersですが、「このオブジェクトはプレイヤが持ち運べる」ということを表すインタフェースを定義します。
それが Holders.IHoldable です。

「掴む」というメソッドがbool TryHold()になっているのは、掴むことに失敗する可能性もありうるだろうなということでこうなっています。

次にDamagesですが、「ダメージという概念そのもの」と「ダメージを受けることができる」を定義します。
今回は「プレイヤを一定時間、操作不能にする」という効果なのでDamageの中身はただのfloat値になっています。

じゃあなぜ最初からただのfloat値にせず、わざわざDamageオブジェクトを定義したのかというと理由があります。
それは「今後の仕様追加でプレイヤーに対する効果を追加する可能性がある」という予想があるからです。

もちろん予想ですので、取り越し苦労の可能性はあります。
が、float値を型1つでラップするだけでその仕様追加に簡単に対応できるのであるならば、この選択は全然アリだと判断します。

III. 敵を定義する

つづいて敵を定義します。

3.png

(ロボット掃除機のクラス名が「Roomba」なの、ヤバい)

まず敵の基底クラスBaseEnemyを定義し、その派生としてRoombaDogを定義します。
そしてBaseEnemyIHoldableを実装しているため、「敵」のオブジェクトはプレイヤが持ち運べるという意味がここで表現されています。

また、犬はプレイヤーに対してダメージを与えることができます。
ただ今後、もしかしたら「犬」が他のオブジェクトにダメージを与える可能性も考慮します。
ここではさっき定義したIDamageApplicableを経由してプレイヤにダメージを与えることにしておきます。

チームメンバーから出た質問「HoldableBehaviour」を定義しちゃだめなのか

質問

ここでチームメンバーから質問がでました。
「持ち運べる」という処理が頻発するならば、「HoldableBehaviour」という基底クラスを定義した方が楽ではないか、というものです。

つまり、こういう形になぜしないのかという質問です。

holdableBehaviour.png
(提案された設計)

この設計は自分としてはNGです。理由としては、「is-aの関係を満たしていないから」です。

継承は「本質的に同じ概念のオブジェクトをまとめるものであり、派生クラスは基底クラスと完全に置換可能でないといけない」というルールがあります。

今回は「持ち運ぶことができる」という「性質」についてを扱っています。

これは「本質的に同じ概念であるか」というとNOであり、単に振る舞いの1つを表しているだけにすぎません。

これを基底クラスとして定義することを許してしまうと、「共通した機能は雑に基底クラスにまとめてしまおうwww」ということになりかねません。これは設計の崩壊を招く可能性があり大変危険です。

また今回はIHoldableだけですが「じゃあここにIDamageApplicableも載せれば共通化できて便利じゃん」と話が膨らむ可能性は大いにあります。

bad.png

(いろいろグチャグチャになりはじめた最悪なパターン)

こうなってくると基底クラスはどんどん膨らみ、本来は別個の処理が1つのクラスに集中してしまい身動きが取れなくなっていきます。

SimpleEasyは違う、ということをしっかり意識しないとすぐに破綻を迎えます。

ではどうするべきか

基底クラスは使ってはいけない。だが頻発する処理を何度も実装はしたくない。

こういう場合は「委譲」を使います。

簡単にいうと、「共通した処理を切り出した別のクラスを作ってそれを呼び出すようにする」というだけです。

delegation.png

こうすると基底クラスが膨らむこともなく、処理の共通化を行うことができます。

IV. パーツを定義する

閑話休題。続いてパーツを定義していきます。

4.png

特に特筆することはありません。
Partはピュアな構造体として定義されており、それを1つだけ保持するPartObjectというGameObjectが定義されています。

V. プレイヤーを定義する

プレイヤーを定義します。
といってもこの辺は毎年やっているので、例年どおりといった感じです。

5.png

プレイヤーの制御に必要なコンポーネントを列挙して、それぞれの関係性をつないだだけです。

VI. ステージを定義する

続いてステージを定義します。

6.png

  • パーツを放り込んで出荷するときに使うエリア「AssemblyArea
  • ベルトコンベア「BeltConveyor
  • 出荷ボタン「ShippingButton

をそれぞれ定義します。

ここで妥協点が1つあります。「ShippingButtonIHoldableを実装している」ということ。
これは「プレイヤーの掴むというアクションと、ボタンを押すというアクションを1つの操作にまとめてしまいたい」という欲求から来ています。

本来、ShippingButtonは持ち運び不可能なオブジェクトです。
しかしそこに「掴む」というイベントを流用したいがためにIHoldableを実装してしまうという形になっています。

別にインタフェースを切ろうかとかなり悩んだのですが「設計的な正しさと実装時のややこしさ」を天秤にかけた結果ここは妥協しました。

VII. マネージャクラスを定義する

今回はクラス数が少なくて余裕があったので、マネージャ周りの設計もしました。思いつく限り、必要そうなマネージャを列挙しています(実際これだけでは足りませんでしたが)。

7.png

「オーダー」の概念はどこで扱うか迷い、ScoreManagerに付随する形で定義することにしました。
(このときまだオーダーという呼び名が決まっていなかったので、仮にComboSetという名前にしてある)

完成

class.png

以上でクラス設計は終わりです。

今回はかなり規模が小さいため、あっさり実装が終わりました。

check.jpg

これはクラス図を印刷して進捗管理をしていた写真です。
土曜日の15時くらいの写真ですが、このときですでに全体の8割ほどは実装が完了していました。

もらった質問など

設計や実装のやり方で、いろんな人から質問をいただくのでこのタイミングで回答しておきます。

Q. プロジェクトファイルの管理はどうしているの

自分は「Unity Collaborate」をゲームジャムでは推しています。
理由としては次のとおり。

  • Unity Editorに統合されているので追加インストール不要
  • データのPush/Pullさえできればいいならこれで十分
  • Gitはデザイナやプランナがわからない場合にコストが高い
  • どうせGitを使ったところでブランチ切った開発しない

Unity Collaborate、本来は有料機能なのですが、GGJ向けにUnityの人がライセンスを発行してくれることがあります。
自分はこのライセンスがあるならUnity Collaborateを使いますが、無いなら諦めて Git + GitHub で開発します。

Q. UniRxを入れると初心者プログラマがついてこれないのでは

ゲームジャムでUniRxの凝った使い方はほとんどしません。

ReactivePropertySubscribe()の使い方さえわかればなんとかなるパターンが大半です。

そのため30分も学習すれば問題なく使える範疇ですので、自分はUniRxを導入しています。

また金曜日の夜中に自分が設計している間は他のプログラマーの手が空きます。
このタイミングで自分が過去に書いたUniRxの資料を渡し、目を通しておいてと頼んでいます。

Q. ゲームジャムで事前に設計するなんて聞いたことない

実際やっているのは自分くらいでしょう。ただ事前に設計することはいろいろメリットが大きいのでかなりオススメです。

  • 事前に規模感が把握できる
  • インタフェースが決まっているので結合時に揉めない
  • タスクを振って分担作業しやすい
  • 具体的な作業指示が出しやすい
  • 全体の進捗を可視化できる

ただし注意点もあります。

  • 設計をミスると負債を抱えることになる
  • 一度決めた仕様がブレない、という前提が必要
  • 金曜日の24時ごろまでに仕様がfixしないと厳しい

Q. ZenjectなどのDIフレームワーク、ゲームジャムに必要なのか

実際Zenjectは難しいですが、メリットはかなり大きいです。

まず、SerializeFieldの設定漏れを防止することができます。
またシーンをまたいだデータの受け渡しや、ScriptableObjectの読み込みなどもZenject経由にするとかなり楽になります。

ちなみにゲームジャムにおいてはInstallerの定義はほとんど必要ありません。SceneBindingZenAutoInjectorを使えばUnity Editor上の操作のみでだいたい解決します。あとはZenjectSceneLoaderあたりも便利なので使っていますが、これもそんなに難しくはないです。

結論としては「便利なのでどんどん使おう。機能を絞って使えばそんなに難しくはない」です。

Q.設計がうまくできない

こればかりは経験と勘、としか言いようがありません。

コツとしては「クラス間の関係性を中心に書く」です。
特にUnityの場合は「実装によってかなりブレる部分」というのがあります。

例:「プレイヤーがどうやって目の前のパーツを拾うのか」

このような処理は、「OnTriggerEnterを使う」「CircleCastを使う」「相手の座標から計算して探す」などいろいろ実装方法があります。
ですがこのへんはやってみないとどれが最適なのかわからないですし、のちの調整で変わる可能性もあります。

そのため必要最低限の「クラス間の関係性」に絞って、「どこでどのような処理が実行されるのか」、かまでは踏み込まないほうが書いていて混乱しません。

q.png

Q. どうやってタスクを分業しているのか

  • 誰がどう実装しても同じような実装になる部分
    • 構造が決まっている構造体やクラスの定義
    • 設計済みのインタフェース定義
  • どんな実装であれ要件を満たすなら問題ない部分
    • プレイヤーの移動処理
    • オブジェクトの生成処理

こういう部分からまず分業して実装をしていきます。
逆に次のような部分は分業せず、誰か1人に任せたほうがいいです。

  • 多くのクラスを用いて処理を行う部分
    • ゲーム終了時に点数計算して表示する部分
    • シーンの初期化と終了部分

Q. テストって書いたほうがいいの

書ける部分があるなら、書いたほうがいいです。けどわからないなら無理に書かなくてもいいです。

今回は1箇所、点数計算ロジックだけテストが書けそうだったので書いています。

Q. 設計/実装で妥協する部分とそうでない部分の違いがわからない

設計時や実装時に、「面倒だけどちゃんとしたコードにするか」「サボって楽なコードにするか」を迷うことがあるでしょう。

自分はここの指標は次のようにしています。

  • それを許した結果、あとに負債となるなら絶対にNG
  • クラス名と一致しない処理はそのクラスに実装してはいけない

これだけです。あとはケースバイケースです。

まとめ

  • ゲームジャムは学びが多いのでぜひ参加してほしい
  • 技術的なスキルよりも、コミュニケーションの面で学びがとにかく多い
  • プロジェクトの進捗管理などを経験したいプランナーなどにもお勧め
  • 設計はこだわるところと、妥協すべき点をハッキリするのが大事
  • チームプレイなので、周りのケアも大事

そして何より、めちゃくちゃ楽しいのでみんな参加しましょう!!!
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

unityでCSVからステージを作成するエディター拡張

Unityエディター拡張でステージを作ってみる

今回はこのように3Dでステージを展開できるエディター拡張の紹介です
2019.2のバージョンでは動作確認してあります。
キャプチャ.JPG

今回のコードはこちらになります
https://gist.github.com/rx1242/c574f5b9289293a5fce5984c59fe979a

CSV読み込みについて

読み込みはReadCsvメソッドで行っていて
このメソッドは、読み込みボタンを押すと呼ばれます。
1. EditorUtility.OpenFilePanelを使いエクスプローラーで直接CSVファイルを指定して、パスを取得してきます。
2. 取得したパスを使い、StreamReaderでCSVをstringに変換します。
3. stringをカンマごとに分割して、分割データの配列から行列の数値を取得。
4. 二十配列に番地ごとのデータを入力していきます

マップチップデータ

これは簡単
各マップチップのデータをスクリプタブルオブジェクトで作成したクラスに設定するだけ!

ステージ展開

展開はApplyメソッドで行っています。

二重for文で行列ごとのデータと一致する番号のマップチップをInstantiateしていきます。

この時xyのポジションを行列と合うように移動します。

キャンセル処理

キャンセルはRollbackメソッドで行っています。
生成したステージを削除しているだけです。

工夫したポイント

そもそもの制作目的が15人チームでストラテジーゲームを作るときに、ステージの作成の高速化というのがあります。
複数人で使用されるだろうという前提があります。
ここで問題なのがファイル構造の問題で、人によってCSVを保存する場所が違うと予想。

EditorUtility.OpenFilePanelを使い直接保存場所からパスを取得できるようにしています。
これで使用するときにAssetsフォルダー内にCSVが無くてもOK!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む