Unityで2Dゲームを一から作る:キャラクターの配置、コード、実行とテスト

Unityは、初心者からプロのチームまで広く使われているゲームエンジンです。2Dゲームは複雑さが少なく、シーン、GameObject、コンポーネント、物理演算、入力の読み取りといった基本の考え方を学ぶのに適しています。この記事では、新しいプロジェクトを作り、キャラクターをシーンに配置し、歩いたりジャンプしたりするスクリプトを書き、カメラがキャラクターを追従するようにし、最後にコードが正しく動くかをテストします。

注:この記事のコードには、元のタイ語版のコメントがそのまま残っています。コードの動作はどの言語でも同じです。コメントを翻訳して使っても構いません。

Unityウィンドウの概略図(実際のスクリーンショットではありません):1 上部のPlayボタン、2 Hierarchyの+ボタン(オブジェクト作成)、3 InspectorのAdd Component、4 Projectパネル(スクリプト作成)
Unityウィンドウの概略図(実際のスクリーンショットではありません):1 上部のPlayボタン、2 Hierarchyの+ボタン(オブジェクト作成)、3 InspectorのAdd Component、4 Projectパネル(スクリプト作成)

必要なもの

ステップ1:2Dプロジェクトを作る

1. Unity Hub を開き、**New project** を押して **Universal 2D**(URP)または **2D**(Built-in)のテンプレートを選びます(Unity Hub の表示名に合わせてください)。

2. `MyPlatformer` などの名前を付け、**Create project** を押します。

3. Unityが開くまで待ちます。`SampleScene` という空のシーンが表示されます。

シーンはゲームの遊び場です。キャラクター、地面、カメラなど、シーン内のすべてが **GameObject** で、それぞれに付けたコンポーネントが振る舞いを決めます。

Edit → Project Settings → Player → Active Input Handling の概略図(実際のスクリーンショットではありません):1 Editメニュー、2 Project Settings、3 Playerタブ、4 Active Input Handlingの選択欄
Edit → Project Settings → Player → Active Input Handling の概略図(実際のスクリーンショットではありません):1 Editメニュー、2 Project Settings、3 Playerタブ、4 Active Input Handlingの選択欄

ステップ2:入力の設定

この記事のスクリプトは従来の入力方式(Input Manager)を使います。有効にするには次の手順に従ってください。

1. **Edit → Project Settings → Player** を開きます。

2. **Active Input Handling** を見つけ、**Both** または **Input Manager (Old)** を選びます。

3. Unity が再起動を求めたら承諾します。

**Horizontal**(左右)と **Jump**(ジャンプ、Space)は標準設定に含まれています。左右は A/D キーまたは矢印キーです。

ステップ3:地面を作る

1. Hierarchy を右クリックし **2D Object → Sprites → Square** を選びます。名前を `Ground` にします。

2. Inspector で Scale を `X = 20`、`Y = 1` にして長い床にします。

3. Position を `X = 0`、`Y = -2` にします。

4. **Add Component** から **Box Collider 2D** を追加します。これでキャラクターが地面に立てます。

5. `Ground` という名前のレイヤーを作ります。Inspector上部の **Layer** のプルダウンから **Add Layer** を選び、戻って `Ground` にレイヤー `Ground` を設定します。

このレイヤーがあると、スクリプトは足の下が床なのか、壁や敵なのかを判断できます。

ステップ4:キャラクターを追加する

1. Hierarchy を右クリックし **2D Object → Sprites → Square** を選びます。名前を `Player` にします。

2. Position を `X = 0`、`Y = 1` にして、地面の上から始まるようにします。

3. キャラクター画像があれば、PNGをSprite Rendererの **Sprite** 欄にドラッグします。

4. **Rigidbody 2D** を追加し、**Body Type** を Dynamic にして、**Freeze Rotation Z** にチェックを入れます。これで倒れなくなります。

5. **Box Collider 2D** を追加します。

Rigidbody 2D は、オブジェクトを重力で落下させ、他のオブジェクトと衝突させます。これがないと、キャラクターはその場に浮いたままになります。

ステップ5:接地判定の位置を作る

1. Hierarchy で `Player` を右クリックし **Create Empty** を選びます。名前を `GroundCheck` にします。

2. キャラクターの足元へ移動します。たとえば Position を `X = 0`、`Y = -0.5` にします。

スクリプトはこの点を中心とした小さな円で、足元に地面があるかを確かめます。

