命令することを学ぶ

最近出会った素晴らしい開発プロセスを共有したかった。 私は以前にそのようなアプローチを見たことはなく、人々は、彼らがそれに慣れるとすぐに、長い間、このゲームを構築する方法を理解し、受け入れることができません。 そして、正直なところ、私自身は最初の週にすべてを理解していませんでした。 しかし、いくつかのマスタリングの後、ゲームを別の方法で作成する方法をすでに忘れていました。 一連の記事を書く予定ですが、少しずつ始めましょう。徐々に、そして何が何で、何であるかについての理解を深めていきます。



一部の人がすでに推測しているように、今日は「コマンド」パターンと、それを使用してUnity 3Dエンジンを使用してゲームを開発する方法について説明します。 これは、このアプローチの重要なパターンの1つです。 コードは簡素化されますが、機能し、プロセスを理解する必要があります。



プロローグ



開発者がUnityでアクターを使用する方法について語る記事を見たことがあるでしょうか? そうでない場合は、例を使用して簡単にポイントを説明します。ゲームには、さまざまな方法でジャンプしなければならないゲームキャラクターがたくさんいます。 もちろん、誰もが好きなポリモーフィズムによって問題を解決できます。ベースユニットを作成し、各ユニットの仮想ジャンプメソッドを単純にオーバーロードします。



そのようなもの
public class UnitController : MonoBehaviour { public Rigidbody AttachedRigidbody; //... public virtual void Jump() { rigidbody.velocity = new Vector3 (0, 10, 0); } //... }
      
      





