柔軟なバフ/デバフシステムを実装する方法は何ですか?


66

概要:

RPGのような統計を備えた多くのゲームでは、キャラクターの「バフ」が可能です。単純な「25%の追加ダメージ」から「ヒット時に攻撃者に15ダメージを戻す」などのより複雑なものまであります。

各タイプのバフの詳細は、実際には関係ありません。私は(おそらくオブジェクト指向の)任意のバフを処理する方法を探しています。

詳細:

私の特定のケースでは、ターンベースの戦闘環境に複数のキャラクターがいるため、「OnTurnStart」、「OnReceiveDamage」などのイベントにバフが結び付けられることを想定しました。関連するイベントのみがオーバーロードされます。その後、各キャラクターに現在適用されているバフのベクトルを持たせることができます。

この解決策は理にかなっていますか?私は確かに何十ものイベントタイプが必要であることを見ることができます。バフごとに新しいサブクラスを作成するのはやり過ぎだと感じており、バフの「相互作用」を許可していないようです。つまり、25%の追加ダメージを与える10個のバフがあったとしても、250%の追加ではなく100%の追加のみを行うように、ダメージブーストに上限を設定したい場合です。

そして、理想的には制御できるより複雑な状況があります。ゲーム開発者としては望まないかもしれない方法で、より洗練されたバフが潜在的に相互作用する方法の例を誰もが思い付くことができると確信しています。

比較的経験の浅いC ++プログラマー(私は一般に組み込みシステムでCを使用しました)として、私のソリューションは単純であり、おそらくオブジェクト指向言語を十分に活用していないように感じます。

考え?ここで誰かがかなり堅牢なバフシステムを設計したことがありますか?

編集:回答について:

主に詳細と質問に対する確固たる回答に基づいて回答を選択しましたが、回答を読むことでさらに洞察が得られました。

おそらく驚くことではないが、異なるシステムまたは微調整されたシステムは、特定の状況により良く適用されるように思われる。ゲームに最適なシステムは、適用できるバフの種類、分散、および数によって異なります。

ほぼすべての装備がバフの強さを変更できるDiablo 3(後述)のようなゲームでは、バフは単なるキャラクターのステータスですシステムであり、可能な限り良いアイデアのようです。

私がいるターンベースの状況では、イベントベースのアプローチがより適しているかもしれません。

いずれにせよ、私はまだ誰かが私を適用するためにできるようになります空想「OO」魔法の弾丸と一緒に来て願っていますターンあたり2移動距離バフ、ダメージの50%が攻撃者に戻って撮影した取引バフをし、単一のシステムで3つ以上のタイル離れたバフから攻撃された場合+ 5の強さのバフを独自のサブクラスに変えることなく、自動的に近くのタイルにテレポートします。

一番近いのは私がマークした答えだと思うが、床はまだ開いている。入力してくれたみんなに感謝します。


ブレインストーミングしているだけなので、これを回答として投稿するのではありませんが、バフのリストはどうですか?各バフには定数と因子修飾子があります。定数は+10ダメージ、係数は+ 10%ダメージブーストの1.10になります。ダメージの計算では、すべてのバフを繰り返して合計モディファイアを取得し、必要な制限を課します。これは、あらゆる種類の変更可能な属性に対して行います。ただし、複雑な場合には特別なケースメソッドが必要です。
ウィリアムマリアガー

ちなみに、装備可能な武器やアクセサリーのシステムを作成していたとき、私はすでにStatsオブジェクトにそのようなものを実装していました。あなたが言ったように、それは既存の属性を変更するだけのバフには十分なソリューションですが、もちろん、特定のバフはXターン後に期限切れになり、他のバフはエフェクトがY回発生すると期限切れになります。すでに長くなってきたので、主な質問でこれに言及してください。
gkimsey

1
メッセージングシステムによって、または手動で、または他の方法で呼び出される「onReceiveDamage」メソッドがある場合、被害を受けている人/ものへの参照を含めるのは簡単です。そのため、この情報をバフが利用できるようにすることができます

そうです、抽象Buffクラスの各イベントテンプレートには、そのような関連パラメーターが含まれると予想していました。それは確かに動作しますが、うまくスケールしないように感じるので、私はためらっています。数百の異なるバフを持つMMORPGが、100の異なるイベントから選択して、各バフに個別のクラスを定義していると想像するのは大変です。それほど多くのバフ(おそらく30に近い)を作成しているわけではありませんが、よりシンプルで、よりエレガントで、より柔軟なシステムがあれば、それを使用したいと思います。より柔軟なシステム=より興味深いバフ/能力。
gkimsey

4
これは相互作用の問題に対する良い答えではありませんが、デコレーターパターンはここでうまく適用されるように思えます。より多くのバフ(装飾)を重ねて適用するだけです。たぶん、バフを「マージ」することでインタラクションを処理するシステムがあります(たとえば、10x 25%が1つの100%バフにマージされます)。
ashes999

回答:


32

