# 🕹️ Nhận input và di chuyển nhân vật

By Ngọc Vương Trương

> Sign in to view the full content of this lesson.

Ở bài trước bạn đã biết vòng lặp game chạy `Update` mỗi frame và project Unity 2D được tổ chức ra sao. Giờ là lúc "thổi sự sống" vào nhân vật: đọc input người chơi và biến nó thành chuyển động. Đây là viên gạch đầu tiên của prototype capstone, vì va chạm và tính điểm ở bài sau đều dựa trên việc nhân vật di chuyển đúng.

Sau bài này, bạn sẽ:

- Đọc được input từ bàn phím (và hiểu cách mở rộng sang touch/joystick trên mobile).
- Viết được script C# di chuyển nhân vật bằng cả `transform` lẫn `Rigidbody2D`.
- Giải thích được vì sao phải nhân với `Time.deltaTime` để tốc độ ổn định ở mọi frame rate.
- Tránh được lỗi kinh điển: đi chéo nhanh hơn đi thẳng.

## Từ phím bấm đến chuyển động: input đi qua đâu?

Mỗi frame, Unity hỏi hệ điều hành: "người chơi đang giữ phím nào?". Câu trả lời nằm trong lớp `Input`. Bạn không cần biết phím cụ thể, chỉ cần biết **trục (axis)**:

- `Horizontal`: A/D hoặc phím mũi tên trái/phải, trả về khoảng -1 đến 1.
- `Vertical`: W/S hoặc mũi tên lên/xuống.

Trong Unity Editor, các phím này đã được map sẵn trong *Project Settings > Input Manager*. Trên mobile, cùng đoạn code đó có thể nhận giá trị từ một on-screen joystick (thường là một `Image` + script joystick ghi vào cùng trục), nên logic di chuyển không phải viết lại.

## Hai hàm đọc trục: GetAxis và GetAxisRaw

Đây là chỗ người mới hay nhầm:

| Hàm | Trả về | Dùng khi nào |
| --- | --- | --- |
| `Input.GetAxis("Horizontal")` | float đã làm mượt, tăng dần từ 0 đến 1 | Game cần cảm giác "trượt", phù hợp joystick analog |
| `Input.GetAxisRaw("Horizontal")` | -1, 0 hoặc 1, không làm mượt | Game platformer, phản hồi tức thì, dễ đoán |

Với game 2D nhịp nhanh, **`GetAxisRaw` thường là lựa chọn tốt hơn** vì nhân vật dừng ngay khi bạn nhả phím.

> **Note:** 
> Nếu project của bạn dùng package **Input System** mới, cú pháp là `Keyboard.current.aKey.isPressed` hoặc một `InputAction`. Cách đọc khác nhưng nguyên lý giống hệt: lấy vector input rồi nhân với tốc độ. Bài này dùng `Input` cổ điển vì nó chạy được ngay, không cần cấu hình.

**Bài tập nhanh:** Bạn muốn nhân vật di chuyển theo kiểu "trượt trên băng" (tăng tốc và giảm tốc từ từ). Chọn hàm nào?

**Xem gợi ý**

Dùng `Input.GetAxis` (có smoothing) kết hợp `Rigidbody2D` với drag, hoặc tự làm mượt bằng `Vector2.Lerp` giữa vận tốc hiện tại và vận tốc mục tiêu. `GetAxisRaw` sẽ cho cảm giác dừng đột ngột, không hợp với băng trượt.

## Di chuyển bằng transform hay bằng Rigidbody2D?

Có hai cách, và chọn sai là nguyên nhân của phần lớn lỗi "nhân vật xuyên tường".

```csharp
// Cách 1: transform, đơn giản, không có physics
public class PlayerMove : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;

    void Update()
    {
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");
        Vector2 dir = new Vector2(x, y).normalized;
        transform.Translate(dir * moveSpeed * Time.deltaTime);
    }
}
```

```csharp
// Cách 2: Rigidbody2D, tôn trọng va chạm
public class PlayerMoveRb : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    private Rigidbody2D rb;
    private Vector2 input;

    void Awake() => rb = GetComponent<Rigidbody2D>();

    void Update()
    {
        input = new Vector2(Input.GetAxisRaw("Horizontal"),
                            Input.GetAxisRaw("Vertical")).normalized;
    }

    void FixedUpdate()
    {
        rb.MovePosition(rb.position + input * moveSpeed * Time.fixedDeltaTime);
    }
}
```

