Unity C#で実践!メソッドチェーンとコールバックでアクションを柔軟に設計する
ゲーム開発をしていると、ダメージ処理みたいに「ちょっと複雑だけど柔軟に設定したい!」っていう場面がよく出てくるのだ。
例えば「敵を倒したときだけエフェクトを出したい」とか、「攻撃が外れたときだけ音を鳴らしたい」ときなどなのだ!
この記事では、メソッドチェーンを使って設定を容易にし、さらにC#のActionデリゲートを利用したコールバックを組み込む方法について解説するのだ!
1. メソッドチェーンによる設定の簡略化
メソッドチェーンとは
メソッドチェーンというのは、オブジェクトに対していくつかの設定を順番に呼び出して、だんだん組み立てていく書き方のことなのだ。
メソッドチェーンはビルダーパターンを使うときによく見ると思うのだ。ここでは、ビルダーパターンそのものではなく、その中の「メソッドチェーンで設定できる仕組み」だけを取り入れているのだ。
このような書き方は Fluent Interface(流暢なインターフェース) とも呼ばれるのだ!
ちなみにメソッドチェーンとは次のように、主な処理のMainMethod
に Method1()
Method2()
などのメソッドを繋げていくことを言うのだ!
MainMethod().Method1().Method2();
コード例(DamageAction
)の解説
// 攻撃に処理に必要な情報をまとめるためのクラス
public class DamageAction
{
// ... フィールドの定義(target, damage, 各種CallBack) ...
// コンストラクタで必須のパラメータを渡すのだ
public DamageAction(CharacterStatus target, int damage)
{
this.target = target; // 攻撃する対象
this.damage = damage; // 与えるダメージ
}
// OnKill/OnMiss/OnDamage がメソッドチェーン役なのだ
public DamageAction OnKill(Action callback)
{
killCallBack = callback; // 倒した時に発火する処理
return this; // 自分自身を返すのがポイント!
}
// ... OnMiss, OnDamage も同じように書けるのだ ...
}
💡 メソッドチェーンでスッキリ書けるのだ
ダメージを与えるだけの場合は次ようにすれば良いのだ!
// status.attack(攻撃ステータス)の値だけ相手にダメージを与える
enemy.TakeDamage(new DamageAction(enemy.status, status.attack));
OnKill()
やOnMiss()
がthisを返してるから、メソッドをつなげて書けるのだ!
// ダメージを与える以外にも、倒すとLevelUpをしたり、ダメージを与えるとエフェクト出したりできるのだ
enemy.TakeDamage(new DamageAction(enemy.status, status.attack)
.OnKill(LevelUp) // 倒したときの処理:レベルアップ
.OnDamage(PlayHitEffect) // ダメージを与えたときの処理:エフェクト
);
こうすると、「何が起きるか」がひと目で分かるようになるのと、必要な時に、必要なだけ機能を追加できるのだ!すごいのだ!
2. C# Action
デリゲートとコールバック
コールバックとは
コールバックは「この処理が終わったら、代わりにこのメソッドを呼んでね!」っていう仕組みのことなのだ。
ゲームだと「敵が倒れたら経験値を加える」とか「攻撃が外れたら音を鳴らす」とかに使えるのだ。
C#では、主にデリゲート(Action
やFunc
)を使って実現するのだ。
コード例(DamageAction
)の解説
DamageAction
では、標準で用意されてるAction型をコールバック用に使っているのだ。
// 攻撃に処理に必要な情報をまとめるためのクラス
public class DamageAction
{
// コールバックを格納するためのフィールド
public Action killCallBack = null;
public Action missCallBack = null;
public Action damageCallBack = null;
// ...
}
そしてTakeDamage
の中で、ちゃんと呼び出してあげるのだ!
// 攻撃のダメージ処理の関数
public void TakeDamage(DamageAction damageAction)
{
if(status.armor >= damageAction.damage)
{
// ダメージを防いだとき(ミス)
damageAction.missCallBack?.Invoke(); // nullじゃなければ実行
}
else
{
// HPを減らす処理 ...
if(status.hp > 0)
{
damageAction.damageCallBack?.Invoke(); // 生き残ったときにdamageCallBackを発火
}
else
{
damageAction.killCallBack?.Invoke(); // 倒したときにkillCallBackを発火
}
}
}
💡 ?.Invoke()
ってなに?
damageAction.missCallBack?.Invoke()
の「?
(Null条件演算子)はnullチェックを省略できる便利な書き方なのだ!
もしコールバックが設定されてなかったら(null
だったら)、そのまま何もせずスルーしてくれるのだ。
3. まとめと応用
このパターンを使うと、こんな良いことがあるのだ!
-
見やすい!(可読性の向上):
メソッドチェーンで「どういうときに何が起きるか」が一目でわかるのだ。 -
変更しやすい!(柔軟性の確保):
本体のTakeDamage
のコードをいじらなくても、外から自由に「倒したとき」「外れたとき」の処理を追加できるのだ。 -
いろんな場面で使える!(再利用性):
例えば通常攻撃、スキル攻撃、毒ダメージ…どんなケースでも「このときだけ特別な処理」を簡単に設定できるのだ。
つまり、メソッドチェーンとコールバックの合わせ技は、ゲームのアクションやイベントを設計する上で超便利な武器になるのだ!
Unityのプロジェクトでもきっと役立つから、ぜひ試してみてほしいのだ!
おまけとして、今回の例のclassの全体を載せるのだ。
CharacterStatus
はキャラクターのステータスを再現するためのクラスなのだ
public class CharacterStatus
{
public int hp;
public int armor;
public int attack;
}
DamageAction
が今回大事なメソッドチェーンの内容なのだ!メソッドチェーンを使えば、必要な時に必要なだけの情報を追加できるのだ!
public class DamageAction
{
public CharacterStatus target = null;
public int damage = 0;
public Action killCallBack = null;
public Action missCallBack = null;
public Action damageCallBack = null;
public DamageAction(CharacterStatus target, int damage)
{
this.target = target;
this.damage = damage;
}
public DamageAction OnKill(Action callback)
{
killCallBack = callback;
return this;
}
public DamageAction OnMiss(Action callback)
{
missCallBack = callback;
return this;
}
public DamageAction OnDamage(Action callback)
{
damageCallBack = callback;
return this;
}
}
TestCharacter
はキャラクターの動きを再現するためのクラスなのだ。
public class TestCharacter
{
public CharacterStatus status;
public void Attack()
{
TestCharacter enemy = null;
enemy.TakeDamage(new DamageAction(enemy.status, status.attack));
enemy.TakeDamage(new DamageAction(enemy.status, status.attack).OnKill(LevelUp));
}
public void TakeDamage(DamageAction damageAction)
{
if (status.armor >= damageAction.damage)
{
damageAction.missCallBack?.Invoke();
}
else
{
status.hp += status.armor - damageAction.damage;
if (status.hp > 0)
{
damageAction.damageCallBack?.Invoke();
}
else
{
damageAction.killCallBack?.Invoke();
}
}
}
public void LevelUp()
{
status.hp += 10;
}
}