これは複雑な問題です。なぜなら、(最近では)「バフ」としてひとまとめにされるいくつかの異なることについて話しているからです。

  • プレーヤーの属性の修飾子
  • 特定のイベントで発生する特殊効果
  • 上記の組み合わせ。

特定のキャラクターのアクティブエフェクトのリストを使用して、常に最初のものを実装します。リストからの削除は、期間に基づいているか明示的に行われているかに関係なく、ここでは説明しません。各エフェクトには属性修飾子のリストが含まれており、単純な乗算を介して基になる値に適用できます。

次に、変更された属性にアクセスする関数でラップします。例えば。:

def get_current_attribute_value(attribute_id, criteria):
    val = character.raw_attribute_value[attribute_id]
    # Accumulate the modifiers
    for effect in character.all_effects:
        val = effect.apply_attribute_modifier(attribute_id, val, criteria)
    # Make sure it doesn't exceed game design boundaries
    val = apply_capping_to_final_value(val)
    return val

class Effect():
    def apply_attribute_modifier(attribute_id, val, criteria):
        if attribute_id in self.modifier_list:
            modifier = self.modifier_list[attribute_id]
            # Does the modifier apply at this time?
            if modifier.criteria == criteria:
                # Apply multiplicative modifier
                return val * modifier.amount
        else:
            return val

class Modifier():
    amount = 1.0 # default that has no effect
    criteria = None # applies all of the time

これにより、乗算効果を簡単に適用できます。付加的な効果も必要な場合は、それらを適用する順序を決定し(おそらく付加的な最後)、リストを2回実行します。(おそらく、Effectには、乗法用、加法用に別々の修飾子リストがあります)。

基準値は、「+ 20%vsアンデッド」を実装することです-エフェクトにUNDEAD値を設定し、UNDEAD値のみを渡します get_current_attribute_value()することです。し、アンデッド敵に対するダメージロールを計算するときにます。

ちなみに、基になる属性値に直接値を適用および適用解除するシステムを作成しようとするつもりはありません-最終結果は、エラーのために属性が意図した値から逸脱する可能性が非常に高いということです。(たとえば、何かを2倍してからキャップする場合、再度2で割ると、最初よりも低くなります。)

「ヒットしたときに攻撃者に15のダメージを与える」などのイベントベースのエフェクトについては、そのためのEffectクラスにメソッドを追加できます。ただし、明確で任意の動作が必要な場合(たとえば、上記のイベントの一部の効果はダメージを反映し、一部は回復し、ランダムにテレポートする可能性があります)、それを処理するカスタム関数またはクラスが必要になります。エフェクトのイベントハンドラーに関数を割り当てて、アクティブなエフェクトのイベントハンドラーを呼び出すことができます。

# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
    for effect in character.all_effects:
        effect.on_receive_damage(character, damage_info)

class Effect():
    self.on_receive_damage_handler = DoNothing # a default function that does nothing
    def on_receive_damage(character, damage_info):
        self.on_receive_damage_handler(character, damage_info)

def reflect_damage(character, damage_info):
    damage_info.attacker.receive_damage(15)

reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)

明らかに、Effectクラスにはあらゆるタイプのイベントのイベントハンドラーがあり、それぞれの場合に必要な数のハンドラー関数を割り当てることができます。それぞれがEffectをサブクラス化する必要はありません。それぞれが含まれる属性修飾子とイベントハンドラの構成によって定義されるためです。(おそらく、名前、期間なども含まれます)


2
優れたディテールの場合は+1。これは、私が見たように私の質問に正式に答えることに最も近い回答です。ここでの基本的なセットアップは、多くの柔軟性と、そうでなければ面倒なゲームロジックになる可能性のあるものの小さな抽象化を可能にするようです。あなたが言ったように、よりファンキーなエフェクトは独自のクラスを必要としますが、これは典型的な「バフ」システムのニーズの大部分を処理すると思います。
-gkimsey

ここに隠された概念的な違いを指摘するために+1。すべてが同じイベントベースの更新ロジックで動作するわけではありません。まったく異なるアプリケーションについては、@ Rossの回答を参照してください。両方が隣接して存在する必要があります。
ctietze

22

私がクラスで友人と一緒に取り組んだゲームでは、ユーザーが背の高い草やスピードアップタイルに閉じ込められたときとそうでないとき、および出血や毒のようないくつかのマイナーなもののためのバフ/デバフシステムを作成しました。

アイデアはシンプルで、Pythonで適用したものの、かなり効果的でした。

