選択肢に名前を付けよう:enum(列挙型)とswitch文【Unityスタートガイド6-15】

WalkerのFacingがNorth、Current TownがStart Village。再生すると、Consoleに右を向いたログ4行、町に着いたログ、属性の倍率が並ぶ

キャラクターの向き、ゲームの状態、今いる町、攻撃の属性・・・。ゲームには「決まった選択肢の中から1つ」を持っておきたい場面がたくさんあります。これを0や"north"のような数字や文字で持っていると、打ちまちがいや「0ってどっちだっけ?」が起きがちです。今回は、選択肢そのものに名前を付けられるenum(イーナム、列挙型)と、enumとセットで使うswitch文を、ゲームでよくある場面と一緒にご紹介します!

目次

この記事で学べること

  • enumとは何か(数字や文字で持つときの困りごと)
  • enumの作り方と、Inspectorのドロップダウンで選ぶ方法
  • switch文で、選択肢ごとに処理を分ける方法
  • ゲームでよく使う例:東西南北の向き・ゲームの状態・町の順番・属性の相性
  • enumの中身は数字であることと、途中に足すときの注意

前提知識

enumってどんなもの?

たとえば、キャラクターが東西南北のどちらを向いているかを持っておきたいとします。数字や文字で持つと、次のようになります。

int facing = 0;          // 0=北、1=東、2=南、3=西・・・のつもり
string facingName = "north";
  • 数字:0が北なのか南なのか、コメントを見ないと分からない。7のような、ありえない数も入ってしまう
  • 文字:"nroth"のように打ちまちがえても、エラーにならない。動かしてみるまで気づけない

enumを使うと、選択肢そのものに名前を付けて、その中からしか選べないようにできます。

public enum Direction
{
    North, // 北
    East,  // 東
    South, // 南
    West   // 西
}
Direction facing = Direction.North;
  • Direction.Nrothと打ちまちがえると、その場でエラーになるので、すぐに気づける
  • 入れられるのは、North・East・South・Westの4つだけ
  • VS CodeでDirection.まで打つと、候補の一覧が出る

enumを作ってみよう

enumは、クラスの外(または中)に書きます。いろいろなスクリプトから使いたいときは、クラスの外に書いておくのがオススメです。

  • Projectウインドで右クリック>Create>MonoBehaviour Script で「Walker」を作り、中身を次のように書き換える
using UnityEngine;

// 東西南北の向き
public enum Direction
{
    North, // 北
    East,  // 東
    South, // 南
    West   // 西
}

// 向いている方向を Inspector で選べるようにする
public class Walker : MonoBehaviour
{
    [SerializeField] private Direction facing = Direction.North;

    private void Start()
    {
        Debug.Log($"今の向き:{facing}");
    }
}
  • public enum Direction { … }
    • enum 名前 { 選択肢, 選択肢, … }の形で書く。選択肢は,で区切る
    • 名前は、クラスと同じく大文字で始めるのが決まりです
  • [SerializeField] private Direction facing
    • enumの変数をInspectorに出すと、ドロップダウンで選べるようになる
  • {facing}
    • enumを文字にすると、選択肢の名前(North)になる

空のGameObjectにWalkerを付けて、Inspectorを見てみましょう。

PlayerのWalker。FacingにNorthが選ばれ、Current TownはStart Village

Facingをクリックすると、North・East・South・Westの4つがドロップダウンで出てきます。InspectorではNorthが「North」、PortTownのような2語の名前は「Port Town」と、見やすく表示されます。

switch文で選択肢ごとに分けよう

enumとセットでよく使うのがswitch文です。「この値ならこれ、あの値ならあれ」と、選択肢ごとに処理を分けられます。

向きに合わせて、進む方向(Vector2)を返すメソッドを作ってみましょう。Walkerに足します。

    // 向きから、1マス進む方向を返す
    private Vector2 ToVector(Direction direction)
    {
        switch (direction)
        {
            case Direction.North:
                return Vector2.up;
            case Direction.East:
                return Vector2.right;
            case Direction.South:
                return Vector2.down;
            case Direction.West:
                return Vector2.left;
            default:
                return Vector2.zero;
        }
    }
  • switch (direction)
    • ()の中の値を見て、同じ値のcaseへ飛ぶ
  • case Direction.North:
    • 「値が北のとき」。その下に処理を書く
  • return / break
    • caseの処理の最後には、return(メソッドを抜ける)かbreak(switchを抜ける)が必要。書き忘れるとエラーになります
  • default:
    • どのcaseにも当てはまらなかったとき。enumの選択肢を全部書いていても、念のため入れておくと安心です

