Unityでインディゲームを作る!

Unityでのゲーム制作を目指し、それに関わる話題についてのブログ

Unity Timeline のActivation Track の Post-playback stateについて

 UnityのTimelineにおける基本トラックの中でも特に重要度の高い、Activation Trackのプロパティ Post-playback stateについて少し書きます。アクティベーション・トラック自体は簡単に使えますが、これは目立たないながらもかなり大事な機能だと思ったので。

Activation Track

 Activation TrackはTimelineにおいて、対象となるゲームオブジェクトの有効無効を切り替えられるトラックであり、Activeクリップがある部分は有効、それ以外の空きエリアでは無効となります。

 そのTimelineが再生されると、それまでの状態は関係なくActivation Trackによって状態を完全にコントロールされ始めます。

 

Activation Track Inspector

 ここで問題なのが、Timeline再生後のそのゲームオブジェクトの状態です。Activation Trackを選択するとインスペクターにPost-playback stateが表示されます。全部で4つの設定がありますが、デフォルトではLeave As Isになっています。

Active (有効)

Inactive (無効)

Revert (元に戻す)

Leave As Is (そのまま継続)

 ActiveとInactiveは再生中の状態は関係なく、終了後に必ずその状態に。Revertは再生開始前の状態に戻り、Leave As IsはTimeline終了時の状態が継続されます。

 基本的にはデフォルトのLeave As Isで問題ないのかもしれませんが、モノによっては厳格にここで設定した方がいいかもしれません。

 

 特にTimelineの再生を途中で止められるようにする場合(例えばカットシーンのスキップ)Leave As Isでは消えるべきオブジェクトが残ってしまう等の問題が発生すると思います。

 

 Timelineの再生が終わったけど、なんか動かない?アレが見当たらない!?といった事態が起きたらPost-playback stateの設定を確認してみると良いかもしれません。

 

UnityのオブジェクトプールAPIについて [ UnityEngine.Pool ]

ObjectPool_Cover

 オブジェクト・プールというのはプログラミング・パターンのひとつで、あらかじめオブジェクトを生成してプール(備蓄)しておくことで実行中の負荷を減らそうという手法です。

 Unityでもよく使われるパターンのひとつですが、Unity 2021にてオブジェクト・プールAPIが追加され、それを使えばゼロから全部自分でコードを書く必要がなくなりました。今回はそのAPIの使い方についてです。

※2024/03/15 間違いや不十分な部分があったため、記事全体を見直し必要な部分は加筆修正しました。

 

 今回の記事はUnity公式が発行した、プログラミング・パターンについての解説本である"Level Up Your Code With Programming Patterns"を参考にしています。

 

UnityEngine.Pool

BeamShooterScript_IObjectPool

使い始めるための準備は、using 部に UnityEngine.Poolと書き込んで、プール対象となるタイプのIObjectPool<T>を宣言するだけです。

 

 このプールを初期化するためにはいくつかのメソッドが必要になりますが、今回はプールでオブジェクトを生成する際に実行される、Createメソッドを作成します。

CreateBeam method

 ここで注意が必要なのがInstantiateメソッドで生成するタイプについてです。今回はBeamControllerをプールするので、プレハブ用の変数をBeamControllerとして_beamPrefabを宣言しています。

 プール対象のオブジェクトが自ら自身をプールに戻す場合、プールへの参照が必要になることからも、そのオブジェクトをコントロールしているスクリプトをタイプとして指定するのが多くの場合好ましいはずです。

InitializeObjectPool

 ObjectPoolの初期化には、前述のCreateメソッドの他にオブジェクトをGet, Release, Destroyする時に実行されるメソッドも必要ですが、基本的にはSetActiveかDestroyを行うだけなので、ラムダ式で省略してしまっています。

(BeamController beamController) => { }

という風に空のラムダ式を書くことも可能です。もちろんより多くの処理を行いたい場合は、それぞれきちんとメソッドを書く必要が出てくると思います。

 

シューティングゲームの弾

SkyFighter_Shooting

 というわけで実例に移りますが、シューティングの弾をプーリングしたいと思います。シューティングゲームにおける弾オブジェクトは大量に必要です。リアルタイムに生成、破壊を繰り返すのはものすごい負担になるのでオブジェクト・プールを使うのに最適かつ代表的なケースになります。

 今回はSkyFighterBeamShooterBeamControllerという二つのクラスが登場します。