基本的に、次のようになりました。

  • ユーザーには現在適用されているバフとデバフのリストがあります(バフとデバフは比較的同じであり、結果が異なるだけであることに注意してください)
  • バフには、情報を表示するための期間、名前、テキスト、存続時間など、さまざまな属性があります。重要なのは、生存時間、持続時間、およびこのバフが適用されるアクターへの参照です。
  • バフの場合、player.apply(buff / debuff)を介してプレーヤーにアタッチすると、start()メソッドが呼び出されます。これにより、速度の増加や減速などの重要な変更がプレーヤーに適用されます。
  • その後、更新ループで各バフを反復処理し、バフが更新されます。これにより、生存時間が増加します。サブクラスは、プレイヤーを中毒させたり、プレイヤーにHPを与えたりするなどのことを実装します。
  • timeAlive> = durationを意味するバフが行われた場合、更新ロジックはバフを削除し、finish()メソッドを呼び出します。これは、プレーヤーの速度制限を削除することから小さな半径(爆弾効果を考える) DoT後)

今、実際に世界からバフを適用する方法は別の話です。しかし、ここに私の考えの食べ物があります。


1
これは私が上で説明しようとしていたことのより良い説明のように聞こえます。それは比較的簡単で、確かに理解しやすいです。あなたは本質的にそこに3つの「イベント」(OnApply、OnTimeTick、OnExpired)を言及し、それをさらに私の思考に関連付けました。現状では、ヒット時などにダメージを返すなどのことはサポートされませんが、多くのバフに対してはより優れた拡張性があります。私はむしろ、バフができることを制限しません(これは、メインゲームロジックによって呼び出される必要があるイベントの数を制限する)が、バフのスケーラビリティがより重要な場合があります。ご意見ありがとうございます!
-gkimsey

ええ、そのようなものは実装していません。それは本当にきちんとしていて素晴らしいコンセプトに聞こえます(いばらのようなものです)。
ロス

@gkimsey Thornsやその他のパッシブバフのようなものについては、Mobクラスのロジックを、ダメージやヘルスに似たパッシブスタットとして実装し、バフを適用するときにこのスタットを増やします。これにより複数のソーンバフがある場合と、インターフェイスをきれいに保つ(10バフでは10ではなく1のダメージが返される)場合が大幅に簡素化され、バフシステムがシンプルなままになります。
ダブロン

これはほとんど直感に反するほど簡単なアプローチですが、ディアブロ3をプレイするときに自分のことを考え始めました。確かに、D3には世界で最も複雑なバフシステムやインタラクションはありませんが、簡単なことではありません。これは非常に理にかなっています。それでも、潜在的に15種類のバフがあり、12種類の効果がこれに該当します。キャラクターの統計シートに奇妙なパディングが入っているようです
。...-gkimsey

11

まだこれを読んでいるかどうかはわかりませんが、この種の問題に長い間苦労しています。

私はさまざまな種類の影響システムを設計しました。ここで簡単に説明します。これはすべて私の経験に基づいています。私はすべての答えを知っていると主張していません。


静的修飾子

このタイプのシステムは、ほとんどの場合、変更を決定するために単純な整数に依存しています。たとえば、最大HP +100、攻撃+10などです。このシステムもパーセントを処理できます。スタックが制御不能にならないようにする必要があります。

このタイプのシステムで生成された値を実際にキャッシュしたことはありません。たとえば、何かの最大ヘルスを表示したい場合、その場で値を生成します。これにより、エラーが発生しにくくなり、関係者全員が理解しやすくなりました。

(私はJavaで作業しているため、以下はJavaベースですが、他の言語の一部の変更で機能するはずです)このシステムは、変更タイプの列挙を使用して、整数を使用して簡単に実行できます。最終結果は、キーと値の順序のペアを持つある種のコレクションに入れることができます。これは高速なルックアップと計算になるため、パフォーマンスは非常に良好です。

全体として、静的修飾子をフラット化するだけで非常にうまく機能します。ただし、修飾子を使用するには、getAttack、getMaxHP、getMeleeDamageなどの適切な場所にコードが存在する必要があります。

この方法が失敗するのは(私にとって)バフ間の非常に複雑な相互作用です。少しゲットーすることを除いて、相互作用をする本当の簡単な方法はありません。いくつかの単純な相互作用の可能性があります。そのためには、静的修飾子の格納方法を変更する必要があります。列挙型をキーとして使用する代わりに、文字列を使用します。この文字列は、Enum名+追加変数になります。10回のうち9回、余分な変数は使用されないため、列挙名をキーとして保持します。

簡単な例を見てみましょう:アンデッドクリーチャーに対するダメージを修正したい場合、次のような順序のペアを作成できます:(DAMAGE_Undead、10)DAMAGEはEnumであり、Undeadは追加の変数です。そのため、戦闘中に次のようなことができます。

dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead

とにかく、それはかなりうまく機能し、高速です。しかし、複雑な相互作用やどこにでも「特別な」コードがあると失敗します。たとえば、「死亡時にテレポートする確率が25%」という状況を考えてみましょう。これは「かなり」複雑なものです。上記のシステムはそれを処理できますが、次のものが必要なため、簡単ではありません。

  1. プレイヤーにこのmodがあるかどうかを確認します。
  2. どこかに、成功した場合、テレポーテーションを実行するためのコードがあります。このコードの場所はそれ自体が議論です!
  3. Modマップから適切なデータを取得します。値はどういう意味ですか?彼らもテレポートする部屋ですか?プレイヤーが2つのテレポートMODを持っている場合はどうなりますか?? 金額は加算されませんか?????? 失敗!

