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

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

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

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

Unity

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

Q&A

1回答

2318閲覧

ScriptableObjectから実態classへのスマートな値の受け渡し

退会済みユーザー

退会済みユーザー

総合スコア0

C#

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

Unity

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

0グッド

0クリップ

投稿2021/08/06 05:06

編集2021/08/06 05:09

前提,実現したいこと

Assetとして保存しているScriptableObjectから,
実際にデータを保持してゲーム内でのふるまいを実装したclassへの値の受け渡しについて悩んでいます.
Unity, C#ともに初心者です.
ScriptableObjectについて調べてみても,
データの受け渡しについて詳しく書かれたものは見つかりませんでした.
何か妙案あればアドバイスいただきたいです.

例えば,以下のようなScriptableObjectとその実態クラスがあります.

C#

1using System.Collections.Generic; 2using UnityEngine; 3 4public class Character { 5 private string name; 6 private int max_hp; 7 private int max_mp; 8 private int atk; 9 private int def; 10 . 11 . 12 . 13 // ここまで CharacterData の field と全く同じ 14 15 // ここから Character のみ持つ field 16 private int hp; 17 private int mp; 18 19 public void Damage(int damage) { 20 this.hp -= damage 21 } 22 public int Attack() { 23 int damage = this.atk; 24 return damage; 25 } 26 . 27 . 28} 29 30public class CharacterData : ScriptableObject { 31 public string name; 32 public int max_hp; 33 public int max_mp; 34 public int atk; 35 public int def; 36 . 37 . 38 . 39 40 public Dictionary<string,T> getFieldDict() { 41 // {{"name", "hogeman"}, {"max_hp", 10}, ....} 42 // のように{{field_name, value}, ...}を返す 43 return field_dict; 44 } 45}

このCharacterDataからCharacterにどのようにデータを渡すのが,
Unityの設計思想に合いつつ,スマートになるのか悩んでいます.

現在考えているのは以下です.

まずCharacterDataを読み込みます.

C#

1var path = "CharacterDataのAssetへのpath"; 2var data = Resources.Load(path) as CharacterData;
  1. Character classにDictonaryで値を渡す.

C#

1 public Character(Dictonary<string,T> status) { 2 // 辞書で受け取るコンストラクタ 3 // リフレクションを使って dictionary から field に 値を入れる処理 4 }

このようなコンストラクタを持った Character class を以下のようにインスタンス化

C#

1var character = new Character(data.getFieldDict());
  1. Character classにCharacterDataをそのまま渡す.

C#

1 public Character(CharacterData status) { 2 // CharacterDataをそのまま保持するのか, 3 // リフレクションを使って CharacterData field から Character fieldへ値を入れるのか 4 }

このようなコンストラクタを持った Character class を以下のようにインスタンス化

C#

1var character = new Character(data);

現在考えているのはこの2通りです.
スマートさでいうと2番だと思うのですが,
マスタデータをそのまま実態クラスが保持することは,
Unityの設計思想に合ってなさそうな気がしています.
これ以外のスマートで設計思想に合った方法や,
この方法はもっとこうできるでというアイデア,
皆様がいつもやっている,マスタデータから実態へのデータの受け渡しの方法など
ご教授ください.

また,次の前提でやっています.

  • CharacterDataのfieldは増減する

(その度にCharacterのfieldは書き換える)

  • CharacterDataのfieldはゲーム中には変動しない(させない)が,Characterのfieldは変動する

(バイキルトをかけるとatkが二倍になったりする)

  • CharacterDataはEditor上でcsvから読み込んでAssetとして保存済み
  • コメントアウトで実装が省かれている部分は動くようにできる

よろしくお願いします.

バージョン

  • Unity 2020.3.13f1
  • .NET 4.0

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

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

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

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

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

guest

回答1

0

そもそも ScriptableObject を使う目的が何であるかに戻りましょうか。
ScriptableObject には下記のような記載があります。

値に対し複数のコピーを避けることによってプロジェクトのメモリ消費を削減すること

上記が活かされているか否かをベースに考えてみましょう。

1番の方法は getFieldDict で Dictionary を生成して、各 CharacterData のフィールドをそこに追加していると思いますが、その時点で下記ソースコードのように値に対してコピーをしていると思います。

C#

1public Dictionary<string,T> getFieldDict() { 2 var field_dict = new Dictionary<string,T>(); 3 field_dict.Add("name", name); 4 // ここで値のコピーが行われている 5 field_dict.Add("max_hp ", max_hp ); 6 field_dict.Add("max_mp ", max_mp ); 7 field_dict.Add("atk", atk); 8 field_dict.Add("def", def); 9 return field_dict; 10}

これでは値のコピーによるメモリ消費が発生しているため、ScriptableObject の目的が果たされていません。

2番の方法はオブジェクトを渡しているため値ではなく status の参照(status が持つ値に割り当てられているメモリのアドレス情報)のみをコピーを渡しています。

C#

1private readonly CharacterData _status; 2public Character(CharacterData status) { 3 _status = status; // 参照型の代入は、値のコピーを行わない 4}

これは値のコピーを行っていないので、ScriptObject の目的に沿っています。

また、2番の方法の派生として下記のような書き方も考えつくかとは思いますが、これも値のコピーが行われます。

C#

1public Character(CharacterData status) { 2 name = status.name; 3 max_hp = status.max_hp; // 値型の代入は、値のコピーを行うので無駄なメモリを消費する 4 max_mp = status.max_mp; 5 atk = status.atk; 6 def = status.def; 7 // フィールドの増減に合わせて下記のように処理を加える 8 // luk = status.luk; 9}

値のコピー、参照のコピーがよくわからない場合は下記を確認してください。
この仕様は C# でコードを書く上では必ず知らないといけません。
値型と参照型

結論

以上を考慮すると2番を使うのが理に適っていて、ソースコードもシンプルになり良いことしかないと思います。

また、ドキュメントのサンプルコードを見ると分かりますが MonoBehavior に相当するオブジェクトが ScriptableObject を保持しています。
( Spawner has SpawnManagerScriptableObject の形になっています)

ですから、ScriptableObject を保持することが Unity の設計思想に合わないということはないかと思います。

投稿2021/08/06 06:57

編集2021/08/06 07:20
BluOxy

総合スコア2663

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

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

退会済みユーザー

退会済みユーザー

2021/08/06 08:21

BluOxy様: 質問をまたいでのご回答ありがとうございます. 考え方のご提示,アドバイスありがとうございます. 確かにScriptableObjectへのreadonlyな参照を持てば, Characterからの変更を防げて強固な作りに見えます!! 正直readonkyの存在を知らなかったのでとてもありがたいです. ただ,公式のドキュメントでは, ゲーム内に実態を持たない管理者であるいわゆるGameManagerに参照を持たせているのは確認できます. 管理される側であるCharacterオブジェクトに直接参照を持たせるべきかどうかはもう少し考えてみます. 勉強になります,ありがとうございます.
BluOxy

2021/08/06 08:43

readonlyに関して余談です。今回の場合、readonlyは_statusに対する再代入を防ぐことはできますが、下記のようなコードは通ることに気をつけてください。 _status.max_hp = 20; このような変更も防ぎたい場合は各フィールドに readonly をつけるか、get-only プロパティとして定義し直す等の対処が必要です。
BluOxy

2021/08/06 09:09 編集

> ゲーム内に実態を持たない管理者であるいわゆるGameManagerに参照を持たせているのは確認できます これも余談です。(余談ばかりですみません) 個人記事でGameManagerという名前はよく見ますが、公式ドキュメントにそのようなクラスを使ったサンプルはあったでしょうか。というのも、GameManagerという名前は、名前からして役割が曖昧ですから、あまりよろしくない命名かと思っています。 命名が良くないか悪いかは諸説あるかもしれませんが、好きか嫌いかでいえば好きではありません。 公式ドキュメントでそういった曖昧な命名を本当に使っているのか疑問に思いました。 > 管理される側であるCharacterオブジェクトに直接参照を持たせるべきかどうかはもう少し考えてみます. 少なくとも、GameManagerがGameの何をどう管理するクラスかがわからないので、CharacterDataをGameManagerに持たせるべきか否かの判別も現状では難しいのではないでしょうか。 なので、命名が大事だと思います。「名は体を表す」とよく言いますが、その言葉を意識してクラスやメソッド、フィールドなどを命名し、その命名に見合ったものだけを実装するということを意識して作れば、CharacterDataをどう取り扱うかも見えてくるかと思います。 また、管理する管理される関係なく、CharacterDataはCharacterの初期化をすることが目的のオブジェクトでしょうから、CharacterがCharacterDataを知らないのは逆に違和感があります。
退会済みユーザー

退会済みユーザー

2021/08/07 07:01

> このような変更も防ぎたい場合 ScriptableObject の public field は readonly に設定できないようです. get-only プロパティについてはリフレクションを使用してのfieldへのアクセスに少し工夫がいりそうですが, 調べてみようと思います.ありがとうございます. > GameManagerという名前 すみません,管理者の概念の話をしているつもりでした. 私はGameManagerという名前のクラスを作成するのは, ゲームの起動から終了までの全てを管理するときだけだと思います. 個人記事を作成されている方も,概念をそのままクラス名にしたのでしょうか. pythonの解説記事では何でもかんでもhogeやfugaと命名しているので,比較すると良心的には感じます. Unityの記事には非エンジニアの方が執筆したものも散見されますので, エンジニアの方がフラストレーションがたまるのもわかります. 私は概念としてのGameManager,中間管理職も含めたものの話でした. > CharacterがCharacterDataを知らないのは逆に違和感があります マスタデータはゲーム世界の設定ですので,地球の自転が1日周期である というようなことを持たせているつもりです. もちろん,私たちCharacterが地球の自転を2日にしたり1時間にするべきではありませんしできません. このような感覚で地球の自転を2日にできちゃう参照をCharacterに渡すべきではなく, 神様であるCharacterManager(GameManagerの概念を持ったクラス)に渡して,管理してもらおう思想がUnityの思想かなと感じています.
BluOxy

2021/08/07 16:43 編集

リフレクションを使用しなくても get-only プロパティにそのままアクセスできます。 リフレクションは実装が複雑になるので避けられるならば避けた方が良いと思います。 動的にエラーが発生するようになり、本来コンパイルエラーで止められたエラーが止められなくなるためです。静的型付け言語としてのメリットを失い、デバッグが困難になります。 > このような感覚で地球の自転を2日にできちゃう参照をCharacterに渡すべきではない get-only プロパティ経由でのみアクセスできるようにすれば、良いのです。フィールドは下記のように private にして隠蔽します。 ---- [SerializeField] private string name; public string Name { get { return name; } } ---- 下記のように短縮させることもできます。上記の実装と同様の動きになります。 ---- [SerializeField] private string name; public string Name { get => name; } ---- 下記のように短縮させることもできます。上記の実装と同様の動きになります。 ---- [SerializeField] private string name; public string Name => name; ---- 自動プロパティがScriptableObjectで使えるならばフィールドの定義をせずに public string Name { get; } だけで対応することも出来ます。 プロパティ https://ufcpp.net/study/csharp/oo_property.html > もちろん,私たちCharacterが地球の自転を2日にしたり1時間にするべきではありませんしできません.このような感覚で地球の自転を2日にできちゃう参照をCharacterに渡すべきではなく プロパティを使う目的は 実装の隠蔽 で、まさしく mori.hey さんの要件に沿っていると思います。 https://ufcpp.net/study/csharp/oo_conceal.html
BluOxy

2021/08/07 16:39 編集

CharacterクラスはCharacterDataを参照して値を取得できないと初期化しようがありません。 もちろんCharacterDataからコレクションに値をコピーして、コレクションからCharacterへ値を渡す方法でもCharacterの初期化はできるとは思いますが、実装が冗長的です。 CharacterDataを参照して外部から書き込めてしまうことが真の問題ではないでしょうか。 ですから、外部から書き込めないように get-only プロパティを使い実装を隠蔽しましょう。そうすれば外部から書き込まれる心配もせずに済み、気兼ねなく Character が CharacterData を参照するように実装できます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問