質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

シリアライゼーション

シリアライゼーションとはデータ構造を特定のフォーマットに変換するプロセスのことです。これはデータの保存・送信・もしくは後々の再構築を容易にするために行われます。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

3回答

4365閲覧

デシリアライズにより失われる参照関係への対策

m3m3

総合スコア1

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

シリアライゼーション

シリアライゼーションとはデータ構造を特定のフォーマットに変換するプロセスのことです。これはデータの保存・送信・もしくは後々の再構築を容易にするために行われます。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

1グッド

1クリップ

投稿2020/06/13 10:46

前提・実現したいこと

Unity/C#によるゲーム開発をしています。
データのセーブ・ロードをJSONシリアライズ/デシリアライズにより実現しようと考えていたところ、
下記の問題に思い当たりました。

発生している問題・エラーメッセージ

「シリアライズによりオブジェクト参照関係が失われ、デシリアライズ時には複数のインスタンスが生成されてしまう」

例えばRPGで考えますと、キャラクターを管理するCharacterクラスのインスタンスと、
パーティーを管理するList<Character>のインスタンスがあるとします。
また、キャラクタはA, B, Cの3人。パーティーは1つだけで、キャラクタA,Bが所属している(Listに含まれている)と考えます。

PGMの初めの実行時(メモリ上)では、パーティーのインスタンスにはキャラクタA,Bへの参照として情報が保持されています。
そのため、アイテムの装備画面などでキャラクタAの情報を更新した場合でも、パーティーのインスタンスにも情報が反映されています。
(もちろん、同じオブジェクトを参照しているので当たり前のことです)

つまり、戦闘を行う場合は、パーティーインスタンスの情報だけを持っていれば問題ないということです。

しかし、セーブ・ロードのためのシリアライズ/デシリアライズをかませることで、問題が発生します。
パーティーのインスタンスと、キャラクタのインスタンスをデシリアライズした結果、オブジェクトの参照関係が失われてしまうからです。
上記の例でいうと、パーティーに所属しているキャラクタAのインスタンスと、個別のキャラクタのインスタンスAが、別々に存在してしまいます。
アイテムの装備画面でキャラクタAの情報を更新した場合、パーティーのインスタンスのAには反映されなくなってしまいます。

検討したこと

対策案1.
シリアライズするクラスは、他のクラスへのオブジェクト参照は持たずに、間接的に情報を持つようにする。
上記の例でいうと、パーティーの管理はList<Character>ではなく、CharacterにIDを持たせて、そのIDを参照するList<int>として、実装する。

 メリット:やりたいことは実現できそう
デメリット:直感的なロジックではなくなる

対策案2.
オブジェクトへの参照箇所を1つにする。
個別にCharacterのインスタンスを持つのではなく、List<Character>のみを管理するようにする。

 メリット:やりたいことはできそう。
デメリット:アイテムの装備画面など、Characterだけを相手にしたいときは多いはず。そのようなときにノイズとなる。また、例のような簡単なケースではなく、規模が大きくなるほどに問題も膨れ上がりそう。

対策案1を採用することになるのかなと考えていますが、検討してみた対策に自信がなかったため、質問をさせていただきました。
上記の対策案の是非、他のやり方などを教えていただければ幸いです。

###補足
このデシリアライズによる問題点を調べたのですが、困っているという情報すら出てこなかったため、困惑しております。
そもそもの私の設計思想が間違っているため、この問題に突き当たっている可能性もあるのかなと考えてはいます。

dodox86👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

Unityを使用したことがなく、業務系アプリしか作ったことがないのですが下記のような感じでどうでしょうか?
y_waiwai様の内容をコードにした程度ではありますが・・
基本的な考えとしてこのような状況では自分もy_waiwai様と同じでオブジェクト参照を回復するという方法で管理すると思います。
なのであまりそういった情報が無い&ライブラリ等も無いのかなと思います。

C#

