思考日記:アイテムや魔法を「使う」はどう実装するのか問題(1)

もくじへ戻る


アイテムや魔法の効果は様々である.
(※いちいち「アイテムや魔法」とか書くのも面倒なので,以降は単に「魔法」という話として書く)

とにかく魔法には種類がたくさんあって→その効果というのは,HPを回復したり,敵にダメージを与えたり,毒を直したり……その他にもどこかにワープするだとか,とにかくいろいろあり得る.
そしたら,ユーザが「この魔法を使うよ!」という操作を行ったときの処理というのはどう実装すれば良いのだろう?

「キャラクタに魔法を使わせる処理というのは,選択された魔法の効果に応じた処理をせねばならない.その処理の種類はN通りである」 というのを実装する必要があるというわけだ.
N種類の全く異なる処理があるならば→とにかく最終的にN通りに分岐させるしかないと思うが,その実装はどういう形でやるのが良いのだろうか?

※とりあえず今回作る物では「ワープ」とかは無いことが確定しているので,「効果」というのは「キャラクタに何らかの影響を与える処理」だと限定したうえで考える.


「効果」の実装方法とは?

N種類の効果処理ってのをどう書くのか?

(1) 定数ですか?

N種類の値があれば,それを見てN分岐できる……ハズ.

//効果の種類を示す定数
//※ "Efficacy" とは「効能」みたいな意味らしい
enum class EfficacyType{ /*N種類の値*/ };

//魔法の定義
struct MagicDefinition
{
  //この魔法の「効果」を示す値
  EfficacyType Efficacy;
};

とかいうのがあるとしたら,使う魔法の定義から取り出した定数値を以下のような関数への引数として用いれば何かできる的な.

//引数 Efficacy が示す効果を引数 TgtChar で指定したキャラクターに与えて,その結果情報を返す
//(戻り値の型が具体的にどうなるのかは未定)
ProcResult Affect( EfficacyType Efficacy, Character &TgtChar )
{
  switch( Efficacy )
  { /*壮大な分岐*/ }
}

もちろんこれでは「壮大な分岐」がつらすぎると思われ. 「HPを5回復する効果」と「HPを10回復する効果」とがあったら,

enum class EfficacyType
{
  RecovHP_5,  //HPを5回復
  RecovHP_10,  //HPを10回復
  /*...他にもたくさん!*/
};

とかするとでもいうのか? これではちょっとやってらんない.

(2) 関数ですか?

いやいや,回復量が異なる「HP回復効果」があるならば,そこは

//引数 TgtChar で指定したキャラクターのHPを 引数 RecoverAmount で指定した量だけ回復する関数
ProcResult RecoverHP( int RecoverAmount, Character &TgtChar ){  TgtChar.ChangeHP(RecoverAmount);  /*...*/  }

とか書くんじゃねーの? 常識的に考えて.

しかし「HP回復」の他にも様々な種類の効果があるのだ.
それらを関数群として作ることを考えると,関数のシグネチャ(引数とか戻り値とか)が処理毎に異なっているといろいろと面倒なことになりそう.
シグネチャを統一できれば関数ポインタで「この処理を~」と指定することが可能になるのだが,処理ごとに必要な情報が異なるだろうし,そこをどうやって統一するのか? という課題がある.

(3) 継承ですか?

関数だとつらそうなので,class を考えてみる. 何かしらの共通の基底クラス:

//「効果」用のインタフェースクラス
struct IEfficacy
{
  //効果の適用処理
  //※引数や戻り値は具体的にはまだわからないが,この例だと
  //  「何らかの効果を引数で指定したキャラクターに与えて,その結果情報を返す」感じ.
  virtual ProcResult Affect( Character &TgtChar ) const = 0;
};

みたいなのから派生した型として各効果を

//HPを一定量だけ回復する効果
class RecoverHP : public IEfficacy
{
public:
  //Affect()で必要になる情報(ここでは回復量)を事前に設定しておく
  RecoverHP( int RecoverAmount ) : m_RecovAmount{RecoverAmount} {}

  virtual ProcResult Affect( Character &TgtChar ) const
  {
    TgtChar.ChangeHP( m_RecovAmount );  //対象キャラクタのHPを変更
    return ???;  //「このキャラクタのHPが変わった」みたいな結果情報を返す
  }
private:
  int m_RecovAmount;  //回復量
}

みたく実装するという方向性.
メソッド Affect() の引数では情報不足な場合には,どうにかして別ルートで事前解決しておく必要がある(この例だと ctor 引数で与えている).

(4) std::function ですか?

うーん,しかし上記の IEfficacy みたいなのだと,簡単な関数で済むような効果があったとしてもとにかく派生型として実装しないとダメってのが何だかめんどくさい気がする.
例えば「毒を直す」みたいな効果であれば,上記例の RecoverHP とは異なり,特に事前の情報設定は要らないだろうから,それなら単純に

//引数で指定したキャラクターの毒を直す
ProcResult CurePoison( Character &TgtChar ){ /*略*/ }

みたいな関数でも十分そうじゃないか.
…って感じで,各効果の実装方法をもうちょっと自由に選べるようにしたい(気がする),というわけで

//何らかの効果を引数で指定したキャラクターに与えて,その結果情報を返す
using Efficacy = std::function< ProcResult( Character & ) >;

ということにして,

//魔法の定義
struct MagicDefinition
{
  //この魔法の「効果」を取得
  const Efficacy &Efficacy() const;
};

のようにしておけばいいのかな.

