おまけ

C#の「イベント」(event)

概念

ここまで、C#で学んだ主な概念は

ここで、新しい C#の機能「イベント」を学ぶ。イベントは「あるインスタンスが通知できること」と考えらる。次の例について考えてみてください。

格闘ゲームを開発中に、「体力」(HitPoints)を実装しようとする。ここで:

体力は変わらないので、Enemy と Player のゲームオブジェクトは全く同じ体力を使うので、同じ「HitPoints」のスクリプトをアタッチする

using UnityEngine;

// 体力クラス
public class HitPoints : MonoBehaviour
{
    // 体力の値
    private int hp;

    private void Start()
    {
        hp = 100;
    }

    // 現在の体力を返す
    public int GetHP()
    {
        return hp;
    }

    // ダメージを受ける
    public void Hit(int damage)
    {
        hp = Mathf.Max(0, hp -damage);
    }
}

こうすると、ゲーム監督クラス「GameManager」では、どれの勝ちなのか確認したいので、Update で定期的に問い合わせるしかない。

using UnityEngine;

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // 人間プレーヤーの体力
    [SerializeField] 
    private HitPoints playerHP;

    // CPUプレーヤーの体力
    [SerializeField] 
    private HitPoints cpuHP;

    private void Update()
    {
        // プレーヤーの負け
        if (playerHP.GetHP() == 0)
        {
            Debug.Log("YOU LOSE!");
        }

        // CPUの負け
        if (cpuHP.GetHP() == 0)
        {
            Debug.Log("YOU WIN!");
        }
    }
}

この作り方で問題なく、ゲームが動くが、体力が変わっていないのに、何度も問い合わせるのは無駄な処理になる。逆に、体力が変わった瞬間にGameManagerに知らせ、処理すれば効率が良い

Updateで問い合わせる

 

image.png
image.png
image.png

event で連絡待ち

 

image.png
image.png

 

image.png

仕組み

イベントを実装するには、2つのクラスが関連する:「イベントが発生する側」と「イベントを聞き取る側」

イベントを発生する側

イベントを発生するクラスは「通知ができる」ことを宣言しないといけない。または、イベントの条件が満たしたら、イベントを発生しなければならない。

宣言

クラスの先頭に「event」キーワードを使い、イベントを宣言する

書式(引数なし)

public event System.Action イベント名;

書式(引数あり)

public event System.Action<引数型1, 引数型2, ...> イベント名;

今回の格闘ゲームだと:

// 体力クラス
public class HitPoints : MonoBehaviour
{
    // 体力がゼロになった時に発生するイベント。
    // 引数はゼロになった体力コンポーネントの参照
    public event System.Action<HitPoints> ZeroHP;
        
    // 体力の値
    private int hp;

    // (後は同じ)
}

ここの例だと:

(※)引数は任意であり、必要なときに使ってください。不要なら「引数なし」の宣言方法を利用してください。

発生

条件を満たしたら、イベントを発生しなければならない。これは、「Invoke」で行う。

書式(引数なし)

イベント名?.Invoke();

書式(引数あり)

イベント名?.Invoke(引数1, 引数2, ...);

今回の格闘ゲームだと:

// ダメージを受ける
public void Hit(int damage)
{
    hp = Mathf.Max(0, hp -damage);
   
    // 体力がゼロになったら、イベントを発生
    // 引数は「this」(自分の参照)
    if (hp == 0)
        ZeroHP?.Invoke(this);
}

ここの例だと:

イベントを聞き取る側

イベントを発生するのはただ半分であり、聞き取る側もある。ここで、イベントに登録・登録解除とそのイベントを処理するメソッドを作成しなければならない。

登録・登録解除

まず、定めたイベントを処理するため、先に登録しなければならない。また、イベントが不要になった場合、必ず登録解除してください。

書式:登録

オブジェクト名.イベント名 += イベントを処理するメソッド;

書式:登録解除

オブジェクト名.イベント名 -= イベントを処理するメソッド;