だから、これは私に私の次のものをもたらします:


究極のコンプレックスバフシステム

私はかつて自分で2D MMORPGを作成しようとしました。これはひどい間違いでしたが、私は多くを学びました!

影響システムを3回書き直しました。最初のものは、上記のそれほど強力ではないバリエーションを使用しました。2つ目は、私が話したいことです。

このシステムには、変更ごとに一連のクラスがあったため、ChangeHP、ChangeMaxHP、ChangeHPByPercent、ChangeMaxByPercentなどがあります。TeleportOnDeathのようなものでさえ、私にはこれらの人が何百万人もいました。

私のクラスには、次のことを行うものがありました。

  • applyAffect
  • removeAffect
  • checkForInteraction <---重要

自身の説明を適用および削除します(パーセントなどの場合、影響はHPがどれだけ増加したかを追跡し、影響が消えるのを確実にしますが、追加した量のみを削除します。これはバグでした、笑、それが正しいことを確認するのに長い時間がかかった。それでも私はそれについて良い感じがしなかった。

checkForInteractionメソッドは、恐ろしく複雑なコードです。各影響(つまり:ChangeHP)クラスには、入力影響によってこれを変更する必要があるかどうかを判断するコードがあります。たとえば、次のようなものがあった場合...

  • バフ1:攻撃時に10火炎ダメージを与える
  • バフ2:すべての火炎ダメージが25%増加します。
  • バフ3:すべての火炎ダメージが15増加します。

checkForInteractionメソッドは、これらすべての影響を処理します。これを行うには、近くにいるすべてのプレイヤーに対するそれぞれの影響をチェックする必要がありました!! これは、ある領域のスパンで複数のプレーヤーを扱った影響のタイプが原因です。これは、コードが上記のような特別なステートメントを決して持たないことを意味します-「私たちが死んだばかりなら、死亡時にテレポートを確認する必要があります」。このシステムは、適切なタイミングで自動的に正しく処理します。

このシステムを作成しようとすると、2か月ほどかかり、頭で数回爆発させました。しかし、それは非常に強力であり、非常に多くのことを行うことができます-特に私のゲームの能力について次の2つの事実を考慮すると:1.ターゲット範囲(つまり、シングル、セルフ、グループのみ、PB AEセルフ) 、PB AEターゲット、ターゲットAEなど)。2.能力は、複数の影響を与える可能性があります。

上で述べたように、これはこのゲームの3番目の影響システムの2番目でした。なぜ私はこれから離れたのですか?

このシステムは今まで見た中で最悪のパフォーマンスでした!進行中の各事柄について非常に多くのチェックを行わなければならなかったため、非常に遅くなりました。私はそれを改善しようとしましたが、失敗とみなしました。

そこで、3番目のバージョン(および別の種類のバフシステム)に進みます。


ハンドラーを含む複雑な影響クラス

