ビールが飲みたい。

ゲーム開発の備忘録などを書きます。

【Unity】スキルなどの色んな種類の効果に対応するためのコードを考え中

※追記18/9/11

この記事もポリモーフィズムによる効果の種類作成に関わるものですので、よろしければ御覧下さい。

eggame.hateblo.jp

MapPinEffectから派生したHealEffect等は、復元した後は is で判定できるものの、jsonから復元する際には相変わらず型がわからないので、enumによるType判定は必要になります。

概要

オヒサシブリデス

RPGのスキルなどの”効果”を設計していて、もうちょっとスマートに出来ないかなぁと悩んだため、記事として公開しました。
備忘録でもあります。

全体像を先に言いますと、ポリモーフィズムで効果の発動をカスタマイズしつつ、パラメータをScriptableObjectを使って保存して実機で動かします。

動作環境

  • Unity 2017.4.9f1

とりあえず動かしたコード

まずはコード

すごろくゲームのフィールドで使用出来るスキルです。
※開発中なので仮置きなフィールドが多数。あまり参考にならなそうな気がします。

スキルのModel

MapPinTemplate.cs

    [Serializable]
    public class MapPinTemplate : Resource
    {
        public const string PREFIX = "MapPin";

        // 所有上限、下限などを設定する値群。
        public StackProperty stackProperty = new StackProperty();
        // ほぼ全てのマスターデータに共通して設定する値群。
        public AssetProperty asset = new AssetProperty();

        //移動力
        public int move;
        
        //使用ロケーション(どのタイル上で使用可能か)
        public List<TileType> tileType = new List<TileType>() { TileType.Field };

        //反映される値(何の効果を発生させるか(数字をいじる、タイルに敵を出現させる...など))
        public List<MapPinAbility> abilitys = new List<MapPinAbility>();
        
        //アニメーション(別で登録して引っ張り出す)
        public int animIdx;
    }

”ファイア”とか”ケアル”とかいった物を定義するクラス。

ここで大事なのはabilitysがそのスキルを発動した際に実行する効果を持っていること。
1つのスキルに複数効果を持たせて発動できるようにしています。


MapPinAbility.cs

    [Serializable]
    public class MapPinAbility
    {
        // 能力名
        public string name;

        //使用トリガー(自ら、ダイス扱いした時...など)
        public UsePinTrigger trigger;
        //適用対象(自分、その場にいる敵、狙ったターゲット、狙ったタイル、モンスター全員、敵プレイヤー全員....などなど)
        public UsePinTarget target;
        public UsePinTargetGroup targetGroup;
        public int targetCount;
        //効果時間(即時、ターン数、ターン経過後)
        public UsePinEffectTime effectTime;

        // スキルの効果を設定する各種
        public MapPinEffectType effectType;
        public string effectJson;
        [NonSerialized] public MapPinEffect effect;
    }

スキルの持つ効果名や、その発動条件、効果を持つ部分です。
大事なのは、ここのMapPinEffect型のフィールドが実行時に効果を処理する部分だということです。

何やら下3つが怪しい雰囲気を醸し出していますが、以下にMapPinEffectを記載した後説明します。

効果を処理するオブジェクト

MapPinEffectという名前の、効果を解決する部分は、以下の構成でポリモーフィズムを作っています。

  • public abstract class MapPinEffect
    あらゆる効果を一つのフィールドに持たせるための基底クラス

  • public abstract class CharacterMapPinEffect : MapPinEffect
    これは継承の仕方の一例です。フィールドにいるキャラクターに対して効果を適用するスキルの処理に使用します。

  • public class HealEffect : CharacterMapPinEffect
    これも一例です。回復の効果を解決するのに使用します。

MapPinEffect.cs

    [System.Serializable]
    public abstract class MapPinEffect
    {
#if UNITY_EDITOR
        // マスターデータエディターに入力を作るために使用します。
        public virtual MapPinEffect Draw(string json) { return null; }
#endif
    }

効果を保存するフィールドを作るための大本です。
Editor拡張でマスターデータの編集を行っているため、Editor Windowから編集のためのレイアウトを呼び出す関数が定義してあります。


CharacterMapPinEffect.cs

    [System.Serializable]
    public abstract class CharacterMapPinEffect : MapPinEffect
    {
        public virtual void Resolve(Character character) { }
    }

今回、効果はResolveという名前の関数を実行すると対象の値が書き換えられますが、ここでまず値をいじる対象を引数として定義します。
対象のパターンの数だけこれと同様のクラスが増えます。