BeamController_IObjectPool

 弾自体をコントロールするためのクラスとして"BeamController"スクリプトを書きました。(ビームのゲームオブジェクト及び、そのプレハブに装着されます)

 このクラスにIObjectPool<PlayerBeamController>のプロパティを持たせます。ここでは自動実装を使っていますが、本来なら private フィールドを宣言した方が良いでしょう。

 

ShooterScript_CreateBeam

 SkyFighterBeamShooterクラスでは前述の通り、CreateBeamメソッド内でプレハブから弾をInstantiateして、そのIObjectPoolプロパティにSkyFighterBeamShooterで作成したプールを代入します。

 これで各ビームが自分達を蓄えるプールへの参照を持ち、これを利用して、Releaseメソッドでビームのオブジェクトをプールに戻します。

 

ShooterScript_Awake

 では、Awake内でIObjectPoolを初期化します。前述の通り、その他の必要なメソッドはラムダ式として入力しています。

 キャパシティ、最大値はそれぞれプール内オブジェクトの基本的な数とそれを超えるとしても最大何個までオブジェクトを生成するかを設定するものです。最大値を超えるとオブジェクトは破壊されます。

 

 ここで注意なのですが、このままだとプールにオブジェクトは無い状態なので、結局実行時に生成されることになってしまいます。公式ドキュメントなどでは行われていないのですが、あらかじめ生成してプールしておいた方が良いです。(このままだとゲーム開始時に一定数が生成されることになります)

 

 なので、for文を使って必要な数だけビーム・オブジェクトを生成します。処理としてはCreateBeam内とほぼ変わらず、オブジェクト生成してBeamController内のIObjectPoolプロパティに当該のプールを設定し、Releaseしてオブジェクトをプールに収めるだけです。

PooledBeamInHierarchy

 これでシーンがロードされる前に、弾をあらかじめ生成できました。ゲーム実行時からプール内にオブジェクトが十分にあるので負荷が減ります。プレイヤーを実行するとあらかじめプールされたオブジェクト群がヒエラルキー上にずらりと並びます。

 

GetとRelease

 実際にコード内でプールからオブジェクトを取得するためにはGetメソッドを使います。これによりInstantiateメソッドを取り除き、余計なGCガベージコレクション)の発生を防ぐことが出来るようになりました。

ShooterScript_ShootBeam

上画像のメソッド内では弾の発射位置だけ入力しています。

BeamController_OnTriggerEnter

 ビーム弾のコントロールとして、敵などに衝突した後の処理の中でReleaseを使います。オブジェクト・プールを使わない場合、ここでDestroyを行うので大きな負担になっていました。Releaseするとプールに設定していたonRelease実行によりオブジェクトは無効化された後でプールに戻され、またGetメソッドで呼び出されるまで待機することになります。

 

まとめ

 慣れてしまえば、かなり簡単にオブジェクトプール・パターンを使えるようになる便利なAPIだと思います。

 上述したように、プール作成後にAwakeかStartで必要な分だけオブジェクトを生成してプールを満たしておくのが良いはずです。

 

 UnityにはIncremental GC機能が追加され、GCによるパフォーマンス低下が抑えられるようになりましたが、GCを発生させないことこそが最大の最適化と言われますし、これからこの機能はどんどん使っていきたいと思います。

 

MadeWithUnity探検隊! "GunSmith Simulator" 鉄砲鍛冶シミュレーターで銃の構造を学ぼう!

GunSmith Simulator

 今回のMadeWithUnity探検隊は、初のシミュレーション作品となります。日本語訳では『鉄砲鍛冶シミュレーター』というなんとも言えない邦題になっていますが、"GumSmith Simulator"という作品です。

 まだアーリーアクセス版ですので若干動作や操作性に不安定な所がありますが、そのコンセプトは既にしっかりと楽しめる状態になっています。あと何気にしっかり日本語訳されています。

 

 自分はガンマニアと言うほどではありませんが、エアガンがそれなりに好きだったりYoutubeで海外の銃動画をよく見たりしていて、FPSをプレイしているのもあり実銃の構造や機構にはずっと興味がありました。

 ある時、このゲームがMadeWithUnityということを知り、じゃあ是非やってみようとなったのでした。

 