したがって、これは最初の2つの組み合わせです。多くの機能と追加データを含むAffectクラスに静的変数を含めることができます。それから、何かをしたいときに、ハンドラー(私にとっては、特定のアクションのサブクラスではなく、いくつかの静的なユーティリティメソッドを呼び出します。

Affectクラスには、ターゲットタイプ、期間、使用回数、実行の機会など、ジューシーで優れたものがすべて含まれています。

状況に対処するために、たとえば死のテレポートなどの特別なコードを追加する必要があります。戦闘コードでこれを手動で確認する必要があり、存在する場合は影響のリストを取得します。このエフェクトのリストには、死亡時にテレポートするプレイヤーに現在適用されているすべてのエフェクトが含まれています。次に、それぞれを見て、実行されて成功したかどうかを確認します(最初に成功した時点で停止します)。成功したので、ハンドラーを呼び出して処理します。

必要に応じて、相互作用を行うことができます。プレーヤーなどで特定のバフを探すためのコードを記述する必要があります。優れたパフォーマンスを備えているため(以下を参照)、それを行うのはかなり効率的です。より複雑なハンドラーなどが必要になるだけです。

そのため、最初のシステムのパフォーマンスは非常に高く、2番目のシステムと同様に多くの複雑さがあります(ASほどではありません)。少なくともJavaでは、MOSTの場合の最初のほとんどのパフォーマンスを得るためにいくつかのトリッキーなことを行うことができます(つまり、enumマップ(http://docs.oracle.com/javase/6/docs/api/javaを持つ) /util/EnumMap.htmlキーとして、ArrayListを値として、これにより、すぐに影響があるかどうかを確認できます(リストが0になるか、マップにenumがないため)理由なしにプレイヤーの影響リストを継続的に繰り返します。現時点で必要な場合、影響を繰り返し処理してもかまいません。問題が生じた場合は後で最適化します)。

現在、2005年に終了したMUDを再開しました(元のFastROMコードベースの代わりにJavaでゲームを書き換えています)。最近、バフシステムをどのように実装したいのでしょうか。以前の失敗したゲームでうまく機能したため、このシステムを使用します。

うまくいけば、どこかで誰かがこれらの洞察のいくつかを役に立つと思うでしょう。


6

それらのバフの振る舞いが互いに異なる場合、各バフの異なるクラス(またはアドレス指定可能な関数)は過剰ではありません。1つは+ 10%または+ 20%のバフ(もちろん、同じクラスの2つのオブジェクトとして表現する方がよい)を持つこと、もう1つはカスタムコードを必要とする大幅に異なる効果を実装することです。ただし、ゲームロジックをカスタマイズする標準的な方法がある方が良いと思います各バフに好きなことをさせるのではなく(そして、予期しない方法で互いに干渉し、ゲームのバランスを乱す可能性もあります)。

各「攻撃サイクル」をステップに分割することをお勧めします。各ステップにはベース値、その値に適用できる変更の順序付けられたリスト(上限がある場合があります)、および最終的な上限があります。各変更にはデフォルトとして恒等変換があり、ゼロ以上のバフ/デバフの影響を受ける可能性があります。各変更の詳細は、適用されるステップによって異なります。サイクルの実装方法はあなた次第です(これまで議論してきたイベント駆動型アーキテクチャのオプションを含む)。

攻撃サイクルの一例は次のとおりです。

  • プレーヤーの攻撃を計算します(ベース+ MOD)。
  • 対戦相手の防御を計算します(ベース+ MOD)。
  • 違いを行い(そしてmodを適用し)、基本ダメージを決定します;
  • 受け流し/鎧の効果(基本ダメージの修正)を計算し、ダメージを適用します。
  • リコイル効果(基本ダメージの修正)を計算し、攻撃者に適用します。

注意する重要なことは、サイクルの早い段階でバフが適用されるほど、結果に与える影響大きくなることです。したがって、より「戦術的な」戦闘(プレイヤーのスキルがキャラクターレベルよりも重要な場合)が必要な場合は、基本的な統計に多くのバフ/デバフを作成します。よりバランスのとれた戦闘(レベルが重要な場合-MMOGでは進行速度を制限するために重要)が必要な場合は、サイクルの後半でのみバフ/デバフを使用してください。

前に述べた「修正」と「バフ」の区別には目的があります。ルールとバランスに関する決定は前者に実装できるため、それらの変更は後者のすべてのクラスの変更に反映する必要はありません。OTOH、バフの数と種類はあなたの想像力によってのみ制限されます。なぜなら、それぞれは、彼らと他の人(または他人の存在さえ)の間の可能な相互作用を考慮することなく、所望の行動を表現できるからです。

だから、質問に答える:各バフのクラスを作成するのではなく、各修正(タイプ)にクラスを作成し、修正をキャラクターではなく攻撃サイクルに結び付けます。バフは単純に(変更、キー、値)タプルのリストにすることができ、キャラクターのバフのセットに追加または削除するだけで、バフをキャラクターに適用できます。キャラクターの統計情報は、バフが適用されたときにをまったく変更する必要がない(したがって、バフが期限切れになった後に統計を間違った値に復元するリスクが少なくなります)。


これは、私が検討していた2つの実装の間のどこかにあるため、興味深いアプローチです。つまり、バフをかなり単純なステータスと結果のダメージモディファイヤに制限するか、または何でも処理できる非常に堅牢だがオーバーヘッドの高いシステムを作成するかのいずれかです。これは、前者の一種の拡張であり、シンプルなインターフェースを維持しながら「いばら」を許可します。私はそれが私が必要とするものの魔法の弾丸であるとは思わないが、確かにそれは他のアプローチよりもはるかに簡単にバランスをとるように見えるので、それが進むべき道かもしれない。ご意見ありがとうございます!
-gkimsey

3

あなたがまだそれを読んでいるかどうかはわかりませんが、私は今それをどのようにやっているのですか(コードはUE4とC ++に基づいています)。問題を2週間以上熟考した後(!!)、ようやくこれを見つけました。

http://gamedevelopment.tutsplus.com/tutorials/using-the-composite-design-pattern-for-an-rpg-attributes-system--gamedev-243

そして、クラス/構造内に単一の属性をカプセル化することは、結局のところそれほど悪い考えではないと思いました。ただし、コードリフレクションシステムに組み込まれたUE4を非常に活用しているので、多少の手直しがなければ、これはどこでも適切ではない可能性があります。

とにかく、属性を1つの構造体にラップすることから始めました。

USTRUCT(BlueprintType)
struct GAMEATTRIBUTES_API FGAAttributeBase
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY()
        FName AttributeName;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float BaseValue;
    /*
        This is maxmum value of this attribute.
    */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float ClampValue;
protected:
    float BonusValue;
    //float OldCurrentValue;
    float CurrentValue;
    float ChangedValue;

    //map of modifiers.
    //It could be TArray, but map seems easier to use in this case
    //we need to keep track of added/removed effects, and see 
    //if this effect affected this attribute.
    TMap<FGAEffectHandle, FGAModifier> Modifiers;

public:

    inline float GetFinalValue(){ return BaseValue + BonusValue; };
    inline float GetCurrentValue(){ return CurrentValue; };
    void UpdateAttribute();

    void Add(float ValueIn);
    void Subtract(float ValueIn);

    //inline float GetCurrentValue()
    //{
    //  return FMath::Clamp<float>(BaseValue + BonusValue + AccumulatedBonus, 0, GetFinalValue());;
    //}

    void AddBonus(const FGAModifier& ModifiersIn, const FGAEffectHandle& Handle);
    void RemoveBonus(const FGAEffectHandle& Handle);

    void InitializeAttribute();

    void CalculateBonus();

    inline bool operator== (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName == AttributeName);
    }

    inline bool operator!= (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName != AttributeName);
    }

    inline bool IsValid() const
    {
        return !AttributeName.IsNone();
    }
    friend uint32 GetTypeHash(const FGAAttributeBase& AttributeIn)
    {
        return AttributeIn.AttributeName.GetComparisonIndex();
    }
};

