ロールバックを許可するためにゲームの動きを保存する最良の方法は何ですか?


8

ゲームフローを制御するゲームクラスと、ゲームクラスに接続されたプレーヤーを含むボードゲームを開発しています。ボードは単なるビジュアルクラスですが、動きの制御はすべてゲームによって行われます。
プレーヤーは、この種の動きが許可されていないことをプレイヤーに伝えるゲームにつながる誤った動きを試みる可能性があります(その背後にある理由を伝えます)。しかし、場合によっては、プレイヤーは動きを作ってそれが最良の動きではないことに気づくか、ボードをクリックするのを忘れるか、または別のアプローチを試してみたいだけです。
ゲームを保存して読み込むことができるので、移動前にすべてのデータを保存してロールバックを許可せずに、ユーザーが最後に自動保存されたターンを読み込むことができます。これは素晴らしいアプローチのように見えますが、ユーザーとのやり取りが含まれており、ボードの再描画はユーザーにとって退屈な作業になる可能性があります。この種のことをするより良い方法はありますか、それともアーキテクチャが本当に重要なのですか?

ゲームクラスとプレーヤークラスは複雑ではないため、クラスのクローンを作成することをお勧めします。または、ゲームデータをゲームルールから分離してより適切な方法を選択します。

更新:このゲームの仕組み:移動(簡単な移動)を行うメインボードと、その移動に反応するプレーヤーボードがメインボード上にあります。また、他のプレイヤーの動きや自分の動きに応じたリアクションムーブがあり、自分の番でないときにリアクションしてボード上で行動することもできます。すべての動きを元に戻すことはできないかもしれませんが、現在の答えの1つに浮かぶ元に戻す/やり直しのアイデアが気に入っています。



1
ボード上のすべてのオブジェクトの履歴を毎ターン保存するだけです。
ラムハウンド2013年

2
@Ramhound:そのようにすると、プログラムは簡単にかなり...よくなるかもしれません... RAMハウンド。;)
メイソンウィーラー

1
@MasonWheeler-特定の時点を超えて履歴を追跡する必要はありません。多くのプログラムは同様の機能で大量のデータを保存し、メモリの問題はありません。
ラムハウンド

スタックに移動座標を保存できますか?これはチェスゲームで行われます。YMMVは、ターンベースのボードゲームか、ポンのような滑らかな動きかによって異なります。
mike30 2013年

回答:


16

行われたすべての移動の履歴(およびその他の非決定的なイベント)を保存しないのはなぜですか?そうすれば、特定のゲーム状態をいつでも再構築できます。

これは、すべてのゲームの状態を格納するよりも大幅に少ないストレージ領域を必要とし、実装はかなり簡単です。


2
追加ボーナスとして、ある時点で、StarCraftと同様のリプレイ機能を追加できます。
ジョナサンリッチ

2
+1これは、他のコンテキストで使用されるイベントソーシングと呼ばれる手法に似ています。
MattDavey 2013年

1
@MattDaveyによるコメントこの手法を素晴らしい方法で説明しています。ゲームがどれほど複雑であっても、実装はかなり簡単なので、これを受け入れます。
gbianchi 2013年

イベントソーシングは進むべき道です-ゲームの任意のポイントに移動します
Murph

8

Undoシステムの構築は、概念的には非常に単純です。変更を追跡する必要があるだけです。スタックと、ゲームの状態の一部を表すオブジェクトタイプが必要です。スタックは、ゲーム状態オブジェクトの配列/リストのスタックでなければなりません。

トリックは、人々がする動きを記録しないことです。これは他の回答でも見られますが、実行することはできますが、はるかに複雑です。代わり、移動が行われる前のゲームボードの外観を記録します。たとえば、駒を移動すると、2つの変更が加えられました。駒は1つの正方形を残し、駒は新しい正方形に配置されました。移動を開始する前にこれらの2つの正方形がどのようになっていたかを示す配列を作成した場合、移動が行われる前の状態に2つの正方形を復元するだけで、移動を元に戻すことができます。