銃を直して稼ごう!

GunSmith Simulator_WorkShop

あらゆる道具、設備を備えた工房

 あなたは銃職人であった祖父から工房を譲り受け、ガンスミスとして働くことになります。祖父からの依頼をクリアすることで新しい能力をアンロックし、その技術で銃を修理、改造していくのがこのゲームの主な目的です。

 

 仕事の依頼はネット上で探すことが出来て、好きな(銃の)依頼を選んで引き受けることが出来ます。またネットオークションで銃を落札することも可能です。手に入れた銃は修理して好きに改造できるようになります。

GunSmith Simulator_Shooting Range

アメリカらしい広々とした射撃場、クレー射撃も可

 そして、直した銃は射撃場で好きに打つことが出来ます。依頼された銃もテストと称して打ちまくりましょう!また、CQBスペースも別にあるので屋内での戦闘を模した訓練も行うことが出来ます。

 

実銃の構造そのままに

GunSmith Simulator_Winchester21

有名ショットガンを部品単位で分解、修理できる

 このゲームのすごいところは、実銃をそのまま部品単位でモデリングしており、それらを完全に分解できるということです。(依頼によっては必要な部分しか分解できない場合があります)

 つまり実銃がどんな構造になっているのかを知ることが出来ます。各部品ごとに解説も表示され、どんな名前でどんな機能を持っているのかを知ることも出来ます。

GunSmith Simulator_Luger

伝説的な名銃も登場!

 アーリーアクセス版である現状でも拳銃、ショットガン、ライフルなどの各種において有名な銃は抑えられており、様々な銃火器の構造について学ぶことが出来ます。今後のアップデートで銃の数は増やしていくようです。

 

 個人的に拳銃で一番カッコいい銃だと思っているColt M1911コルトガバメント)の修理からこのゲームは始まるのでテンション上がりましたね。

 逆に言えば、実銃に興味が無い人にとっては何も琴線に触れない、面白味のないゲームかもしれません。現状、とにかく銃を直してお金を稼いで銃を落札したりパーツを購入して改造するくらいしかありませんし、将来的にもその基本的な部分は変わらないでしょうから。しかし、シミュレーションというのはそういうモノですからね。

 

シミュレーションとして

 実銃を触ったことすら人間からすればどのくらいのリアルなのかは正直分からないのですが、かなり本格的だと思えるこだわりは感じられます。

GunSmith Simulator_Sand Blast

部品に砂(研磨剤)を吹き付けて錆を取る

 個人的にここまで出来るのか!と驚いたのは、錆びたパーツをサンドブラストして綺麗にして、そのあとで表面保護のための酸化処理する、という作業です。

 また、コンピュータ制御の金属加工マシンがあったり、旋盤加工も出来たりと、銃火器に関するあらゆる作業がシミュレーションされています。

GunSmith Simulator_Cleaning

スキルが上がれば、清掃作業も高速化

 スキルシステムが存在し、レベルが上がるとスキルポイントを貰え、色んな作業のスピードを上げることができます。また、銃ごとの熟練度があり、同じ銃を何回も修理すると分解するスピードも上げることが可能です。

 

 シミュレーションゲームというのはゲーム的な面白さよりもリアリティに重点が置かれるものですが、銃に興味がある人間にとってはこの上ない魅力にあふれたゲームになっていると思います。

 

気になった点

 現状、操作性が若干悪い部分がありますが、設定を変えたり慣れれば大丈夫な範囲かなと思っています。不安定な部分も少しありますが、それも改善されていくはずです。

 フォントについては日本語、英語共に少し読みにくいですが、これは自分の環境が悪いのかもしれません。

 

 このゲームはデモ版があるようなので、気になった方はまずそれをプレイしてみるのが良いと思います。(すいません、現在はもうデモ版は無かったです)

 

 アーリーアクセス版に関しては、開発会社のやる気を見定める必要があるのですが、このゲームに関してはまぁ大丈夫なのかなと思っています。基本的なコンセプトは完成していますし。

 

