▶
【Unity】3Dアクションゲームを作ろう!#7 ステージの作成(Skybox・落下判定)
▶
【Unity】3Dアクションゲームを作ろう!#8 ステージの作成(スイッチ・扉)
▶
【Unity】3Dアクションゲームを作ろう!#9 プレイヤーのHP管理
▶
【初心者Unity】JsonUtilityクラスでJSONを扱う方法
▶
【初心者Unity】スクリプトからコンポーネントを追加する方法
1.はじめに
本記事では、C#における構造体について解説していきます。
構造体自体はあまり耳慣れない単語だと思いますが、機能としては皆さん一度は使ったことがあるはずです。
それらの機能を正しく理解するためにも、基礎となる構造体の知識をしっかりと獲得しましょう。
Unity上で動作するサンプルコードも載せていますので、是非一緒に手を動かして確認してみてください!
2.構造体とは
構造体とは、複数の変数をまとめて管理するためのものです。
と、言葉で説明してもイメージし辛いと思いますので、具体例を考えてみましょう。
生徒情報を整形して表示するメソッドを作成する。
生徒情報は以下のデータで構成される。
・学籍番号
・名前
・評点
上記のようなシナリオを考えていきます。
まずは構造体を使用しない例を見てみましょう。
void Start()
{
Format(100, "田中太郎", 3.01f);
}
void Format(int id, string name, float score)
{
// $ は 文字列補間式
Debug.Log($"学籍番号:{id}\n名前:{name}\n評点:{score}");
}
■出力結果
学籍番号:100
名前:田中太郎
評点:3.01
構造体を使用しない場合、メソッドの引数には学籍番号、名前、評点の3つの値を渡す必要があります。
3つ程度ではそれほど手間ではないかも知れませんが、生徒情報の項目数が増えた場合、呼び出し元のメソッドでの記述が大変そうです。
次に、構造体を使用した例を見てみます。
void Start()
{
StudentInfo si;
si.id = 100;
si.name = "田中太郎";
si.score = 3.01f;
Format(si);
}
void Format(StudentInfo si)
{
Debug.Log($"学籍番号:{si.id}\n名前:{si.name}\n評点:{si.score}");
}
struct StudentInfo // 構造体の定義
{
public int id;
public string name;
public float score;
}
長くなりましたね…。
14行目からの記述が構造体の定義になります。
struct StudentInfo // 構造体の定義
{
public int id;
public string name;
public float score;
}
学籍番号(int)、名前(string)、評点(float)と型の異なる3つのデータを、StudentInfoという構造体でひとまとめにしています。
構造体の構文は以下の通りです。
struct 構造体名
{
// 変数等...
}
Startメソッドでは構造体内の変数(フィールド)に値を代入しています。
void Start()
{
StudentInfo si;
si.id = 100;
si.name = "田中太郎";
si.score = 3.01f;
Format(si);
}
ここで抑えていただきたいのは、構造体名が型名になる、ということです。(3行目)
また、各フィールドへのアクセスは、構造体を表す変数名.フィールド名という形で記述します。
Formatメソッドの引数はStudentInfo型で定義されています。
void Format(StudentInfo si)
{
Debug.Log($"学籍番号:{si.id}\n名前:{si.name}\n評点:{si.score}");
}
受け渡す値を一つにすることで、シンプルに記述できます。
以上の通り、構造体を使用することで複数の変数を一つの変数にまとまることができます。
今回の例だけではあまりメリットがイメージできないかも知れませんが、他のメソッドでも生徒情報を使用する場合には、その恩恵を享受できるかと思います。
次の例では、構造体を使用した場合の方が記述がシンプルになってることがわかります。
void Start()
{
StudentInfo si;
si.id = 100;
si.name = "田中太郎";
si.score = 3.01f;
// 構造体を使用しない場合
Format(100, "田中太郎", 3.01f);
AnotherMethod1(100, "田中太郎", 3.01f);
AnotherMethod2(100, "田中太郎", 3.01f);
AnotherMethod3(100, "田中太郎", 3.01f);
// 構造体を使用した場合
Format(si);
AnotherMethod1(si);
AnotherMethod2(si);
AnotherMethod3(si);
}
3.構造体の構成要素
上でも説明しましたが、構造体の構文は次のようになります。
struct 構造体名
{
// 変数等...
}
ここでは、構造体の中に記述できる要素について見ていきましょう。
変数
先ほどの説明でも登場しましたが、変数は構造体の構成要素として記述することができます。
struct StudentInfo
{
public int id;
public string name;
public float score;
// Static変数も記述可
public static int count = 100;
}
変数へのアクセスは構造体を表す変数名.フィールド名という形で記述します。
void Start()
{
StudentInfo si;
si.id = 100;
Debug.Log(si.id);
}
■出力結果
100
Static変数へのアクセスは構造体名.フィールド名という形で記述します。
void Start()
{
Debug.Log(StudentInfo.count);
}
■出力結果
100
メソッド
変数同様メソッドも記述することができます。
struct StudentInfo
{
public int id;
public string name;
public float score;
public string CreateMailString()
{
// $ は 文字列補間式
return $"学籍番号 {id}番 {name} 様\nあなたの今期GPAは {score} です。";
}
}
コンストラクタ
コンストラクタを記述することができます。
コンストラクタは初期値を設定する用途で使用されます。
struct StudentInfo
{
public int id;
public string name;
public float score;
public StudentInfo(int id, string name, float score)
{
this.id = id;
this.name = name;
this.score = score;
}
}
コンストラクタはnew演算子の直後に呼び出します。
void Start()
{
StudentInfo si = new StudentInfo(100, "田中太郎", 3.01f);
Debug.Log(si.id);
}
■出力結果
100
4.構造体の使用例
Vector3
Unityにおける構造体の使用例の代表格はVector3(またはVector2)でしょう。
Vector3のソースコードは以下のように記述されています(大幅に省略しています)。
⋮
public struct Vector3 : IEquatable<Vector3>, IFormattable
{
⋮
public float x;
public float y;
public float z;
⋮
public Vector3(float x, float y);
public Vector3(float x, float y, float z);
⋮
public static Vector3 Lerp(Vector3 a, Vector3 b, float t);
⋮
public static float Magnitude(Vector3 vector);
⋮
}
変数やメソッド、コンストラクタが定義されているのが分かりますね。
int
また、意識することはあまり無いとは思いますが、intやfloatも実は構造体として定義されています。
以下はInt32のソースです(intはInt32という構造体の別名です)。
⋮
public struct Int32 : IComparable, IComparable<Int32>, IConvertible, IEquatable<Int32>, IFormattable
{
public const Int32 MaxValue = 2147483647;
public const Int32 MinValue = -2147483648;
public static Int32 Parse(string s, IFormatProvider provider);
public static Int32 Parse(string s, NumberStyles style, IFormatProvider provider);
public static Int32 Parse(string s, NumberStyles style);
public static Int32 Parse(string s);
public static bool TryParse(string s, NumberStyles style, IFormatProvider provider, out Int32 result);
public static bool TryParse(string s, out Int32 result);
⋮
}
int.TryParseメソッドなどを使ったときに「intにメソッドがあるの?」と思われた方もいるかもしれませんね。
構造体なのでメソッドがある訳です。
5.構造体の使用場面
クラスについて学んだことのある方は、構造体とクラスの使い分けに悩まれるかもしれません。
Microsoftの公式ドキュメントでは以下の指針が示されています。
➡クラスまたは構造体の選択
❌ 型が次のすべての特性を持つ場合を除き、構造体を定義することは避けてください。
- プリミティブ型 (int、double など) と同様に、論理的に単一の値を表す。
- インスタンスのサイズが 16 バイト未満である。
- 不変である。
- 頻繁にボックス化する必要がない。
その他すべての場合は、型をクラスとして定義する必要があります。
クラスと構造体の大きな違いは、参照型であるか値型であるか、というところにあります。
構造体を初期化する際にnew演算子を使うため勘違いされることが多いですが、C#において構造体は値型として定義されています。
下記の例では、SomeMethod内で引数に受け取った構造体のフィールドを書き換えていますが、呼び出し元には影響していないことが確認できます。
これはSomeMethodにsiのコピーが渡されているためです。
void Start()
{
StudentInfo si = new StudentInfo(100, "田中太郎", 3.01f);
SomeMethod(si);
Debug.Log(si.name);
}
void SomeMethod(StudentInfo si)
{
si.name = "山田太郎";
Debug.Log(si.name);
}
struct StudentInfo
{
public int id;
public string name;
public float score;
public StudentInfo(int id, string name, float score)
{
this.id = id;
this.name = name;
this.score = score;
}
}
■出力結果
山田太郎
田中太郎
インスタンスのコピーを渡したい場合などに構造体が使用できるかも知れませんが、利用機会は多くないかもしれません。
6.おわりに
構造体について解説しました。
実際に自分で構造体を作る機会はそれほど多くないかもしれません(多くの場合、クラスの方が有用でしょう)が、Vector3に代表されるように、Unityが提供する機能では当たり前のように使用されています。
こうした背景の知識を理解することで、それらを使用する際に、思わぬバグが発生してしまうことを抑制できるのではないでしょうか。
今まで何となくnew Vector3()と書いていた方が、構造体のコンストラクタを呼び出しているのだな、と思えるようになっていたら嬉しいです。