3rd term 5th week - dsuz/csharp GitHub Wiki

今回のテーマ

  • ジェネリック
    • 型パラメーターに条件を付ける
  • パーシャル クラスとパーシャル メソッド
  • UnityEvent 活用法
    • UnityEngine.Events.UnityEvent クラス
    • トラブルシューティング
    • 機能を追加する

準備

  1. パッケージ Cinemachine, ProBuilder がプロジェクトに追加されていない場合は追加する
  2. 今回の教材では以下のアセットを使っているので、Asset Store からダウンロードしてインポートする
  3. CSharp3-5.unitypackage をダウンロードして Unity のプロジェクトにインポートする

型パラメーターに制約条件を付ける

ジェネリックで型パラメーター <T> を受け取った時、その T に条件(例: T が MonoBehaviour を継承したクラスであること)をつけることができる。

指定できる条件は 型パラメーターの制約 や参考資料に詳しく書かれているが、よく使われるのは「T があるクラス、またはその派生クラスであること」と「T が特定のインターフェイスを実装していること」であり、それぞれ以下のように書く。

class ClassName<T> where T : ISomeInterface<T> // 「型 T が ISomeInterface を実装していること」という制約を付けている
{
    ...
}
class ClassName<T> where T : SomeClass // ここで「型 T は SomeClass またはその派生クラスであること」という制約を付けている
{
    ...
}

3rd term 4th week の「シングルトン パターンの実装方法」 で触れている「ジェネリックを利用したシングルトンのベースクラスを継承して作る」で紹介している書き方は、これを使って T に条件を付けている。

パーシャル クラスとパーシャル メソッド

「パーシャル クラス」の仕組みを使うと、一つのクラスの定義を2つのソースファイルに分けることができる。書き方としてはクラスを定義する時、partial キーワードを付けるだけでよい。

ただし、パーシャル クラスのクラス定義のすべてに partial が付いていないといけないので、パーシャル クラスにしたい時はあらかじめ決めておく必要がある(パーシャルでないクラスの定義を変更せずにメンバを追加することはできない)。

分散されたクラスのコードがどこにあるかわかりにくい時は Visual Studio を使うことで素早くメンバを探すことができる。クラスビュー(メニューの表示 > クラスビュー もしくは Shift + Ctrl + C で表示できる)から該当のクラスを選択すると、メンバの一覧が表示され、メンバをダブルクリックすると、その定義・宣言にジャンプする。

パーシャル クラスの中では「パーシャル メソッド」を定義することができる。これは「宣言」と「定義」を別のファイルに分けることができる。今回は Main.cs に宣言、Sub2.cs に定義を書いている。

パーシャル クラスの定義があるファイルを削除してもコンパイル エラーにはならない。つまり、Sub2.cs を削除しても、コンパイル エラーにはならない。

参考資料

  • 型パラメーターに条件を付ける
    • 独習 C# 9.5 - ジェネリック
      • 9.5.3 - 型パラメーターの制約条件
  • パーシャル クラスとパーシャル メソッド
    • 独習 C# 9.4 - 特殊なクラス
      • 9.4.2 パーシャルクラス
      • 9.4.3 パーシャルメソッド

UnityEvent 活用法

UnityEngine.Events.UnityEvent とは

UnityEvent 型のメンバ変数を SerializeField(もしくは public)にすることで、インスペクターから GameObject を指定してコンポーネント・メソッド・引数を(複数)指定できる。UnityEvent.Invoke() を呼ぶことで、UnityEvent 型の変数にセットされたメソッドやプロパティを全て呼び出すことができる。つまり「ゲームオブジェクトを指定して」「GetComponent し」「メソッドあるいはプロパティを呼び出す・もしくは値をセットする」という処理は、UnityEvent 型の変数を使う事で、非常に少ないコードで実行することができるし、またコードを変更せずに後から処理を追加することができる。

今回のサンプルミニゲームでは UnityEvent を駆使して、少ない・短いコードでゲームを作っている。