処理

処理は上記の「イベントを処理するメソッド」で行う。引数はイベント宣言の引数を一致しなければならない

今回の格闘ゲームだと:

using UnityEngine;

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // 人間プレーヤーの体力
    [SerializeField] 
    private HitPoints playerHP;

    // CPUプレーヤーの体力
    [SerializeField] 
    private HitPoints cpuHP;

    private void Start()
    {
        // 人間、またはCPUプレーヤーの体力がゼロになるイベントに登録
        playerHP.ZeroHP += OnZeroHP;
        cpuHP.ZeroHP += OnZeroHP;
    }

    private void OnDestroy()
    {
        // ゲーム監督クラス廃止のとき、イベントに登録解除する
        playerHP.ZeroHP -= OnZeroHP;
        cpuHP.ZeroHP -= OnZeroHP;
    }
    
    // イベントを処理
    // sender: ゼロになった体力コンポーネント
    private void OnZeroHP(HitPoints sender)
    {
        // プレーヤーの負け
        if (sender == playerHP)
        {
            Debug.Log("YOU LOSE!");
        }

        // CPUの負け
        if (sender == cpuHP)
        {
            Debug.Log("YOU WIN!");
        }
    }
    
    // Updateは要らなくなった
    private void Update()
    {
    }
}

もちろん、別々の処理メソッドを作れるので、以下の書き方も可能:

using UnityEngine;

// ゲーム監督
public class GameManager : MonoBehaviour
{
    // 人間プレーヤーの体力
    [SerializeField] private HitPoints playerHP;

    // CPUプレーヤーの体力
    [SerializeField] private HitPoints cpuHP;

    private void Start()
    {
        // 人間、またはCPUプレーヤーの体力がゼロになるイベントを登録
        playerHP.ZeroHP += OnPlayerZeroHP;
        cpuHP.ZeroHP += OnCPUZeroHP;
    }

    private void OnDestroy()
    {
        // ゲーム監督クラス廃止のとき、イベントの登録解除を行う
        playerHP.ZeroHP -= OnPlayerZeroHP;
        cpuHP.ZeroHP -= OnCPUZeroHP;
    }

    // イベントを処理(プレーヤーの負け)
    // sender: ゼロになった体力コンポーネント
    private void OnPlayerZeroHP(HitPoints sender)
    {
        Debug.Log("YOU LOSE!");
    }

    // イベントを処理(CPUの負け)
    // sender: ゼロになった体力コンポーネント
    private void OnCPUZeroHP(HitPoints sender)
    {
        Debug.Log("YOU WIN!");
    }

    // Updateは要らなくなった
    private void Update()
    {
    }
}

練習

まず、このプロジェクトをダウンロードし、Unityに追加してください。このミニゲームでは、車が走行し、「ゴール」を通過すれば、Debug.Log でメッセージが表示される。

ここで、event を使用し、画面上でレースの結果を表示しましょう。

  1. 発生する側:車の通過を検知する「CarDetector」にイベントを宣言し、車を通過したらイベントを発生
  2. 聞き取る側:UIManager で CarDetector のイベントに登録し、車を通過したら、表示する
CarDetector の答え
// 車を検知する
public class CarDetector : MonoBehaviour
{
    // 車を検知したイベントを宣言
    // 引数:通過した車
    public event System.Action<RaceCar> CarDetected; 
    
    private void OnTriggerEnter(Collider other)
    {
        // RaceCarのコンポーネントがついている?
        RaceCar raceCar = other.GetComponent<RaceCar>();
        if (raceCar == null)
            return;

        // 検知しました!
        CarDetected?.Invoke(raceCar);
    }
}
UIManager の答え
// UIを管理する
public class UIManager : MonoBehaviour
{
    // 車の順位を表示
    [SerializeField] private TextMeshProUGUI[] carPosition;

    // 車の通過を検知するため
    [SerializeField] private CarDetector detector;