まとめ

 というわけで、本企画としては初のシミュレーションゲームとなる"GunSmith Simulator"を取り上げました。実は音楽も結構良かったりします。アメリカンロックと銃は似合いますね。

 こうしたリアルフォト系のグラフィックのUnityゲームも珍しくなくなってきましたが、だいぶ綺麗になったなぁと感慨深いです。

 

 こうしたシミュレーション系のゲームにも当然Unityは使えますし、Unityの汎用性を考えるとむしろ向いていると思います。工業製品のシミュレーションにも使われたりしていますし、今後とも用途が広がっていくことに期待ですね。

 

シリアライザ MessagePack-C#をUnity(IL2CPP環境)で使ってみる!事前のコード生成に注意!

 今回はC#リアライザであるMessagePack-C#のUnity (2022.3.8f1)での使い方についてまとめたいと思います。著者もまだまだ使い始めであり、あくまで最低限使えるようになるまでの入門編ですので、ご了承ください。(約3500文字)

[追記] この記事を書いた直後にMessagePackの上位互換とも言えるMemoryPackの存在を知りまして、これも当然Unityで簡単に使うことが出来て本記事で書いたような事前のコード生成も必要ありません。なので素直にMemoryPackを使った方が良いでしょう。後日にMemoryPackについても記事を作成しようかと思いますが、本記事は記録として残しておきます。

[追記終わり]

 

 MessagePackについては、GitHub上の公式リリースページにUnityパッケージが用意されており、それをダウンロードしてインポートすれば直ぐに使えるようになります!落とし穴がありますが。今回の記事はその落とし穴を埋める方法についてです。

 

 なお、本記事ではファイルへの直接的な読み書きを扱います。その際のトラブルについては一切責任を負えませんので、十分注意して行ってください。

 

jsonの上位互換として

 きっかけはUI ToolKitのサンプルプロジェクト"UI ToolKit Sample"内のとあるコードにあったコメントからでした。意訳すると『あくまでサンプルプロジェクト用にJsonUtilityを使ってるけど本番環境ではMessagePackとかを使った方が良いよ!』という内容です。

 このサンプルプロジェクトではゲームデータ(プレイヤーデータやオプション設定)の保存にjsonを使っています。ゲームではプレイデータ等をセーブ&ロードする必要が出てきますが、jsonは文字列を使っているため直接的に人間がデータを読むことが出来る反面、容量が大きくなったり処理が重くなるというデメリットがあります。

 

 それに対して、MessagePackはbyte列としてデータをシリアライズするので、データ量が少なく処理も軽いのが売りです。ずっと気になっていたのですが、ようやく触ることが出来ました。

 

MessagePackの落とし穴

 しかし、MessagePackを使う上で落とし穴があります。それはUnityには大きく分けてMono環境とIL2CPP環境がありますが、IL2CPP環境だとランタイム中のコード生成が出来ないため、あらかじめ事前にコード生成をしないと使えない、ということです。

 

 詳しくは後述しますが、ここで一瞬折れそうになりました。MessagePackの使い方に関しての記事は、検索すれば分かるように結構あります。しかし、自分が調べた限りではこの落とし穴について書いた記事はなかったのです・・・。というわけで自分のこの経験と成果を記事として書いていきます。

 

まずは対象となるクラスを書く

 今回はゲームデータをMessagePackを使ってシリアライズして、セーブ&ロードできるようにすることを目標にしています。

 

 それではMessagePackで取り扱うクラスを設計します。MonoBehaviourを継承しない普通のC#クラスです。

MSP_GameDataTest class

 MessagePackの基本的な使い方としては、対象となるクラスに[MessagePackObject]アトリビュートを付け、[Key]アトリビュートで各データメンバーにインデックスか文字列を振り分ける、というものです。コンストラクタを作る場合は[SerializationConstructor]アトリビュートを付けます。また、シリアライズに含みたくないメンバーには[IgnoreMenber]を付けます。

 という風に、クラス設計に関してはこれだけでいいので簡単ですよね。

 

コード生成

 ですが、問題はここからです。前述の通り、環境設定によっては自分で必要なコードを生成する必要があります。

 

 MessagePackのUnityPackageにはコードジェネレーターが付属しており、エディタ上部のWindowsメニューからWindow > MessagePack > Code Generatorでジェネレータを起動できます。