1using System; 2using System.Collections.Generic; 3using System.Linq; 4using System.Runtime.Serialization.Json; 5using System.IO; 6 7namespace ConsoleApp1 { 8 9 class Program { 10 11 static void Main(string[] args) { 12 13 Party = new List<Character>(); 14 Characters = new Dictionary<string, Character>(); 15 16 var characterA = Add(new Character() { Name = "A", Job = "勇者" }); 17 var characterB = Add(new Character() { Name = "B", Job = "賢者" }); 18 var characterC = Add(new Character() { Name = "C", Job = "戦士" }); 19 var characterD = Add(new Character() { Name = "D", Job = "戦士" }); 20 var characterE = Add(new Character() { Name = "E", Job = "戦士" }); 21 22 Party.AddRange(new[] { characterA, characterB, characterC }); 23 24 Save(); 25 26 Load(); 27 28 System.Diagnostics.Debug.WriteLine(Party[1].Job); 29 Characters["B"].Job = "忍者"; 30 System.Diagnostics.Debug.WriteLine(Party[1].Job); 31 } 32 33 private static Character Add(Character character) { 34 Characters.Add(character.Name, character); 35 return character; 36 } 37 38 [Serializable()] 39 private class Character { 40 public string Name { set; get; } 41 public string Job { set; get; } 42 } 43 44 private static List<Character> Party; 45 private static Dictionary<string, Character> Characters; 46 47 private static void Save() { 48 49 using (var stream = new FileStream("Characters.dat", FileMode.Create)) { 50 var serializer = new DataContractJsonSerializer(typeof(Dictionary<string, Character>)); 51 serializer.WriteObject(stream, Characters); 52 } 53 54 using (var stream = new FileStream("Party.dat", FileMode.Create)) { 55 var serializer = new DataContractJsonSerializer(typeof(IEnumerable<string>)); 56 serializer.WriteObject(stream, Party.Select(x => x.Name)); 57 } 58 } 59 60 private static void Load() { 61 62 using (var stream = new FileStream("Characters.dat", FileMode.OpenOrCreate)) { 63 var serializer = new DataContractJsonSerializer(typeof(Dictionary<string, Character>)); 64 Characters = (Dictionary<string, Character>)serializer.ReadObject(stream); 65 } 66 67 IEnumerable<string> names; 68 69 using (var stream = new FileStream("Party.dat", FileMode.OpenOrCreate)) { 70 var serializer = new DataContractJsonSerializer(typeof(IEnumerable<string>)); 71 names = (IEnumerable<string>)serializer.ReadObject(stream); 72 } 73 74 Party = new List<Character>(); 75 foreach (var name in names) { 76 if (Characters.TryGetValue(name, out var character)) { 77 Party.Add(character); 78 } 79 } 80 } 81 82 } 83}

投稿2020/06/16 03:55

dekaaki

総合スコア292

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

m3m3

2020/06/17 15:53 編集

コードの作成もしていただき、ありがとうございます。 やはりコードで見ると理解が進みますね。 私もあれから検討した結果、オブジェクトの参照を回復させる方向で考えています。 具体的には、Unityで用意されているインタフェース(Unityの話になりますが、ISerializationCallbackReceiver らしいです)を利用することで、該当のクラスがシリアライズ/デシリアライズされる際に、自動でオブジェクト参照を回復してくれるようにするつもりです。 本題とはそれますが、上記に関係して地味な悩みが出てきました。 オブジェクト参照を回復させるための、関連付けをする情報をどこに持つべきなのか、という問題です。 書いていただいたコード var characterA = Add(new Character() { Name = "A", Job = "勇者" }); にあるように、DictionaryなどでCharacterクラスの外に関連付けの情報(上記ではName = "A")を持つべきか、Characterクラスの中に、Name=Aのように持つべきか。 管理のしやすさ(直感的なわかりやすさ、バグのはいる余地の少なさなど)と、PGM実行効率のメリデメを考えて判断しようとしていますが、なかなか答えがでませんね。 話がそれてしまいましたが、コメントありがとうございました。 やはり、オブジェクト参照は各自が回復させることを考えて、ライブラリにはないのでしょうね。 個人的に、ライブラリ側で対応することを希望する人が多いかと思っていたので、そのようなライブラリがないことに動揺しましたが、実行速度を求める人の方が多いのかなと、今では納得しています。
dekaaki

2020/06/18 00:26

