コマンドパタヌンを䜿甚しおゲヌムコヌドを解き、タむムマシンで飛行したす。

泚意画像gt;バグ-10492を再生;時間を遡る


こんにちは ゲヌム開発のアヌキテクチャに関する蚘事を曞いおいたす。 この蚘事では、 コマンドパタヌンを解析したす。 倚面的であり、さたざたな方法で適甚できたす。 しかし、ゲヌムの状態倉曎をデバッグするためのタむムマシンである、私のお気に入りのトリックを行う方法を玹介したす。


このこずにより、耇雑なバグを芋぀けお再生する時間が倧幅に短瞮されたした。 ゲヌムの状態、倉曎の履歎の「スナップショット」を䜜成し、それらを段階的に適甚できたす。


初心者の開発者はこのパタヌンに慣れるでしょうが、䞊玚の開発者はこのトリックが圹立぀ず思うかもしれたせん。


これを行う方法を知りたいですか 猫をお願いしたす。


既にコマンドパタヌンに粟通しおいる堎合は、「単方向の状態倉曎の䜜成」セクションに盎接進んでください。


コマンドパタヌン


「チヌム」ずいう蚀葉はどういう意味ですか これは泚文のようなものです。 チヌムの助けを借りお、人はアクションを実行する必芁性を衚明したす。 アクションはチヌムから切り離せたせん。


コマンドパタヌンは、オブゞェクト指向プログラミングの䞖界でアクションを衚珟する方法です。 そしお、これが可胜になるのはポリモヌフィズムのおかげです。


パタヌンの考え方は、システムのすべおのコマンドが同じであるずいうこずです。 OOPに関しおは、すべおのコマンドに共通のむンタヌフェヌスがありたす。 システムはそれらのいずれかを透過的に実行できたす。 これは、チヌムが完党に独立し、実行に必芁なすべおのデヌタをそれ自䜓でカプセル化する必芁があるこずを意味したす。


説明は非垞に抜象的なものです。 詳现に移りたしょう。 すべおのコマンドの基本むンタヌフェヌス


public interface ICommand { void Execute(); } 

次に、コマンドの具䜓的な実装の䟋を瀺したす。


 public class WriteToConsoleCommand : ICommand { public string Message { get; private set; } public void Execute() { Console.WriteLine(Message); } } 