MessagePack_CodeGenerator

 ただし、初回起動時には先にインストールする必要があるので注意。インストール後に上画像の通りジェネレーターが使えるようになります。ここで入力パスと出力パスを入力します。注意点はファイルの拡張子まで書くということです。

 

 ../はそのプロジェクト・フォルダまでのパスを省略して入力してくれます。そのプロジェクトに含まれるC#プロジェクト・ファイルを指定するので、input pathは・・・

../Assembly-CSharp.csproj

と入力します。何を入力するかについては結局まだ良くわからない所があるのですが、目ぼしいC# Projectファイルはコレくらいでしょう。とりあえずは自分の環境ではコレで動いてくれました。

 次にoutput pathですが、出力する場所とファイル名を含みます。ここではGeneratedMSPとしたので、output pathは・・・

../Assets/Scripts/TestScripts/GeneratedMSP.cs

としました。ここではプロジェクト・フォルダのAssets以下のいくつかのフォルダを含んだパスになっていますが、好きな場所に出力できます。

MSP_ConsoleLog

 Generateを押すとコンソールに処理の経過が表示され、Completedと表示されれば指定したパスにcsファイル(C# script)が生成されているはずです。

 指定したフォルダに無い場合、生成されているがUnityに認識されていない時があるので、その場合はエクスプローラーでフォルダを開き、ファイルがあることを確認した後でUnityに戻れば読み込まれて、Unityエディタ上で表示されるようになると思います。

 

 この生成コードについては、対象となるクラスを書き直す度に改めて出力する必要があります。

StaticCompositeResolverに登録する

 こうして生成したコードをStaticCompositeResolverに登録する必要があります。このコードについてはGitHubの公式ドキュメントに載っているデモコードほぼそのままですが、これで十分動きます。

MSP_StaticCompositeResolver

MSP_StaticCompositeResolver_01

 これもMonoBehaviourを継承したクラスではありませんが、[RuntimeInitializeOnLoadMethod]アトリビュートがあるので、プロジェクト内にスクリプトがあれば、そのままで実行されます。

 

 以上で、MessagePackを使う基本的な準備は整いました。それでは次項ではMessagePackを使ってシリアライズ、デシリアライズしてデータを読み書きする、ということに挑戦したいと思います。

 

シリアライズしたデータをセーブロードする

 データ(ここではGameDataTestオブジェクト)をMessagePackSerializer.Serializeメソッドを使ってbyte arrayにシリアライズし、それをSystem.IOのファイル操作を使ってdatファイルに書き込みます。

MSP_GameDataManager

MSP_GameDataManager_01

 OnApplicationQuitを使って、ゲームを閉じた時にGameDataTestオブジェクトをシリアライズして、File.WriteAllBytesを使ってプレイデータを保存するようにします。

 

 Application.persistentPathでそのアプリケーションの内部データを保存するフォルダのパスを取得し、そこにファイル名と拡張子を書き足します。エディタでは、

C:\Users\(ユーザー名)\AppData\LocalLow\(Company Name)\(Product Name)

という感じのProject SettingsのPlayerページで設定できる、Company NameとProduct Nameに基づいたパスに記録されます。このパスについてはプラットフォーム毎に変わるようです。

 persistentPathを使えばデータの保存場所についても本番を想定できますね。

 

 そして、ゲーム起動時にそのファイルを当該のパスからFile.ReadAllBytesで読み込み、DeserializeメソッドでGameDataTestオブジェクトに戻し、ゲーム内データに反映させます。この例ではプレイヤーのリスポーン地点も保存しているので、プレイヤーの位置をそこに移動させています。

 

まとめ

 一度は挫折しかけてしまいましたが、分かってしまえば軽く使う分には簡単に使えるシリアライザでした。ちょっとしたゲームの簡単なセーブ機能ならば、コレで十分ではないかと思います。

 ただし、本番環境で使うにはセキュリティ的な問題も当然考慮しなければならず、そこら辺は非常に頭が痛くなりますね。とは言え、セーブ機能の実装については気になるところではあったので自分なりの取っ掛かりにはなったのは良かったです。

 

結局、UnityのScriptableObjectとは何なのか?についての解答を思いつきました。

ScriptableObject

 Unity公式のe-bookでScriptableObjectへの理解が進んだとは言え、モヤモヤは残っていました。結局、ScriptableObjectって何なのか、と。しかし、遂に自分なりの答えを見つけることが出来たのでここに記します。

 

ScriptableObjectとは、アセット化することのできる特殊なインスタンスである。

 

 まず先にオブジェクト指向プログラミングにおけるクラスとインスタンスについて再確認します。

 

OOPにおけるクラス、インスタンス

 クラスとはデータ構造と振る舞い(メソッド)をまとめる、データタイプの設計図でインスタンスはそこから生み出された実体です。クラスそれだけではプログラムは動かず、実行中にそこからインスタンスが生成され、それらが実働することによってデータが処理されます。

 この時、インスタンスはメモリ上に生成されるのでプログラムが終了すれば当然消えてしまいます。

 

 インスタンスはそれぞれ固有のパラメータを持つことの出来る個体であり、例えばHumanクラスがあったとして、あるインスタンスは日本人で男かもしれませんし、他のインスタンスアメリカ人の女性かもしれません。

 つまりクラスとは(複雑な)データタイプの原型であり、インスタンスはそれを元に生み出される個々のオブジェクトということです。

 

UnityのPrefab

 ここでUnityの重要な機能のひとつであるPrefabに目を向けたいと思います。Prefabとはシーンのヒエラルキー上で作ったGameObject(ゲームオブジェクト)をアセット化する機能です。

 GameObjectとはまさしくUnityにおける個の存在を定義する単位であり、オブジェクトです。様々なコンポーネントを追加したり、別のGameObjectをネストすることでデータ構造や振る舞いを設計することができます。

 

 しかし、そのままだとUnityシーン上でしか存在できない儚いオブジェクトです。そこで利用するのがPrefab機能であり、シーン上のGameObjectをAssetsフォルダ内(あるいはその下階層の特定フォルダ)にドラッグ&ドロップするとアセット化できます。

 これにより、Prefabとしてある存在の原型をアセットとしてプロジェクト内に保有することが可能となり、あらゆるシーンでそのPrefabから生成されるGameObjectを再利用することができるようになります。

 

 前述のクラスとインスタンスの関係からすると、Unityのプレハブはインスタンスから逆にクラスを作り出すような機能と考えられるのではないでしょうか。

 

ScriptableObjectクラスとインスタンス

 というわけで、いよいよ本題です!ScriptableObjectを使おうと思ったらまずC#スクリプトを作成することになります。これはつまりScriptableObjectクラスを継承したクラスの設計なんですよね。そのクラスがどんなデータを持っているかを書きます。だから、この時点ではScriptableObjectは存在しませんし、プロジェクト内には、ただのC#スクリプト・アセットがあるだけです。

 ScriptableObjectクラスにCreateAssetMenuアトリビュートを付けると、右クリックのCreateメニューからそのクラスで定義したScriptableObjectを作成できるようになります。作成するとScriptableObjectがプロジェクト上に生成されます。これはいくつでも作ることが出来て、それぞれに固有のパラメータを入力し保存することが出来ます。アセットなので、Unityエディターを終了しても消えません。

 

 つまり、これって消えないインスタンスなのでは?と思ったのです。ScriptableObjectはデータコンテナとしての使い方が主であり、例えば敵のデータやアイテムのデータを定義し、アセットとして保存してそれをGameObjectが参照することが出来ます。

 しかし、ScriptableObjectはメソッドを持つことも出来、単純なデータ保存アセットではありません。じゃあ一体何なのかって話だったわけですが、一応自分の中ではこのような一定の答えが出てきたので納得は出来ました。そういう風に考えたらメチャクチャ便利な機能ですよね。

 

まとめ

 というわけで、簡単にですがScriptableObjectについての考察を書いてみました。分かったようで分からず悩んでいたのですが、アセット化できる特殊なインスタンス及び、特殊なクラスによってアセット化されたインスタンスである、と考えればこんなに素晴らしい機能はないだろう、と気づくことが出来ました。