アクションに副作用があるターンベースのゲームの設計


19

私はゲームDominionのコンピューター版を書いています。アクションカード、​​トレジャーカード、および勝利ポイントカードがプレイヤーの個人デッキに蓄積される、ターンベースのカードゲームです。クラス構造はかなりよく発達していて、ゲームロジックの設計を始めています。私はpythonを使用していますが、pygameで簡単なGUIを後で追加するかもしれません。

プレイヤーのターンシーケンスは、非常に単純なステートマシンによって管理されます。時計回りに回すと、プレイヤーはゲームが終了するまでゲームを終了できません。シングルターンのプレイもステートマシンです。一般的に、プレイヤーは「アクションフェーズ」、「購入フェーズ」、「クリーンアップフェーズ」をこの順番で通過します。ターンベースのゲームエンジンを実装する方法の質問への回答に基づいて、状態マシンはこの状況の標準的な手法です。

私の問題は、プレイヤーのアクションフェーズ中に、自分自身または他のプレイヤーの1人以上に副作用のあるアクションカードを使用できることです。たとえば、1枚のアクションカードを使用すると、プレーヤーは現在のターンの終了後すぐに2ターン目を取ることができます。別のアクションカードを使用すると、他のすべてのプレーヤーは手札から2枚のカードを捨てます。さらに別のアクションカードは、現在のターンでは何も行いませんが、プレーヤーは次のターンに追加のカードを引くことができます。物事をさらに複雑にするために、新しいカードを追加するゲームへの新しい拡張が頻繁にあります。すべてのアクションカードの結果をゲームのステートマシンにハードコーディングすると、見苦しくなり、適応できなくなります。ターンベースの戦略ループへの答え この問題を解決するための設計に取り組む詳細レベルには入りません。

ターン内で行われるアクションによってターンを取るための一般的なパターンを変更できるという事実を包含するために、どのようなプログラミングモデルを使用すべきですか?ゲームオブジェクトは、すべてのアクションカードの効果を追跡する必要がありますか?または、カードに独自の効果を実装する必要がある場合(たとえば、インターフェイスを実装することによって)、十分なパワーを与えるにはどのようなセットアップが必要ですか?この問題に対するいくつかの解決策を考えましたが、それを解決する標準的な方法があるかどうか疑問に思っています。具体的には、アクションカードがプレイされた結果としてすべてのプレイヤーがしなければならないアクションを追跡する責任を負うオブジェクト/クラス/その他、および通常のシーケンスの一時的な変更との関係を知りたいターンステートマシン。


2
こんにちは、Apis Utilis、GDSEへようこそ。あなたの質問はよく書かれており、関連する質問を参照したことは素晴らしいことです。しかし、あなたの質問は多くの異なる問題を扱っているので、それを完全に網羅するには、おそらく膨大な質問が必要になるでしょう。あなたはまだ良い答えを得るかもしれませんが、あなたがあなたの問題をさらに分解するならば、あなた自身とサイトは利益を得るでしょう。シンプルなゲームの構築から始めて、Dominionを構築することもできますか?
michael.bartnett

1
...私は修正がゲームの状態という各カードにスクリプトを与えることから始めたい、と奇妙な何も起こっていない場合、デフォルトのターンのルールに頼る
ヤリKomppa

回答:


11

強力なスクリプト言語を使用してカード効果を定義することが道だと、Jari Komppaに同意します。しかし、最大の柔軟性の鍵はスクリプト可能なイベント処理であると信じています。

カードが後のゲームイベントとやり取りできるようにするために、スクリプトAPIを追加して、ゲームフェーズの開始と終了、またはプレーヤーが実行できる特定のアクションなど、特定のイベントに「スクリプトフック」を追加できます。つまり、カードがプレイされたときに実行されるスクリプトは、特定のフェーズに到達したときに呼び出される関数を登録できることを意味します。各イベントに登録できる機能の数に制限はありません。複数ある場合、それらは登録の順番で呼び出されます(もちろん、何か違うことを言う中核的なゲームルールがない限り)。

これらのフックは、すべてのプレーヤーまたは特定のプレーヤーのみに登録できるはずです。また、フックを呼び出し続けるかどうかを自分で決定するために、フックの可能性を追加することをお勧めします。これらの例では、フック関数の戻り値(trueまたはfalse)を使用してこれを表現しています。

ダブルターンカードは次のようになります。

add_event_hook('cleanup_phase_end', current_player, function {
     setNextPlayer(current_player); // make the player take another turn
     return false; // unregister this hook afterwards
});

(Dominionに「クリーンアップフェーズ」のようなものがあるかどうかはわかりません。この例では、プレイヤーのターンの仮想の最終フェーズです)