ステップ6:キャラクター制御スクリプトを書く

Project ウィンドウを右クリックし **Create → C# Script** を選びます。名前を `PlayerController2D` にして開き、次のコードを貼り付けます。

using UnityEngine;

// ควบคุมตัวละคร 2D: เดินซ้ายขวา กระโดด และหันหน้าตามทิศทาง
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(SpriteRenderer))]
public class PlayerController2D : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;      // ความเร็วเดิน
    [SerializeField] private float jumpForce = 12f;     // แรงกระโดด
    [SerializeField] private Transform groundCheck;     // จุดตรวจพื้น
    [SerializeField] private float groundRadius = 0.15f;
    [SerializeField] private LayerMask groundLayer;     // เลเยอร์ที่นับว่าเป็นพื้น

    private Rigidbody2D rb;
    private SpriteRenderer sprite;
    private float moveInput;
    private bool isGrounded;
    private bool jumpRequested;

    public bool IsGrounded => isGrounded;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        sprite = GetComponent<SpriteRenderer>();
    }

    // Update ทำงานทุกเฟรม ใช้อ่านปุ่มกด แต่ยังไม่กระโดดทันที
    // เพราะค่า isGrounded ถูกอัปเดตใน FixedUpdate ถ้ากระโดดใน Update อาจใช้สถานะพื้นเก่า
    private void Update()
    {
        moveInput = Input.GetAxisRaw("Horizontal");   // -1 ซ้าย, 0 ไม่กด, 1 ขวา

        if (Input.GetButtonDown("Jump"))
        {
            jumpRequested = true;                      // จำไว้ว่าผู้เล่นกดกระโดด
        }

        if (moveInput != 0)
        {
            sprite.flipX = moveInput < 0;             // หันหน้าไปทางที่เดิน
        }
    }

    // FixedUpdate ทำงานตามจังหวะฟิสิกส์ ตรวจพื้นก่อน แล้วค่อยตัดสินใจกระโดด
    private void FixedUpdate()
    {
        // ถ้ายังไม่ได้ผูก groundCheck ใน Inspector ให้ใช้ตำแหน่งของตัวละครแทน
        Vector2 checkPosition = groundCheck != null ? groundCheck.position : transform.position;
        isGrounded = Physics2D.OverlapCircle(checkPosition, groundRadius, groundLayer);

        float verticalSpeed = rb.velocity.y;
        if (jumpRequested && isGrounded)
        {
            verticalSpeed = jumpForce;
        }
        jumpRequested = false;                         // ใช้คำขอกระโดดแล้ว

        rb.velocity = new Vector2(moveInput * moveSpeed, verticalSpeed);
    }
}

重要なポイント

Playerの Inspector の概略図(実際のスクリーンショットではありません):1 PlayerController2Dスクリプト、2 Freeze Rotation Z にチェックを入れた Rigidbody 2D、3 Ground Check 欄、4 Ground Layer 欄
Playerの Inspector の概略図(実際のスクリーンショットではありません):1 PlayerController2Dスクリプト、2 Freeze Rotation Z にチェックを入れた Rigidbody 2D、3 Ground Check 欄、4 Ground Layer 欄

ステップ7:スクリプトを接続する

1. `PlayerController2D` スクリプトを `Player` にドラッグします。

2. Inspector で `GroundCheck` を **Ground Check** 欄にドラッグします。

3. **Ground Layer** を `Ground` に設定します。

4. **Move Speed** を 5、**Jump Force** を 12 に設定します。好みの値に変えても構いません。

ステップ8:カメラをキャラクターに追従させる

`CameraFollow2D` という名前のスクリプトを作り、次のコードを貼り付けます。

using UnityEngine;

// กล้องติดตามตัวละครแบบนุ่มนวล และไม่ให้ตกเกินขอบล่างของฉาก
public class CameraFollow2D : MonoBehaviour
{
    [SerializeField] private Transform target;          // ตัวละครที่จะติดตาม
    [SerializeField] private float smoothTime = 0.2f;   // ยิ่งน้อยยิ่งตามทันเร็ว
    [SerializeField] private float minY = -2f;          // ขอบล่างของกล้อง

    private Vector3 velocity;

    private void LateUpdate()
    {
        if (target == null) return;

        Vector3 desired = new Vector3(target.position.x, Mathf.Max(target.position.y, minY), transform.position.z);
        transform.position = Vector3.SmoothDamp(transform.position, desired, ref velocity, smoothTime);
    }
}