同じことはif文でも書けますが、選択肢が多いときはswitch文のほうが「全部の選択肢を書いたか」がひと目で分かります。

VS Codeでは、switchの中にcaseの足りない選択肢があると、電球マーク(クイックフィックス)から「足りないcaseを追加」できることがあります。選択肢の書き忘れを防げるので、出てきたら使ってみてください。

ゲームでよく使う例

ここからは、ゲームでよくある場面ごとに、enumの使い方を見ていきましょう。「決まった選択肢から1つ」が出てきたら、enumの出番です。

例1:東西南北の向きと「右を向く」

ダンジョンを歩くゲームで、「右を向く」ボタンを押すと向きが変わるようにします。

enumの中身は、実は書いた順に0・1・2・3の数字になっています(North=0、East=1、South=2、West=3)。これを使うと、「次の向き」を計算で出せます。

    // 右を向く(北→東→南→西→北…)
    private Direction TurnRight(Direction direction)
    {
        int next = ((int)direction + 1) % 4;
        return (Direction)next;
    }
  • (int)direction
    • enumを数字にする。Eastなら1
  • % 4
    • 4で割ったあまり。West(3)の次は4ではなく0(North)に戻る
  • (Direction)next
    • 数字をenumに戻す

この計算は、enumを「北・東・南・西」の時計回りの順に書いているから成り立ちます。順番を入れかえると、右を向いたつもりが反対を向いたりします。順番に意味があるenumには、コメントで「時計回りの順」と書いておきましょう。

例2:ゲームの状態

タイトル・プレイ中・一時停止・ゲームオーバーのように、「今ゲームがどの場面か」もenumの定番です。

using UnityEngine;

// ゲームの場面
public enum GameState
{
    Title,    // タイトル
    Playing,  // プレイ中
    Paused,   // 一時停止
    GameOver  // ゲームオーバー
}

// 場面ごとに、毎フレームの処理を分ける
public class GameFlow : MonoBehaviour
{
    [SerializeField] private GameState state = GameState.Title;

    private void Update()
    {
        switch (state)
        {
            case GameState.Title:
                // スタートボタンを待つ
                break;
            case GameState.Playing:
                // プレイヤーを動かす・敵を出す
                break;
            case GameState.Paused:
                // 何もしない(止めておく)
                break;
            case GameState.GameOver:
                // リトライを待つ
                break;
        }
    }

    public void ChangeState(GameState next)
    {
        Debug.Log($"場面が変わった:{state} → {next}");
        state = next;
    }
}

「プレイ中なのに、タイトルのボタンが反応する」のような事故は、boolをいくつも並べて場面を持っているときによく起きます。enumなら今の場面はいつも1つだけなので、そういう事故が起きにくくなります。

例3:町の順番と「もう行ける?」

RPGで、はじまりの村から王都まで、町を順番に進んでいくとします。

// 町(旅の順番に並べる)
public enum Town
{
    StartVillage, // はじまりの村
    ForestTown,   // 森の町
    PortTown,     // 港町
    Capital       // 王都
}

enumは数字なので、<や>=で比べられます。「港町より先に進んでいたら、船に乗れる」のような判定が1行で書けます。

    [SerializeField] private Town currentTown = Town.StartVillage;

    private bool CanTakeShip()
    {
        return currentTown >= Town.PortTown;
    }

    // 次の町に進む(王都より先は無い)
    private void GoToNextTown()
    {
        if (currentTown == Town.Capital)
        {
            Debug.Log("もう王都に着いています");
            return;
        }
        currentTown = currentTown + 1;
        Debug.Log($"{TownName(currentTown)}に着いた!");
    }
  • currentTown >= Town.PortTown
    • 港町(2)か王都(3)なら true
  • currentTown + 1
    • enumに1を足すと、次の選択肢になる

画面に出す日本語の名前は、switch文で別に用意しておきます。

    // 画面に出す町の名前
    private string TownName(Town town)
    {
        switch (town)
        {
            case Town.StartVillage:
                return "はじまりの村";
            case Town.ForestTown:
                return "森の町";
            case Town.PortTown:
                return "港町";
            case Town.Capital:
                return "王都";
            default:
                return "?";
        }
    }

enumの選択肢の名前には、日本語を使わないのがオススメです。ほかの人のパソコンや、セーブデータに書き出したときに文字化けすることがあります。名前は英語、画面に出す文字は別に用意、と分けておきましょう。

例4:属性の相性