すべてのプレイヤーがドローフェーズの開始時に追加のカードを引くことを可能にするカードは、次のようになります。

add_event_hook('draw_phase_begin', NULL, function {
    drawCard(current_player); // draw a card
    return true; // keep doing this until the hook is removed explicitely
});

ターゲットプレイヤーがカードをプレイするたびにヒットポイントを失うカードは、次のようになります。

add_event_hook('play_card', target_player, function {
    changeHitPoints(target_player, -1); // remove a hit point
    return true; 
});

カードを引く、ヒットポイントを失うなどのゲームアクションをハードコーディングすることはできません。それらの完全な定義(「カードを引く」とは正確に何を意味するか)はコアゲームメカニクスの一部です。たとえば、何らかの理由でカードを引く必要があり、デッキが空の場合、ゲームに負けてしまうTCGを知っています。このルールは、ルールブックにあるため、カードを引くすべてのカードに印刷されるわけではありません。そのため、すべてのカードのスクリプトでその損失状態をチェックする必要はありません。そのようなことをチェックすることは、ハードコーディングされたdrawCard()関数の一部である必要があります(ちなみに、これはフック可能なイベントの良い候補にもなります)。

ちなみに、将来のエディションが思い付く可能性のあるすべての不明瞭なメカニックについて事前に計画できる可能性は低いため、何をするにしても、将来のエディションに時々新しい機能を追加する必要があります(これでケース、ミニゲームを投げる紙吹雪)。


1
ワオ。そのカオス紙吹雪のこと。
ヤリコンパ

@Philippというすばらしい答えです。これは、Dominionで行われる多くのことを処理します。ただし、カードがプレイされるとすぐに発生しなければならないアクションがあります。つまり、他のプレイヤーに自分のライブラリの一番上のカードを裏返し、現在のプレイヤーに「キープ」または「ディスカード」と言わせるカードをプレイします。イベントフックを作成して、そのような即時のアクションを処理しますか、それともカードをスクリプト化する追加の方法を考え出す必要がありますか?
-18:

2
すぐに何かを行う必要がある場合、スクリプトはフック関数を登録せずに適切な関数を直接呼び出す必要があります。
フィリップ

@JariKomppa:Ungluedセットは意図的に無意味で、意味のないクレイジーなカードでいっぱいでした。私のお気に入りは、特定の単語を言ったときに全員にダメージを与えるカードでした。「the」を選択しました。
ジャックエイドリー

9

私はこの問題-柔軟なコンピューター化されたカードゲームエンジン-を少し前に考えました。

最初に、Chez GeekやFluxx(およびDominion)のような複雑なカードゲームでは、カードをスクリプト化可能にする必要があります。基本的に、各カードにはさまざまな方法でゲームの状態を変更する可能性のある独自のスクリプトが付属しています。これにより、スクリプトは現在考えられないことを実行できる可能性がありますが、将来の拡張が行われる可能性があるため、システムに将来の保証を与えることができます。

第二に、厳格な「ターン」が問題を引き起こしている可能性があります。

「2枚のカードを捨てる」などの「特別なターン」を含む、ある種の「ターンスタック」が必要です。スタックが空になると、デフォルトの通常のターンが続行されます。

Fluxxでは、1ターンが次のようになる可能性があります。

  • N個のカードを選択します(現在のルールに記載されているとおり、カードで変更可能)
  • N枚のカードをプレイします(現在のルールに記載されているとおり、カードで変更可能)
    • カードの1つは「テイク3、プレイ2」です。
      • それらのカードの1つは、「次のターン」
    • カードの1つは「破棄して引く」場合があります
  • ターンを開始したときよりも多くのカードを選択するようにルールを変更した場合、より多くのカードを選択してください
  • 手の少ないカードのルールを変更した場合、他の全員がすぐにカードを破棄する必要があります
  • ターンが終了したら、N枚のカード(再びカードで変更可能)になるまでカードを捨ててから、別のターンを取ります(上記の混乱の中で「別のターンを取る」カードをプレイした場合)。

..などなど。したがって、上記の乱用を処理できるターン構造を設計することは、かなり注意が必要です。さらに、「いつでも」カード(「chez geek」など)を使用した多数のゲームでは、「いつでも」カードは、最後にプレイしたカードをキャンセルするなどして、通常のフローを中断できます。

基本的に、非常に柔軟なターン構造の設計から始め、スクリプトとして記述できるように設計します(各ゲームは、基本的なゲーム構造を処理する独自の「マスタースクリプト」を必要とするため)。次に、すべてのカードがスクリプト可能になっている必要があります。ほとんどのカードはおそらく奇妙なことはしませんが、他のカードはします。カードにはさまざまな属性があります-手元に置いて、いつでもプレイできるか、資産として保存できるか(fluxの「キーパー」など)、または「chezオタク」のさまざまなものを食べ物のように)...