Quy tắc chọn:

- **Chưa cần va chạm** (test nhanh, camera bay, UI bay): `transform` trong `Update`.
- **Có va chạm, có physics** (nhân vật capstone của bạn): `Rigidbody2D`, và thao tác physics đặt trong `FixedUpdate`.

Lý do: `FixedUpdate` chạy theo nhịp cố định (mặc định 0.02 giây), đúng nhịp engine mô phỏng physics. Đặt `MovePosition` trong `Update` sẽ gây giật và đôi khi xuyên vật thể.

**Bài tập nhanh:** Script của bạn dùng `Rigidbody2D` nhưng nhân vật vẫn xuyên qua tường khi đi nhanh. Kể tên hai thứ cần kiểm tra.

**Xem đáp án**

1. `Collider2D` trên nhân vật và tường đã bật `Is Trigger` chưa (nếu bật thì không chặn được), và `Rigidbody2D` có để `Body Type = Dynamic` không.
2. Có đang set `transform.position` trực tiếp ở đâu đó không, hoặc `moveSpeed` quá cao khiến mỗi bước nhảy xa hơn bề dày tường (lúc đó cần dùng `Continuous` collision detection).

## Vì sao phải nhân với Time.deltaTime

Giả sử bạn viết `transform.Translate(dir * 5f)`. Ở máy chạy 60 FPS, mỗi giây bạn cộng 300 đơn vị. Ở máy 120 FPS, bạn cộng 600 đơn vị. Nhân vật chạy nhanh gấp đôi chỉ vì máy khỏe hơn. Đó là lỗi **frame-rate dependent**.

`Time.deltaTime` là số giây của frame vừa qua. Nhân vào, phép tính trở thành "5 đơn vị **mỗi giây**", không phải "5 đơn vị mỗi frame".

$$
v = \text{moveSpeed} \times \Delta t
$$

Trong `FixedUpdate`, dùng `Time.fixedDeltaTime` (hằng số 0.02) cho khớp nhịp, hoặc `Time.deltaTime` cũng chấp nhận được vì Unity trả về đúng bước cố định trong ngữ cảnh này. Đừng trộn lẫn hai hàm giữa hai chỗ.

## Đi chéo không được nhanh hơn: phép normalized

Nếu bạn cộng thẳng `x` và `y`, vector `(1, 1)` có độ dài √2 ≈ 1.41. Nghĩa là đi chéo nhanh hơn 41%. `.normalized` kéo vector về độ dài 1, giữ nguyên hướng. Đây là lý do dòng `new Vector2(x, y).normalized` xuất hiện trong hầu hết script di chuyển 2D.

> **Tip:** 
> Nhớ gọi `.normalized` **sau khi** ghép vector, không phải trên từng trục riêng lẻ. Nếu `x` và `y` đều bằng 0, `normalized` trả về `(0,0)`, nhân vật đứng yên, đúng như mong muốn.

## 

## Những lỗi hay gặp khi cho nhân vật di chuyển

- **Quên `Time.deltaTime`**: nhân vật chạy như tên lửa trên máy khỏe. Kiểm tra đầu tiên khi test trên nhiều thiết bị.
- **Gọi `GetComponent` trong `Update`**: tốn hiệu năng vô ích. Cache trong `Awake` hoặc `Start`.
- **Trộn `transform.position` với `Rigidbody2D`**: physics sẽ "đánh nhau" với bạn, gây rung và xuyên tường. Chọn một cách và đi theo nó.
- **Đặt `moveSpeed` quá lớn**: mỗi frame nhảy xa hơn bề dày collider, dẫn đến xuyên tường dù code đúng. Với game mobile, 4 đến 8 là khoảng hợp lý cho bắt đầu.
- **Dùng `Input.GetKey` khi cần "nhấn một lần"**: `GetKeyDown` chỉ đúng ở frame đầu tiên, phù hợp cho nhảy hoặc bắn.
- **Quên `[SerializeField]`**: biến `private` sẽ không hiện trong Inspector, bạn không tinh chỉnh được tốc độ khi chơi thử. Mẹo: vừa chạy game vừa kéo thanh `moveSpeed` để cảm nhận độ nhanh chậm.

## Ghép lại: checklist trước khi sang bài capstone