    // 車の順位
    private int position = 0;
    
    private void Start()
    {
        // すべて非表示する
        foreach (var item in carPosition)
            item.gameObject.SetActive(false);
        
        // イベントに登録
        detector.CarDetected += OnCarDetected;
    }

    private void OnDestroy()
    {
        // イベントに解除
        detector.CarDetected -= OnCarDetected;
    }

    // ゲームを再開
    public void RestartGame() => 
        SceneManager.LoadScene("Racer");

    // 車が通過したイベントを受け取る
    private void OnCarDetected(RaceCar raceCar)
    {
        ShowCar(raceCar, position);
        position++;
    }

    // 車を定めた位置に表示
    private void ShowCar(RaceCar raceCar, int index)
    {
        var item = carPosition[index];
        item.gameObject.SetActive(true);
        item.color = raceCar.Color;
        item.text = raceCar.name;
    }
}

もちろん、聞き取る側がクラス1つだけではなく、どのクラスでもイベントを受け取ることができる。紙吹雪のエフェクトを実装されているので、Confetti クラスでも CarDetector のイベントに登録し、車を通過したら、車の色の紙吹雪を飛ばすようにしましょう。

Confetti の答え
// 紙吹雪のエフェクトを再生
public class Confetti : MonoBehaviour
{
    // パーティクルエフェクト
    [SerializeField] private ParticleSystem[] effects;

    // 車の検知
    [SerializeField] private CarDetector detector;

    private void Start()
    {
        // イベントに登録
        detector.CarDetected += OnCarDetected;
    }

    private void OnDestroy()
    {
        // 登録解除
        detector.CarDetected -= OnCarDetected;
    }

    // イベントを受け取る
    private void OnCarDetected(RaceCar raceCar)
    {
        Play(raceCar.Color);
    }

    // エフェクト再生
    private void Play(Color color)
    {
        foreach (var effect in effects)
        {
            var main = effect.main;
            main.startColor = color;
            effect.Play();
        }
    }
}

Unityで待機ができる処理:非同期処理

概念

Unityのスクリプトでは、Start()Update()OnTriggerEnter() などの定めたときに実行するメソッドがある(初期の時、フレーム毎、トリガーに入った)

この機能が大変便利であるが、ゲームを開発中には、上記のメソッドを呼び出される瞬間ではなく、少し時間たってから実行したい場合もたくさんある。

例えば:

この場合は「待機」が発生する。何かの条件を満たすまで待たないといけない。Unityでは2つの作戦がある:コルーチン と Awaitable.

コルーチン

コルーチンの書き方は以下の通りである

private IEnumerator コルーチン名 (引数)
{
  // 処理
  // yield return 待機命令
  // 処理
  // yield return 待機命令
  // ...
}

普通のメソッドとよく似ているが、戻り値は必ず IEnumerator になる。そして、中には普通の処理を行い、待機命令を見つけたらコルーチンが一時停止し、待機が終わったら再開する流れになる。

なお、コルーチンを開始するのは、普通のメソッドとして呼び出すことではなく、以下の通りになる:

StartCoroutine(コルーチン名(引数));

また、待機命令は以下から選択ができる

命令 効果

yield return null;

次のフレームまで待つ
yield return new WaitForSeconds(秒数); 定めた秒数を待機する
yield return StartCoroutine(他のコルーチン); 別のコルーチンが終わるまで待機する

この他にもいくつかの待機命令があるが、最も多く使うのは上記に3つである。

例:READY → GO!

あるゲーム開始の時に、「READY」を2秒を表示した後、「GO!」を表示1秒表示し、その後ゲームを開始ができる処理を実装しようとしたら:

// ゲーム開始
public void StartGame()
{
	// コルーチンを開始
	StartCoroutine(ReadyGo());
}

