どうもです、タドスケです。
ゲーム開発で使えそうな部品を提供するこのシリーズ。
今回はバトル系ゲームには必須のダメージ計算機能を作ってみました。
この記事内で公開しているコードは全て自作なので、個人/商用問わずご自由に流用いただいて構いません。 ただしコードを利用して損害が出た場合でも、その責任は負いかねます。
実行例
今回のプログラムを実行すると、以下のような出力が得られます。
威力10、物理攻撃 → 物理防御力8 2 のダメージ! 威力10、火属性魔法攻撃 → 魔法防御力4、耐性なし 6 のダメージ! 威力10、火属性魔法攻撃 → 魔法防御力4、火耐性50% 3 のダメージ! 威力10、物理攻撃、毒特攻2倍 → 物理防御力5、毒 10 のダメージ! 威力10、水属性魔法攻撃、眠り特攻1.5倍 → 魔法防御力2、水耐性75%、眠り 3 のダメージ!
攻撃側は威力、種別(物理/魔法)、属性(火水土風)、状態異常特攻(毒・眠り)の情報を持っています。
防御側は物理/魔法防御力、属性耐性、状態異常の情報を持っています。
両者を利用してダメージ計算した結果が出力されます。
実装方法
実行例のような動作をするためにどのようなプログラムを書けばよいでしょうか。
クラスごとに説明します。
AttackInfo クラス
class AttackInfo:
"""攻撃情報.
:param power: 威力
:param type_: 攻撃タイプ
:param attr: 属性
:param cond_mag_dict: 状態異常特攻の辞書(状態異常種別:倍率)
"""
def __init__(
self,
power: int,
type_: AttackType = AttackType.PHYSICS,
attr: Attribute = Attribute.NONE,
cond_mag_dict: ConditionMagnificationDictType = None) -> None:
if power < 0:
raise ValueError
self._power = power
self._type = type_
self._attr = attr
if cond_mag_dict is not None:
self._cond_mag_dict = cond_mag_dict
else:
self._cond_mag_dict = {}
@property
def power(self) -> int:
"""威力."""
return self._power
@property
def attribute(self) -> Attribute:
"""属性."""
return self._attr
@property
def condition_magnifications(self) -> ConditionMagnificationDictType:
"""状態異常特攻の辞書."""
return self._cond_mag_dict
def is_physics(self) -> bool:
"""物理攻撃か?"""
return self._type == AttackType.PHYSICS
def is_magic(self) -> bool:
"""魔法攻撃か?"""
return self._type == AttackType.MAGIC
def get_condition_magnification(self, cond: Condition) -> float:
"""指定した状態異常特攻の倍率を得ます."""
mag = self._cond_mag_dict.get(cond)
if mag is None:
return 1.0
return mag
攻撃情報を表すクラスです。
威力、種別、属性、状態異常特攻の辞書を持っています。
種別(AttackType)はそのままプロパティとして公開せず、is~ 関数を通して判定するようにしています。
こうすることで、type == AttackType.PHYSICS というような生々しい判定文をあちこちに書かなくて済み、プログラムの見通しが良くなります。
状態異常特攻の辞書についても同じようにしたかったのですが、こちらはダメージ計算時に複数の状態異常をまとめて処理する必要があるため、そのまま公開しました。
DefenceInfo クラス
class DefenceInfo:
"""防御情報.
:param physics: 物理防御力
:param magic: 魔法防御力
:param res_dict: 属性抵抗率の辞書(属性→抵抗率)
:param conditions: 状態異常。正常時は None
"""
def __init__(
self,
physics: int,
magic: int,
res_dict: AttributeResistanceDictType = None,
conditions: Condition = None) -> None:
if physics < 0:
raise ValueError
if magic < 0:
raise ValueError
self._physics = physics
self._magic = magic
if res_dict is None:
self._res_dict = {}
else:
self._res_dict = res_dict
self._conditions = conditions
@property
def physical_power(self) -> int:
"""物理防御力."""
return self._physics
@property
def magical_power(self) -> int:
"""魔法防御力."""
return self._magic
def get_regist(self, attr: Attribute) -> float:
"""属性抵抗率を得ます.
:param attr: 属性
:return: 属性抵抗率。見つからない場合は 1.0
"""
res = self._res_dict.get(attr)
if res is None:
return 1.0
return res
def has_condition(self) -> bool:
"""状態異常を持っているか?"""
return self._conditions is not None
def is_condition(self, condition: Condition) -> bool:
"""指定の状態異常を持っているか?"""
if self._conditions is None:
return False
return bool(self._conditions & condition)
防御情報を表すクラスです。
物理防御力、魔法防御力、属性抵抗率の辞書、状態異常を持っています。
攻撃側はあらかじめどんな攻撃をするか分かっているのに対し、防御側は攻撃されるまでどんな攻撃が来るか分からないので、使う可能性のある情報を全て持っています。
Damage クラス
class Damage:
"""ダメージ.
:param attack: 攻撃情報
:param defence: 防御情報
"""
def __init__(
self,
attack: AttackInfo,
defence: DefenceInfo) -> None:
self._attack = attack
self._defence = defence
def calc(self) -> int:
"""ダメージ値を計算します.
丸め誤差を少なくするため、ギリギリまで float で計算し、最後に int で丸めた値を返します。
:return: ダメージ値
"""
# 基本ダメージ
val = self._calc_basic()
# 状態異常特攻
val = self._calc_cond_mag(val)
# 属性抵抗
val = self._calc_regist(val)
return int(val)
def _calc_basic(self) -> float:
"""基本ダメージ値を計算します.
:return: 基本ダメージ値
"""
at_pow = self._attack.power
if self._attack.is_physics():
# 物理ダメージ
df_pow = self._defence.physical_power
elif self._attack.is_magic():
# 魔法ダメージ
df_pow = self._defence.magical_power
else:
raise NotImplementedError
return float(max(0, at_pow - df_pow))
def _calc_cond_mag(self, val: float) -> float:
"""異状態異常特攻を適用したダメージ値を計算します.
複数の状態異常と一致する場合は、足し合わせた倍率が適用されます.
倍率 1.5 の特攻が3つある場合、2.5倍になります.
:param val: ダメージ値
:return: 適用後のダメージ値
"""
matches = []
for cond, mag in self._attack.condition_magnifications.items():
if self._defence.is_condition(cond):
matches.append(mag)
sum_mag = 1.0
for mag in matches:
sum_mag += (mag - 1.0)
return val * sum_mag
def _calc_regist(self, val: float) -> float:
"""属性抵抗を適用したダメージ値を計算します.
:param val: ダメージ値
:return: 適用後のダメージ値
"""
attr = self._attack.attribute
ratio = self._defence.get_regist(attr)
return val * ratio
ダメージ計算を行うクラスです。
コンストラクタ(init)で攻撃情報と防御情報を受け取り、calc 関数で計算結果のダメージを返します。
まずは威力と物理/魔法防御力から基本ダメージを計算し、状態異常特攻→属性抵抗の順に倍率をかけ合わせていきます。
わざわざ関数を分けているのは、関数ごとのテストを書きやすくするためです。
例えば属性抵抗の計算を行う _calc_regist 関数については、それ単体で見れば与えられた float 値に倍率をかけて返す関数とみなせます。
_calc_regist 関数のテストを書く際には、前後のダメージ計算のことを考える必要はありません。
適当に value=10 とかを与えて、意図した倍率がかかった値が返ってくるかだけを見れば十分です。
同じように基本ダメージ、状態異常特攻についても個別でテストを行い、それぞれがどんな入力値でも正しく動くことが個別に確認できれば、それらを組み合わせた一連のダメージ計算処理も正しいとみなせます。
calc 関数にありとあらゆる値の組み合わせを与えてテストするよりも、ずっと簡潔にテストが書けます。
コード
コード全文は以下の場所にアップしてあります。
実装
テスト
拡張のアイデア
ターンバトルシステムと組み合わせる
先日公開したターンバトルシステムでは、以下のようにダメージ計算を行っています。
def attack(self, target: Character) -> int:
"""攻撃する.
:param target: 攻撃相手
:return: 与えたダメージ
"""
point = target.damage(self._atk)
return point
def damage(self, atk) -> int:
"""ダメージを受ける.
:param atk: 攻撃力
:return: 受けたダメージ
"""
point = max(int(atk - self._def / 2), 0)
self._hp = max(self._hp - point, 0)
return point
攻撃力、防御力には生の int 値を使っていますが、これを AttackInfo、DefenceInfoクラスに置き換えることで、属性なども考慮したダメージ計算処理に置き換えることができます。
また、AttackInfo に入れる威力値をキャラクターの力・武器・スキルなどから計算して入れるようにすれば、よりゲームらしくなります。
状態異常の付与/回復
AttackInfo 側に状態異常の付与率、DefenceInfo 側に防御率を入れれば、「毒攻撃を毒耐性アクセサリで防御する」ということもできます。
ダメージを受けた際にランダムで眠り状態が回復するようにしても良いかもしれません。
状態異常が付与された/回復したかどうかは、calc 関数の戻り値をタプルや Result クラスにして含める方法や、ダメージ計算とは別のクラスで行う方法が考えられます。
(Damage.calc で計算→ダメージが1以上なら、状態異常判定関数を呼ぶ、など)
ランダム要素
ターンバトルシステムの時にも触れましたが、ダメージ値に ±20% の範囲で変動させたり、3%の確率でクリティカルヒット(防御無視、ダメージ○倍)になったりすると、ゲームらしくなります。
以上、ダメージ計算機能の説明でした!
コメント
コメント一覧 (2件)
[…] 関数が攻撃力の代わりに先日公開したダメージ計算機能にある AttackInfo […]
[…] […]