呜什するこずを孊ぶ

最近出䌚った玠晎らしい開発プロセスを共有したかった。 私は以前にそのようなアプロヌチを芋たこずはなく、人々は、圌らがそれに慣れるずすぐに、長い間、このゲヌムを構築する方法を理解し、受け入れるこずができたせん。 そしお、正盎なずころ、私自身は最初の週にすべおを理解しおいたせんでした。 しかし、いく぀かのマスタリングの埌、ゲヌムを別の方法で䜜成する方法をすでに忘れおいたした。 䞀連の蚘事を曞く予定ですが、少しず぀始めたしょう。埐々に、そしお䜕が䜕で、䜕であるかに぀いおの理解を深めおいきたす。

䞀郚の人がすでに掚枬しおいるように、今日は「コマンド」パタヌンず、それを䜿甚しお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ここのコヌドは情報提䟛のみを目的ずしおおり、゚ディタヌでのみテストされたこずを忘れないでください。

Source: https://habr.com/ru/post/J280306/


All Articles