`LateUpdate` は、すべての Update の後に実行されます。そのフレームで動いたばかりのキャラクターを追うカメラには、この順序が適しています。`Vector3.SmoothDamp` によって、カメラはがたつかず滑らかに動きます。

**Main Camera** を選び、`CameraFollow2D` を追加して、`Player` を **Target** 欄にドラッグします。カメラの Z 位置は `-10` にして、シーン内のすべてがカメラの前に来るようにします。

ステップ9:ゲームを実行する

エディタ上部の **Play** を押して、次の操作を試してください。

もう一度 Play を押すと停止します。実行中に Inspector で変えた値は、停止すると元に戻ります。調整は停止中に行い、そのあとプロジェクトを保存してください。

ステップ10:自動テストを書く

小さなゲームなら手で試すだけでも足りますが、コードが大きくなると自動テストのほうが早く間違いを見つけられます。Unity には Unity Test Framework という公式のテスト機能があり、学ぶ価値があります。この記事では、より簡単な方法を使います。普通の C# で Unity のフレームループを模擬するので、エディタを開かずにスクリプトのロジックをテストできます。

次のテストは、Update と FixedUpdate を順に呼び出し、入力と接地の状態を設定し、速度に応じて位置を動かして、結果を確認します。

using System;

public static class PlayerTests
{
    private const float Dt = 1f / 50f;   // FixedUpdate 1回は 1/50 秒
    private static int passed = 0, failed = 0;

    private static void Check(string name, bool condition)
    {
        if (condition) { passed++; Console.WriteLine("PASS  " + name); }
        else { failed++; Console.WriteLine("FAIL  " + name); }
    }

    private static PlayerController2D CreatePlayer(out Rigidbody2D rb, out SpriteRenderer sprite)
    {
        var player = new PlayerController2D();
        rb = new Rigidbody2D();
        sprite = new SpriteRenderer();
        player.Components[typeof(Rigidbody2D)] = rb;
        player.Components[typeof(SpriteRenderer)] = sprite;
        player.Call("Awake");
        return player;
    }

    private static void Step(PlayerController2D player, Rigidbody2D rb)
    {
        player.Call("Update");
        player.Call("FixedUpdate");
        player.transform.position = new Vector3(
            player.transform.position.x + rb.velocity.x * Dt,
            player.transform.position.y + rb.velocity.y * Dt, 0);
    }

    public static int Main()
    {
        // 1. กดขวาแล้วเดินไปทางขวา
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = 1f;
            for (int i = 0; i < 50; i++) Step(player, rb);
            Check("walk right: x velocity equals moveSpeed (5)", Math.Abs(rb.velocity.x - 5f) < 0.001f);
            Check("walk right: x position moves about 5 units per second", Math.Abs(player.transform.position.x - 5f) < 0.2f);
            Check("walk right: faces right (flipX = false)", sprite.flipX == false);
        }

        // 2. กดซ้ายแล้วหันหน้าไปทางซ้าย
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = -1f;
            Step(player, rb);
            Check("walk left: x velocity is negative", rb.velocity.x < 0);
            Check("walk left: faces left (flipX = true)", sprite.flipX == true);
        }

        // 3. ไม่กดอะไรเลย ตัวละครต้องอยู่นิ่ง
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = 0f;
            Step(player, rb);
            Check("no input: x velocity is 0", rb.velocity.x == 0f);
        }

        // 4. กระโดดตอนยืนบนพื้น
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = 0f;
            Step(player, rb);                    // ให้ FixedUpdate ตรวจพื้นก่อน
            Input.JumpPressedThisFrame = true;
            Step(player, rb);
            Input.JumpPressedThisFrame = false;
            Check("jump on ground: y velocity equals jumpForce (12)", Math.Abs(rb.velocity.y - 12f) < 0.001f);
        }

        // 5. กดกระโดดตอนลอยอยู่กลางอากาศ ต้องไม่กระโดดซ้ำ
        {
            Rigidbody2D rb; SpriteRenderer sprite;
            var player = CreatePlayer(out rb, out sprite);
            Physics2D.GroundBelowPlayer = true;
            Input.HorizontalAxis = 0f;
            Step(player, rb);
            Physics2D.GroundBelowPlayer = false; // ตัวละครลอยพ้นพื้น
            rb.velocity = new Vector2(0, -3f);   // กำลังตกลง
            Input.JumpPressedThisFrame = true;
            Step(player, rb);
            Input.JumpPressedThisFrame = false;
            Check("jump in air: y velocity unchanged (still falling)", Math.Abs(rb.velocity.y + 3f) < 0.001f);
        }

        Console.WriteLine();
        Console.WriteLine("Passed " + passed + " / Failed " + failed);
        return failed == 0 ? 0 : 1;
    }
}