// レディー → ゴー!を表示するコルーチン
private IEnumerator ReadyGo()
{
	// READY を表示
	Debug.Log("READY");
	
	// 2秒待機
	yield return new WaitForSeconds(2.0f);
	
	// GO! を表示
	Debug.Log("GO!");
	
	// 1秒待機
	yield return new WaitForSeconds(1.0f);

	// 実際にゲームを開始
	Debug.Log("ゲーム開始");
}

Awaitable

Awaitable(アウェイタブル)は改善されたコルーチンといわれる。基本的に同じことするのが、書き方が少し難しくなる。

private async Awaitable 非同期処理 (引数)
{
  // 処理
  // await 待機命令
  // 処理
  // await 待機命令
  // ...
}

そして、開始するのは、普通のメソッドと同じである

非同期処理 (引数);

また、待機命令は以下の通りになる:

命令 効果

await Awaitable.NextFrameAsync();

次のフレームまで待つ
await Awaitable.WaitForSecondsAsync(秒数); 定めた秒数を待機する
await 他の非同期処理(引数); 別のAwaitableが終わるまで待機する

例:READY → GO!

コルーチンで作った処理をAwaitableにすると:

// ゲーム開始
public void StartGame()
{
	// 処理開始
	ReadyGo();
}

// レディー → ゴー!を表示する非同期メソッド
private async Awaitable ReadyGo()
{
	// READY を表示
	Debug.Log("READY");
	
	// 2秒待機
	await Awaitable.WaitForSecondsAsync(2.0f);
	
	// GO! を表示
	Debug.Log("GO!");
	
	// 1秒待機
	await Awaitable.WaitForSecondsAsync(1.0f);

	// 実際にゲームを開始
	Debug.Log("ゲーム開始");
}

夏休み・練習課題

義務ではないが、練習不足と思う学生にはおすすめ

目標

ポイント:

C# / スクリプト(言語、およびUnityのの基礎)

次の質問をC#で回答してください(1行〜5行程度)

例:2つの整数を足してください:

int sum = 20 + 15;

1. ゲーム実行中に「enemyPrefab」プレハブから具体的なゲームオブジェクト「enemy」を作成してください。

答え
GameObject enemy = Instantiate(enemyPrefab);

2. 「ball」というゲームオブジェクトからRigidbodyのコンポーネントを取得てください。

答え
Rigidbody rb = ball.GetComponent<Rigidbody>();

3. 取得したRigidbodyの速度は、X軸で毎秒5単位、Z軸で毎秒-1単位にしてください。また、Y軸の速度を変えないでください(元の通りに保持する)

答え
Vector3 velocity = rb.linearVelocity;
velocity.x = 5;
velocity.z = -1;
rb.linearVelocity = velocity;

4. 取得したRigidbodyをY軸周りに90度回転してください。

答え

Rigidbodyで動かす場合、Transformをいじるのではなく、Rigidbodyのメソッドを使わないといけない。Rigidbodyを回転させるには「MoveRotation」を使用する。

rb.MoveRotation(Quaternion.Euler(0, 90, 0));

5. スペースキーを押したら、「ball」ゲームオブジェクトを削除する処理を書いてください。