UnityEvent を有効に使うことで、コンポーネントの呼び出しをコードで書く必要がなくなり、制作速度はかなり速くなる。プロトタイピングやゲームジャムには非常に有効である。

一方、UnityEvent を使ってプレハブを作った場合、特にそれを実行中に動的にインスタンス化する場合、プレハブの外部にある GameObject を参照したままプレハブ化することはできないため、かえって手間がかかるケースがある。今回のサンプルでは GunManEnemyPrefab がそれにあたる。このプレハブは GunManEnemyController コンポーネントの OnShoot 変数に対して、インスタンス化した GameObject 毎にその時に実行される処理を設定しなければならない。

実行中に動的にインスタンス化する場合は、Button の OnClick にメソッド等を追加する時と同じ要領UnityEvent 型の変数にメソッドを割り当てることができる。ただし、スクリプトからメソッドを割り当てた場合は、インスペクターにはそれらが表示されないことに留意しておくこと。

ミニゲームの場所

/Assets/3-5 Misc/3 UnityEvent/ 以下にある。実行するシーンは ShootingGame という名前である。

ミニゲームの仕様

このゲームの仕様は以下の通り。

  1. 操作方法は画面に表示されている通りである
  2. 敵を撃つと、各々の敵 (GunEnemyController コンポーネント) に設定された点数を獲得する
  3. 敵は最初、壁の後ろに隠れているが、ランダムな間隔で立ち上がり銃を構えてこちらを攻撃してくる
  4. 敵が隠れている状態を idle、立ち上がって銃を構えている状態を ready と呼ぶ
  5. ready 状態の敵は、こちらから撃つことができる
  6. 敵に攻撃を当てると、得点し、敵はすぐに idle 状態に戻る
  7. ready 状態から時間が経過する(この時間はランダムである)と、敵は銃を撃つ
  8. 敵に銃を撃たれた時は、こちらのライフが1つ減る
  9. ライフが 0 になったらゲームオーバーになり、画面をクリックするとゲームはリスタートする

課題1

シーンを実行すると、以下のエラーが記録される。

NullReferenceException: Object reference not set to an instance of an object
GameManager.Start () (at Assets/_4-2 UnityEvent/Scripts/GameManager.cs:57)

また、攻撃を受ける度に以下のエラーが記録される。

NullReferenceException: Object reference not set to an instance of an object
GameManager.Hit () (at Assets/_4-2 UnityEvent/Scripts/GameManager.cs:129)
UnityEngine.Events.InvokableCall.Invoke () (at <028e4d71153d4ed5ac6bee0dfc08aa3b>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <028e4d71153d4ed5ac6bee0dfc08aa3b>:0)
GunEnemyController.Update () (at Assets/_4-2 UnityEvent/Scripts/GunEnemyController.cs:79)

この問題を調べて、エラーが起きないように、かつそこに書かれた処理が意図した通りに動くように修正せよ。また、何が原因なのか、どのように修正したのか説明せよ。

課題 2

初期状態では敵が GunManEnemyCenter オブジェクトの1体のみがアクティブになっている。敵がこの1体のみである場合は問題なくゲームが進められるが、GunManEnemyLeft, GunManEnemyRight オブジェクトをアクティブにして敵を3体にすると、スコアに表示される点数の値が正しくなくなる。(例:敵を撃つとスコアが減ることがある)

この問題を調べて、スコアに表示される点数が正しくなるように修正せよ。また、何が原因なのか、どのように修正したのか説明せよ。

課題 3

プレイヤーが発砲した時にカメラが揺れるようになっているが、これを スクリプトを編集せずに 敵に撃たれた時にもカメラが揺れるように変更せよ。

またどのようにそれを変更したのか説明せよ。

課題 4

以下の機能を追加せよ。

  • 5000 点おきにライフが 1 増える。つまり、5000 点, 10000 点, 15000 点... を超えるたびにライフが 1 増える。
  • ライフが増えたら /Audio/1up を鳴らす

課題 5

以下の機能を追加せよ。

  • シーンを実行後、クリックするまでゲームが始まらないようにする
    • クリックしたらゲームが始まることを画面に表示すること