ไฟล์จำลอง `UnityStubs.cs` (ดูในเวอร์ชันภาษาไทย) มี `Vector2`, `Rigidbody2D`, `Input`, `Physics2D` และชนิดข้อมูลอื่น ๆ ที่จำเป็น ทำให้เทสต์รันได้นอก Unity ตัวจำลองนี้ไม่ได้จำลองฟิสิกส์หรือการแสดงผลจริง แต่ตรวจการคำนวณและลำดับการทำงานของสคริปต์ ซึ่งเป็นสิ่งที่เทสต์นี้ต้องการ คอมไพล์ไฟล์ทั้งสี่รวมกันด้วยคอมไพเลอร์ C# รุ่นใหม่ แล้วรันไฟล์ `.exe` ที่ได้ ในสคริปต์ที่ใช้กับ Unity ต้องคงบรรทัด `using UnityEngine;` ไว้ และลบออกเฉพาะในสำเนาที่ใช้ทดสอบกับตัวจำลองเท่านั้น

ผลการรันเทสต์

ได้ผลดังนี้เมื่อคอมไพล์ด้วย Roslyn และรัน

PASS  walk right: x velocity equals moveSpeed (5)
PASS  walk right: x position moves about 5 units per second
PASS  walk right: faces right (flipX = false)
PASS  walk left: x velocity is negative
PASS  walk left: faces left (flipX = true)
PASS  no input: x velocity is 0
PASS  jump on ground: y velocity equals jumpForce (12)
PASS  jump in air: y velocity unchanged (still falling)

Passed 8 / Failed 0

บั๊กที่เทสต์พบ

เวอร์ชันแรกของตัวควบคุมกระโดดจาก Update โดยตรง เทสต์ "jump in air" ล้มเหลว เพราะ Update ใช้ค่า `isGrounded` ของเฟรมก่อนหน้าซึ่งยังเป็น true ตัวละครจึงกระโดดซ้ำทั้งที่ลอยอยู่แล้ว ในเกมจริงเรื่องนี้เกิดขึ้นเมื่อเดินพ้นขอบแท่นพื้น วิธีแก้คือเก็บคำขอกระโดดไว้ในตัวแปร `jumpRequested` แล้วให้ FixedUpdate ตรวจพื้นและตัดสินใจกระโดดในขั้นตอนเดียวกัน ซึ่งคือโค้ดในขั้นตอนที่ 6

การทดสอบอัตโนมัติจึงมีประโยชน์ เพราะข้อผิดพลาดแบบนี้มองหาด้วยตาได้ยาก แต่เทสต์ที่ครอบคลุมกรณีขอบเขตจะพบได้ทันที

ขั้นตอนที่ 11:สร้างเกมเป็นไฟล์ที่เล่นได้

1. เปิดเมนู **File → Build Profiles**(เวอร์ชันเก่าเรียกว่า **Build Settings**)

2. เลือกแพลตฟอร์ม เช่น Windows หรือ WebGL แล้วกด **Build**

3. เลือกโฟลเดอร์ Unity จะสร้างไฟล์ `.exe` หรือเวอร์ชันเว็บที่ผู้อื่นเล่นได้

ปัญหาที่พบบ่อยและวิธีแก้

次のステップ

キャラクターが歩いてジャンプできるようになったら、Trigger Collider 2D と `OnTriggerEnter2D` でコインを追加したり、往復する敵を作ったり、体力と死亡の仕組みを足したり、Tilemap で背景を作ったりできます。どれもこの記事と同じ考え方を使います。

まとめ

Unity で2Dゲームを作るには、すべてが部品(コンポーネント)を持つ GameObject であることを理解するところから始まります。制御スクリプトは Update で入力を読み、FixedUpdate で物理を扱います。自動テストは、目で見ただけでは気づきにくい間違いを捕まえてくれます。手順を一つずつ進め、Inspector で値を調整して、自分のゲームに仕上げてください。