Unityに関連する記事です

×
 Unity と PlayFab CloudScript を使っていると「サーバーから返したデータをそのまま Deserialize したい」というケースがあります。
ですがその際、直接デシリアライズのできないデータ型も存在しているため、復元したいデータ型がデータ以外のゲーム内情報を持つ(ドメインモデル)場合には、そのままの復元が難しいです。
例えば抽象クラスやインターフェース、および ReactiveProperty や Observable といったデータ型などがこれらに当たります。

 こういった場合には

DTO / Raw クラスを分離
Factory クラスでオブジェクトを生成

 という構成が復元しやすい形になります。

 DTO とは「Data Transfer Object」の略語で、データ転送オブジェクトと呼ばれます。
クライアント(Unity) とサーバーの間に入って、データのやり取りをするために利用されます。

 この記事では、実際に発生するエラーと、解決までの流れをまとめます。



やりたかったこと


 CloudScript 側でバトル用データを生成し、クライアントへ返します。

 クライアント側では、

var result = JsonConvert.DeserializeObject<CloudScriptResponse<T>>(json);

 で受け取りたかった構成です。このとき復元したいクラスは、以下のようなクラスだとします

[System.Serializable]
public class BattleChara {
    public int positionIndex;
    public UnitDataBase unitData;
    public bool isAlive;
    public OwnerType ownerType;

    public SerializableReactiveProperty<int> Hp = new();
    public SerializableReactiveProperty<int> Special = new();
}

 一見すると問題なさそうに見えます。
しかし実際には、Deserialize 時にエラーが発生します。

 まずは実際に発生するエラーを整理し、その後、原因と構造上の問題点を上げていきます。


発生したエラー �� 抽象クラスは復元できない


 最初に発生したエラーはこちらです。

Could not create an instance of type UnitDataBase.
Type is an interface or abstract class and cannot be instantiated.

 原因となっていたのは、BattleChara クラス内の抽象クラスです。

public abstract class UnitDataBase

 Json データの構造は以下のようになっています。

{
  "unitData": {
    "charaId": 1001
  }
}

 Json.NET はこのような Json の情報を見ても、この unitData が基底クラスなのか、あるいは派生クラスであるのか、いずれのクラスを生成すればいいのか判断できません。
そして基底クラスは先ほどの抽象クラスでした。デシリアライズする際には、インターフェースや抽象クラスは生成できないため、エラーになります。



 一時的な対処方法として、抽象クラスを通常のクラスに戻す形を検討します。

public abstract class UnitDataBase
  ↓
public class UnitDataBase

 このように変更しました。

 さらにクラス内の abstract (抽象)メソッドを virtual (仮想)化しました。

public abstract float GetCurrentStatus(PassiveStatusType passiveStatusType) {}
  ↓
public virtual float GetCurrentStatus(PassiveStatusType passiveStatusType) => 0f;


 こちらの変更により、Json データがインスタンス生成できるようになります。


発生したエラー �� ReactivePropertyは復元できない


 次の復元時のエラーはこちらです。

Could not cast or convert from System.Int64 to SerializableReactiveProperty<int>

 原因は BattleChara クラス内の ReactiveProperty になります。

public SerializableReactiveProperty<int> Hp = new();

 Json データ上では、

"Hp": 2398

 のような単なる数値です。しかし JsonConvert は SerializableReactiveProperty<int> をどう復元すればいいかわかりません。



 ここで再度、復元したい BattleChara クラスの構造を見てみます。

[System.Serializable]
public class BattleChara {
    public int positionIndex;
    public UnitDataBase unitData;
    public bool isAlive;
    public OwnerType ownerType;

    public SerializableReactiveProperty<int> Hp = new();
    public SerializableReactiveProperty<int> Special = new();
}

 BattleChara は ReactiveProperty 以外にも、さらに次のような機能も有していました。

Subject
Observable
UnityAction
Disposable
イベント購読

 これらはデータではなくて、ゲーム内で実際に利用する際に使う情報群です。
つまり BattleChara は、データのみを扱うクラスではなく、「ゲーム実行用クラス」です。データ通信向けではありません。



 BattleChara クラスは

