Unityで2Dゲームを一から作る:キャラクターの配置、コード、実行とテスト
Unityは、初心者からプロのチームまで広く使われているゲームエンジンです。2Dゲームは複雑さが少なく、シーン、GameObject、コンポーネント、物理演算、入力の読み取りといった基本の考え方を学ぶのに適しています。この記事では、新しいプロジェクトを作り、キャラクターをシーンに配置し、歩いたりジャンプしたりするスクリプトを書き、カメラがキャラクターを追従するようにし、最後にコードが正しく動くかをテストします。
注:この記事のコードには、元のタイ語版のコメントがそのまま残っています。コードの動作はどの言語でも同じです。コメントを翻訳して使っても構いません。
必要なもの
- **Unity Hub**(unity.com から入手)を入れたコンピュータ
- Unity Hub からインストールした最新のLTS版Unityエディタ(Unity 6 LTS または 2022 LTS など)
- VS Code または Visual Studio。プロジェクト作成時に Unity 用の連携機能の導入を提案されることがあります
- 小さなキャラクター画像(PNG)。なければ Unity が作る四角形のスプライトで代用できます
ステップ1:2Dプロジェクトを作る
1. Unity Hub を開き、**New project** を押して **Universal 2D**(URP)または **2D**(Built-in)のテンプレートを選びます(Unity Hub の表示名に合わせてください)。
2. `MyPlatformer` などの名前を付け、**Create project** を押します。
3. Unityが開くまで待ちます。`SampleScene` という空のシーンが表示されます。
シーンはゲームの遊び場です。キャラクター、地面、カメラなど、シーン内のすべてが **GameObject** で、それぞれに付けたコンポーネントが振る舞いを決めます。
ステップ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);
}
}
重要なポイント
- `[SerializeField]` は private な変数を Inspector に表示させます。コードを書き換えずに値を調整できます。
- `Input.GetAxisRaw("Horizontal")` は -1、0、1 のいずれかを返し、移動方向がはっきりします。
- `Input.GetButtonDown("Jump")` はキーを押したフレームだけ true になるため、Space を押し続けても連続ジャンプになりません。
- `Physics2D.OverlapCircle` は、`groundRadius` の半径の円が地面レイヤーのコライダーと重なっているかを調べます。
- Update で入力を読み、FixedUpdate で物理を処理します。もしジャンプを Update の中で直接行うと、足場の端を歩いて出た直後でも、`isGrounded` が前の値のままなので跳んでしまうことがあります。ジャンプの要求を保存して FixedUpdate で実行すれば、この問題を防げます。これはステップ10のテストで確認できます。
ステップ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** を押して、次の操作を試してください。
- A キーまたは左矢印:キャラクターが左に歩き、左を向きます。
- D キーまたは右矢印:キャラクターが右に歩き、右を向きます。
- 地面にいるときに Space:キャラクターがジャンプします。
- 空中で Space:もう一度ジャンプすることはありません。
- カメラがキャラクターを滑らかに追います。
もう一度 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` หรือเวอร์ชันเว็บที่ผู้อื่นเล่นได้
ปัญหาที่พบบ่อยและวิธีแก้
- **キャラクターが動かない:** Active Input Handling の設定と、Rigidbody 2D の Body Type が Dynamic かを確認します。
- **キャラクターが地面をすり抜ける:** 地面に Box Collider 2D があるか、キャラクターの Rigidbody 2D が Kinematic になっていないかを確認します。
- **ジャンプできない:** Ground Layer が `Ground` になっているか、GroundCheck がキャラクターの足元にあるかを確認します。
- **キャラクターが倒れる:** Rigidbody 2D の Freeze Rotation Z にチェックを入れます。
- **カメラが追従しない:** `CameraFollow2D` の Target 欄に `Player` が入っているか確認します。
- **velocity が非推奨という警告が出る:** Unity 6 では `linearVelocity` が使えます。お使いのバージョンで `velocity` の警告が出る場合は、`linearVelocity` に名前を変えてください。
次のステップ
キャラクターが歩いてジャンプできるようになったら、Trigger Collider 2D と `OnTriggerEnter2D` でコインを追加したり、往復する敵を作ったり、体力と死亡の仕組みを足したり、Tilemap で背景を作ったりできます。どれもこの記事と同じ考え方を使います。
まとめ
Unity で2Dゲームを作るには、すべてが部品(コンポーネント)を持つ GameObject であることを理解するところから始まります。制御スクリプトは Update で入力を読み、FixedUpdate で物理を扱います。自動テストは、目で見ただけでは気づきにくい間違いを捕まえてくれます。手順を一つずつ進め、Inspector で値を調整して、自分のゲームに仕上げてください。