まだ完成していませんが、基本的な考え方は、この構造体が内部状態を追跡することです。属性は、エフェクトによってのみ変更できます。それらを直接変更しようとすることは安全ではなく、設計者に公開されません。属性と相互作用できるすべてのものが効果であると想定しています。アイテムからのフラットボーナスを含む。新しいアイテムが装備されると、(ハンドルとともに)新しいエフェクトが作成され、無限の期間のボーナス(プレーヤーが手動で削除する必要があるボーナス)を処理する専用マップに追加されます。新しいエフェクトが適用されると、その新しいハンドルが作成され(ハンドルは単にintであり、構造体でラップされます)、そのハンドルはこのエフェクトと対話するための手段として全体に渡され、エフェクトがまだアクティブです。エフェクトが削除されると、そのハンドルがすべての関心のあるオブジェクトにブロードキャストされ、

これの本当の重要な部分はTMapです(TMapはハッシュマップです)。FGAModifierは非常に単純な構造体です。

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

変更のタイプが含まれます。

UENUM()
enum class EGAAttributeOp : uint8
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Set,
    Precentage,

    Invalid
};

そして、属性に適用する最終的な計算値である値。

単純な関数を使用して新しい効果を追加し、次に呼び出します:

void FGAAttributeBase::CalculateBonus()
{
    float AdditiveBonus = 0;
    auto ModIt = Modifiers.CreateConstIterator();
    for (ModIt; ModIt; ++ModIt)
    {
        switch (ModIt->Value.AttributeMod)
        {
        case EGAAttributeOp::Add:
            AdditiveBonus += ModIt->Value.Value;
                break;
            default:
                break;
        }
    }
    float OldBonus = BonusValue;
    //calculate final bonus from modifiers values.
    //we don't handle stacking here. It's checked and handled before effect is added.
    BonusValue = AdditiveBonus; 
    //this is absolute maximum (not clamped right now).
    float addValue = BonusValue - OldBonus;
    //reset to max = 200
    CurrentValue = CurrentValue + addValue;
}

この関数は、エフェクトが追加または削除されるたびに、ボーナスのスタック全体を再計算することになっています。機能はまだ完了していません(ご覧のとおり)が、一般的な考え方は理解できます。

私の最大の不満は、(スタック全体の再計算を伴うことなく)Damaging / Healing属性を処理することです。これはいくらか解決したと思いますが、100%になるにはさらにテストが必要です。

いずれにせよ、属性は次のように定義されます(+ Unrealマクロ、ここでは省略):

FGAAttributeBase Health;
FGAAttributeBase Energy;

また、属性のCurrentValueの処理について100%確信はありませんが、動作するはずです。彼らのやり方は今です。

いずれにせよ、一部の人々の頭のキャッシュを節約できることを願っていますが、これが最善のソリューションか、それとも良いソリューションかはわかりませんが、属性とは無関係にエフェクトを追跡するよりも好きです。この場合、各属性の追跡を独自の状態にする方がはるかに簡単で、エラーが発生しにくいはずです。本質的に1つの障害点のみがあり、それはかなり短く単純なクラスです。


リンクと作品の説明をありがとう!あなたは本質的に私が求めていたものに向かって動いていると思います。頭に浮かぶのは、操作の順序(たとえば、同じ属性に3つの「追加」効果と2つの「乗算」効果があり、どちらが先に発生するか)です。これは純粋に属性のサポートです。対処するトリガー(「ヒットすると1 APを失う」タイプのエフェクトなど)の概念もありますが、それはおそらく別の調査になるでしょう。
gkimsey