または、ゲームの状態が正方形ではなく駒に保持されている場合、記録するのは駒が移動する前の位置です。(そして、あなたの作品がチェスでの捕獲などの他の作品と相互作用する場合、それらが変更される前に他の作品がどこにあったかを記録してください。)

移動が発生した場合:

  • 新しい元に戻すフレーム(配列)を作成する
  • 何かが変更されるたびに、現在のUndoフレームにそのオブジェクトの変更がまだない場合は、変更を適用する前に、その状態を現在のUndoフレームに追加します。
  • 移動が終わったら、元に戻すフレームをスタックにプッシュします。

ユーザーが元に戻すと言うとき:

  • スタックの一番上をポップして、元に戻すフレームをつかみます
  • フレーム内の各オブジェクトを反復処理して復元します
  • (オプション):[元に戻す]フレームを設定したときとまったく同じ方法で、ここで行われた変更を追跡し、フレームをスタックにプッシュします。これがRedoの実装方法です。(これを行う場合、新しいUndoフレームをプッシュすると、Redoスタックもクリアされます。)

4
ゲームの状態が比較的少ないメモリを使用する場合、これは良いアイデアです。私が最初に実際にオープンソースに貢献したのは、Cinelerraのアンドゥシステムを「レコード移動」モデルに変更することでした。大規模なプロジェクトでは、キーを押すたびにメガバイトの状態データがアンドゥスタックにプッシュされるためです。ボードゲームの問題ではないかもしれませんが、メモリ要件がそのように拡大する可能性がある場合は、今すぐ弾丸を噛むことをお勧めします。
カールビーレフェルト

2
@KarlBielefeldt、私はこの回答を、各時点でのゲームの状態全体ではなく、状態への変更のみを保存することを推奨していると考えています。答えには問題についての有用な考察がたくさんありますが、非常によく似たものを実際に提唱することになる、「他の答えが言うように記録の動きを記録しない」という冒頭は嫌いです。

@ dan1111:私は似たようことを主張していますが、微妙に異なります。「動きを記録する」と言うと、「変更を記録する」のように聞こえます。私が説明しているのは、「変更前の状態を記録する」ことです。これにより、元に戻すシステムがよりクリーンになります。
メイソンウィーラー

1
@MasonWheeler、私はあなたがそれを実装することを提案する方法がエレガントであることに同意します。しかし、私にとって、変化したものだけの状態を記録することは、単に動きを記録するための素晴らしい方法です。私はそれが本当にセマンティクスについての単なる議論だと思います...

1
@kuhakuいいえ。行われた動きを記録することは、変更する内容を記録することです。私はここで説明してることは、あなたが変更しているかの記録にする必要があるということですから、をあなたはそれを取り戻すことができるようにすることを、。
メイソンウィーラー

2

これを実装する1つの良い方法は、移動をCommandオブジェクトの形式でカプセル化することです。メソッドmove(Position newPosition)とを備えたコマンドインターフェースについて考えてみますundo。具象クラスは、このインターフェースを実装して、moveメソッドの場合、ボード上の現在の位置(位置はおそらく行と列の値を保持するクラス)を格納し、で識別される行と列に移動できるようにしnewPositionます。この後、メソッドはコマンド(this)をCommandオブジェクトのグローバルスタックに追加します。

ここで、ユーザーが前の手順にロールバックする必要がある場合は、スタックから最後のCommandインスタンスをポップして、そのundoメソッドを呼び出すだけです。これにより、必要な数のステップにロールバックすることもできます。

お役に立てば幸いです。