「効果」を取得する必要ってあるんですか?

何故,魔法から「効果」を取り出すのか?

struct Magic
{
  //対象にこの魔法の効果を与える
  ProcResult Affect( Character &TgtChar ) const
  { /*このインスタンスが保持している何らかの情報から効果を特定して→処理する*/ }
};

じゃダメなのか? という.

「この魔法の効果はこれです→外部で処理してください」なのか,「~効果を処理する」までを Magic の中で全部やるのか.

出発点が「魔法」でも「アイテム」でも「その他の何か」とかでも,とにかく途中で「効果」という型になった方がいろいろとやりやすいかな? とか思ったんだけど,どうなんだろう?
これは実際やってみないとわかんないかな?

結果情報とは何か?

ProcResult って何ですか? という問題が残っている.
N通りの処理の結果情報を何かしらの単一の型として返す?

using ProcResult = std::veriant< HPChanged, PoisonCured, PoisonInfected, /*その他いろいろ...*/ >;

とかになる?
うーん,他にイマイチ思いつかないから,とりあえずそんな感じでいけるかやってみるしか?

その他

std::function だの std::veriant だのいうやつらを使うと処理効率とかがどうなるのか?」的な話についてはとりあえず考慮しない(というか「知らない」).


ここまでの考えまとめ

魔法には

みたいな話もあるので,そういうのも含めると例えばこんな↓くらいの話が必要だろうか?
うーむ,なんだかごちゃごちゃしてるなぁ……

//効果の処理結果情報
using ProcResult = std::veriant< HPChanged, PoisonCured, PoisonInfected, /*その他いろいろ...*/ >;

//効果の処理
//何らかの効果をキャラクタ(群)に与えて,その結果情報を返す
using Efficacy = std::function<
  std::vector<ProcResult>(  //戻り値は複数個になり得る
    Character &Actor,  //実行者(アイテムや魔法を使うキャラクタ)
    std::vector<Character*> Tgts  //対象キャラクタ(全体攻撃とかの場合には複数)
  )
>;

//効果範囲の種類
enum class TgtRange
{  /*「味方単体」とか「敵全体」みたいな種類を必要なだけ*/  };

//魔法の定義用の型
class Magic
{
public:
  Magic( TgtRange Range, const Efficacy &efficacy ) : m_Range(Range), m_Efficacy(efficacy) {}
  const Efficacy &Efficacy() const { return m_Efficacy; }
  TgtRagne Range() const { return m_Range; }
private:
  TgtRange m_Range;
  Efficacy m_Efficacy;
};

で,【味方のHPを回復する魔法であって,回復量は魔法使用者の「魔力(能力値)」に依存する】とかいうのを考えてみると……

//「誰かのHPが変化したよ!」という結果情報
struct HPChanged
{
  const Character *pTgtChar;  //対象キャラクタ
  int Amount;  //回復量?(この辺の情報は必要に応じたものにする)
};

//HP回復効果(ファンクタ)
class RecoverHP
{
public:  //効果定義用
  //回復効果量決定手段.引数は回復行動実行者
  using CacAmountFunc = std::function< int( const Character & ) >;

  RecoverHP( const CalcAmountFunc &CalcAmount ) : m_CalcAmount(CalcAmount) {}

public:  //処理
  std::vector<ProcResult> operator()(
    Character &Actor,
    std::vector<Character*> Tgts
  ) const
  {
    const int dHP = m_CalcAmount( Actor );  //回復量

    std::vector<ProcResult> Results;
    for( auto Tgt : Tgts )  //対象全員に適用
    {
      Tgt->ChangeHP( dHP );
      Results.emplace_back( HPChanged{ Tgt, dHP } );
    }
    return Results;
  }

private:
  CalcRawAmountFunc m_CalcAmount;
};

//キャラクタの「魔力」に応じた効果量算出器
struct MAGx
{
  int m_Per;  //倍率[%]
  int operator()( const Character &Char ) const {  return Char.MAG() * m_Per / 100;  }
};

//-------------
//魔法群の定義

//魔法の種類
enum class MagicID
{
  Heal_LV,
  Heal_LV2,
  AllHeal_LV,
  /*...*/
}

//どこかにこんなのがある感じか
const Magic MagicDefinitions[] = {
  Magic( TgtRange::味方単体, RecoverHP( MAGx(100) ),
  Magic( TgtRange::味方単体, RecoverHP( MAGx(200) ),
  Magic( TgtRange::味方全体, RecoverHP( MAGx(80) ),
  /*...*/
};

//魔法の定義を参照する手段
const Magic &DefinitionOf( MagicID ID )
{  return MagicDefinitions[(int)ID];  }

ふーむ,これだと「魔法を使う」処理は以下のような手順になるだろうか.

  1. 魔法使用者の決定
  2. 使用する魔法の決定
  3. 対象の決定
  4. 効果発揮処理と,その結果に応じた処理

MEMO : 考慮事項

味方を対象にとる魔法やアイテムに関しては,使う必要がない状態(対象者全員がHP満タン状態だった場合など)では,使用を棄却したい.
(そうじゃないと操作ミスで無駄にMPが減ったり,解毒薬を失ったりすることになる)

効果発揮処理から返されてきた結果情報を見れば「何も効果が無かった」旨がわかるのであれば, 「使用の棄却」方法としては必ずしも事前に状態チェックする必要はなく, 事後の結果情報から判断して「無かったことにする(:表示や消費処理をしない)」という形でもよいと思う.


つづく