Unity & C# 学習教材

チュートリアル: 信号機

スクリプトだけで「信号機」を作ります。Sphere 1つが赤↔青と自動的に切り替わる仕組みを実装し、複数の状態を int で管理するパターン(ステートマシン)を学びます。

学習目標

前提知識


1. スクリプトを準備する

新しいシーンを作成し、球体 GameObject を作ります(メニューバー GameObject → 3D Object → Sphere)。

球体オブジェクトを追加

Hierarchy ビューで追加した球体を Signal という名前に変更してください。この GameObject に Signal という名前のスクリプトを作成してアタッチします(Inspector ビューの Add Component → New script)。

スクリプトの追加

スクリプトを開いて、以降の手順に従いコードを書いていきましょう。

2. 色を変える

Sphere の色は、GetComponent メソッドで Renderer コンポーネントを取得し、その material プロパティ、さらに color プロパティへたどることで変更できます。

Renderer コンポーネントは、GameObject を画面に描画するためのコンポーネントです。Sphere や Cube が画面に見えているのは、この Renderer コンポーネントが見た目の表示を担当しているからです。

流れとしては、まず GetComponent メソッドで Sphere についている Renderer コンポーネントを取得します。次に Renderer.material プロパティで、その Renderer コンポーネントが使っているマテリアルにアクセスします。最後に Material.color プロパティを使うと、表示色を変更できます。マテリアルとは、色やテクスチャ、見た目の質感など、表示のしかたをまとめて管理する設定の集まりです。

GameObject.GetComponent<T>() — この GameObject に追加されているコンポーネントを取得します。

書式:GameObject.GetComponent メソッド