(引数、interfaceにした方が良いかも)


HealEffect.cs

   [Serializable]
    public class HealEffect : CharacterMapPinEffect
    {
        public int fixedHp;
        public int fixedSp;
        [Range(0, 1)] public float ratioHp;
        [Range(0, 1)] public float ratioSp;

        public override void Resolve(Character character) {
            // 回復を与える(固定)
            character.Heal( fixedHp, fixedSp);

            // 回復を与える(割合)
            character.Heal( Mathf.FloorToInt(character.hpMax * ratioHp), Mathf.FloorToInt(character.spMax * ratioSp));
        }

#if UNITY_EDITOR
        public override MapPinEffect Draw(string json)
        {
            var effect = JsonUtility.FromJson<HealEffect>(json);
            fixedHp = EditorGUILayout.IntField("HP回復量(固定)", effect.fixedHp);
            fixedSp = EditorGUILayout.IntField("SP回復量(固定)", effect.fixedSp);
            ratioHp = EditorGUILayout.Slider("HP回復量(割合)", effect.ratioHp, 0f, 1f);
            ratioSp = EditorGUILayout.Slider("SP回復量(割合)", effect.ratioSp, 0f, 1f);
            return this;
        }
#endif
    }

効果の詳細を記述する部分です。
効果の種類の数だけこれと同様のクラスが増えます。
アニメーション等もここで発火する予定ですが、今は回復効果だけ入っています。

話を怪しげな3つのフィールドに戻して...

MapPinAbility.csで見たこれらの怪しげなフィールドについてです。

public MapPinEffectType effectType;
public string effectJson;
[NonSerialized] public MapPinEffect effect;

なおMapPinEffectTypeはenumです。

  • effectTypeで実行時に作るMapPinEffectの種類を特定します
  • effectJsonには、UnityEditorで値を編集した物をjsonシリアライズした物が入ります。
  • effectには実行時にeffectTypeとeffectJsonを使ってHealEffectなどをインスタンス化します。

なぜこの構成になっているのか。

Unityの仕様(ではないかも)で、ポリモーフィズムは単純にシリアライズした物を復元することが出来ません。
もうちょっと具体的にいうと、

HealEffect effect = JsonUtility.FromJson<MapPinEffect>(json);

ではHealEffectが正しくデシリアライズ出来ません。

HealEffect effect = JsonUtility.FromJson<HealEffect>(json);

ならいけます。

それぞれ1クラス1ファイルで定義した上でそれをScriptableObjectとして保存する事で一応設定した値を保持出来るのですが、めっちゃファイル数増えてやだーーー!!!と思ったので、一旦パラメータをjson(string)で保存しておき、実行時にeffectTypeを見てそれに対応するeffectのインスタンスを作り、そのeffectにjsonをデシリアライズ、という事をしています。

もうちょっと具体的にいうとFactoryパターンで

MapPinfactory factory = new MapPinfactory();
MapPinEffect effect = factory.create(effectType, effectJson);

などと書くわけですがfactoryのcreateでは

switch(type){
    case MapPinEffectType.Heal:
        return JsonUtility.FromJson<HealEffect>(effectJson);
}

とかするわけです。

これらが今回のモヤモヤ部分。もっとスマートな方法は無いものか。

※追記 これをEditorで設定するとどんな感じになるか

こんな感じで効果を分けて書けるようにしてます。

f:id:eggame:20180902191245g:plain

今気づいたのですが、効果説明を書く場所を忘れていました。
とはいえ効果説明は自動生成した方が良さそうですね。

思っていること

ScriptableObjectを使っているものの、ゆくゆくはサーバーからマスターデータをjsonで渡したいと考えているのでこういう形にしました。が、ちょっと煩雑に見える。

今はマスターデータの編集をUnity Editorで行っているものの、結局Excel管理のほうがコピペとかちょっとした値の変更とか楽だなーと思います。
が、このjson部分はExcelでどう処理したら....と悩みもあったり。
(これもVBAで引数に値渡したらjson返す関数書いて解決かしら?)

まとめ

  • 実行時に効果のフィールドだけjsonをデシリアライズするとかしないやり方は無いものか。
  • Editor拡張でマスターデータEditor作るのツラい
  • 走り書きしたので後でもう少し読みやすくするかも。(僕がこう言う時、実際にやることは少ないですが。)
  • このコードブロック見辛くない!?ってか、全体的に見づらくない!?テーマ変えようかな・・・。 テーマ変えました。