通知
エフェクト
バフ処理
購読
リアクティブ制御

 などを含む、「ゲーム中の生オブジェクト(ドメインモデル)」です。

 これをそのまま Json 化すると、

抽象クラス
イベント
ReactiveProperty
Subject

 などが Serialize/Deserialize 問題を起こします。

 int 型や bool 型、Enum 型などのプリミティブ型に絞ってデータのみを管理する構造にする必要があります。


解決策

��Raw / DTO クラスを分離する


 BattleChara クラス自体を修正するとなると影響範囲が大きくなります。
そこで BattleChara クラスはそのままに、データ通信専用クラスの BattleCharaRaw クラスを新しく作成します。

 Raw とは「未加工」「未処理」の生データのことを意味する単語です。
BattleCharaRaw とは「まだゲーム内オブジェクトに変換していない、生の通信データ」用の BattleChara クラスの意味を持ちます。
CloudScript との通信データを受け取るためだけの中間クラスです。

[Serializable]
public class BattleCharaRaw {
    public int positionIndex;
    public UnitDataBase unitData;
    public bool isAlive;
    public OwnerType ownerType;

    public int hp;
    public int special;
}

 重要なのは ReactiveProperty を持たせないことです。
通信では単なる int にします。


��CloudScript は Raw を返す


 CloudScript 側でも、BattleChara クラスではなく、BattleCharaRaw クラスを返します。
つまり、ゲーム実行オブジェクト(BattleChara)ではなく通信データ(BattleCharaRaw)を返すようにします。


��クライアント側では Factory を使う


 Unity(クライアント側)では CloudScript から受け取った Raw データを使い、BattleChara を生成します。
Factory クラスのサンプルです。

public static class BattleCharaFactory {

    public static BattleChara Create(BattleCharaRaw raw) {

        BattleChara battleChara = new(
            positionIndex: raw.positionIndex,
            unitData: raw.unitData,
            isAlive: raw.isAlive,
            baseActiveSkillPercent: 0,
            ownerType: raw.ownerType,
            maxSpecialValue: 100,
            auraSlot: null,
            onPlaySpecialEffect: null
        );

        battleChara.Hp.Value = raw.hp;
        battleChara.Special.Value = raw.special;

        return battleChara;
    }
}

 これの手順により

ReactiveProperty 初期化
Subject 初期化
Subscribe
イベント設定

 などを安全に行えます。


最終構成


 最終的には以下の構成になりました。

CloudScript
   ↓
BattleCharaRaw(Json)
   ↓ [Deserialize]
BattleCharaRaw(中間クラス)
   ↓ [Factory]
BattleChara

 非常にシンプルですが、

通信
復元
ゲームロジック

 プログラムにおけるクラスの責務が綺麗に分離されています。


まとめ

�� ドメインモデルを直接通信しない


 BattleChara のようなゲーム実行クラスは通信向きではありません。
データのみを扱うクラスにするか、あるいは今回のように中間クラスを作成して通信するようにします。


�� DTO / Raw を分離する


 もしもゲーム実行クラスを残したい(そのまま使いたい)場合には、通信専用の中間クラス(Raw Data Class)を作ると安全になります。


�� ReactiveProperty は通信向きではない


 ReactiveProperty は、通知・状態監視のための仕組みです。Json 通信には不向きです。
ゲーム内での状態管理をつかさどるので、データのみのクラスには含めない方がいいでしょう。


�� 抽象クラス Deserialize は危険


 Json.NET は抽象クラスを生成できません。
派生型情報を含めるか、DTO を使う必要があります。



 Unity + PlayFab + CloudScript では、「ゲーム実行クラスをそのまま通信しない」ことが非常に重要です。

 特に

ReactiveProperty
Subject
Observable
UnityAction

 これらを持つクラスは DTO 分離をおすすめします。
今回のように、Raw → Factory → 実行クラスという構成にすると、かなり安定した設計になります。


サンプル


 BattleChara クラスのフィールドサンプルです。

[Serializable]
public class BattleChara {
    public int positionIndex;     // どの位置に配置されているか。0-5
    public UnitDataBase unitData;
    public bool isAlive;          // true で生存中
    public OwnerType ownerType;   // Player 側のキャラか、敵側のキャラか