炎・水・草の3すくみのような属性の相性も、enumとswitch文で書けます。

// 属性
public enum Element
{
    Fire,  // 炎
    Water, // 水
    Grass  // 草
}
    // 攻撃の属性と、相手の属性から、ダメージの倍率を返す
    private float DamageRate(Element attack, Element target)
    {
        switch (attack)
        {
            case Element.Fire:
                return target == Element.Grass ? 2f : (target == Element.Water ? 0.5f : 1f);
            case Element.Water:
                return target == Element.Fire ? 2f : (target == Element.Grass ? 0.5f : 1f);
            case Element.Grass:
                return target == Element.Water ? 2f : (target == Element.Fire ? 0.5f : 1f);
            default:
                return 1f;
        }
    }
  • 条件 ? A : B
    • 「条件がtrueならA、falseならB」を1行で書く書き方(三項演算子)
    • 炎で草を攻撃したら2倍、水を攻撃したら0.5倍、それ以外は1倍

ほかにも、ゲームではこんなところでenumがよく使われます。

場面enumの例
アイテムの種類回復・武器・防具・だいじなもの
装備する場所頭・体・右手・左手・足
難易度かんたん・ふつう・むずかしい
敵のAIの状態うろうろ・追いかける・攻撃・逃げる
カードの種類攻撃・防御・回復・特殊
スライム

アイテムの種類を"kaifuku"みたいな文字で持っていたの、全部enumにしたくなってきました・・・

フェアリー

打ちまちがいがエラーで分かるようになるから、直す価値はあるよ!まずは新しく作るところから使ってみてね

動かして確認してみよう!

ここまでのメソッドをまとめて、Walkerで試してみましょう。例1〜例4を入れたWalkerの全体は次のとおりです(GameFlowは別のスクリプトなので、ここには入れていません)。

using UnityEngine;

// 東西南北の向き(時計回りの順)
public enum Direction
{
    North, // 北
    East,  // 東
    South, // 南
    West   // 西
}

// 町(旅の順番に並べる)
public enum Town
{
    StartVillage, // はじまりの村
    ForestTown,   // 森の町
    PortTown,     // 港町
    Capital       // 王都
}

// 属性
public enum Element
{
    Fire,  // 炎
    Water, // 水
    Grass  // 草
}

// enum と switch 文の使い方を、Console で確かめる
public class Walker : MonoBehaviour
{
    [SerializeField] private Direction facing = Direction.North;
    [SerializeField] private Town currentTown = Town.StartVillage;

    private void Start()
    {
        // 例1:右を4回向くと、ぐるっと1周して北に戻る
        for (int i = 0; i < 4; i++)
        {
            Direction next = TurnRight(facing);
            Debug.Log($"右を向いた:{DirectionName(facing)} → {DirectionName(next)}(進む向き {ToVector(next)})");
            facing = next;
        }

        // 例3:王都まで町を進む(船に乗れるようになった瞬間だけ知らせる)
        for (int i = 0; i < 4; i++)
        {
            bool couldTakeShip = CanTakeShip();
            GoToNextTown();
            if (!couldTakeShip && CanTakeShip())
            {
                Debug.Log("船に乗れるようになった");
            }
        }

        // 例4:属性の相性
        Debug.Log($"炎→草:{DamageRate(Element.Fire, Element.Grass)}倍");
        Debug.Log($"炎→水:{DamageRate(Element.Fire, Element.Water)}倍");
        Debug.Log($"草→草:{DamageRate(Element.Grass, Element.Grass)}倍");
    }

    // 向きから、1マス進む方向を返す
    private Vector2 ToVector(Direction direction)
    {
        switch (direction)
        {
            case Direction.North:
                return Vector2.up;
            case Direction.East:
                return Vector2.right;
            case Direction.South:
                return Vector2.down;
            case Direction.West:
                return Vector2.left;
            default:
                return Vector2.zero;
        }
    }

    // 右を向く(北→東→南→西→北…)
    private Direction TurnRight(Direction direction)
    {
        int next = ((int)direction + 1) % 4;
        return (Direction)next;
    }

    // 画面に出す向きの名前
    private string DirectionName(Direction direction)
    {
        switch (direction)
        {
            case Direction.North:
                return "北";
            case Direction.East:
                return "東";
            case Direction.South:
                return "南";
            case Direction.West:
                return "西";
            default:
                return "?";
        }
    }

    private bool CanTakeShip()
    {
        return currentTown >= Town.PortTown;
    }