Bạn đã có đủ mảnh ghép để cho nhân vật chạy. Kiểm tra nhanh bằng cloze dưới đây:

Trong Unity, _____ trả về -1, 0 hoặc 1 mà không làm mượt giá trị.
Nhân tốc độ với _____ để chuyển động không phụ thuộc frame rate.
Di chuyển bằng Rigidbody2D nên đặt trong _____.
Gọi _____ trên vector input để đi chéo không nhanh hơn đi thẳng.
Khi cần phát hiện "vừa nhấn xuống", dùng _____ thay vì Input.GetKey.

Và đây là bộ thẻ để ôn lại:

Front: Input.GetAxis("Horizontal") trả về gì?
Back: Một float đã làm mượt, chạy từ -1 đến 1.

Front: GetAxisRaw khác GetAxis ở điểm nào?
Back: Không làm mượt, trả về đúng -1, 0 hoặc 1.

Front: Time.deltaTime là gì?
Back: Số giây đã trôi qua kể từ frame trước.

Front: Vì sao phải nhân tốc độ với Time.deltaTime?
Back: Để tốc độ tính theo giây, không phụ thuộc frame rate.

Front: Nên đặt di chuyển Rigidbody2D trong hàm nào?
Back: FixedUpdate, vì đây là nhịp cố định của physics.

Front: Time.fixedDeltaTime dùng ở đâu?
Back: Trong FixedUpdate, khi tính quãng đường di chuyển.

Front: Vector2.normalized làm gì?
Back: Giữ nguyên hướng và kéo độ dài vector về 1.

Front: Vì sao cần normalized khi đi chéo?
Back: Để đi chéo không nhanh hơn √2 lần so với đi thẳng.

Front: transform.Translate phù hợp khi nào?
Back: Khi cần di chuyển đơn giản, không cần physics hay va chạm.

Front: Rigidbody2D.MovePosition khác gì set transform.position?
Back: MovePosition tôn trọng physics và va chạm, set trực tiếp thì không.

Front: GetKeyDown khác GetKey thế nào?
Back: GetKeyDown chỉ true ở frame đầu tiên của lần nhấn.

Front: Hai trục input mặc định cho WASD và phím mũi tên?
Back: Horizontal và Vertical.

Front: Nên cache Rigidbody2D ở đâu?
Back: Trong Awake, để không gọi GetComponent mỗi frame.

**Tóm lại:** input chỉ là một vector hai chiều, việc của bạn là chuẩn hóa nó, nhân với tốc độ và với `Time.deltaTime`, rồi đưa vào đúng hàm (`Update` cho `transform`, `FixedUpdate` cho `Rigidbody2D`). Bài tiếp theo, bạn sẽ dùng chính chuyển động này để va vào vật thể, tính điểm và hoàn thiện prototype capstone.

---

## Contents

Part of [🕹️ Unity cho người mới: tự làm game 2D và đóng gói lên iOS, Android](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android.md)

- **🎮 Gameplay 2D cốt lõi trong Unity**
  - [🔁 Vòng lặp game và cấu trúc project Unity 2D](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/vong-lap-game-va-cau-truc-project-unity-2d.md)
  - [🕹️ Nhận input và di chuyển nhân vật](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/nhan-input-va-di-chuyen-nhan-vat.md) (sign-in required) — this lesson
  - [🧱 Capstone: dựng prototype với va chạm và tính điểm](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/capstone-prototype-gameplay-2d.md)
  - [✅ Checkpoint: gameplay 2D cốt lõi](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/checkpoint-gameplay-2d-cot-loi.md)
- **📦 Hoàn thiện và đóng gói game capstone**
  - [⚖️ Cân bằng độ khó và nhịp độ chơi](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/can-bang-do-kho-va-nhip-do-choi.md)
  - [🏁 Capstone: hoàn thiện vòng chơi, UI điểm và game over](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/capstone-hoan-thien-vong-choi-ui-diem-game-over.md) (premium content)
  - [📱 Build bản chơi thử trên Android và iOS](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/build-ban-choi-thu-android-ios.md)
  - [✅ Checkpoint: hoàn thiện và đóng gói](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/checkpoint-hoan-thien-va-dong-goi.md)
- [Course cheatsheet](https://everwise.study/courses/unity-cho-nguoi-moi-tu-lam-game-2d-va-ong-goi-len-ios-android/cheatsheet.md) (premium content)
