- 投稿日:2020-02-06T22:54:58+09:00
UnityでiOS実機とmacエディタでBLEを使う
UnityでiOS実機とmacエディタでBLEを使う
UnityでiOS実機とmacエディタで、CoreBluetoothを使って、Bluetooth Low Energyのデバイスと通信するnative pluginを作成しました。
Unity Packageを配布しています。
リポジトリはこちら→https://github.com/fuziki/UnityCoreBluetoothUnityエディタとiOSで動くBluetoothのプラグイン作った#unity pic.twitter.com/EeHz0iqcmL
— ふじき (@fzkqi) September 24, 2019目的
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の実機を起動するのは手間がかかるので、エディタで開発可能になることで開発が数倍楽になった気がします。
このプラグインの構造などはこちらの記事に書いてあります。
- 投稿日:2020-02-06T22:02:52+09:00
自作パッケージで自作パッケージを使用したい【UnityPackageManager】
package.jsonのdependenciesに自作パッケージは適用できない
まず前提条件として、自作パッケージをパッケージするにあたって必要となる
package.json
だが、このjson内のdependenciesではなぜか、自作パッケージのgitリポジトリURLで依存関係を適用することができない。(Unity起動時に依存解決する際にエラーが発生する)したがって、自作パッケージで自作パッケージを適用したい場合は、別の方法を取る必要がある。適用方法
1. AssemblyDefineに適用するパッケージのguid参照を登録する
パッケージするにあたって、プロジェクトにAssemblyDefineファイルを配置する必要があるが、自作パッケージ通しで別のAssemblyDefineを使用するため、片方が依存関係を持つ場合、もう片方のguidの参照を登録する必要がある。
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", ... } }
- 投稿日:2020-02-06T20:43:31+09:00
SteamVR2.0環境でHMDのコントローラを用いた移動・回転の実装
SteamVR2.0 でのコントローラを用いた移動・回転方法
後輩たちへのメモ程度にSteamVR2.0 での移動・回転方法を記述
1.下記のスクリプトを
[CameraRig]
にアタッチ
2.InspectorのVR_cameraに[CameraRig]
の子オブジェクトCamera
を設定
3.ActionManifestで設定した項目を各SerializeField
に設定
※ここではActionManifestの説明を省くので,詳しくはググってください
気が向けば追記するかもUnityで実行すると,左手で移動,右手で回転ができる
移動・回転スピードは値を変更してくださいVR_Move.csusing 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); } } }
- 投稿日:2020-02-06T18:28:21+09:00
入れ替え、ソート可能な配列「Reorderable-List」を使ってみる
はじめに
最近、順次実行のイベント作成していたときに気づいたのですが
配列で管理していると入れ替えが大変なんですよね。なので今回は入れ替え可能な「ReorderableList」なるものを組み込むことにしました。
これで後々変更があってもラックラクですよ!ダウンロード
まずはダウンロードです。
こちらに最新のパッケージがあります。
https://github.com/cfoulston/Unity-Reorderable-List無事ダウンロードできました。
プロジェクトにインストール
Packagesウィンドウを開く
まずPackagesウィンドウを開きます。
ウィンドウは画面上部のメニュー「Window」→「Package Manager」から開くことが出来ます。package.jsonを読み込む
ダウンロードしたzipファイルを解凍。
そしてPackagesウィンドウの左上にある+マークを押して「Add package from disk...」を押す。
「Unity-Reorderable-List-master」フォルダにある「package.json」を追加します。
これでインストール完了です。
では早速スクリプトで使ってみましょう。2020/02/06追記
差分を見るとこちらのようにローカルを指しているので
プロジェクトにオールインワンとして入れ込みたい場合はAsset直下にフォルダを入れてください。その際、「Package Manager」での追加は不要なので削除しておいてください。
利用方法
使い方はこんな感じです。
継承で専用リストを作成してそれを公開する感じです。sampleList.csusing System.Collections.Generic; public class SampleList : MonoBehaviour { // リストを定義 [System.Serializable] public class ExampleChildList : Malee.ReorderableArray<EventData>{}; // リスト [SerializeField] [Malee.Reorderable] ExampleChildList EventReorderableList; }使用画面
プロパティを編集するとこんな感じになりました。
めちゃくちゃ便利です。最後に
いかがでしたでしょうか?
これまでのリストは自分で削除したり追加したりかなり手間が掛かっていたのでこの機能は凄いありがたいですね。リストを使っていて不便だな~と感じる方はぜひ組み込んでみてくださいね。
ではここまでお読みいただきありがとうございました。
- 投稿日:2020-02-06T17:09:56+09:00
【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テンプレートを使って作成したデフォルトのシーンを使います。
自前でお好きな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 からアクセスできます。
HD Render Pipeline WizardのConfigulation Checkingから、HDRP+DXRタブを選択すると、どの項目を設定すべきかの一覧が表示されます。
「Fix All」をクリックすることで、DXRの有効化に必要な設定の手順を自動で実行してくれます。(※エディタの再起動を要求されます)DXR有効化時に出てくるエラーについて
DXRを有効にすると、以下のエラーが発生します
d3d12: generating mipmaps for array textures is not yet supported.
これは、DX12とDXRがまだプレビュー機能なため、いくつかの機能が欠落しているため生じるエラーだそうです。レイトレ機能の利用上は問題ないので今回は無視します。Volumeを作成してEffectのオーバーライドを行う
DXRを有効にしただけではレイトレの各種機能を有効にすることはできません。HDRPのVolumeで各種エフェクトをレイトレの機能でオーバーライドすることでレイトレの機能を利用することが出来ます。
生成されたオブジェクトの名前は今回はRayTracing Volumeとしておきました。
Volume Profileを作成・編集する
インスペクタのVolumeコンポーネント上の「New」ボタンを押して、新規にVolume Profileを作成します(既存のプロファイルを書き換えて設定する方法でも構いません)。
Volume Profileを作成出来たら、「Add Override」から、上書きしたいエフェクトを選択し、「Ray Tracing」という項目のチェックボックスをOnにします。
今回は以下のエフェクトを追加します。
- Screen Space Reflection
- Ambient Occlusion
- Global Illumination
これで上記の表現をレイトレで行うことが出来ました。
GIのノイズが目立っているので、デノイズの設定をすると以下のようになります。
ノイズが目立たなくなりましたね。
他にもVolume Profileの各値を設定することで、レイトレの結果を調整することができます。マテリアルにScreen Space Reflectionが適用されるようにする
レイトレ反射をさせたいオブジェクトのマテリアルは、Receive SSRのチェックボックスをONにすることで、レイトレによる反射を反映させることができます。
レイトレによって物理的に正確な反射を表現(もちろんリアルタイム)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を利用することが出来ます。
Ray-traced shadowsを適用した結果おまけ
Path Tracingを試す
映画などで活用されているレンダリングアルゴリズムであるPath TracingをDXRを活用することでUnityでも利用することができるので、試してみました。
Path Tracingを有効にする
Path Tracingを有効にするには、先ほどのVolume profileに「Path Tracing」をAdd OverrideしてEnableをOnにして有効化します。
今回は、以下のようなコーネルボックスを使って通常のレンダリングとパストレーシングの場合を比較してみました。
通常のレンダリング(反射・AO・GI・影生成にDXRを使用)Path Tracingのレンダリング結果
パストレーシングでは、反射やAOのような用途のみでレイトレを使うわけではなく、画面全体をレイの計算を使って描画するため、描画処理に非常に時間がかかります。フルHDで1フレームのレイトレを完了するのに50秒くらいかかりました。まだリアルタイムパストレーシングは難しそうですね
画としては、GIや物質の光沢表現が非常にきれいになっています。一方で透過オブジェクトを透過処理できていないことが分かります。また、アルゴリズムの都合上、ノイズが多く、特に輪郭の正確さが失われてしまっています。さいごに
HDRPではDXRを有効化することで簡単にレイトレを使った機能を活用できることが分かりました。
正確な反射やGIを表現したいという方は使ってみてはいかがでしょうか?参考ページ
- 投稿日:2020-02-06T12:48:08+09:00
爆速でオブジェクトを動かすスクリプト
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); } } }
- 投稿日:2020-02-06T10:31:37+09:00
描画負荷がかからない透明なuGUI Imageを作る【Unity】
はじめに
uGUIの操作を制限するため手前に透明なImageを重ねることがあると思いますが、そのままでは1枚分の描画負荷がかかってしまいます。その負荷を軽減する方法を紹介します。
方法
Imageと同じGameObjectについているCanvasRendererの
Cull Transparent Mesh
をオンにしてください。SceneビューのOverdrawモードで確認すると、設定したImage部分の描画がされていないことが確認できます。
StatsでもBatchesが増えていないことが確認できます。
ちなみにCanvasRendererの
Cull Transparent Mesh
で全ての描画がなくなるのは全ての画像領域が完全に透明な場合のみです。アルファ値が0.1でも残っていると描画されてしまう点に注意してください。おまけ: 最初から描画しないスクリプトを自作する
上記の方法とほぼほぼ同じことができるスクリプトです。途中から意味ないと気がついたのですが、折角なので紹介します。raycastも当たるやつです。
スクリプト
Gist: NoRenderImage.cs
NoRenderImage.csusing 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(); } }他の部分は便利に使うための記述です。
^[Add Component]メニューのUIカテゴリから追加できる
^Canvas以下でない階層で生成すると、自動的にCanvasを作ってくれるこの辺りは別の記事でまとめておこうと思います。
余談
Text
コンポーネントも描画するテキストがない場合に描画がされないので、それで良い説もありますね。
- 投稿日:2020-02-06T02:43:26+09:00
グローバルゲームジャムでクラス設計をやった話2020
はじめに
毎年恒例のグローバルゲームジャム。今回も参加してきたのでそのまとめを書きます。
- 2016 -> グローバルゲームジャムでクラス設計をやったらスムーズに開発が進んだ話
- 2017 -> グローバルゲームジャムでクラス設計をやった話2017
- 2019 -> グローバルゲームジャムでクラス設計をやった話2019
グローバルゲームジャムとは
GGJとは全世界同時に行われるゲームジャムのことです。ようするに、世界規模のゲーム開発ハッカソンです。
プログラマ、デザイナ、プランナ、グラフィッカなどさまざまな役職の人をごちゃまぜに、3~8人程度のチームを組み、48時間でゲームを作ろうというイベントです。(前回のコピペ)(前回のコピペ)(前回のコピペ)今回も「ヒューマンアカデミー秋葉原会場」に参加しました。
ゲームの概要
今年のテーマ
今年のテーマは「REPAIR」でした。シンプルにわかりやすいテーマでいいですね。
これをお題としてゲームを1つ作っていきます。作ったゲーム
今年作ったゲームは「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.要件定義
まず金曜日の24時くらいまで何を作るかをFixします。
この時点で決まった仕様は次のとおりです。
- 目的「パーツを集めてレシピのおもちゃを作って点数を稼ぐ」
- ゲームシステム
- 制限時間制(ゲーム中に変更不可)
- プレイ人数は1-4人(1人でも遊べる)
- スコアを多く稼ぐ
- ベルトコンベアでパーツが運ばれてくる
- 中央にトレイがあり、パーツを入れる
- プレイヤーが「出荷ボタン」を押すと出荷される
- 出荷が完了した時点で点数が加算される
- プレイヤー
- 移動
- 掴んで運ぶ/離して置く
- 出荷ボタンを押す
- 敵からダメージを受けて一定時間動けなくなる
- 敵
- プレイヤーに嫌がらせをする
- プレイヤーは敵をもつこともできる
- ロボット掃除機
- 地面に置かれたパーツを消去する
- 犬
- プレイヤーが触れると一定時間動けなくする
- パーツ
- 種類がいくつかある
- それぞれに素点がある(マイナス値は無し)
- 金のパーツや壊れたパーツなどがある(クオリティ)
- パーツは地面に置いておくことができる
- 出荷されると消える
- オーダー
- パーツの組み合わせ(レシピ)の表
- オーダーを満たすとコンボボーナスがもらえる
- パーツのクオリティでさらにボーナスが増える
- 全体のフロー
- メニューシーン
- プレイ人数を選ぶ
- バトルシーン
- 結果表示はオーバーレイするだけ
- メニュー → バトル → 結果表示 → メニュー で1サイクル
正直、毎年似たようなゲームを作っているのでそれと同じ感じになりました。
この時点でたぶん土曜日中にほぼ実装は終わるだろうなという感想でした。
(実際、土曜日中に実装が終わった)仕様書
今回はプランナーの方がとても優秀な方で、かなり詳細なリソースリストを作ってくれました。
そのため設計や実装が非常にやりやすくとても助かりました。(作ってくれたリスト。めっちゃ助かった。)
追記:上記の資料のすべてを金曜日の段階で作ったわけではありません。3日間通して最終的に作られた資料一覧が↑です。
2.設計指針
去年同様、設計の方針を立てます。
- 分業を意識して1コンポーネント1機能に抑える
- 実装が必須なクラスのみをクラス図に書く
- ただしUI周りなどパターン化している部分は省略
- クラス間の関係性が維持されているなら、実装はどれだけ汚くても許容する
- 「妥協するべき点」と「設計上守るべき部分」をハッキリ区分する
- 後述
- DIを使う
- UniRxやUniTaskはどんどん使う
今年はメンバーの1人が設計についてかなり質問を投げてきました。
どのような内容の質問をしてきたかは後にまとめます。3. 完成したクラス図
作ったクラス図がこちらです。
去年と比べると、登場人物が少ないので結構コンパクトに収まりました。
4.設計の順序
では、どのように設計していったかを解説します。
I.要素を洗い出す
毎回やっていますが、登場人物をまず洗い出します。
概念を列挙して、それぞれに名前空間を定義します。まず、わかりきっているものを挙げて名前空間を切っていきます。
- プレイヤー Players
- 敵 Enemies
- おもちゃのパーツ Parts
- ステージ Stages
- ゲームのマネージャ群 Managers
これでも十分ですが、もうちょっと概念を切り分けます。
- 持って運ぶの概念を扱う Holders
- ダメージの概念を扱う Damages
これらも追加します。
「レシピ」をどこで扱うかを迷ったのですが、とりあえずこのままいくことにしました。
II. わかりやすいところから埋める
まずは予想がすぐできる「Damages」と「Holders」を先に埋めました。
まずHoldersですが、「このオブジェクトはプレイヤが持ち運べる」ということを表すインタフェースを定義します。
それがHolders.IHoldable
です。「掴む」というメソッドが
bool TryHold()
になっているのは、掴むことに失敗する可能性もありうるだろうなということでこうなっています。次にDamagesですが、「ダメージという概念そのもの」と「ダメージを受けることができる」を定義します。
今回は「プレイヤを一定時間、操作不能にする」という効果なのでDamage
の中身はただのfloat
値になっています。じゃあなぜ最初からただの
float
値にせず、わざわざDamage
オブジェクトを定義したのかというと理由があります。
それは「今後の仕様追加でプレイヤーに対する効果を追加する可能性がある」という予想があるからです。もちろん予想ですので、取り越し苦労の可能性はあります。
が、float
値を型1つでラップするだけでその仕様追加に簡単に対応できるのであるならば、この選択は全然アリだと判断します。III. 敵を定義する
つづいて敵を定義します。
(ロボット掃除機のクラス名が「Roomba」なの、ヤバい)
まず敵の基底クラス
BaseEnemy
を定義し、その派生としてRoomba
とDog
を定義します。
そしてBaseEnemy
はIHoldable
を実装しているため、「敵」のオブジェクトはプレイヤが持ち運べるという意味がここで表現されています。また、犬はプレイヤーに対してダメージを与えることができます。
ただ今後、もしかしたら「犬」が他のオブジェクトにダメージを与える可能性も考慮します。
ここではさっき定義したIDamageApplicable
を経由してプレイヤにダメージを与えることにしておきます。チームメンバーから出た質問「HoldableBehaviour」を定義しちゃだめなのか
質問
ここでチームメンバーから質問がでました。
「持ち運べる」という処理が頻発するならば、「HoldableBehaviour」という基底クラスを定義した方が楽ではないか、というものです。つまり、こういう形になぜしないのかという質問です。
この設計は自分としてはNGです。理由としては、「is-aの関係を満たしていないから」です。
継承は「本質的に同じ概念のオブジェクトをまとめるものであり、派生クラスは基底クラスと完全に置換可能でないといけない」というルールがあります。今回は「持ち運ぶことができる」という「性質」についてを扱っています。
これは「本質的に同じ概念であるか」というとNOであり、単に振る舞いの1つを表しているだけにすぎません。これを基底クラスとして定義することを許してしまうと、「共通した機能は雑に基底クラスにまとめてしまおうwww」ということになりかねません。これは設計の崩壊を招く可能性があり大変危険です。
また今回は
IHoldable
だけですが「じゃあここにIDamageApplicable
も載せれば共通化できて便利じゃん」と話が膨らむ可能性は大いにあります。(いろいろグチャグチャになりはじめた最悪なパターン)
こうなってくると基底クラスはどんどん膨らみ、本来は別個の処理が1つのクラスに集中してしまい身動きが取れなくなっていきます。
SimpleとEasyは違う、ということをしっかり意識しないとすぐに破綻を迎えます。
ではどうするべきか
基底クラスは使ってはいけない。だが頻発する処理を何度も実装はしたくない。
こういう場合は「委譲」を使います。簡単にいうと、「共通した処理を切り出した別のクラスを作ってそれを呼び出すようにする」というだけです。
こうすると基底クラスが膨らむこともなく、処理の共通化を行うことができます。
IV. パーツを定義する
閑話休題。続いてパーツを定義していきます。
特に特筆することはありません。
Part
はピュアな構造体として定義されており、それを1つだけ保持するPartObject
というGameObject
が定義されています。V. プレイヤーを定義する
プレイヤーを定義します。
といってもこの辺は毎年やっているので、例年どおりといった感じです。プレイヤーの制御に必要なコンポーネントを列挙して、それぞれの関係性をつないだだけです。
VI. ステージを定義する
続いてステージを定義します。
- パーツを放り込んで出荷するときに使うエリア「
AssemblyArea
」- ベルトコンベア「
BeltConveyor
」- 出荷ボタン「
ShippingButton
」をそれぞれ定義します。
ここで妥協点が1つあります。「
ShippingButton
がIHoldable
を実装している」ということ。
これは「プレイヤーの掴むというアクションと、ボタンを押すというアクションを1つの操作にまとめてしまいたい」という欲求から来ています。本来、
ShippingButton
は持ち運び不可能なオブジェクトです。
しかしそこに「掴む」というイベントを流用したいがためにIHoldable
を実装してしまうという形になっています。別にインタフェースを切ろうかとかなり悩んだのですが「設計的な正しさと実装時のややこしさ」を天秤にかけた結果ここは妥協しました。
VII. マネージャクラスを定義する
今回はクラス数が少なくて余裕があったので、マネージャ周りの設計もしました。思いつく限り、必要そうなマネージャを列挙しています(実際これだけでは足りませんでしたが)。
「オーダー」の概念はどこで扱うか迷い、
ScoreManager
に付随する形で定義することにしました。
(このときまだオーダーという呼び名が決まっていなかったので、仮にComboSet
という名前にしてある)完成
以上でクラス設計は終わりです。
今回はかなり規模が小さいため、あっさり実装が終わりました。
これはクラス図を印刷して進捗管理をしていた写真です。
土曜日の15時くらいの写真ですが、このときですでに全体の8割ほどは実装が完了していました。もらった質問など
設計や実装のやり方で、いろんな人から質問をいただくのでこのタイミングで回答しておきます。
Q. プロジェクトファイルの管理はどうしているの
自分は「Unity Collaborate」をゲームジャムでは推しています。
理由としては次のとおり。
- Unity Editorに統合されているので追加インストール不要
- データのPush/Pullさえできればいいならこれで十分
- Gitはデザイナやプランナがわからない場合にコストが高い
- どうせGitを使ったところでブランチ切った開発しない
Unity Collaborate、本来は有料機能なのですが、GGJ向けにUnityの人がライセンスを発行してくれることがあります。
自分はこのライセンスがあるならUnity Collaborateを使いますが、無いなら諦めて Git + GitHub で開発します。Q. UniRxを入れると初心者プログラマがついてこれないのでは
ゲームジャムでUniRxの凝った使い方はほとんどしません。
ReactiveProperty
とSubscribe()
の使い方さえわかればなんとかなるパターンが大半です。
そのため30分も学習すれば問題なく使える範疇ですので、自分はUniRxを導入しています。また金曜日の夜中に自分が設計している間は他のプログラマーの手が空きます。
このタイミングで自分が過去に書いたUniRxの資料を渡し、目を通しておいてと頼んでいます。Q. ゲームジャムで事前に設計するなんて聞いたことない
実際やっているのは自分くらいでしょう。ただ事前に設計することはいろいろメリットが大きいのでかなりオススメです。
- 事前に規模感が把握できる
- インタフェースが決まっているので結合時に揉めない
- タスクを振って分担作業しやすい
- 具体的な作業指示が出しやすい
- 全体の進捗を可視化できる
ただし注意点もあります。
- 設計をミスると負債を抱えることになる
- 一度決めた仕様がブレない、という前提が必要
- 金曜日の24時ごろまでに仕様がfixしないと厳しい
Q. ZenjectなどのDIフレームワーク、ゲームジャムに必要なのか
実際Zenjectは難しいですが、メリットはかなり大きいです。
まず、
SerializeField
の設定漏れを防止することができます。
またシーンをまたいだデータの受け渡しや、ScriptableObject
の読み込みなどもZenject経由にするとかなり楽になります。ちなみにゲームジャムにおいては
Installer
の定義はほとんど必要ありません。SceneBinding
とZenAutoInjector
を使えばUnity Editor上の操作のみでだいたい解決します。あとはZenjectSceneLoader
あたりも便利なので使っていますが、これもそんなに難しくはないです。結論としては「便利なのでどんどん使おう。機能を絞って使えばそんなに難しくはない」です。
Q.設計がうまくできない
こればかりは経験と勘、としか言いようがありません。
コツとしては「クラス間の関係性を中心に書く」です。
特にUnityの場合は「実装によってかなりブレる部分」というのがあります。例:「プレイヤーがどうやって目の前のパーツを拾うのか」
このような処理は、「
OnTriggerEnter
を使う」「CircleCast
を使う」「相手の座標から計算して探す」などいろいろ実装方法があります。
ですがこのへんはやってみないとどれが最適なのかわからないですし、のちの調整で変わる可能性もあります。そのため必要最低限の「クラス間の関係性」に絞って、「どこでどのような処理が実行されるのか」、かまでは踏み込まないほうが書いていて混乱しません。
Q. どうやってタスクを分業しているのか
- 誰がどう実装しても同じような実装になる部分
- 構造が決まっている構造体やクラスの定義
- 設計済みのインタフェース定義
- どんな実装であれ要件を満たすなら問題ない部分
- プレイヤーの移動処理
- オブジェクトの生成処理
こういう部分からまず分業して実装をしていきます。
逆に次のような部分は分業せず、誰か1人に任せたほうがいいです。
- 多くのクラスを用いて処理を行う部分
- ゲーム終了時に点数計算して表示する部分
- シーンの初期化と終了部分
Q. テストって書いたほうがいいの
書ける部分があるなら、書いたほうがいいです。けどわからないなら無理に書かなくてもいいです。
今回は1箇所、点数計算ロジックだけテストが書けそうだったので書いています。
Q. 設計/実装で妥協する部分とそうでない部分の違いがわからない
設計時や実装時に、「面倒だけどちゃんとしたコードにするか」「サボって楽なコードにするか」を迷うことがあるでしょう。
自分はここの指標は次のようにしています。
- それを許した結果、あとに負債となるなら絶対にNG
- クラス名と一致しない処理はそのクラスに実装してはいけない
これだけです。あとはケースバイケースです。
まとめ
- ゲームジャムは学びが多いのでぜひ参加してほしい
- 技術的なスキルよりも、コミュニケーションの面で学びがとにかく多い
- プロジェクトの進捗管理などを経験したいプランナーなどにもお勧め
- 設計はこだわるところと、妥協すべき点をハッキリするのが大事
- チームプレイなので、周りのケアも大事
- 投稿日:2020-02-06T00:31:12+09:00
unityでCSVからステージを作成するエディター拡張
Unityエディター拡張でステージを作ってみる
今回はこのように3Dでステージを展開できるエディター拡張の紹介です
2019.2のバージョンでは動作確認してあります。
今回のコードはこちらになります
https://gist.github.com/rx1242/c574f5b9289293a5fce5984c59fe979aCSV読み込みについて
読み込みはReadCsvメソッドで行っていて
このメソッドは、読み込みボタンを押すと呼ばれます。
1. EditorUtility.OpenFilePanelを使いエクスプローラーで直接CSVファイルを指定して、パスを取得してきます。
2. 取得したパスを使い、StreamReaderでCSVをstringに変換します。
3. stringをカンマごとに分割して、分割データの配列から行列の数値を取得。
4. 二十配列に番地ごとのデータを入力していきますマップチップデータ
これは簡単
各マップチップのデータをスクリプタブルオブジェクトで作成したクラスに設定するだけ!ステージ展開
展開はApplyメソッドで行っています。
二重for文で行列ごとのデータと一致する番号のマップチップをInstantiateしていきます。
この時xyのポジションを行列と合うように移動します。
キャンセル処理
キャンセルはRollbackメソッドで行っています。
生成したステージを削除しているだけです。工夫したポイント
そもそもの制作目的が15人チームでストラテジーゲームを作るときに、ステージの作成の高速化というのがあります。
複数人で使用されるだろうという前提があります。
ここで問題なのがファイル構造の問題で、人によってCSVを保存する場所が違うと予想。EditorUtility.OpenFilePanelを使い直接保存場所からパスを取得できるようにしています。
これで使用するときにAssetsフォルダー内にCSVが無くてもOK!