##この記事について
ふとUnity Learnで勉強しようと思いたち、中級レベルのスクリプティングという記事を現在進行系で見ている。
動画の日本語化もまだらしく、後学の為にもノート代わりにQiitaにまとめてみる。知ってる人からするとうざったらしい位丁寧に書く予定です。
自分も学習中の身なのでここ違うよってところがあったらコメントお願いします。
プログラム部に関してimport部分はブラケットごと省略
まったり執筆予定
##プロパティ
###プロパティとは
フィールドをカプセル化するための構文(Wikiの超訳)
getとsetの2種類のアクセサーからなる
※カプセル化とそのメリットについてはこちらのサイトの記事が大変分かりやすい
###なぜプロパティを使うのか
publicなフィールド変数と比較して以下の2つの出来ることが存在するため
- getとsetのアクセサーを関数の様に扱うことが出来る
- getとsetのアクセサーはそれぞれ省略可能であり、それぞれ省くとフィールドをwriteonlyとreadonlyのような扱いに出来る
これら2つの利点に関しては応用の方で見ていく
###実例
Unity Learnの記事から拝借
個人的注釈コメントが多分に含まれてます
基本的なプロパティの書き方と動き方
public class Player
{
private int experience; // privatなフィールドなため、外部クラスから直接書き換え不可能
// プロパティの命名規則として、メンバ変数の戦闘大文字がグッド
public int Experience
{
get
{
// Experienceが呼び出される際に実行される
return experience;
}
set
{
// Experienceに代入する際に呼び出される。valueには代入する値が勝手に入る。便利
experience = value;
}
}
外部のクラスであるGameクラスからprivateなフィールドのexperienceにプロパティExperienceを経由してアクセス可能となる(この状態がいわゆるカプセル化されたフィールド)
public class Game : MonoBehaviour
{
void Start ()
{
Player myPlayer = new Player();
// プロパティは変数と同じように使用可能
myPlayer.Experience = 5; // この場合はsetアクセサーが働き、experience = 5 となる
int x = myPlayer.Experience; // この場合はgetアクセサーが働き、x = experience となる
}
}
###応用
以降プロパティ部分のみ記述
1.関数のような処理の記述
プロパティを使用するメリット1つ目の実例
getアクセサーとsetアクセサー内では他の処理を記述する事が可能なため、関数のように処理が可能
public int Experience
{
get
{
return experience;
}
set
{
// ボーナスタイム中ならば経験値を2倍獲得
if (isBonus)
{
experience = value * 2;
}
else
{
experience = value;
}
}
}
2.直接でないプロパティでのカプセル化
RPGゲームを仮定し、Playerクラスにレベルを実装する場合を考える
レベルは経験値(experience)が1000貯まる毎に1上がるものとする
この場合、下記のようなLevelプロパティをPlayerクラスに導入することでExperienceプロパティを使わずともexperienceフィールドをカプセル化しつつPlayerクラスにレベルを実装出来る
public int Level
{
get
{
// 戻り値はレベルの値。1000毎に1レベルアップなのでexperience/1000
return experience / 1000;
}
set
{
// レベルを受け取り、Playerの経験値量を計算しexperienceに格納する
experience = value * 1000;
}
}
3.自動実装プロパティ
下記のようにプロパティを記述することで、実質的にフィールドと同じような意味合いのプロパティを作成する事が出来る
挙動としては実例で挙げたExperienceプロパティと同様の挙動を示す
public int Experience { get; set; }
4.privateなgetアクセサーとsetアクセサー
getアクセサーとsetアクセサーにはそれぞれprivateを付けることができ、アクセス制限をかけれる
public int Experience { get; private set; } // 外部からexperienceを参照可能だが、代入は不可能
public int Experience { private get; set; } // 外部からexperienceは参照不可能だが、代入は可能(使い所なさそー)
5.readonlyとwriteonlyなプロパティ
プロパティを使うメリット2つ目の実例
getとsetはそれぞれ省略可能なため、get省略時はwriteonly、set省略時はreadonlyな挙動となる
public int Experiment { get; } // setが無いので代入不可(readonly)
public int Experiment { set; } // getが無いので参照不可(writeonly)
###余談
C# 9.0でinitアクセサーという便利な物が追加されたが、追加された事をアナウンスするだけにとどめておく
クラス初期化時のみ使用できるsetアクセサーのようなものと自分は認識している(違ったらごーめん)
##三項演算子
###三項演算子とは
条件分岐を司る演算子の1種、条件、真の時、偽の時の3つの項目を使用する
『第一項 ? 第二項 : 第三項』というフォーマットで記述され、第一項は条件を、第二項は条件が真の時の値を、第三項は偽の時の値を示している
###なぜ三項演算子を使うのか
if~else~を1行で書き表す事が出来るから、冗長なコードは避けようねって話
###実例
毎度のごとくUnity Learn記事から引用
public class TernaryOperator : MonoBehaviour
{
void Start ()
{
int health = 10;
string message;
// 変数healthの値に応じてmessageに代入する値が分岐している
// healthが正の値ならコロンの左の文字列を、healthが負の値ならコロンの右の文字列が代入される
message = health > 0 ? "Player is Alive" : "Player is Dead";
}
}
上記のコードをif文を使って記述すると下記のようになる。パッと見での可読性は高いが長い
public class TernaryOperator : MonoBehaviour
{
void Start ()
{
int health = 10;
string message;
if (health > 0)
{
message = "Player is Alive";
}
else
{
message = "Player is Dead";
}
}
}
###応用
if~else~が入れ子での条件分岐出来るように、三項演算子の中に三項演算子を入れることで複数の条件分岐が可能
下記はHPが30未満時に分岐を行っている
message = health > 0 ? health < 30 ? "Player is low HP" : "Player is Alive" : "Player is Dead";
if文バージョン
if (health > 0)
{
if (health < 30)
{
message = "Player is low HP";
}
else
{
message = "Player is Alive";
}
}
else
{
message = "Player is Dead";
}
正直2個以上の三項演算子を同時に使うのは著しい可読性の低下を招くと思っているので個人的には使用したくない
括弧で括ったりするならまだ許容できそうかも?
message = health > 0 ? (health < 30 ? "Player is low HP" : "Player is Alive") : "Player is Dead";
###余談
三項演算子のフォーマットはエクセルやスプレッドシートのif文に近いので、そちらを知っている人は割とすんなり読めると思う
##static修飾子
###static修飾子とは
static(静的)な変数、関数、クラスを表す際に使用する修飾子
staticな〇〇というのは、最初から領域を確保しておくという意味合いであり、一貫した状態が保持される
一貫した状態が保持されるため、同じクラスを複数インスタンス化しても、staticな変数は全てのクラスで共通の値となる
###実例
敵キャラのクラスEnemyを考える
敵キャラが何体生成されたかを敵キャラは共通の認識として知っているものとする
public class Enemy
{
// enemyCountは全てのEnemyクラスで共通の値を保持したいためstaticな変数とする
public static int enemyCount = 0;
public Enemy()
{
// コンストラクタにenemyCountのインクリメントを入れる事で
// 何体の敵が生成されたか(何回インスタンス化されたか)
enemyCount++;
}
}
外部クラスからstaticな変数にアクセス
public class Game
{
void Start ()
{
Enemy enemy1 = new Enemy();
Enemy enemy2 = new Enemy();
Enemy enemy3 = new Enemy();
// クラス名とドット(.)演算子を使用するとstatic変数にアクセス可能
int x = Enemy.enemyCount; // 上で3回Enemyクラスをインスタンス化しているため、x=3となる
}
}
###応用
staticなクラスやメソッド
staticなクラスやメソッドは、クラスをインスタンス化せずとも使用可能なため、どんな場面でも使える汎用的な処理などをstaticで宣言しておくと非常に便利
public static class Utilities
{
// 静的メソッドは、クラスのオブジェクトなしで呼び出すことが可能
public static int Add(int num1, int num2)
{
return num1 + num2;
}
}
Utilitiesを使う外部クラス
public class UtilitiesExample : MonoBehaviour
{
void Start()
{
// Utilitiesクラスのインスタンス化なしにクラス名+ドット(.)で関数にアクセス可能
int x = Utilities.Add (5, 6);
}
}
ただしstaticなクラスやメソッドは、staticでないメンバ変数にアクセスすることが出来ない
以下はエラーが出る例
public class test1
{
private int num0 = 10;
// staticでない変数num0を使用しているため以下のメソッドMultipleは宣言出来ないためエラー
public static int Multiple(int num1)
{
return num0 * num1;
}
}
public static class test2
{
// staticでない変数をstaticなクラス内で宣言することは出来ないためエラー
private int num0 = 10;
}
###余談
Unityが提供しているInput
クラスやGameObject
クラスなんかもstaticなクラスであるため、特にインスタンス化などの手続きを踏まずともInput.GetKey()
のようにいきなり使用可能となっている
##メソッドのオーバーロード
###オーバーロードとは
同じ名前のメソッドを複数定義すること
本来は同様の名前は宣言できないが、メソッドに関してはシグネチャが一意に定まっていればメソッド名が同様でも問題ない
※シグネチャ:「メソッド名」「パラメータ数と順序、パラメータの型」「返り値の型」のセットのこと
###実例
オーバーロードを行わない場合
public class SomeClass
{
// int型を足し合わせるAddInt関数
public int AddInt(int num1, int num2)
{
return num1 + num2;
}
// string型を足し合わせるAddString関数
public string AddString(string str1, string str2)
{
return str1 + str2;
}
}
public class UseClass
{
void Start()
{
SomeClass someClass = new SomeClass();
Debug.Log(someClass.AddInt(5, 6)); // 出力は11
Debug.Log(someClass.AddString("Hello,", "World"); // 出力はHello,World
}
}
オーバーロードを使用する場合
public class SomeClass
{
// 最初の Add メソッドのシグネチャは "Add(int, int)"
public int Add(int num1, int num2)
{
return num1 + num2;
}
// 2 番目の Add メソッドのシグネチャは "Add(string, string)"
public string Add(string str1, string str2)
{
return str1 + str2;
}
}
public class UseClass
{
void Start()
{
SomeClass someClass = new SomeClass();
Debug.Log(someClass.Add(5, 6)); // 出力は11
Debug.Log(someClass.Add("Hello,", "World"); // 出力はHello,World
}
}
###余談
オーバーロードされたメソッドの適用優先順
1.シグネチャの完全一致
2.最低量の変換が必要なメソッド
3.エラー
##ジェネリクス
###ジェネリクスとは
プログラミングは通常、型を指定しないといけないが、型自体をパラメータとして受け取る事でその型に対応したクラスやメソッドを生成する機能をジェネリクスという
Unityのスクリプトで一番使うジェネリクスはおそらくGetComponent<T>()
と思われる
使用する際に<T>
にはそのGameObjectが持つコンポーネント名(型)を指定する、これが型を自体をパラメータとして受け取るメソッドということ
###実例
ジェネリクスメソッドとその使用法
public class SomeClass()
{
// 返り値、引数などに使用されているTの事をジェネリクスという
public T GenericMethod<T>(T param)
{
return param;
}
}
使用する側のクラス
public class UseSomeClass() : MonoBehaviour
{
void Start()
{
someClass = new SomeClass();
// ジェネリクスメソッドを使用する際にはやま括弧内に使用する型名を指定する
Debug.Log(someClass.GenericMethod<int>(3)); // 3が表示される
}
}
ジェネリクスはクラス自体に使用出来る
public class GenericClass<T>()
{
T item;
public void UpdateItem<T>(T param)
{
item = param;
}
}
ジェネリッククラス使用例
public class UseGenericClass() : MonoBehaviour
{
void Start()
{
GenericClass<int> genericClass = new GenericClass<int>();
genericClass.UpdateItem(5);
}
}
###応用
ジェネリックがどんな型も取れてしまうとint型のtransformを返すといったありえない挙動が発生しかねない
そのためジェネリクスにはパラメータに取る型に制限をかけることが出来る
具体的にはwhere 変数名 : 制約条件
で記述する
ジェネリックの制約に関してはこちらの記事がとてもわかり易く解説されている
##継承
###継承とは
指定したクラスのコンストラクタ以外のフィールドやメソッドを引き継いで新たなクラスを作ること
継承元に指定されたクラスを親クラスまたは基本クラスと呼び、親クラスを継承するクラスを子クラスと呼ぶ
子クラスは親クラスと同じ様に振る舞うことが出来る
実装方法としては継承先のクラス : 継承元のクラス
で実装可能
継承関係は「is a」関係と称されており「子クラス is a 親クラス」で表すことが出来る関係において継承を使用できるといった具合である
継承に関して重要な修飾子protected
が存在する
今までの修飾子と合わせて、継承時の振る舞いを下に列挙する
-
public
:継承云々に関係なくクラス内外からアクセス可能 -
protected
:継承先のクラスのみアクセス可能、継承関係に無い外部のクラスからはアクセス不可能 -
private
:継承先のクラスであってもアクセス不可能、外部のクラスからは当然アクセス不可能
実際のコードを交えたprotected
の説明はコチラのページを確認して欲しい
###実例
フルーツとりんごの親子関係をクラスで表す場合を想定する
親クラスのフルーツと子クラスのりんごを作成(「りんご is a フルーツ」)
// これは親クラスです
public class Fruit
{
public string color;
// これは Fruit クラスの最初のコンストラクター
// 子クラスには継承されない
public Fruit()
{
color = "orange";
Debug.Log("1st Fruit Constructor Called");
}
// これは Fruit クラスの 2 番目のコンストラクタ
// 子クラスには継承されない
public Fruit(string newColor)
{
color = newColor;
Debug.Log("2nd Fruit Constructor Called");
}
public void Chop()
{
Debug.Log("The " + color + " fruit has been chopped.");
}
public void SayHello()
{
Debug.Log("Hello, I am a fruit.");
}
}
public class Apple : Fruit
{
// これは Apple クラスの最初のコンストラクタ
// 実行前に親クラスのコンストラクタを呼び出す
public Apple()
{
// Apple が親の Fruit クラスの一部である public 変数 colorにアクセス出来ている
color = "red";
Debug.Log("1st Apple Constructor Called");
}
// これは Apple クラスの2番目のコンストラクタ
// "base "キーワードを使用して、どの親コンストラクタが呼び出されるかを指定する
public Apple(string newColor) : base(newColor)
{
// 親クラスのコンストラクタ内で、引数として渡された色を設定しているので、
// こちらのコンストラクタでは color は指定しない
Debug.Log("2nd Apple Constructor Called");
}
}
継承が行われているかどうか確認するクラス
public class FruitSalad : MonoBehaviour
{
void Start ()
{
// デフォルトコンストラクターを使って
// 継承がどのように行われているか確認する
Debug.Log("Creating the fruit");
Fruit myFruit = new Fruit();
Debug.Log("Creating the apple");
Apple myApple = new Apple();
// Fruit クラスのメソッドを呼び出す
myFruit.SayHello();
myFruit.Chop();
// Apple クラスのメソッドを呼び出す
// Apple クラスが Fruit クラスのすべての public メソッドに
// アクセスできている
myApple.SayHello();
myApple.Chop();
// 次は、文字列を読み取るコンストラクターを使用して
// 継承がどのように行われているか見てみましょう。
Debug.Log("Creating the fruit");
myFruit = new Fruit("yellow");
Debug.Log("Creating the apple");
myApple = new Apple("green");
// Fruit クラスのメソッドを呼び出します。
myFruit.SayHello();
myFruit.Chop();
// Apple クラスのメソッドを呼び出します。
// Apple クラスが Fruit クラスのすべての public メソッドに
// アクセスできることに注意してください。
myApple.SayHello();
myApple.Chop();
}
}
/*実行結果(本来入っていないが、見やすくするため改行を入れる)
Creating the fruit
1st Fruit Constructor Called <- 親クラスの最初のコンストラクタ
Creating the apple
1st Apple Constructor Called <- 子クラスの最初のコンストラクタ
Hello, I am a fruit <- 親クラスのSayHelloメソッド
The orange fruit has been chopped <- 親クラスのChopメソッド
Hello, I am a fruit <- 子クラスのSayHelloメソッド
The red fruit has been chopped <- 子クラスのChopメソッド
Creating the fruit
2nd Fruit Constructor Called <- 親クラスの2番目のコンストラクタ
Creating the apple
2nd Apple Constructor Called <- 子クラスの2番目のコンストラクタ
Hello, I am a fruit <- 親クラスのSayHelloメソッド
The yellow fruit has been chopped <- 親クラスのChopメソッド
Hello, I am a fruit <- 子クラスのSayHelloメソッド
The green fruit has been chopped <- 子クラスのChopメソッド
*/
親クラスのSayHelloメソッドとChopメソッドはpublicなので、子クラスはその2つを使用可能
引数を指定した2回目のインスタンス化では、2番目のコンストラクタがしっかり呼びされており、親クラスのcolor
に引数が格納されている
###余談
Unityでスクリプトを作成するとHogeHoge : MonoBehaviour
とあるように、Unityで作成したクラスはMonoBehaviourの子クラスとなる
そのため最初からStartメソッドやUpdateメソッドを使用可能