属性のボーナスを計算するだけの場合の操作の順序は簡単です。ここに、私がそこにいて切り替えていることがわかります。現在のすべてのボーナス(加算、減算、乗算、除算など)を繰り返し処理し、それらを累積します。あなたはBonusValue =(BonusValue * MultiplyBonus + AddBonus-SubtractBonus)/ DivideBonusのようなことをします。または、この方程式を調べたい場合でも。エントリポイントが1つしかないため、簡単に試すことができます。それは私の周り熟考別の問題があるため、トリガーのように、私は、それについて書いていない、と私はすでに3-4(上限)しようとした
ルカシュバランを

解決策、それらはどれも私が望んだように機能しませんでした(私の主な目的は、それらがデザイナーに優しいことです)。私の一般的なアイデアは、タグを使用して、タグに対する着信効果をチェックすることです。タグが一致する場合、エフェクトは他のエフェクトをトリガーできます。(タグは、Damage.Fire、Attack.Physicalなどの単純な人間の読み取り可能な名前です)。コアは非常に簡単な概念です。問題は、データを整理し、簡単にアクセスできるようにし(検索が高速)、新しいエフェクトを簡単に追加できるようにすることです。あなたはここにコードを確認することができますgithub.com/iniside/ActionRPGGame(GameAttributesはあなたが興味があるだろうモジュールです)
ルカシュバラン

2

私は小さなMMOに取り組み、すべてのアイテム、パワー、バフなどに「効果」がありました。効果は、「AddDefense」、「InstantDamage」、「HealHP」などの変数を持つクラスでした。パワー、アイテムなどは、その効果の持続時間を処理します。

パワーをキャストしたり、アイテムを置いたりすると、指定された持続時間の間、キャラクターにエフェクトが適用されます。次に、主な攻撃などの計算では、適用される効果が考慮されます。

たとえば、防御力を高めるバフがあります。そのバフには、少なくともEffectIDとDurationがあります。キャストするとき、指定された期間、EffectIDをキャラクターに適用します。

アイテムの別の例には、同じフィールドがあります。しかし、持続時間は無限であるか、アイテムをキャラクターから外して効果が除去されるまでです。

このメソッドを使用すると、現在適用されているエフェクトのリストを反復処理できます。

この方法を十分に明確に説明してほしい。


最小限の経験で理解しているように、これはRPGゲームでstat modを実装する従来の方法です。うまく機能し、理解と実装が簡単です。欠点は、「いばら」バフのようなこと、またはより高度な、または状況的なことをする余地がないように見えることです。また、RPGのエクスプロイトの原因でもありますが、それらは非常にまれですが、シングルプレイヤーゲームを作っているので、誰かがエクスプロイトを見つけたとしても、私はそれほど心配していません。入力いただきありがとうございます。
-gkimsey

2
  1. あなたが単一のユーザーである場合、ここから始めましょう:http : //www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

私はScriptableOjectsをバフ/スペル/タレントとして使用しています

public class Spell : ScriptableObject 
{
    public SpellType SpellType = SpellType.Ability;
    public SpellTargetType SpellTargetType = SpellTargetType.SingleTarget;
    public SpellCategory SpellCategory = SpellCategory.Ability;
    public MagicSchools MagicSchool = MagicSchools.Physical;
    public CharacterClass CharacterClass = CharacterClass.None;
    public string Description = "no description available";
    public SpellDragType DragType = SpellDragType.Active; 
    public bool Active = false;
    public int TargetCount = 1;
    public float CastTime = 0;
    public uint EffectRange = 3;
    public int RequiredLevel = 1;
    public virtual void OnGUI()
    {
    }
}

UnityEngineを使用。System.Collections.Genericを使用します。

public enum BuffType {Buff、Debuff} [System.Serializable] public class BuffStat {public Stat Stat = Stat.Strength; public float ModValueInPercent = 0.1f; }

public class Buff : Spell
{
    public BuffType BuffType = BuffType.Buff;
    public BuffStat[] ModStats;
    public bool PersistsThroughDeath = false;
    public int AmountPerTick = 3;
    public bool UseTickTimer = false;
    public float TickTime = 1.5f;
    [HideInInspector]
    public float Ticktimer = 0;
    public float Duration = 360; // in seconds
    public float ModifierPerStack = 1.1f;
    [HideInInspector]
    public float Timer = 0;
    public int Stack = 1;
    public int MaxStack = 1;
}

BuffModul:

using System;
using RPGCore;
using UnityEngine;

public class Buff_Modul : MonoBehaviour
{
    private Unit _unit;

    // Use this for initialization
    private void Awake()
    {
        _unit = GetComponent<Unit>();
    }

    #region BUFF MODUL

    public virtual void RUN_BUFF_MODUL()
    {
        try
        {
            foreach (var buff in _unit.Attr.Buffs)
            {
                CeckBuff(buff);
            }
        }
        catch(Exception e) {throw new Exception(e.ToString());}
    }

    #endregion BUFF MODUL

    public void ClearBuffs()
    {
        _unit.Attr.Buffs.Clear();
    }