 public class RabitUnitController : UnitController { //... public override void Jump () { //very high jump } //... }
      
      







しかし、この場合、異なるジャンプ、同じ方法でジャンプするいくつかの既製のユニットを作成する必要がある場合、クラス階層をわずかに修正するか、適切なコードをすべての必要なクラスにコピーアンドペーストする必要があります(恐ろしいです)。



アクターの助けを借りて、このタスクは異なる方法で解決されます。 このアプローチを使用すると、仮想Jumpメソッドの代わりにユニットクラスを記述し、一連の個々のUnitJumperコンポーネントを記述し、適切なコンポーネントを正しいユニットに単純にフックする必要があります。 そして、ジャンプ時に、接続されたコンポーネントでJumpメソッドを呼び出します。



アクターコード
 public class UnitJumper : MonoBehaviour { public virtual void Jump(Rigidbody rigidbody) {} } public class RegularJumper : UnitJumper { public override void Jump (Rigidbody rigidbody) { base.Jump (rigidbody); rigidbody.velocity = new Vector3 (0, 10, 0); } } public class MajesticAFJumper : UnitJumper { public override void Jump (Rigidbody rigidbody) { base.Jump (rigidbody); rigidbody.velocity = new Vector3 (0, 15, 10); /* * some magic here */ } }
      
      







そして、コントローラーは
 public class UnitController : MonoBehaviour { [SerializeField] private UnitJumper _unitJumper; public Rigidbody AttachedRigidbody; //... public virtual void Jump() { if (_unitJumper != null) _unitJumper.Jump (AttachedRigidbody); else Debug.Log("UnitJumper Component is missing"); } //... }
      
      







これですべてがシンプルで美しいものになりました。 階層の問題が少ないため、ジャンプコードは別の小さなクラスに移動され、変更が容易になります。 各ジャンプメソッドには、必要な数のパラメーターを含めることができます。パラメーターを変更しても、実行中など、壊れることはありません。 また、ユニットのジャンプ方法の変更も非常に簡単になりました。 さらに、環境自体がそのようなアーキテクチャに従うように促し、[RequireComponent()]属性の助けを借りて、エディターをいじることもできます。 今、あなたはなぜ私がこれをすべて語っているのか、そして何が関係なのかを尋ねなければなりません。 それで、コマンドパターンへの論理的な移行の時間です。



論理遷移



この例のすべてのジャンプコードを1つのクラスに書き込むことから既に移動しましたが、ユニットが単独で異なるジャンプをするだけでなく、たとえば、状況に応じてジャンプメソッドを変更できるようにしたい場合はどうでしょうか(宙返りをする、壁に沿って走る)? これがチームを必要とする場所です。



本質は同じままです-すべての基本的なアクションを別々のクラスに取ります。 使用する直前に必要なコンポーネントをユニットに追加します。これにより、ユニットの動作をいつでも変更できるようになり、Actorの場合のような強力な接続はなくなります。 コマンドの小さな基本クラスを作成しますが、これまでのところ、特定のオブジェクトでコマンドを呼び出すだけです。



ベースチーム
 public class Command : MonoBehaviour { public static T ExecuteOn<T>(GameObject target) where T : Command { return target.AddComponent <T>(); } private void Start() { OnStart (); } protected virtual void OnStart() {} }
      
      







上記のコードは、コンポーネントをオブジェクトに簡単に追加するためだけのものであり、OnStart()メソッドは、現在(ただし今のところ)インテリセンス専用です。



基本クラスができたので、簡単なジャンプを実装できます。



おおよそのジャンプクラス
 public class RegularJumpCommand : Command { protected override void OnStart () { base.OnStart (); gameObject.GetComponent <Rigidbody>().velocity = new Vector3(0, 10, 0); } }
      
      







そして今、ユニットをジャンプさせるには、コマンドを実行するだけです:



チームコール
 public class SomeController : MonoBehaviour { //don't forget to set this in editor public UnitController _targetUnit; private void Start() { if (_targetUnit != null) { Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject); } } }
      
      







最初に目を引くのは、速度値が一定であることです。 したがって、ジャンプをもう少し高くするだけでは機能しません。 以前は、引数をjumpメソッドに渡すことでこれを解決していましたが、ここでもそれをしましょう。 美しいチームを書き直しましょう:



引数を持つチーム
 public class Command : MonoBehaviour { private object[] _args; public static T ExecuteOn<T>(GameObject target, params object[] args) where T : Command { T result = target.AddComponent <T>(); result._args = args; return result; } private void Start() { OnStart (_args); } protected virtual void OnStart(object[] args) {} }
      
      







コマンドに引数を渡すことで、ジャンプの高さと方向を変更できるようになりました(引用することを忘れないでください)。 Start()はオブジェクトの作成より少し遅れて呼び出されるため、引数はOnStart(object [] args)メソッドに正しく渡されます。



ジャンプコマンドはほとんど変更されませんが、外部から渡された引数を使用できます。



コマンドでの引数の使用
 public class RegularJumpCommand : Command { protected override void OnStart (object[] args) { base.OnStart (args); gameObject.GetComponent <Rigidbody> ().velocity = (Vector3)args [0]; } }
      
      







コマンドの呼び出しはもう少し変更されます:



引数を指定してコマンドを呼び出す
 public class SomeController : MonoBehaviour { // don't forget to set this in editor public UnitController _targetUnit; private void Start() { if (_targetUnit != null) { Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject, new object[]{new Vector3(0, 10, 0)}); } } }
      
      







操作後、チームは柔軟になり、宙返りの場合にのみ別のクラスが必要になります。 ただし、パラメーターを初期化するには、OnStartメソッド(オブジェクト[] args)を使用するだけです。



2番目の問題は、ジャンプするたびに、高価なGetComponent()メソッドが呼び出されることです。 これを解決するために、アクターにはすべての重要なコンポーネントへのリンクを保持するコントローラーがあり、チームに必要なものすべてを要求することを思い出してください。 コントローラを引数に渡すこともできます。これをもう少し形式化することを提案します。 コマンドのコントローラーを持つ子クラスを作成しましょう。



コントローラー付きコマンド
 public class CommandWithType<T> : Command where T : MonoBehaviour { protected T Controller { get; private set; } protected override void OnStart (object[] args) { base.OnStart (args); Controller = args [0] as T; } }
      
      







その後のチーム自体では、使用する引数の数のみが変更されていますが、これについても忘れないでください。 しかし、GetComponent()に頼らずにコントローラーを取得する便利な方法がありました。 また、base.OnStart(args)を呼び出す必要があります。そうしないと、コントローラーを使用できません。



コントローラーの使用
 public class RegularJumpCommand : CommandWithType<UnitController> { protected override void OnStart (object[] args) { base.OnStart (args); Controller.AttachedRigidbody.velocity = (Vector3)args [1]; } }
      
      







コマンド呼び出しも少し異なりました:
 public class SomeController : MonoBehaviour { //don't forget to set this in editor public UnitController _targetUnit; private void Start() { if (_targetUnit != null) { Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject, new object[]{_targetUnit ,new Vector3(0, 10, 0)}); } } }
      
      







これですべてが非常に良くなりました。コントローラーなしで実行できるチーム(広告を表示、どこかに何かを投稿)とコントローラーが必要なチーム(実行、実行、飛行)があります。 コントローラを使用したコマンドは、クラスファミリでの作業用に強化されており、他のファミリでは使用できません。これにより、追加の秩序が導入されます。 また、アクターの利点もあります。 そして、あなたは彼らがどれほど小さくてきれいであるかに気づかずにはいられませんでした。 コントローラーもこれの恩恵を受けただけです。コントローラーは、必要なコンポーネントへのリンクの同じ簡潔なコンテナになりました(もちろん、後でもっと重くします)。 しかし、これはまだ始まったばかりであり、そのような機能を備えているので、私たちはあまり行きません。



ジャンプからそれほど遠くに行かないように、まだ見逃しているものを見てみましょう。 この厄介ではあるが有用な例を、論理的な結論に導きましょう。



最初に目を引くのは、10回ジャンプした後、完全に役に立たない10個のチームがオブジェクトにぶら下がることです。 2番目に注意することは、開始時に速度が変化し、これが物理エンジンの正しい動作を保証しないことです。



これまでのところ、私たちは最初のものに集中し、続けて、それが起こるように、2番目が決定されます。 最も論理的なことは、チームが台無しになったらすぐにクリーンアップすることです。 コマンド完了機能と2つのフラグを追加して、チームが実行を完了したかどうかを確認します(詳細は後ほど)。 そして、小さな変革の後、チームは進化します(しかし、これは最後の形でさえありません)。



チームの整理
 public class Command : MonoBehaviour { private object[] _args; private bool _started = false; private bool _isReleased = false; public bool IsRunning { get{ return _started && !_isReleased;} } public static T ExecuteOn<T>(GameObject target, params object[] args) where T : Command { T result = target.AddComponent <T>(); result._args = args; return result; } private void Start() { _started = true; OnStart (_args); } protected virtual void OnStart(object[] args) {} private void OnDestroy() { if (!_isReleased) OnReleaseResources (); } protected virtual void OnReleaseResources() { _isReleased = true; } protected void FinishCommand() { OnReleaseResources (); Destroy (this, 1f); } protected virtual void OnFinishCommand(){} }
      
      







さて、適切なタイミングで、チームはまともな市民であり社会のメンバーであるため、自滅します。必要な操作をすべて行った後、FinishCommand()メソッドを呼び出すだけです。 Destroy()は少し遅れて、それを必要とするすべての人が消える前にコマンドを使用できるようになります(データを取得しますが、後で詳しく説明します)。IsRunningフラグは、チームが事前に開始しないように必要です。完了。 イベントからのすべてのサブスクライブ解除とリソースの解放は、OnReleaseResources()またはOnFinishCommand()で簡単に実行できます。 そして、あなたが誤ってOn Destory ()を書くことを恐れてはいけません。



これで、2番目の問題を解決できます。



FixedUpdateで速度を変更する
 public class RegularJumpCommand : CommandWithType<UnitController> { private Vector3 _velocity; protected override void OnStart (object[] args) { base.OnStart (args); _velocity = (Vector3)args [1]; } private void FixedUpdate() { if (!IsRunning) return; Controller.AttachedRigidbody.velocity = _velocity; FinishCommand (); } }
      
      







速度値は、Start-a後の物理エンジンの最初の反復の瞬間に変化します。 開発のこの段階にあるチームは、呪文のrunning唱、ランニング、さまざまなジャンプ、視覚効果などのタスクに素晴らしく対処します。



しかし、どうでしょう?!



これはあなたが望むところにはまだ使用するようになりません。 構成をダウンロードするか、ユーザーアクションを検証する必要がある場合(子供が親のお金でパンを買わないようにする)、またはジグザグ実行の一部としてチームを使用する場合。 多くの場合、チームが完了したかどうか、正常に完了したかどうかを確認する必要がある場合があり、完了している場合は、そこから必要なデータを取得します(破壊が遅延しました)。 要するに、コールバックなしでは理解できません。 個人的に、私はそれらのために新しいUnity UIシステムが好きですが、それらがコードに追加またはコードから削除されるときだけです(エディターでこれを行うことは罪です、そのようにしないでください)。



イベントですべてを行うことができ、考える必要さえありませんが、実行中のすべてのコマンドへのリンクを維持したくないので、適切なタイミングで登録を解除してください。 痛みは背中のすぐ下にあります。登録を解除するのを忘れた場合、喜びをもたらす人はほとんどいません。 最初に、コマンドを最大限に活用し始めるために何をする必要があるかを規定しましょう。そうすれば、後で編集ができなくなります。 主なタスクは、コマンドが正常に完了した場合と失敗した場合のコールバックを作成することです。 それらを署名するのに便利で、購読解除に従う必要はありません。 また、コマンドを渡すときに、コマンド自体をコールバック引数に渡すと、クラス内に別のフィールドが保持されないようになります。 それでも、外部からチームを停止する方法は実装していません。



まず、これらのコールバック用の小さなラッパーを作成します。 言われるとすぐに、私たちプログラマーは単純な人です。 次のようなものを得ました:



コールバック
  public class Callback<T> where T : Command { public readonly Action<T> Succeed; public readonly Action<T> Fault; public Callback (Action<T> succeed) { this.Succeed = succeed; } public Callback (Action<T> succeed, Action<T> fault) { this.Succeed = succeed; this.Fault = fault; } }
      
      







シンプルで便利。 デフォルトでは、コールバックが1つしかない場合、コマンドの正常な完了のみに関心があり、この場合にのみ呼び出されると自動的に信じていることに注意してください。 次の論理ステップは、これらの非常にコールバック用のコンテナを作成することです。これは、1つで十分なためです。 そして、私たちはこれを得ました:



コールバックトークン
  public class CallbackToken <T> where T : Command { private List<Callback<T>> _callbacks; private T _command; public CallbackToken (T _command) { this._command = _command; _callbacks = new List<Callback<T>>(); } public void AddCallback(Callback<T> callback) { _callbacks.Add (callback); } public void RemoveCallback(Callback<T> callback) { _callbacks.Remove (callback); } public void FireSucceed() { foreach (Callback<T> calback in _callbacks) { calback.Succeed(_command); } } public void FireFault() { foreach (Callback<T> callback in _callbacks) { if (callback.Fault != null) { callback.Fault (_command); } } } }
      
      







CallbackTokenをチームに追加し、適切なタイミングで呼び出すだけです。 そして、チームを成功させることを可能にすることを忘れないでください。 そしてすぐに最終的なコード:



コールバックとチーム
 public class Command : MonoBehaviour { private object[] _args; private bool _started = false; private bool _isReleased = false; public CallbackToken<Command> CallbackToken { get; private set; } public Command () { CallbackToken = new CallbackToken<Command> (this); } public bool IsRunning { get{ return _started && !_isReleased;} } public static T ExecuteOn<T>(GameObject target, params object[] args) where T : Command { T result = target.AddComponent <T>(); result._args = args; return result; } private void Start() { _started = true; OnStart (_args); } protected virtual void OnStart(object[] args) {} private void OnDestroy() { if (!_isReleased) OnReleaseResources (); } protected virtual void OnReleaseResources() { _isReleased = true; } protected void FinishCommand(bool result = true) { if (!IsRuning) return; OnReleaseResources (); OnFinishCommand (); if (result) CallbackToken.FireSucceed (); else CallbackToken.FireFault (); Destroy (this, 1f); } protected virtual void OnFinishCommand(){} public void Terminate(bool result = false) { FinishCommand (result); }
      
      







これで、FinishCommand()メソッドは実行を引数として受け入れ、Terminate()メソッドは外部からコマンドの作業を中断するために使用されます。



次に、サブスクリプションがどのように見えるかを見てみましょう。



コールバックサブスクリプション
 public class SomeController : MonoBehaviour { //don't forget to set this in editor public UnitController _targetUnit; private void Start() { if (_targetUnit != null) { Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject, new object[]{_targetUnit ,new Vector3(0, 10, 0)}) .CallbackToken.AddCallback (new Callback<Command>(OnJumpFinish)); } } private void OnJumpFinish (Command command) { Debug.Log(string.Format("{0}", "Successfully jumped")); } }
      
      







これで、2番目のタスクを簡単に解決できます。コマンドからデータを取得する(結局、コールバックメソッドで取得する)ために、必要な情報と出来事のパブリックgeterを作成するだけです。



終わり!



結論として、このアプローチはUnityでゲームを開発するのに非常に成功していると言いたいです。 すべてがシンプルで美しく、変更が簡単で、実用的なコマンド(ダウンロード、投稿)をプロジェクトに簡単に転送できます。 この記事が気に入ったら、嬉しいだけでなく、有限状態マシン、MVC、戦略、1つのプロジェクトでの生き方と共存について話すインセンティブもあります。



PS:ここのコードは情報提供のみを目的としており、エディターでのみテストされたことを忘れないでください。



All Articles