    public bool isSpecialSkillTriggered;          // 奥義発動済有無。true で発動済
    public bool isAttackOrActiveSkillTriggered;   // 通常攻撃(アクティブスキル)実行済有無。true で実行済

    public SerializableReactiveProperty<int> Hp = new();
    public SerializableReactiveProperty<int> Special = new();

    private int maxSpecialValue;                               // 奥義ゲージの最大値

    private CompositeDisposable disposables = new();
    private AuraSlot auraSlot;
    private UnityAction<string, string, BattleChara> onPlaySpecialEffect;
    private readonly System.Random random = new();
    private readonly float burnRate = 0.5f;  

    // コンディションの List(バフ・デバフ)
    public ObservableList<ConditionData> ConditionList = new();  // BattleCharaView で購読

    private Dictionary<PassiveStatusType, Action<float>> statusSetters = new(); // バフ・デバフ用のステータスのマッピング用

    // 最終的な戦闘用ステータス(レベル、ランク、覚醒、装備、パッシブ、コンディションなどすべて適用後の値)
    public int battleMaxHp;                       // 最大HP
    public int battleAttack;
    public int battleDefense;
    public float battleCritial;
    public float battleCriticalResistance;
    public float battleCriticalDamage;

    // バトル内でのバフ・デバフによる修正値
    public float attackBonus;
    public float defenseBonus;
    public float criticalBonus;
    public float criticalResistanceBonus;
    public float criticalDamageBonus;

    public float battleActiveSkillRate;           // アクティブスキルの発生の現在値
    public float baseActiveSkillPercent;          // アクティブスキルの発生初期値

    public int rankCount = 0;                     // Enemy は 0 固定
    public int limitCount = 0;                    // Enemy は 0 固定

    // バトルログ
    public int totalDealt;   // 与えたダメージの合計ログ
    public int totalTaken;   // 受けたダメージの合計ログ
    public int totalHealed;  // 回復値の合計ログ

    // 購読用
    private Subject<BattleChara> onDeathSubject = new();   // 死亡時の通知
    public Observable<BattleChara> DeathNotifier => onDeathSubject;

    private Subject<OwnerType> onMoveStartSubject = new();   // キャラの行動順が来た時の通知
    public Observable<OwnerType> MoveStartNotifier => onMoveStartSubject;

    private Subject<OwnerType> onMoveEndSubject = new();    // キャラの行動が終った時の通知
    public Observable<OwnerType> MoveEndNotifier => onMoveEndSubject;

    private Subject<EffectType> onPlayEffectSubject = new();  // エフェクト再生の通知
    public Observable<EffectType> PlayEffectNotifier => onPlayEffectSubject;

    private Subject<(int, FloatingValueType, bool, TriResult)> onShowFloatingViewSubject = new();   // フロート表示実行時の通知(ダメージ(回復)量, ダメージか回復か, クリティカル有無) を渡す
    public Observable<(int, FloatingValueType, bool, TriResult)> ShowFloatingViewNotifier => onShowFloatingViewSubject;

    private Subject<ConditionData> onAddConditionSubject = new();
    public Observable<ConditionData> AddConditionNotifier => onAddConditionSubject;

    private Subject<(string, string, BattleChara)> onPlaySpecialEffectSubject = new();
    public Observable<(string, string, BattleChara)> PlaySpecialEffectNotifier => onPlaySpecialEffectSubject;

    private Subject<Unit> onShakeView = new();
    public Observable<Unit> PlayShakeView => onShakeView;

    private Subject<(Color, float)> onChangeFilterColorSubject = new();
    public Observable<(Color, float)> ChangeColorFilterNotifier => onChangeFilterColorSubject;

    private Subject<(string, float)> onPlayActiveSkillPictureSubjct = new();
    public Observable<(string, float)> PlayActiveSkillPictureNotifier => onPlayActiveSkillPictureSubjct;


  この下にコンストラクタ、メソッドがあります。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



基礎

技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3Dレールガンシューティング(応用編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます

広告募集中