私は実際にこれを実装し始めたことはないので、実際には他の多くの課題を見つけるかもしれません。開始する最も簡単な方法は、実装するシステムについて知っているものから始め、スクリプト可能な方法でそれらを実装し、可能な限り最小限に設定することです。基本システム-多く。=)


これは素晴らしい答えであり、もし可能なら両方を受け入れたでしょう。私は評判の低い人の答えを受け入れることでネクタイを破りました:)
アピスウティリス

問題ありません、私は今までに慣れています.. =)
ヤリコンパ

0

ハースストーンは関連性のあることをしているようで、正直なところ、データ指向設計のECSエンジンを使用することで、柔軟性を実現する最良の方法だと思います。ハースストーンのクローンを作ろうとしていたが、そうでなければ不可能だった。すべてのエッジケース。あなたがこれらの奇妙なエッジケースの多くに直面している場合、それはおそらくそれについて行くための最良の方法です。このテクニックを試した最近の経験から私はかなり偏見があります。

編集:ECSは、必要な柔軟性と最適化の種類によっては必要ない場合もあります。これを実現する方法の1つにすぎません。DOD私は手続き型プログラミングと誤解していましたが、それらは多く関連しています。私が言いたいのは OOPを完全にまたは少なくともほとんど廃止することを検討し、代わりにデータとその編成方法に注意を集中する必要があること。継承とメソッドを避けてください。代わりに、公開データ(システム)に焦点を合わせて、カードデータを操作します。各アクションは、テンプレート化されたものやあらゆる種類のロジックではなく、代わりに生データです。システムがそれを使用してロジックを実行する場所。整数の切り替えの場合、または整数を使用して関数ポインターの配列にアクセスすると、入力データから目的のロジックを効率的に把握できます。

従うべき基本的なルールは、ロジックをデータと直接結び付けないようにすること、データが相互に依存することを可能な限り避けること(例外が適用される場合があります)、および手が届かないと感じる柔軟なロジックが必要な場合です...データに変換することを検討してください。

これを行うことには利点があります。各カードには、アクションを表す列挙値または文字列が含まれている場合があります。このインターンを使用すると、テキストファイルまたはjsonファイルを使用してカードを設計し、プログラムでカードを自動的にインポートできます。プレイヤーのアクションをデータのリストにすると、特にハースストーンのような過去のロジックにカードが依存している場合や、ゲームを保存したい場合やゲームのリプレイを任意の時点で保存したい場合に、これにより柔軟性がさらに高まります。AIをより簡単に作成できる可能性があります。特に、「ビヘイビアツリー」ではなく「ユーティリティシステム」を使用する場合。ポリモーフィックな可能性のあるオブジェクト全体をネットワーク経由で転送する方法や、事後のシリアル化の設定方法を把握する必要がないため、ネットワーキングも簡単になります。すでにゲームオブジェクトを持っているのは、単純なデータにすぎず、最終的には簡単に移動できます。最後になりますが、間違いなく、これにより、コードを心配する時間を無駄にする代わりに、プロセッサがデータをより簡単に処理できるように、データをより適切に整理できるため、最適化が容易になります。Pythonには問題があるかもしれませんが、「キャッシュライン」とゲーム開発との関係を調べてください。おそらくプロトタイプの作成には重要ではありませんが、将来的には大いに役立つでしょう。

いくつかの便利なリンク。

注:ECSでは、実行時に変数(コンポーネントと呼ばれる)を動的に追加/削除できます。ECSがどのように見えるかを示すCプログラムの例(それを行う方法がたくさんあります)。

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

テクスチャと位置データを含むエンティティの束を作成し、最後にテクスチャコンポーネント配列の3番目のインデックスにあるテクスチャコンポーネントを持つエンティティを破棄します。風変わりですが、物事を行う1つの方法です。テクスチャコンポーネントを持つすべてをレンダリングする方法の例を次に示します。

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}

1
この特定の問題を解決するために、データ指向のECSをセットアップし、それを適用することをお勧めする方法についてさらに詳しく説明した場合、この回答の方が良いでしょう。
DMGregory

それを指摘してくれてありがとう。
Blue_Pyro

一般的に、この種のアプローチを設定する方法を誰かに「どのように」伝えるのは悪いと思いますが、代わりに独自のソリューションを設計させます。それは練習の良い方法であることが証明され、問題に対する潜在的に良い解決策を可能にします。このようにロジックよりもデータについて考えると、同じことを達成する方法がたくさんあり、すべてがアプリケーションのニーズに依存することになります。プログラマーの時間/知識と同様。
Blue_Pyro
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.