2
コマンドはクラシックなデザインパターンで、まさに私が提案しようとしていたものです。さらに、各コマンドには、移動を行ったプレーヤーへのポインターが必要です。これにより、リストをたどって各コマンドの「toString」タイプの関数を呼び出すだけで、ゲーム全体のトランスクリプトを最後に取得できます。たとえば、「Annieはカジュアルな顧客(小麦、カボチャ、カブ)とヘルパー(買い物客)を追加しました。通常の顧客(小麦、カボチャ)に+8現金で配達された請求書」は、2人のプレーヤーがターンインした場合の説明になります。 「ロヤンの門で」。
John Munsch 2013年

いい提案のジョン。
Samir Hasan

1

古代のスレッドをぶつけますが、少なくとも複雑なシナリオでこの問題を解決するために私が見つけた最も簡単な方法は、ユーザー操作の前にすべてのいまいましいものをコピーすることです...

...無駄に聞こえますよね?それが本当に無駄である場合を除いて、次のステップはコピーを安くして、次のステップで変更されない部分が深くコピーされないようにすることです。

私の場合、ギガバイトのデータを扱うことがよくありますが、ユーザーは操作のためにメガバイトに触れるだけなので、すべてをコピーすることは通常、非常に高価で途方もなく無駄になります。しかし、これらのデータ構造を浅いものを中心に設定しました変更されないものをコピーすると、これが最も簡単な解決策であり、不変の永続データ構造、より安全なマルチスレッディング、何かを入出力する新しいネットワーク、非破壊編集、インスタンス化など、多くの新しい可能性が開かれますオブジェクト(高価なメッシュデータを浅くコピーできるが、クローンはメモリをほとんど使用せずに独自の位置に配置する)など

store all scene/application state in undo
perform user operation

on undo/redo:
   swap stored state with application state

それ以降のすべてのプロジェクトは、すべての小さな状態変化を注意深く記録する必要がなく、同種モデルに適合しない数十の異なるタイプのデータ(画像/テクスチャのペイント、プロパティの変更、メッシュの変更、重量の変更、階層的なシーンの変更、プロパティに関係のないシェーダーの変更、別のものとの交換、エンベロープの変更など)。代わりに、それがメモリと時間の点で無駄になりすぎる場合は、より洗練された取り消しシステムを考え出すのではなく、最も高価なすべてのデータ構造で部分的なコピーを作成できるように、コピーを最適化しようとします。これは、常に正しく機能する取り消し機能をすぐに正しく機能させることができる最適化の詳細であり、常に正しく機能することは、過去に、

もう1つの方法は、デルタ(変更)を個別に記録することです。以前はこれを使用していましたが、非常に複雑な大規模なソフトウェアの場合、プラグイン開発者にとって本当に簡単だったため、エラーが発生しやすく、面倒でした。システムにまったく新しい概念が導入されたときに、元に戻すスタックの変更を記録するのを忘れます。

ゲーム状態全体をコピーするか、デルタを記録するかに関係なく、1つのことは、可能であればポインター(CまたはC ++を想定)を回避することです。そうしないと、状況が大幅に複雑になります。インデックスを使用すると、コピー(アプリケーションの状態全体またはその一部のいずれか)によってインデックスが無効になることがないため、代わりにすべてが単純になります。グラフデータ構造の例として、全体をコピーするか、ユーザーがグラフで新しい接続を作成したり接続を解除したりしたときにデルタを記録したりする場合、状態は古いノードへのリンクを保存する必要があり、実行することができます。無効化の問題やその種のもの。接続が配列への相対インデックスを使用する場合、これらのインデックスがポインターよりも無効になるのを防ぐのがはるかに簡単であるため、はるかに簡単です。


1
古代のスレッド..取り消しシステムでの毎日の問題;)
gbianchi

0

プレイヤーが行った(有効な)動きの取り消しリストを維持します。
リスト内の各アイテムには、移動が行われる直前の状態にゲームの状態を復元するのに十分な情報が含まれている必要があります。存在するゲーム状態の量と、サポートしたい元に戻す手順の数に応じて、これはゲーム状態のスナップショット、またはゲームエンジンに移動を元に戻す方法を指示する情報になる可能性があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.