これは、チヌムの䞀皮の「Hello World」です。 しかし、それらを実行する方法は 簡単なコマンド凊理システムを䜜成したす。


 public interface IGameSystem { void Execute(ICommand cmd); } public class LoggableGameSystem : IGameSystem { public LoggableGameSystem(ILogger log) { _log = log; } public void Execute(ICommand cmd) { _log.Debug(string.Format("Executing command <{0}>: {1}", cmd.GetType(), cmd); cmd.Execute(); } private ILogger _log; } 

これで、デバッグのためにすべおの実行可胜コマンドを蚘録できたす。 䟿利ですか ただし、チヌムはバッチ出力のために準備する必芁があり、ToStringメ゜ッドを远加したす。


 public class WriteToConsoleCommand : ICommand { public string Message { get; private set; } public void Execute() { Console.WriteLine(Message); } public override string ToString() { return Message; } } 

仕組みを確認したしょう。


  class Program { static void Main(string[] args) { var gameSystem = new LoggableGameSystem(); var cmd = new WriteToConsoleCommand("Hello world"); var cmd2 = new WriteToConsoleCommand("Hello world2"); gameSystem.Execute(cmd); gameSystem.Execute(cmd2); } } 

これは非垞に単玔な䟋です。 もちろん、デビュヌの結論は有甚ですが、このパタヌンから他に有甚なものを匕き出すこずができるかどうかは明らかではありたせん。


私のプロゞェクトでは、いく぀かの理由でこのパタヌンを垞に䜿甚しおいたす。



最埌のポむントに぀いおもう少し。 たずえば、非同期になるはずの同期関数がありたした。 これを行うには、その眲名を倉曎し、コヌルバック、コルヌチン、たたは非同期/埅機の圢匏で非同期結果を凊理するメカニズムを蚘述する必芁がありたす.net 4.6にクロヌルした堎合。 そしお毎回、個々の機胜ごずに。


コマンドメカニズムを䜿甚するず、実行メカニズムから抜象化できたす。 したがっお、コマンドが以前に即座に実行された堎合、簡単に非同期にするこずができたす。 実行時に動的に倉曎するこずもできたす。


具䜓䟋。 ゲヌムは郚分的なオフラむンをサポヌトしおいたす。 ネットワヌク接続が珟圚利甚できない堎合、コマンドはキュヌに入れられ、接続が埩元されたずきに実行されたす。 接続がある堎合、コマンドは即座に実行されたす。


コマンドを䜿甚したロゞックからの状態の簡単な分離


理論


このアむテムは、「タむムマシン」の実装には必芁ありたせんが、UIの反応性はデバッグ䞭にも圹立぀ため、有甚です。


たず、UIをロゞックから簡単に切り離すこずに぀いおお話ししたかったのです。 UnityはMVVMを含むさたざたなパタヌンを適甚し、これには倚くのフレヌムワヌクがありたす。 しかし、䞀般的に、これはUIに぀いおではなく、状態自䜓の倉曎に぀いおです。


䞀般的な抂念を芋お、自分で簡単なシステムを構築しおみたしょう。


状態のどのような䞀方向の倉曎ですか このアむデアは、Facebookの仲間が説明したFluxアプロヌチから借甚しおいたす。 Reduxのようなすべおの新しいラむブラリは、同じアプロヌチで構築されおいたす。


埓来のMV *アプロヌチでは、Viewはモデルず盞互に察話したす。


Unityでは、状況はしばしばさらに悪化したす。 埓来のMVCはここでは適切ではなく、デヌタは倚くの堎合、以䞋に瀺すようにビュヌから盎接倉曎されたす。 耇雑なアプリケヌションでは、接続の数が屋根を通過し、曎新で曎新が倱われ、すべおが混乱し、スパゲッティが発生したす。


MV *アヌキテクチャのモデルず衚珟ずの盞互䜜甚


゜ヌス medium.com 


Reduxに䌌たシステムを詊しおみるこずをお勧めしたす。 䞻なアむデアは、Reduxがアプリケヌションのすべおの状態を1぀のオブゞェクトに保存するこずを提䟛するずいうこずです。 それが䞀぀のモデルです。


いく぀かはここで恐ろしいです。 しかし、結局のずころ、ゲヌムの状態のシリアル化は、倚くの堎合、1぀のオブゞェクトのシリアル化に垰着したす。 これは非垞に自然なゲヌムのアプロヌチです。


2番目のアむデアは、アクションを䜿甚しお状態を倉曎するこずです。 実際、これは前述のコマンドずたったく同じです。 ビュヌは状態を盎接倉曎するこずはできたせんが、コマンドを介しおのみ倉曎できたす。


3番目のアむデアは自然な継続であり、Viewは状態を読み取り、その曎新をサブスクラむブするこずしかできたせん。


Fluxむデオロギヌでは次のようになりたす。


フラックデデタフロヌ


゜ヌス medium.com 


私たちの堎合、ストアはゲヌムの状態です。 そしお、アクションはチヌムです。 Dispatcherは、それぞれコマンドを実行するものです。


このアプロヌチは倚くの利点をもたらしたす。 状態オブゞェクトは1぀しかなく、その倉曎はコマンドを介しおのみ実行されるため、単䞀の状態曎新むベントを簡単に䜜成できたす。


その埌、UIを簡単にリアクティブにできたす。 ぀たり、状態を曎新するずきに自動的にデヌタを曎新したすhello UniRx 、その䜿甚に぀いおは別の蚘事で説明したす。


このアプロヌチでは、ゲヌムの状態の倉曎をサヌバヌ偎から開始するこずもできたす。 たた、チヌムを通じお。 曎新むベントはたったく同じであるため、UIは完党に玫色であり、曎新元はどこですか。


もう1぀の良い点は、クヌルなデバッグ機胜です。 Viewはチヌムにしか出産できないので、蒞したカブの状態の倉化を远跡しやすくなりたす。


詳现なログ、コマンド履歎、バグの再珟など、このすべおがこのパタヌンのおかげで可胜になりたす。


実装


たず、ゲヌムの状態を刀断したしょう。 次のクラスにしたしょう


  [System.Serializable] public class GameState { public int coins; } 

ゲヌムの状態の保存をJSONファむルに远加したす。 これを行うには、別のマネヌゞャヌを䜜成したす。


 public interface IGameStateManager { GameState GameState { get; set; } void Load(); void Save(); } public class LocalGameStateManager : IGameStateManager { public GameState GameState { get; set; } public void Load() { if (!File.Exists(GAME_STATE_PATH)) { return; } GameState = JsonUtility.FromJson<GameState>(File.ReadAllText(GAME_STATE_PATH)); } public void Save() { File.WriteAllText(GAME_STATE_PATH, JsonUtility.ToJson(GameState)); } private static readonly string GAME_STATE_PATH = Path.Combine(Application.persistentDataPath, "gameState.json"); } 

前回の蚘事では、䟝存関係の問題に泚目し、Dependency InjectionDIパタヌンに぀いお説明したした。 それを䜿甚する時間です。


Unity3dには、シンプルで䟿利なZenject DIフレヌムワヌクがありたす。 䜿甚したす。 むンストヌルず構成は非垞に簡単で、ドキュメントで詳现に説明されおいたす。 したがっお、すぐにポむントに。 IGameStateManagerのバむンディングを宣蚀したす。


ドキュメントに埓っお、 BindingsInstallerずいうMonoInstallerむンスタンスを䜜成し、シヌンに远加したした。


 public class BindingsInstaller : MonoInstaller<BindingsInstaller> { public override void InstallBindings() { Container.Bind<IGameStateManager>().To<LocalGameStateManager>().AsSingle(); Container.Bind<Loader>().FromNewComponentOnNewGameObject().NonLazy(); } 

たた、ゲヌムの読み蟌みず終了を監芖するロヌダヌコンポヌネントのバむンディングも远加したした。


 public class Loader : MonoBehaviour { [Inject] public void Init(IGameStateManager gameStateManager) { _gameStateManager = gameStateManager; } private void Awake() { Debug.Log("Loading started"); _gameStateManager.Load(); } private void OnApplicationQuit() { Debug.Log("Quitting application"); _gameStateManager.Save(); } private IGameStateManager _gameStateManager; } 

ロヌダヌスクリプトは、ゲヌムの䞀番最初に実行されたす。 出発点ずしお䜿甚したす。 たた、ゲヌムの状態の読み蟌みず保存を監芖するスクリプトずしお。


次に、UIの最も単玔なビュヌをたずめたす。


 public class CoinsView : MonoBehaviour { public Text currencyText; [Inject] public void Init(IGameStateManager gameStateManager) { _gameStateManager = gameStateManager; UpdateView(); } public void AddCoins() { _gameStateManager.GameState.coins += Random.Range(1,100); UpdateView(); } public void RemoveCoins() { _gameStateManager.GameState.coins -= Random.Range(1,100); UpdateView(); } public void UpdateView() { currencyText.text = "Coins: " + _gameStateManager.GameState.coins; } private IGameStateManager _gameStateManager; } 

ここで、任意の数のコむンを远加および削陀する2぀の方法を远加したした。 コヌドでよく目にする暙準的なアプロヌチは、ビゞネスロゞックを盎接UIにプッシュするこずです。


だからそれをしないでください:)。 しかし、今のずころ、小さなプロトタむプが機胜するこずを確認したしょう。


UIスクリヌンショットト


ボタンが機胜し、起動時に状態が保存および埩元されたす。


それでは、コヌドを確認したしょう。


GameStateを倉曎する別のタむプのチヌムを䜜成したしょう。


 public interface ICommand { } public interface IGameStateCommand : ICommand { void Execute(GameState gameState); } 

単䞀のタむプのコマンドを瀺すために、共通むンタヌフェヌスを空にしたす。 GameStateを倉曎するコマンドの堎合、状態をパラメヌタヌずしお受け取るExecuteメ゜ッドを瀺したす。


先ほど瀺したような状態を倉曎するコマンドを実行するサヌビスを䜜成したしょう。 あらゆるタむプのコマンドに適合するように、むンタヌフェヌスを汎甚化したす。


 public interface ICommandsExecutor<TCommand> where TCommand: ICommand { void Execute(TCommand command); } public class GameStateCommandsExecutor : ICommandsExecutor<IGameStateCommand> { public GameStateCommandsExecutor(IGameStateManager gameStateManager) { _gameStateManager = gameStateManager; } public void Execute(IGameStateCommand command) { command.Execute(_gameStateManager.GameState); } private readonly IGameStateManager _gameStateManager; } 

マネヌゞャヌをDIに登録したす。


 public class BindingsInstaller : MonoInstaller<BindingsInstaller> { public override void InstallBindings() { Container.Bind<IGameStateManager>().To<LocalGameStateManager>().AsSingle(); Container.Bind<Loader>().FromNewComponentOnNewGameObject().AsSingle().NonLazy(); // added this line Container.Bind<ICommandsExecutor<IGameStateCommand>>().To<GameStateCommandsExecutor>().AsSingle(); } } 

次に、チヌム自䜓の実装を行いたす。


 public class AddCoinsCommand : IGameStateCommand { public AddCoinsCommand(int amount) { _amount = amount; } public void Execute(GameState gameState) { gameState.coins += _amount; } private int _amount; } 

コマンドを䜿甚するようにCoinsViewを倉曎したす。


 public class CoinsView : MonoBehaviour { public Text currencyText; [Inject] public void Init(IGameStateManager gameStateManager, ICommandsExecutor<IGameStateCommand> commandsExecutor) { _gameStateManager = gameStateManager; _commandsExecutor = commandsExecutor; UpdateView(); } public void AddCoins() { var cmd = new AddCoinsCommand(Random.Range(1, 100)); _commandsExecutor.Execute(cmd); UpdateView(); } public void RemoveCoins() { var cmd = new AddCoinsCommand(-Random.Range(1, 100)); _commandsExecutor.Execute(cmd); UpdateView(); } public void UpdateView() { currencyText.text = "Coins: " + _gameStateManager.GameState.coins; } private IGameStateManager _gameStateManager; private ICommandsExecutor<IGameStateCommand> _commandsExecutor; } 

CoinsViewはGameStateを読み取り専甚ずしお䜿甚するようになりたした。 たた、状態の倉曎はすべおチヌムを通じお行われたす。


ここで写真を台無しにしおいるのは、UpdateViewの手動呌び出しです。 圌に電話するのを忘れるこずができたす。 たたは、別のビュヌからコマンドを送信しおステヌタスを曎新できたす。


ICommandExecutorステヌタス曎新むベントを远加したす。 さらに、Executorのゲヌム状態コマンド甚に別のむンタヌフェむス゚むリアスを䜜成しお、ゞェネリックの䜙分なタむプを非衚瀺にしたす。


 public interface ICommandsExecutor<TState, TCommand> { // added event event System.Action<TState> stateUpdated; void Execute(TCommand command); } public interface IGameStateCommandsExecutor : ICommandsExecutor<GameState, IGameStateCommand> { } 

DIで登録を曎新する


 public class BindingsInstaller : MonoInstaller<BindingsInstaller> { public override void InstallBindings() { Container.Bind<IGameStateManager>().To<LocalGameStateManager>().AsSingle(); Container.Bind<Loader>().FromNewComponentOnNewGameObject().AsSingle().NonLazy(); // updated this line Container.Bind<IGameStateCommandsExecutor>() .To<DefaultCommandsExecutor>().AsSingle(); } } 

むベントをDefaultCommandsExecutor远加したす。


 public class DefaultCommandsExecutor : IGameStateCommandsExecutor { // this event added public event Action<GameState> stateUpdated { add { _stateUpdated += value; if (value != null) { value(_gameStateManager.GameState); } } remove { _stateUpdated -= value; } } public DefaultCommandsExecutor(IGameStateManager gameStateManager) { _gameStateManager = gameStateManager; } public void Execute(IGameStateCommand command) { command.Execute(_gameStateManager.GameState); // these lines added if (_stateUpdate != null) { _stateUpdated(_gameStateManager.GameState); } } private readonly IGameStateManager _gameStateManager; // this line added private Action<GameState> _stateUpdated; } 

むベントの実装に泚意を払う䟡倀がありたす。 executorはむベント内でのみ状態をファンブルするため、サブスクラむブするずきにすぐにプルするこずが重芁です。


最埌に、ビュヌを曎新したす。


 public class CoinsView : MonoBehaviour { public Text currencyText; [Inject] public void Init(IGameStateCommandsExecutor commandsExecutor) { _commandsExecutor = commandsExecutor; _commandsExecutor.stateUpdated += UpdateView; } public void AddCoins() { var cmd = new AddCoinsCommand(Random.Range(1, 100)); _commandsExecutor.Execute(cmd); } public void RemoveCoins() { var cmd = new AddCoinsCommand(-Random.Range(1, 100)); _commandsExecutor.Execute(cmd); } public void UpdateView(GameState gameState) { currencyText.text = "Coins: " + gameState.coins; } private void OnDestroy() { _commandsExecutor.stateUpdated -= UpdateView; } private IGameStateCommandsExecutor _commandsExecutor; } 

IGameStateManagerはGameStateをパラメヌタヌずしお取埗するため、IGameStateManagerはViewに必芁なくなりたした。 玠晎らしい、䞍芁な䟝存症を取り陀きたした UpdateView自䜓は、 IGameStateCommandsExecutorむベントにサブスクラむブしたす。 状態が倉化するず呌び出されたす。 たた、OnDestroyのむベントから退䌚するこずを忘れないでください。


これがアプロヌチです。 ずおもきれい。 耇雑ではありたせん。 今では、月の特定の段階でのみ再珟されるある条件で、ある堎所でUpdateViewを呌び出すこずを忘れるこずはできたせん。


じゃあ 吐き出されお先ぞ進むず、さらに倚くの利点がありたす。


コマンドの履歎をタむムマシンずしお䜿甚しお、耇雑なロゞックをデバッグしたす


バグをどのようにテストしたすか アプリケヌションを起動し、手順に埓っおバグを再珟したす。 倚くの堎合、これらの手順は手動で実行され、UIの操䜜、ボタンの突刺し、すべおの䜜業が行われたす。


バグが単玔な堎合、たたはバグを再珟するための条件を簡単に繰り返すこずができれば、すべおが正垞です。 しかし、バグがネットワヌクロゞックず時間に関係しおいる堎合はどうでしょう。 たずえば、ゲヌムには10分間続くむベントがありたす。 バグはむベントの最埌に発生したす。


各テストの反埩には、少なくずも 10分かかりたす。 通垞、いく぀かの反埩が必芁であり、それらの間で䜕かを修埩する必芁がありたす。


䞊蚘のパタヌンを䜿甚した興味深いトリックを玹介したす。これにより、頭痛が軜枛されたす。


前の段萜のコヌドでは、バグが明らかに入り蟌んでいたす。 結局のずころ、コむンの数は負になる可胜性がありたす。 もちろん、このケヌスは最も難しいものずはほど遠いですが、あなたが良い想像力を持っおいるこずを願っおいたす。


ロゞックが耇雑で、バグを毎回再珟するのは面倒だず想像しおください。 しかし、ここで私たちは、たたはテスタヌが偶然それを芋぀けたした。 このバグを「保存」できるずしたらどうでしょうか


トリック自䜓ゲヌムの開始時にあった状態ず、ゲヌムセッション䞭にコミットしたチヌムの党履歎を保存したしょう。

このデヌタは、䞀瞬で必芁な回数だけバグを再珟するのに十分です。 この堎合、UIを実行する必芁さえありたせん。 結局、壊れた状態のすべおの倉曎は履歎に保存されたす。 小さな統合テストケヌスのようなものです。


実装に移りたす。 この゜リュヌションには、むンタヌフェむスのシリアル化など、少し高床なシリアル化が含たれるため、JsonUtilityでは十分ではありたせん。 したがっお、Unity甚のJson.Netをアセットから配眮したす。


たず、ゲヌムの「初期」状態を別のファむルにコピヌするIGameStateManagerデバッグバヌゞョンを䜜成したしょう。 ぀たり、ゲヌムが開始された時点の状態です。


 public class DebugGameStateManager : LocalGameStateManager { public override void Load() { base.Load(); File.WriteAllText(BACKUP_GAMESTATE_PATH, JsonUtility.ToJson(GameState)); } public void SaveBackupAs(string name) { File.Copy( Path.Combine(Application.persistentDataPath, "gameStateBackup.json"), Path.Combine(Application.persistentDataPath, name + ".json"), true); } public void RestoreBackupState(string name) { var path = Path.Combine(Application.persistentDataPath, name + ".json"); Debug.Log("Restoring state from " + path); GameState = JsonUtility.FromJson<GameState>(File.ReadAllText(path)); } private static readonly string BACKUP_GAMESTATE_PATH = Path.Combine(Application.persistentDataPath, "gameStateBackup.json"); } 

舞台裏では、芪クラスのメ゜ッドを仮想に倉換したした。 これは挔習ずしお残しおおきたす。 他のすべおに、 SaveBackupAsメ゜ッドがSaveBackupAsさSaveBackupAs 。これは、将来的に特定の名前で「キャスト」を保存できるようにするために必芁になりたす。


次に、コマンドの履歎を保存できる゚グれキュヌタヌのデビュヌバヌゞョンを䜜成し、通垞は「初期状態+チヌム」ずいう圢匏の完党なキャストを保存したす。


 public class DebugCommandsExecutor : DefaultCommandsExecutor { public IList<IGameStateCommand> commandsHistory { get { return _commands; } } public DebugCommandsExecutor(DebugGameStateManager gameStateManager) : base(gameStateManager) { _debugGameStateManager = gameStateManager; } public void SaveReplay(string name) { _debugGameStateManager.SaveBackupAs(name); File.WriteAllText(GetReplayFile(name), JsonConvert.SerializeObject(new CommandsHistory { commands = _commands }, _jsonSettings)); } public void LoadReplay(string name) { _debugGameStateManager.RestoreBackupState(name); _commands = JsonConvert.DeserializeObject<CommandsHistory>( File.ReadAllText(GetReplayFile(name)), _jsonSettings ).commands; _stateUpdated(_gameStateManager.GameState); } public void Replay(string name, int toIndex) { _debugGameStateManager.RestoreBackupState(name); LoadReplay(name); var history = _commands; _commands = new List<IGameStateCommand>(); for (int i = 0; i < Math.Min(toIndex, history.Count); ++i) { Execute(history[i]); } _commands = history; } private string GetReplayFile(string name) { return Path.Combine(Application.persistentDataPath, name + "_commands.json"); } public override void Execute(IGameStateCommand command) { _commands.Add(command); base.Execute(command); } private List<IGameStateCommand> _commands = new List<IGameStateCommand>(); public class CommandsHistory { public List<IGameStateCommand> commands; } private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }; private readonly DebugGameStateManager _debugGameStateManager; } 

ここでは、JsonUtilityの暙準機胜では䞍十分であるこずは明らかです。 シリアル化蚭定にTypeNameHandlingを蚭定するTypeNameHandlingたした。これにより、ナゲットをロヌド/保存するずきに、コマンドが型付けされたオブゞェクトにシリアル化解陀されたした。


この゚グれキュヌタヌに぀いお他に泚目すべきこずは䜕ですか



リリヌスプロゞェクトでは、履歎がメモリをいっぱいにしたくないため、DEBUG定矩がある堎合にのみ、このサヌビスをDIに登録したす。


 public class BindingsInstaller : MonoInstaller<BindingsInstaller> { public override void InstallBindings() { Container.Bind<Loader>().FromNewComponentOnNewGameObject().AsSingle().NonLazy(); #if DEBUG Container.Bind<IGameStateManager>().To<DebugGameStateManager>().AsSingle(); Container.Bind<DebugGameStateManager>().AsSingle(); Container.Bind<IGameStateCommandsExecutor>().To<DebugCommandsExecutor>().AsSingle(); #else Container.Bind<IGameStateManager>().To<LocalGameStateManager>().AsSingle(); Container.Bind<IGameStateCommandsExecutor>().To<DefaultCommandsExecutor>().AsSingle(); #endif } } 

そうそう、シリアル化のためにチヌムを準備する必芁がありたす。


 public class AddCoinsCommand : IGameStateCommand { public AddCoinsCommand(int amount) { _amount = amount; } public void Execute(GameState gameState) { gameState.coins += _amount; } public override string ToString() { return GetType().ToString() + " " + _amount; } [JsonProperty("amount")] private int _amount; } 

JsonProperty, . ToString(), .


, "DEBUG" Player Settings -> Other Settings -> Scripting define symbols.


/ Unity. EditorWindow.


 public class CommandsHistoryWindow : EditorWindow { [MenuItem("Window/CommandsHistoryWindow")] public static CommandsHistoryWindow GetOrCreateWindow() { var window = EditorWindow.GetWindow<CommandsHistoryWindow>(); window.titleContent = new GUIContent("CommandsHistoryWindow"); return window; } public void OnGUI() { // this part is required to get // DI context of the scene var sceneContext = GameObject.FindObjectOfType<SceneContext>(); if (sceneContext == null || sceneContext.Container == null) { return; } // this guard ensures that OnGUI runs only when IGameStateCommandExecutor exists // in other words only in runtime var executor = sceneContext.Container.TryResolve<IGameStateCommandsExecutor>() as DebugCommandsExecutor; if (executor == null) { return; } // general buttons to load and save "snapshot" EditorGUILayout.BeginHorizontal(); _replayName = EditorGUILayout.TextField("Replay name", _replayName); if (GUILayout.Button("Save")) { executor.SaveReplay(_replayName); } if (GUILayout.Button("Load")) { executor.LoadReplay(_replayName); } EditorGUILayout.EndHorizontal(); // and the main block which allows us to walk through commands step by step EditorGUILayout.LabelField("Commands: " + executor.commandsHistory.Count); for (int i = 0; i < executor.commandsHistory.Count; ++i) { var cmd = executor.commandsHistory[i]; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(cmd.ToString()); if (GUILayout.Button("Step to")) { executor.Replay(_replayName, i + 1); } EditorGUILayout.EndHorizontal(); } } private string _replayName; } 

. ?


commandHistoryWindowのアニメヌションGIF


"initial" , , .
, , , .


version1.


Step to, "" , .


. . " " , . , "negativeCoins" save.


, negativeCoins.json negativeCoins_commands.json, . , negativeCoins, Load . .


, , UI, . .


. , "", , , .


, .


 public class AddCoinsCommand : IGameStateCommand { public AddCoinsCommand(int amount) { _amount = amount; } public void Execute(GameState gameState) { gameState.coins += _amount; // this is the fix if (gameState.coins < 0) { gameState.coins = 0; } } public override string ToString() { return GetType().ToString() + " " + _amount; } [JsonProperty("amount")] private int _amount; } 

version1 , .


CommandsHistoryWindowで再生される修正されたバグのアニメヌションGIF


, . !


たずめるず


Command. . , .


:



UI, Flux, UI.


, , , , , .


, , , , . , /. =).


. , , UI, , . , GameStateManager'a, UI .


UI — , . , , . .


.


, =), .



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


All Articles