1
public T GetComponent<T>();
名前 説明
T 型パラメーター 取得したいコンポーネントの型(例: Renderer

MonoBehaviour を継承しているスクリプトは、自分がアタッチされているゲームオブジェクトからコンポーネントを取得する GetComponent() メソッドを持ちます。従って、スクリプトの中で GetComponent<Renderer>() と直接書くことで「このスクリプトがアタッチされている GameObject から Renderer を取得する」という意味になります。

もし GetComponent で要求したコンポーネントが見つからない場合は null が返りますnull のまま使おうとすると実行時エラーになるため、状況に応じて null チェックが必要です。

1
2
3
4
5
6
// GetComponent + null チェック
var r = GetComponent<Renderer>();
if (r != null)
{
    // r を安全に使える
}

null チェックを if の条件判定と取得を同時に行いたい場合は TryGetComponent<T>() が便利です。

Component.TryGetComponent<T>() — コンポーネントを取得し、見つかった場合は true、見つからない場合は false を返します。

書式:TryGetComponent メソッド

1
public bool TryGetComponent<T>(out T component);
パラメータ 説明
T 取得したいコンポーネントの型
component 取得できた場合にコンポーネントが入る out 変数
1
2
3
4
5
// TryGetComponent — 取得と null チェックを一度に行う
if (TryGetComponent<Renderer>(out var r))
{
    // r を安全に使える
}

どちらを選ぶかは好みの問題で、機能的な差はありません。パフォーマンス面では、エディター実行時にコンポーネントが見つからなかった場合に GetComponent は内部的に小さなアロケーションが発生することがありますが、TryGetComponent はそれを回避します。Start で一度だけ呼ぶ分には実用上の差はありません。


Renderer.materialRenderer コンポーネントが現在使っているマテリアルを取得します。Renderer コンポーネントから見た目の設定へ進む入口です。

書式:Renderer.material プロパティ

1
public Material material { get; set; }

Material.color — マテリアルのメインカラーを設定・取得します。Renderer.material でレンダラーが使用するマテリアルにアクセスし、.color で色を変更します。

書式:Material.color プロパティ

1
public Color color { get; set; }

この Material.color プロパティから色を変更できます。これを時間経過で行うために、Update() メソッドとは分離して、独自の UpdateSignal メソッドに分けて書きましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UnityEngine;

public class Signal : MonoBehaviour
{
    private int _state = 0;

    private void Start()
    {
        UpdateSignal();
    }

    private void UpdateSignal()
    {
        GetComponent<Renderer>().material.color = _state == 0 ? Color.red : Color.blue;
    }
}

実行結果

UpdateSignal メソッドは Unity で定められた StartUpdate メソッドではないため自動的には実行されません。時間経過によって色を切り替えたいときに実行する想定です。

上記のコードでは Start メソッドから UpdateSignal メソッドを呼び出すことで、初期化時に色を変更してします。このコードでは _state フィールドが表示するべき色を表し、0 であれば赤、そうでなければ青になります。


3. タイマーで赤↔青を切り替える

下準備ができたので、時間経過で色を切り替えるようにプログラムしましょう。Update メソッドを追加して、時間経過によって _state フィールドを切り替えることで色を変更できます。

時間経過による色の切り替えを管理するために、以下のフィールドを追加しましょう。

1
2
3
private float _timer       = 0f; // 経過時間(秒)
private float _redDuration  = 3f;  // 赤の表示時間(秒)
private float _blueDuration = 3f;  // 青の表示時間(秒)

Update でタイマーを積算し、現在の _state に応じた経過時間を超えたら状態を進めます。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void Update()
{
    _timer += Time.deltaTime;

    float duration = _state == 0 ? _redDuration : _blueDuration;

    if (_timer >= duration)
    {
        _timer -= duration;
        _state = (_state + 1) % 2;  // 0 → 1 → 0 → … と循環
        UpdateSignal();
    }
}

(_state + 1) % 2 という式は 01 を交互に返す計算です。0 + 1 = 11 + 1 = 2 と経過時間ごとに状態値が増えていきますが 2 に到達すると 2 % 2 = 0 となり、0 に戻ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using UnityEngine;

public class Signal : MonoBehaviour
{
    private int   _state       = 0;
    private float _timer       = 0f;
    private float _redDuration  = 3f;
    private float _blueDuration = 3f;

    private void Start()
    {
        UpdateSignal();
    }

    private void Update()
    {
        _timer += Time.deltaTime;

        float duration = _state == 0 ? _redDuration : _blueDuration;

        if (_timer >= duration)
        {
            _timer -= duration;
            _state = (_state + 1) % 2;
            UpdateSignal();
        }
    }

    private void UpdateSignal()
    {
        GetComponent<Renderer>().material.color = _state == 0 ? Color.red : Color.blue;
    }
}

💡 ポイント: UpdateSignal は状態が変わったときだけ呼ばれます。毎フレーム GetComponent が呼ばれるわけではないため、パフォーマンスへの影響はほとんどありません。


課題

課題 1: 各フェーズの秒数を Inspector から変更する

_redDuration_blueDuration[SerializeField] 付きにして、Unity の Inspector ビューから値を変更できるようにしてください。

解答を見る
1
2
[SerializeField] private float _redDuration  = 3f;
[SerializeField] private float _blueDuration = 3f;

Play 中に Inspector から値を変えると、次のフェーズ切り替えから新しい時間が反映されます。


課題 2: 黄色フェーズを追加して 3 色にする

赤 → 青 → 黄 → 赤 のサイクルに変更してください。

ヒント: % 2% 3 に変え、_state 2 = 黄 を追加します。duration の切り替えと UpdateSignal も 3 状態に対応させます。

解答を見る
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using UnityEngine;

public class Signal : MonoBehaviour
{
    private int _state = 0;  // 0=赤, 1=青, 2=黄
    private float _timer = 0f;
    private float _redDuration = 3f;
    private float _blueDuration = 3f;
    private float _yellowDuration = 1f;

    private void Start()
    {
        UpdateSignal();
    }

    private void Update()
    {
        _timer += Time.deltaTime;

        float duration;
        if (_state == 0) duration = _redDuration;
        else if (_state == 1) duration = _blueDuration;
        else duration = _yellowDuration;

        if (_timer >= duration)
        {
            _timer -= duration;
            _state = (_state + 1) % 3;  // 0 → 1 → 2 → 0 → …
            UpdateSignal();
        }
    }

    private void UpdateSignal()
    {
        Color color;
        if (_state == 0) color = Color.red;
        else if (_state == 1) color = Color.blue;
        else color = Color.yellow;

        GetComponent<Renderer>().material.color = color;
    }
}

課題 3: 青→赤に切り替わる前に青を点滅させる

赤と青の切り替えのみとして、青フェーズの残り 1 秒で Sphere を点滅させる警告演出を追加してください。

ヒント: _state == 1 && _timer >= _blueDuration - 1f のときに、別タイマーで点滅フラグを用意して反転を表現します。状態が赤に切り替わるときに関連フラグをリセットすることを忘れないでください。

解答を見る
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using UnityEngine;

public class Signal : MonoBehaviour
{
    private int _state = 0;
    private float _timer = 0f;
    private float _redDuration = 3f;
    private float _blueDuration = 3f;
    private float _blinkTimer = 0f;
    private bool _isBlinkOn = true;

    private Renderer _renderer;

    private void Start()
    {
        _renderer = GetComponent<Renderer>();
        TransitionTo(_state);
    }

    private void Update()
    {
        _timer += Time.deltaTime;

        switch (_state)
        {
            case 0:  UpdateRed();  break;
            case 1: UpdateBlue(); break;
        }
    }

    private void UpdateRed()
    {
        if (_timer >= _redDuration)
            TransitionTo(1); // 青に遷移
    }

    private void UpdateBlue()
    {
        // 残り 1 秒で点滅
        if (_timer >= _blueDuration - 1f)
        {
            _blinkTimer += Time.deltaTime;
            if (_blinkTimer >= 0.25f)
            {
                _blinkTimer -= 0.25f;
                _isBlinkOn = !_isBlinkOn;
                _renderer.material.color = _isBlinkOn ? Color.blue : Color.gray;
            }
        }

        if (_timer >= _blueDuration)
            TransitionTo(0); // 赤に遷移
    }

    private void TransitionTo(int next) // 状態遷移(リセット)
    {
        _timer = 0f;
        _blinkTimer = 0f;
        _isBlinkOn = true;
        _state = next;
        _renderer.material.color = next == 0 ? Color.red : Color.blue;
    }
}

まとめ


理解度チェック

以下の問いに答えられるか確認しましょう。

  1. (_state + 1) % 201 を交互に返す理由を説明してください。
  2. UpdateSignalUpdate 内で毎フレーム呼ばず、状態が変わるときだけ呼ぶのはなぜですか?
  3. 4 つの状態を循環させるには % 2 をどう変えればよいですか?
解答を見る
  1. %(剰余)は割り算の余りを返す演算子。(0 + 1) % 2 = 1(1 + 1) % 2 = 0 となり、0 と 1 を交互に繰り返す。
  2. 毎フレーム呼ぶと不要な GetComponent と color 代入が発生するため。状態変化時だけ呼ぶことで効率的になる。
  3. % 4 にする。

次のステップ

Rigidbody で力を加える では、GetComponent でコンポーネントをキャッシュし、AddForce でオブジェクトを物理的に動かす方法を学びます。