    public void AddBuff(string buffName)
    {
        var buff = Instantiate(Resources.Load("Scriptable/Buff/" + buffName, typeof(Buff))) as Buff;
        if (buff == null) return;
        buff.name = buffName;
        buff.Timer = buff.Duration;
        _unit.Attr.Buffs.Add(buff);
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
    }

    public void RemoveBuff(Buff buff)
    {
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat]  /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
        _unit.Attr.Buffs.Remove(buff);
    }

    void CeckBuff(Buff buff)
    {
        buff.Timer -= Time.deltaTime;
        if (!_unit.IsAlive && !buff.PersistsThroughDeath)
        {
            if (buff.ModStats != null)
                foreach (var stat in buff.ModStats)
                {
                    _unit.Attr.StatsBuff[stat.Stat] = 0;
                }

            RemoveBuff(buff);
        }
        if (_unit.IsAlive && buff.Timer <= 0)
        {
            RemoveBuff(buff);
        }
    }
}

0

これは私にとって実際の質問でした。私はそれについて一つの考えを持っています。

  1. 前述したようBuffに、バフのリストとロジックアップデータを実装する必要があります。
  2. 次に、Buffクラスのサブクラスのフレームごとに特定のプレーヤー設定をすべて変更する必要があります。
  3. 次に、変更可能な設定フィールドから現在のプレーヤー設定を取得します。

class Player {
  settings: AllPlayerStats;

  private buffs: Array<Buff> = [];
  private baseSettings: AllPlayerStats;

  constructor(settings: AllPlayerStats) {
    this.baseSettings = settings;
    this.resetSettings();
  }

  addBuff(buff: Buff): void {
    this.buffs.push(buff);
    buff.start(this);
  }

  findBuff(predcate(buff: Buff) => boolean): Buff {...}

  removeBuff(buff: Buff): void {...}

  update(dt: number): void {
    this.resetSettings();
    this.buffs.forEach((item) => item.update(dt));
  }

  private resetSettings(): void {
    //some way to copy base to settings
    this.settings = this.baseSettings.copy();
  }
}

class Buff {
    private owner: Player;        

    start(owner: Player) { this.owner = owner; }

    update(dt: number): void {
      //here we change anything we want in subclasses like
      this.owner.settings.hp += 15;
      //if we need base value, just make owner.baseSettings public but don't change it! only read

      //also here logic for removal buff by time or something
    }
}

この方法では、Buffサブクラスのロジックを変更せずに、新しいプレーヤーの統計を簡単に追加できます。


0

私はこれがかなり古いことを知っていますが、新しい投稿でリンクされていたので、共有したいことについていくつかの考えがあります。残念ながら、現時点ではメモがありませんので、私が話していることの概要を説明し、目の前で詳細とサンプルコードを編集します。私。

まず、デザインの観点から、ほとんどの人はどのような種類のバフを作成でき、それらがどのように適用され、オブジェクト指向プログラミングの基本原則を忘れているかにあまりにも追いついていると思います。

どういう意味ですか?何かがバフであるかデバフであるかは実際には関係ありません。どちらも肯定的または否定的な方法で何かに影響を与える修飾子です。コードはどちらがどちらかを気にしません。その点については、最終的には、何かが統計を追加するのか、それとも乗算するのかは重要ではありません。これらは単なる演算子であり、コードはどちらがどちらであるかを気にしません。

だから私はこれでどこに行くのですか?良い(読みやすい:シンプルでエレガントな)バフ/デバフクラスの設計はそれほど難しくはありません。ゲームの状態を計算して維持するシステムを設計するのは難しいことです。

バフ/デバフシステムを設計している場合、ここで考慮すべき事項がいくつかあります。

  • 効果自体を表すbuff / debuffクラス。
  • バフが何にどのように影響するかについての情報を含むバフ/デバフタイプクラス。
  • キャラクター、アイテム、そして場合によっては場所はすべて、バフとデバフを含むリストまたはコレクションのプロパティを持っている必要があります。

バフ/デバフの種類に含める必要のある詳細:

  • 誰/何に適用できるか、IE:プレイヤー、モンスター、場所、アイテムなど
  • 効果のタイプ(ポジティブ、ネガティブ)、乗算または加算、および影響する統計のタイプ、IE:攻撃、防御、移動など
  • チェックする必要がある場合(戦闘、時刻など)。
  • 削除できるかどうか、削除できる場合はどのように削除できるか。

それはほんの始まりに過ぎませんが、そこからあなたはただあなたが望むものを定義し、あなたの通常のゲーム状態を使ってそれに基づいて行動しています。たとえば、移動速度を低下させる呪われたアイテムを作成したいとします...

適切なタイプを配置している限り、次のようなバフレコードを作成するのは簡単です。

  • タイプ:呪い
  • ObjectType:アイテム
  • StatCategory:ユーティリティ
  • 影響を受ける統計:MovementSpeed
  • 期間:無限
  • トリガー:OnEquip

などなど、バフを作成するときは、BuffTypeにCurseを割り当てるだけで、他のすべてはエンジン次第です...

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