答え
if(Input.GetKeyDown(KeyCode.Space)
{
    Destroy(ball);
}

6.「box」というゲームオブジェクトの位置を「x=1, y=0.5, z=0」にしてください。

答え
box.transform.position = new Vector3(1, 0.5f, 0);

7. 「box」というゲームオブジェクトの位置を「x=5」にし、「y」と「z」は元の数値のままにしてください。

答え
Vector3 pos = box.transform.position;
pos.x = 5;
box.transform.position = pos;

8.「box」というゲームオブジェクトの位置を現在の位置よりも、左の方に1単位に動かしてください。

答え

少なくても、2つのやり方がある

Vector3 pos = box.transform.position;
pos.x -= 1;
box.transform.position = pos;

 または 

box.transform.Translate(-1, 0, 0);

9. Unityで「1〜100」の間に乱数10個を生成してください。

答え
for (int i = 0; i < 10; i++)
{
  // 整数の場合は最大値を除く(1~100)
  int rnd_i = Random.Range(1, 101); 

  // 小数型の場合は、最大値を含む
  float rnd_f = Random.Range(1.0f, 100.0f);
}  

10. 「cat」と「mouse」の2つのゲームオブジェクトの間の距離を求めてください。

答え
Vector3 catPos = cat.transform.position;
Vector3 mousePos = mouse.transform.position;
Vector3 direction = mousePos - catPos

float distance = direction.magnitude;

11. 「cat」と「mouse」の距離が「1.5」単位よりも小さければ、「mouse」は「cat」から「3」単位を離れる。

答え
if (distance < 1.5f)
{
    mousePos = catPos + direction.normalized * 3.0f;
    mouse.transform.position = mousePos;
}

12. 4種類のTシャツのサイズ(Small、 Medium、 Large、 ExtraLarge)を表す列挙型「ShirtSize」を宣言してください。

答え
enum ShirtSize
{
    Small,
    Medium,
    Large,
    ExtraLarge
}

13. ShirtSizeの列挙から「myShirt」の変数を宣言し、ラージサイズにする

答え
ShirtSize myShirt = ShirtSize.Large;

14. Debug.Logを使い、Tシャツのサイズにより、日本語でサイズを表示してください:
      Small:“小”、Medium:”並”、Large:”大”、ExtraLarge:”特大”

答え
switch (myShirt)
{
    case ShirtSize.Small:
        Debug.Log("小");
        break;
    case ShirtSize.Medium:
        Debug.Log("中");
        break;
    case ShirtSize.Large:
        Debug.Log("大");
        break;
    case ShirtSize.ExtraLarge:
        Debug.Log("特大");
        break;
}

15. 入力クラス(Input)を使い、上の矢印キー(↑)を放せば、Debug.Logで”JUMP!”というメッセージを表示してください。

答え
if (Input.GetKeyUp(KeyCode.UpArrow))
{
    Debug.Log("JUMP!");
}

16. 入力クラス(Input)を使い、InputManager で定義した ”Attack” アクションが行った場合、Debug.Logで”攻撃”を表示してください。

答え
if (Input.GetButtonDown("Attack"))
{
    Debug.Log("攻撃");
}

シーン、ゲームオブジェクト、コンポーネント

 次の質問を文章で回答してください:

17. ゲームオブジェクトとコンポーネントの違いを説明してください。

答え

ゲームオブジェクト:シーン(Hierarchy)の中にある1つのもの(例:プレーヤー、敵対、木、壁、スコアの表示など)

コンポーネント:ゲームオブジェクトにアタッチするものであり、ゲームオブジェクトの機能を増やす(例:物理演算:Rigidbody、当たり判定:Collider、サウンド再生:AudioSource、カスタム機能:スクリプト)

18. Rigidbodyというコンポーネントはいつ使う?

答え あるゲームオブジェクトを物理的に移動させたいとき(重力、速度、加速度などで移動させる)

19. Colliderというコンポーネントはいつ使う?

答え

 当たり判定の処理のために使う。衝突モードとトリガーモード2種類がある

20. コライダーをトリガーモード(isTrigger)をチェックするとなにが変わる?

答え

 ゲームオブジェクトを衝突せず、コライダーをすり抜けるが、その範囲の中に他のゲームオブジェクトが入っているかどうか「IsTriggerXXX」で確認できる。

21. RigidbodyがないColliderを使える?なぜ?

答え

 使える。当たり判定(衝突)のみが発生し、動かないので、壁、床面などに最適

22. RigidbodyのlinearVelocityはなんですか?

答え リジッドボディの移動速度である。

23. RigidbodyのAddForceで何ができる?linearVelocityと何が違う?

答え

力与えて移動させる(ゲームオブジェクトを押したら、引っ張ったりする)。linearVelocityと異なって、Rigidbodyの重さ(質量)の影響が発生する。つまり、AddForceを使う場合、Rigidbodyが重ければ重いほど、動き辛くなるが、linearVelocityを変えると直接に速度が変わるので、重さが関係ない。

24.「プレハブ」とは何ですか?はいつ使う?

答え

スクリプトやコンポーネントをアタッチし、独立に使えるゲームオブジェクトのかたまりである。プレハブは「図形」として使えることができ、実行中、またはUnityエディタで何度重複ができる。

25. 「Animation Clip」「Animator」と「Animation Controller」の連携を説明してください。

答え

26. UIを作るために、最も重要なコンポーネントを記述してください。

答え Canvas。キャンバスの中に、ボタン、テキスト、画像などを配置する。

27. あるRigidbodyの重力が効かないでほしいとき、どうすれば良いでしょうか?

答え

Unity の Inspector で「Use Gravity」のチェックを外す

28. ベタベタの壁を作りたくて、何か衝突したら、弾みなく、衝突したオブジェクトを壁に張り付くような現象を作りたい。どうすれば良いでしょうか?

答え

2つの解決策が考えられる。

最終的な効果が多少異なるが、べたべたの壁をエフェクトを作成できる。

29. SpriteRendererとは何でしょうか?

答え 2Dの画像を表示する(2Dゲーム専用のコンポーネント)

30. InspectorでスプライトをY軸で逆さまにしたいなら、どうすれば良い?

答え

Inspectorで「Sprite Renderer」の「Flip Y」をチェックする

Transform の Scale Y を -1 にする手もあるが、コライダーに悪影響がある。

31. スプライトのモード「Single」と「Multiple」の違いは何でしょうか?

答え Singleは「単体」という意味で、画像の中にスクリプトが1つしかない。Multipleは「複数」という意味であり、同じ画像に中ではたくさんのスプライトが入っている。この場合は、Sprite Editorで切り分けないといけない。

32. シーンを切り替えるために、LoadSceneを呼び出す前に、どこに追加すれば良いでしょうか?

答え

File > Build Profiles(ファイル > ビルドプロフィール)画面で、「Scene List」の中に切り替えたいシーンを追加しないといけない。

33. 「タグ」は何に使える?

答え

ゲームオブジェクトの特定に使う。「xxxx.CompareTag("タグ")」を使うと、同じタグかどうかを確認できる。

34. TextMeshProというコンポーネントをなんのために使うのでしょうか?

答え

UIでテキストを表示するためのコンポーネントである。

35. UI の「Anchor」(アンカー)の使い方について説明してください。

答え

 UIの要素(画像、テキスト、ボタンなど)を画面の角や中央から相対的に配置するためのものである。いろんな画面サイズを対応するため、UIの要素を絶対的ではなく、アンカーを使って、相対的に配置するべき。

36.「Ray」(レイ)というものはなんでしょうか?使い方を説明してください。

答え

Rayは「ビーム」のようなもので、スクリプトで当たり判定を確認できるためにつかう。Rayは「原点」(origin)と「向き」(direction)をもち、Physics.Raycastで何か当たるかどうかを確認できる。

使い道がいっぱいある。例えば、シューティングゲームで、弾が当たったかどうかを確認できるし、画面をクリックすると、3D空間で何を選択したのかを確認できる。

C# / スクリプト

次の質問を文章で回答してください:

37. 「FixedUpdate」「Update」「LateUpdate」の違いを説明してください。それぞれいつ使うと思う?

答え

38. ゲームオブジェクトの「Destroy」と「SetActive(false)」の違いを説明してください。

答え

39. 「Time.deltaTime」とは何でしょうか?

答え

前フレームから過ごした時間である。タイマーや速度に依存する移動量などに使う数値。

40. 「Time.timeScale = 0」にすれば、ゲームはどうなる?

答え

時間に依存する処理が止まる。単純なポーズ(Pause)に使える。

41. 「public」と「private」の違いについて説明してください。

答え

 「public」と「private」はアクセス修飾子である。「public」のメソッドや変数が他のクラス(スクリプト)からアクセス(読み込みこと、書き込むこと)ができる。一方「private」のメソッドと変数は、宣言されたクラスのみでアクセスができ、他のクラスからアクセスができない。

42. 「OnCollisionEnter」と「OnTriggerEnter」の違いを説明してください。

答え

43. 「OnTriggerEnter」「OnTriggerStay」「OnTriggerExit」の違いを説明してください。

答え

すべてのメソッドはトリガーモードになっているコライダーの時に発生する

44. 「OnCollisionEnter」の引数(Collision)は何に使えるでしょうか?

答え

 OnCollisionXXXXの「Collision」には衝突に関する情報が入っている。

たとえば、衝突してきたオブザーバーは「Collision.gameObject」であり、衝突した3D位置は「Collision.point」である。

詳細:https://docs.unity3d.com/6000.4/Documentation/ScriptReference/Collision.html

45. 「SerializeField」どこで使う?なんのため?

答え 非公開(private)のメンバー変数を Unity の Inspector で変えられるようにする。

46. 「event」というキーワードは何に使うのでしょうか?

答え

C#のイベントを宣言するときに使う。イベントを作るには、「発生する側」と「受け取る側」の2つが必要。

発生する側:

受け取る側:

スクリプト作成

47. 「Countdown」というスクリプトをを作成し、ゲームが起動してから3秒後にConsoleで「時間切れ」を表示してください(1回だけ)

答え
using UnityEngine;

public class Countdown : MonoBehaviour
{
    // 残りの時間
    private float timer = 3.0f;
    
    // 時間が切れたか?
    private bool isTimeout = false;
    
    private void Update()
    {
        // 時間が切れたか? → 何もしない
        if (isTimeout)
            return;
        
        // 時間を減らす
        timer -= Time.deltaTime;
        
        // まだ時間ある? → 何もしない
        if (timer > 0)
            return;
            
        // 時間切れにする
        isTimeout = true;

        // コンソールで表示
        Debug.Log("時間切れ");
    }
}

48. 「Countdown」を編集し、「Timeout」というイベントを追加してください。時間が切れたら、イベントを発生してください。

答え
using UnityEngine;

public class Countdown : MonoBehaviour
{
    // タイムアウトイベントの宣言
    public event System.Action Timeout

    // 残りの時間
    private float timer = 3.0f;
    
    // 時間が切れたか?
    private bool isTimeout = false;
    
    private void Update()
    {
        // 時間が切れたか? → 何もしない
        if (isTimeout)
            return;
        
        // 時間を減らす
        timer -= Time.deltaTime;
        
        // まだ時間ある? → 何もしない
        if (timer > 0)
            return;
            
        // 時間切れにする
        isTimeout = true;
        
        // コンソールで表示
        Debug.Log("時間切れ");

        // イベント発生
        Timeout?.Invoke();
    }
}

49.「CountdownWait」の新しいスクリプトを作成し、Unity上でCountdownとの紐づけできるようにしてください。そして、CountdownのTimeoutイベントを登録し、イベントを受け取ったら、「イベントを受け取った」を表示してください。

答え
using UnityEngine;

public class CountdownWait : MonoBehaviour
{
    // Unityで紐づけ
    [SerializeField]
    private Countdown countdown;

    private void Start()
    {
        countdown.Timeout += OnTimeout;
    }

    private void OnTimeout()
    {
        Debug.Log("イベントを受け取った");
    }
}

スクリプト解析

スクリプト A

以下のスクリプトを解析し、下にある質問を回答してください

public class ObjectPusher : MonoBehaviour
{
    public float speed = 10;

    void OnCollisionEnter(Collision other)
    {
       var rbody = GetComponent<Rigidbody>();
       var direction = other.transform.position - transform.position;
       rbody.linearVelocity = direction.normalized * speed;
    }
}

自分(スクリプトを持っているゲームオブジェクト)と相手(衝突してきたゲームオブジェクト)が衝突した場合は9行目で「linearVelocity」を変えているが…

50. どちらのlinearVelocityが変わるでしょうか?(自分 or 相手)

答え 「自分」 - 取得したRigidbodyは、本スクリプトと同じゲームオブジェクトにあるので(7行目)

51. どの方向にlinearVelocityが変わる?(自分の方 or 相手の方)

答え

「相手」 - directionベクトルは相手の方に向いている(方位 = 相手 - 自分)

52. 全体的に、このスクリプトは何をするのかを説明してください。

答え

2つのゲームオブジェクトを衝突したら、自分が相手に向かって速度を設定する。

スクリプト B

以下のスクリプトを解析し、下にある質問を回答してください

public class InstanceMaker : MonoBehaviour
{
    [SerializeField]
    private GameObject original;

    private GameObject [] copies = new GameObject[10];
    private int lastCopy = 0;

    void Update()
    {
        if (Input.GetKeyUp(KeyCode.Space))
        {
            if (lastCopy < 10)
            {
                copies[lastCopy] = Instantiate(original);
                lastCopy++;
            }
            else
            {
                for(int i = 0; i < 10; i++)
                {
                    Destroy(copies[i]);
                }
                lastCopy = 0;
            }
        }
    }
}

53. このスクリプトを実行し、スペースキーを押したらどうなるのか説明してください。

答え

あるプレハブからオブジェクトをコピーし、シーンに追加する。ただし、11個目を作成しょうとしたら、作成したゲームオブジェクトをすべて削除する。

スクリプト C

以下のスクリプトを解析し、下にある質問を回答してください

public class Player : MonoBehaviour
{
    private int HP = 100;   // ヒットポイント
    [SerializeField]
    private int MP = 100;   // マジックポイント

    void Start()
    {
       HP = 50;
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("fire"))
        {
            HP -= 5;
        }
    }

    void Update()
    {
        if (Input.GetKey(KeyCode.Q))
        {
            HP += 10 * Time.deltaTime;
            MP -=  5 * Time.deltaTime;
        }
    }
}