    // 次の町に進む(王都より先は無い)
    private void GoToNextTown()
    {
        if (currentTown == Town.Capital)
        {
            Debug.Log("もう王都に着いています");
            return;
        }
        currentTown = currentTown + 1;
        Debug.Log($"{TownName(currentTown)}に着いた!");
    }

    // 画面に出す町の名前
    private string TownName(Town town)
    {
        switch (town)
        {
            case Town.StartVillage:
                return "はじまりの村";
            case Town.ForestTown:
                return "森の町";
            case Town.PortTown:
                return "港町";
            case Town.Capital:
                return "王都";
            default:
                return "?";
        }
    }

    // 攻撃の属性と、相手の属性から、ダメージの倍率を返す
    private float DamageRate(Element attack, Element target)
    {
        switch (attack)
        {
            case Element.Fire:
                return target == Element.Grass ? 2f : (target == Element.Water ? 0.5f : 1f);
            case Element.Water:
                return target == Element.Fire ? 2f : (target == Element.Grass ? 0.5f : 1f);
            case Element.Grass:
                return target == Element.Water ? 2f : (target == Element.Fire ? 0.5f : 1f);
            default:
                return 1f;
        }
    }
}

再生すると、Consoleに次のように出れば成功!

Walkerの実行結果。右を向いた:北 → 東 から 西 → 北 までの4行、森の町・港町・船に乗れるようになった・王都・もう王都に着いています、炎→草:2倍、炎→水:0.5倍、草→草:1倍
  • 右を4回向くと、北→東→南→西→北とぐるっと1周する
  • 港町に着いたところから「船に乗れるようになった」が出る
  • 王都の先には進まない
  • 炎→草は2倍、炎→水は0.5倍、同じ属性は1倍

注意:途中に選択肢を足すとき

enumの中身は書いた順の数字なので、途中に選択肢を足すと、うしろの番号が全部ずれます。

public enum Town
{
    StartVillage, // 0
    ForestTown,   // 1
    DesertTown,   // 2 ← あとから足した
    PortTown,     // 3(前は2だった)
    Capital       // 4(前は3だった)
}
  • Inspectorで選んでおいた値や、セーブデータに数字で保存していた値は、番号のまま残る
  • 前に「港町(2)」を選んでいたものが、足したあとは「砂漠の町(2)」になってしまう
スライム

セーブデータを作ったあとで町を足したら、ロードした瞬間に別の町にワープしました・・・

順番に意味がないenum(アイテムの種類など)は、新しい選択肢を一番うしろに足すのが安全です。町の順番のように途中に入れたいときは、Inspectorで選んでいる値や、セーブデータを見直しましょう。

数字を自分で決めることも出来ます(PortTown = 20のように書く)。番号を固定しておきたいときに使いますが、例1のように「+1」で次に進む使い方とは相性が悪いので、最初は書いた順のままで大丈夫です。

よくあるエラーと対処法

エラー:Cannot implicitly convert type ‘int’ to ‘Direction’

  • 原因:数字を、そのままenumの変数に入れようとした
  • 対処法:(Direction)nextのように、(enumの名前)を付けて変換する

エラー:’Direction’ does not contain a definition for ‘north’

  • 原因:選択肢の名前の大文字・小文字がちがう(Northをnorthと書いた)
  • 対処法:enumに書いたとおりの名前にする。VS Codeの候補から選ぶとまちがえにくい

エラー:Control cannot fall through from one case label (‘case Direction.North:’) to another

  • 原因:caseの処理の最後にbreakやreturnを書き忘れた
  • 対処法:caseごとにbreak;かreturn …;を書く

Inspectorで選んでいた値が変わった

  • 原因:enumの途中に選択肢を足した(または消した)ので、番号がずれた
  • 対処法:新しい選択肢は一番うしろに足す。ずれてしまった値は、Inspectorで選び直す

まとめ

  • enumは「決まった選択肢の中から1つ」に名前を付ける型。打ちまちがいがエラーで分かる
  • [SerializeField]を付けると、Inspectorのドロップダウンで選べる
  • switch文で、選択肢ごとに処理を分けられる。caseの最後はbreakかreturn
  • 中身は書いた順の数字(0・1・2…)。(int)で数字に、(enumの名前)でenumに戻せる。<や>=で比べられる
  • 途中に選択肢を足すと番号がずれるので、順番に意味がなければ一番うしろに足す

次のステップ

  • カードの種類(攻撃・防御・回復・特殊)をenumで作る回:デッキ構築カードゲームの作り方#2(準備中)
  • Unityスタートガイド 目次
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次