最終更新:
orika_ex_miyako 2026年05月27日(水) 13:18:18履歴
ですがその際、直接デシリアライズのできないデータ型も存在しているため、復元したいデータ型がデータ以外のゲーム内情報を持つ(ドメインモデル)場合には、そのままの復元が難しいです。
例えば抽象クラスやインターフェース、および 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 データがインスタンス生成できるようになります。
次の復元時のエラーはこちらです。
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 型などのプリミティブ型に絞ってデータのみを管理する構造にする必要があります。
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 側でも、BattleChara クラスではなく、BattleCharaRaw クラスを返します。
つまり、ゲーム実行オブジェクト(BattleChara)ではなく通信データ(BattleCharaRaw)を返すようにします。
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 のようなゲーム実行クラスは通信向きではありません。
データのみを扱うクラスにするか、あるいは今回のように中間クラスを作成して通信するようにします。
ReactiveProperty は、通知・状態監視のための仕組みです。Json 通信には不向きです。
ゲーム内での状態管理をつかさどるので、データのみのクラスには含めない方がいいでしょう。
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;
この下にコンストラクタ、メソッドがあります。




コメントをかく