สร้างเกม 2D ด้วย Unity ตั้งแต่ศูนย์: เอาตัวละครเข้าเกม เขียนโค้ด รัน และทดสอบ
Unity เป็นเครื่องมือสร้างเกมที่นิยมที่สุดเครื่องมือหนึ่งสำหรับทั้งมือใหม่และทีมมืออาชีพ เกม 2D มีข้อดีคือโครงสร้างไม่ซับซ้อนมาก เหมาะกับการเรียนรู้หลักการสำคัญ เช่น ฉาก ออบเจกต์ คอมโพเนนต์ ฟิสิกส์ และการอ่านปุ่มกด บทความนี้จะพาคุณตั้งโปรเจกต์ใหม่ เอาตัวละครเข้าไปในฉาก เขียนสคริปต์ให้เดิน กระโดด และให้กล้องตามตัวละคร แล้วทดสอบว่าโค้ดทำงานถูกต้อง
สิ่งที่ต้องเตรียม
- คอมพิวเตอร์ที่ติดตั้ง **Unity Hub** จากเว็บไซต์ unity.com
- ติดตั้ง Unity Editor รุ่น LTS ล่าสุด (เช่น Unity 6 LTS หรือ 2022 LTS) ผ่าน Unity Hub
- VS Code หรือ Visual Studio ที่มีส่วนเสริมสำหรับ Unity (Unity Hub จะเสนอให้ติดตั้งให้ตอนสร้างโปรเจกต์ได้)
- ภาพตัวละครขนาดเล็ก (ไฟล์ 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** ซึ่งต่อกับ **Component** เพื่อให้ทำหน้าที่ต่าง ๆ
ขั้นที่ 2: ตั้งค่าระบบรับปุ่มกด
สคริปต์ในบทความนี้ใช้ระบบรับปุ่มแบบดั้งเดิม (Input Manager) ตรวจให้แน่ใจว่าตั้งค่าไว้ดังนี้
1. เมนู **Edit → Project Settings → Player**
2. หา **Active Input Handling** แล้วเลือก **Both** หรือ **Input Manager (Old)**
3. Unity จะถามให้รีสตาร์ทเอดิเตอร์ ให้กดยอมรับ
แกนนำทาง **Horizontal** (ซ้ายขวา) และ **Jump** (กระโดด) มีมาให้ในเทมเพลตอยู่แล้ว ปุ่มมาตรฐานคือ A/D หรือลูกศรสำหรับซ้ายขวา และ Space สำหรับกระโดด
ขั้นที่ 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` โดยกดที่ช่อง **Layer** ด้านบนของ Inspector แล้วเลือก **Add Layer** จากนั้นกลับไปที่ `Ground` แล้วเลือกเลเยอร์ `Ground`
การสร้างเลเยอร์เพื่อให้สคริปต์ตรวจได้ว่าสิ่งที่เท้าแตะคือพื้น ไม่ใช่กำแพงหรือศัตรู
ขั้นที่ 4: เพิ่มตัวละคร
1. คลิกขวาที่ Hierarchy เลือก **2D Object → Sprites → Square** ตั้งชื่อเป็น `Player`
2. ตั้ง Position เป็น `X = 0`, `Y = 1` ให้ตัวละครอยู่เหนือพื้น
3. ถ้ามีรูปตัวละคร ลากไฟล์ PNG ใส่ช่อง **Sprite** ของ Sprite Renderer
4. เพิ่ม **Rigidbody 2D** ตั้ง **Body Type** เป็น Dynamic และติ๊ก **Freeze Rotation Z** เพื่อไม่ให้ตัวละครล้ม
5. เพิ่ม **Box Collider 2D** ให้ตัวละคร
Rigidbody 2D ทำให้วัตถุตกตามแรงโน้มถ่วงและชนกับวัตถุอื่นได้ ถ้าไม่มี Rigidbody ตัวละครจะลอยอยู่กับที่
ขั้นที่ 5: สร้างจุดตรวจพื้น
1. คลิกขวาที่ `Player` ใน Hierarchy เลือก **Create Empty** ตั้งชื่อเป็น `GroundCheck`
2. ลาก `GroundCheck` ไปไว้ที่เท้าของตัวละคร เช่น 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")` เป็นจริงเพียงเฟรมที่เพิ่งกดปุ่ม จึงไม่กระโดดซ้ำขณะกดค้าง
- `Physics2D.OverlapCircle` ตรวจว่ามีคอลไลเดอร์ในเลเยอร์พื้นอยู่ในวงกลมรัศมี `groundRadius` รอบ GroundCheck หรือไม่
- การแยก Update กับ FixedUpdate: Update อ่านปุ่ม FixedUpdate ทำงานกับฟิสิกส์ ถ้ากระโดดจาก Update โดยตรง ตัวละครอาจกระโดดได้ทั้งที่เพิ่งเดินพ้นขอบ เพราะค่า `isGrounded` ยังเป็นค่าเก่า การเก็บคำขอกระโดดไว้จนถึง FixedUpdate แก้ปัญหานี้ได้ ซึ่งเราจะเห็นจากการทดสอบในขั้นที่ 10
ขั้นที่ 7: ผูกสคริปต์และค่าต่าง ๆ
1. ลากสคริปต์ `PlayerController2D` ไปวางบน `Player`
2. ใน Inspector ของ Player ช่อง **Ground Check** ให้ลาก `GroundCheck` เข้าไป
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) ที่ใช้ได้จริง ส่วนตัวอย่างนี้ใช้วิธีที่เข้าใจง่ายกว่า คือจำลองเฟรมของ Unity ด้วยโค้ดธรรมดา
การทดสอบต่อไปนี้จำลองพฤติกรรมการทำงานของ Unity ได้แก่ การเรียก Update และ FixedUpdate การอ่านปุ่ม การตรวจพื้น และการเลื่อนตำแหน่งจากความเร็ว เพื่อตรวจว่า logic ของสคริปต์ถูกต้อง
using System;
public static class PlayerTests
{
private const float Dt = 1f / 50f; // 1 FixedUpdate เท่ากับ 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("เดินขวา: ความเร็ว x เท่ากับ moveSpeed (5)", Math.Abs(rb.velocity.x - 5f) < 0.001f);
Check("เดินขวา: ตำแหน่ง x เพิ่มขึ้นประมาณ 5 หน่วยต่อวินาที", Math.Abs(player.transform.position.x - 5f) < 0.2f);
Check("เดินขวา: หันหน้าไปทางขวา (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("เดินซ้าย: ความเร็ว x เป็นลบ", rb.velocity.x < 0);
Check("เดินซ้าย: หันหน้าไปทางซ้าย (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("ไม่กดปุ่ม: ความเร็ว x เป็น 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); // ให้ตรวจพื้นก่อน
Input.JumpPressedThisFrame = true;
Step(player, rb);
Input.JumpPressedThisFrame = false;
Check("กระโดดบนพื้น: ความเร็ว y เท่ากับ 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("กระโดดกลางอากาศ: ไม่เปลี่ยนความเร็ว y (ยังคงตกลง)", Math.Abs(rb.velocity.y + 3f) < 0.001f);
}
Console.WriteLine();
Console.WriteLine("ผ่าน " + passed + " / ไม่ผ่าน " + failed);
return failed == 0 ? 0 : 1;
}
}
ตัวจำลอง Unity API (`Vector2`, `Rigidbody2D`, `Input`, `Physics2D` และอื่น ๆ) ต้องมีไฟล์แยกต่างหากเพื่อให้โค้ดข้างบนคอมไพล์และรันนอก Unity ได้ ตัวจำลองนี้ไม่ได้จำลองฟิสิกส์จริงหรือการแสดงผล แต่ตรวจการคำนวณและลำดับการทำงานของสคริปต์ได้
ไฟล์จำลอง Unity API (UnityStubs.cs)
ไฟล์นี้วางไว้ในโฟลเดอร์ `test` ร่วมกับ `PlayerTests.cs` โดยเป็นตัวจำลองขั้นต่ำให้โค้ดคอมไพล์ได้นอก Unity
using System;
using System.Collections.Generic;
public struct Vector2
{
public float x, y;
public Vector2(float x, float y) { this.x = x; this.y = y; }
public static implicit operator Vector2(Vector3 v) { return new Vector2(v.x, v.y); }
}
public struct Vector3
{
public float x, y, z;
public Vector3(float x, float y, float z) { this.x = x; this.y = y; this.z = z; }
public static Vector3 SmoothDamp(Vector3 current, Vector3 target, ref Vector3 velocity, float smoothTime)
{
// จำลองแบบง่าย: เข้าใกล้เป้าหมายครึ่งทางต่อการเรียก 1 ครั้ง
return new Vector3((current.x + target.x) / 2, (current.y + target.y) / 2, target.z);
}
}
public static class Mathf
{
public static float Max(float a, float b) { return a > b ? a : b; }
}
public struct LayerMask { }
public class Transform
{
public Vector3 position;
}
public class Rigidbody2D
{
public Vector2 velocity;
}
public class SpriteRenderer
{
public bool flipX;
}
public static class Input
{
public static float HorizontalAxis; // ค่าที่ทดสอบจะตั้ง: -1, 0 หรือ 1
public static bool JumpPressedThisFrame;
public static float GetAxisRaw(string name)
{
if (name != "Horizontal") throw new ArgumentException("unknown axis " + name);
return HorizontalAxis;
}
public static bool GetButtonDown(string name)
{
if (name != "Jump") throw new ArgumentException("unknown button " + name);
return JumpPressedThisFrame;
}
}
public static class Physics2D
{
public static bool GroundBelowPlayer; // ค่าที่ทดสอบจะตั้งว่ามีพื้นอยู่ใต้เท้าหรือไม่
public static bool OverlapCircle(Vector2 point, float radius, LayerMask mask)
{
return GroundBelowPlayer;
}
}
public class SerializeFieldAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RequireComponentAttribute : Attribute
{
public RequireComponentAttribute(Type type) { }
}
public class MonoBehaviour
{
// เก็บคอมโพเนนต์ที่ผู้ทดสอบเพิ่มให้ GameObject ตัวนี้
public readonly Dictionary<Type, object> Components = new Dictionary<Type, object>();
public Transform transform = new Transform();
public T GetComponent<T>() where T : class
{
object value;
return Components.TryGetValue(typeof(T), out value) ? (T)value : null;
}
// เรียกเมธอดวงจรชีวิต (Awake, Update, FixedUpdate) ตามชื่อ เหมือนที่ Unity เรียกให้ในแต่ละเฟรม
public void Call(string methodName)
{
var method = GetType().GetMethod(methodName,
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
method.Invoke(this, null);
}
}
การคอมไพล์และรันทดสอบ: คัดลอกไฟล์ทั้งสี่ (`UnityStubs.cs`, `PlayerTests.cs`, `PlayerController2D.cs`, `CameraFollow2D.cs`) ไว้ในโฟลเดอร์เดียวกัน แล้วรันคำสั่งคอมไพล์ด้วยคอมไพเลอร์ Roslyn ของ Visual Studio หรือ `csc` รุ่นใหม่ จากนั้นรันไฟล์ `.exe` ที่ได้ ในไฟล์สคริปต์จริงที่ใช้กับ Unity ต้องมี `using UnityEngine;` แต่ตอนคอมไพล์ด้วยตัวจำลองให้ลบบรรทัดนั้นออกจากสำเนาที่ใช้ทดสอบ
ผลการคอมไพล์และรันการทดสอบ
ทดสอบด้วยคอมไพเลอร์ C# รุ่นใหม่ (Roslyn) ผลที่ได้คือ
PASS เดินขวา: ความเร็ว x เท่ากับ moveSpeed (5)
PASS เดินขวา: ตำแหน่ง x เพิ่มขึ้นประมาณ 5 หน่วยต่อวินาที
PASS เดินขวา: หันหน้าไปทางขวา (flipX = false)
PASS เดินซ้าย: ความเร็ว x เป็นลบ
PASS เดินซ้าย: หันหน้าไปทางซ้าย (flipX = true)
PASS ไม่กดปุ่ม: ความเร็ว x เป็น 0
PASS กระโดดบนพื้น: ความเร็ว y เท่ากับ jumpForce (12)
PASS กระโดดกลางอากาศ: ไม่เปลี่ยนความเร็ว y (ยังคงตกลง)
ผ่าน 8 / ไม่ผ่าน 0
การทดสอบพบข้อบกพร่องและการแก้ไข
ก่อนปรับปรุง เวอร์ชันแรกของ `PlayerController2D` กระโดดจาก Update โดยตรง การทดสอบข้อ "กระโดดกลางอากาศ" ล้มเหลว เพราะ 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 ใน Inspector ตั้งเป็นเลเยอร์ `Ground` และ GroundCheck อยู่ที่เท้าจริง
- **ตัวละครหมุนล้ม:** ติ๊ก Freeze Rotation Z ใน Rigidbody 2D
- **กล้องไม่ตาม:** ตรวจว่าใส่ Player ในช่อง Target ของ CameraFollow2D แล้ว
- **เตือนว่า velocity ถูกเลิกใช้:** ใน Unity 6 มีพร็อพเพอร์ตี้ `linearVelocity` ให้ใช้ ถ้าเวอร์ชันของคุณแจ้งเตือนเรื่อง `velocity` ให้เปลี่ยนชื่อเป็น `linearVelocity`
แนวทางต่อยอดหลังจากนี้
เมื่อตัวละครเดินและกระโดดได้แล้ว คุณสามารถเพิ่มการเก็บเหรียญโดยใช้ Trigger Collider 2D และ `OnTriggerEnter2D` เพิ่มศัตรูที่เดินไปมา เพิ่มระบบชีวิตและการตาย และเพิ่มฉากหลังด้วย Tilemap ทั้งหมดนี้ใช้หลักการเดียวกับที่ทำในบทความนี้
สรุป
การสร้างเกม 2D ด้วย Unity เริ่มจากการเข้าใจว่าทุกสิ่งเป็น GameObject ที่ประกอบด้วย Component สคริปต์ควบคุมตัวละครใช้ Update อ่านปุ่ม และใช้ FixedUpdate จัดการฟิสิกส์ การทดสอบอัตโนมัติช่วยจับข้อผิดพลาดที่ตรวจด้วยมือได้ยาก อย่างการกระโดดขณะลอยอยู่กลางอากาศ ลองทำตามขั้นตอนทีละขั้น แล้วปรับแต่งค่าใน Inspector เพื่อให้เกมเป็นของคุณเอง