>DictionaryなどでCharacterクラスの外に関連付けの情報(上記ではName = "A")を持つべきか、Characterクラスの中に、Name=Aのように持つべきか。 私自身のやり方としてはあまりプログラム起点として考えるのではなく、現実世界でのやり方をプログラム化するという考えでいつも考えてます。 例えば 私の出した例で考えると ・複数人の冒険者がいて冒険者に連絡(アクセス)を取りたい。 ・複数の冒険者でパーティを組む ということを現実世界の管理に当てはめると ■複数人の冒険者がいて冒険者に連絡を取りたい。 ・ある冒険者を特定するためにはどうすればいいか  →何らかの固有名詞(ユニークな情報)を使用=Nameプロパティ ・冒険者へのアクセス  →前述の固有名詞の一覧から冒険者を特定し連絡=DictionaryのキーにNameプロパティの値、値には冒険者のインスタンス ■複数の冒険者でパーティを組む 前述の冒険者を特定するための情報(Nameプロパティ)を使用してパーティ台帳に記帳 パーティ台帳を見れば冒険者へのアクセスは可能  →パーティ台帳に記帳=保存時にNameプロパティを保存 といった感じでしょうか
dekaaki

2020/06/18 00:46

そのうえで >DictionaryなどでCharacterクラスの外に関連付けの情報(上記ではName = "A")を持つべきか、Characterクラスの中に、Name=Aのように持つべきか。 に対する私の回答としては 冒険者の一覧の管理は必要と考え、冒険者の一覧から特定の冒険者にアクセスする機能があれば便利  →Dictionaryを使用   例えばある友人に電話で連絡したいとき、電話帳に1000人登録されていれば名前で検索しますよね 特定条件下の冒険者一覧を抽出  →DictionaryのValuesプロパティから条件に一致する冒険者を抽出   例えば電話帳に1000人登録されていて同級生を抽出するような場合は面倒でも電話帳に登録されている人を全て調べる   ただ上記の抽出がたびたび行われるのであれば毎回調べるのは面倒なので県別の一覧を作成 となり、Dictionaryでの管理は必要と考えます。 なんか長文になってしまいすみません^_^;
guest

0

ベストアンサー

  • シリアライザ・デシリアライザのカスタマイズが可能ならカスタム処理を書く
  • 別のシリアライザにする(バイナリでもいいならMessagePack for C#とか)

JSON形式に拘りがあるかどうかという点が気になります。

投稿2020/06/13 14:51

退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

m3m3

2020/06/14 00:53

回答ありがとうございます。 JSON形式への拘りはないため、バイナリでも構いません。 しかし、JSON/バイナリで、挙動に違いが出るのでしょうか? どちらもオブジェクト参照関係の保存、復活は出来ないもので、実行速度と確認のしやすさに違いがあるだけかと思っていました。 それとも、形式というよりは、シリアライザ(の実装)によるのでしょうか? もう1つ書いていただいたシリアライザのカスタムはできるため、そちらも検討してみます。 (どれとどれが参照関係にあるのか、情報を保持して関係を復活させる) ps.MessagePackは初めて知りましたが、速度だけみてもかなり魅力的ですね.
退会済みユーザー

退会済みユーザー

2020/06/14 01:58 編集

よく質問を見返してみましたが、問題の本質を少し勘違いしていたかもしれません。参照関係は対策案1のようにIDを割り振って、自力で戻す必要があるかもしれません。
m3m3

2020/06/14 10:37

コメントありがとうございます。 あれから引き続き調べたのですが、オブジェクト参照までカバーしてくれるライブラリが見つからず、またunity公式にもネスト構造をシリアライズしないことを推奨とありました。 使いやすさと実行速度のトレードオフでは使いやすさを取りたかったのですが、実装が困難・推奨されていないことから、案1の方向性で進めたいと思います。 ご対応ありがとうございました。
guest

0

一案ですが、
他クラスのオブジェクトともに、インデックスも持たせるようにすれば。
デシリアライズ時に、インデックスからオブジェクト参照を回復させればいい。

投稿2020/06/13 12:45

y_waiwai

総合スコア88042

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

m3m3

2020/06/14 00:25

回答ありがとうございます。 間接的に参照するのではなく、参照関係を回復する点で、案1に比べて直感的なプログラムを作れそうです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問