54. ゲームが開始のとき、プレーヤーはHPとMPはそれぞれ何ポイントでしょうか?

答え

MPは100点、HPは50点

55. 11行目〜17行目でどんなことが行われていると思う?

答え  トリガーに入ったとき、入ってきたゲームオブジェクトのタグは「fire」だったら、HPが5点減る

56. HPの回復処理を説明してください。

答え

「Q」キーをおっしぱなしで、1秒当たりにMPの5点を消費し、HPの10点を回復する

57. "Q”キーを5秒押すと、MPはどうなる?

答え

MPは25点が減る

58. Inspectorで編集できる変数を指定してください。

答え

MPはInspectorで編集ができる

59. HPとMPは「ゼロ」以下にならないように、スクリプトの適切なところに編集してください。

答え
void OnTriggerEnter2D(Collider2D other)
{
    if (other.CompareTag("fire"))
    {
        HP -= 5;
        
        // 0以下にならないように
        HP = Mathf.Max(HP, 0); 
    }
}

void Update()
{
    if (Input.GetKey(KeyCode.Q))
    {
        HP += 10 * Time.deltaTime;
        MP -=  5 * Time.deltaTime;
        
        // 0以下にならないように
        MP = Mathf.Max(MP, 0); 
    }
}

60. “Q”を固定せず、Inspectorでキーを設定できるようにしてください。

答え

まず、スクリプトの頭に:

// 回復のキーをUnityで設定
[SerializeField]
private KeyCode restoreKey;

そして、Updateでif文の条件を変える:

if (Input.GetKey(restoreKey))

61. HPが「ゼロ」になったら、ゲームオブジェクトを削除してください。

答え
void OnTriggerEnter2D(Collider2D other)
{
    if (other.CompareTag("fire"))
    {
        HP -= 5;
        if (HP <= 0)
            Destroy(gameObject);
    }
}