Vòng lặp game và cấu trúc project Unity 2D
Vì sao hiểu vòng lặp game lại là việc đầu tiên
Đây là bài mở đầu của khóa, và cũng là bài đặt nền cho toàn bộ project capstone: một game 2D hoàn chỉnh mà bạn sẽ dựng dần qua từng bài, từ di chuyển, va chạm, tính điểm cho tới khi build lên Android và iOS. Mọi thứ bạn thêm vào sau này (nhân vật chạy, coin biến mất, điểm tăng, màn hình game over) đều chỉ là một mắt nhỏ trong cùng một vòng lặp: mỗi frame, Unity hỏi "có input mới không, cập nhật gì, vẽ lại gì?". Hiểu nhịp chạy đó thì bạn sẽ biết đặt code ở đâu và đặt lúc nào.
Sau bài này, bạn cần làm được:
- Giải thích được vòng lặp game gồm những bước nào và lặp liên tục ra sao.
- Phân biệt
Update,FixedUpdate,LateUpdatevà biết việc gì nên đặt ở hàm nào. - Dùng
Time.deltaTimeđể tốc độ di chuyển không phụ thuộc frame rate. - Tạo được project Unity 2D với cấu trúc folder, scene và quy ước đặt tên rõ ràng.
Một frame trong Unity diễn ra thế nào
Vòng lặp game (game loop) là chu trình chạy lặp đi lặp lại suốt thời gian game đang bật. Unity giấu phần lớn chi tiết bên trong engine, nhưng bạn có thể hình dung nó như sau:
flowchart TD
A["📥<br/>Nhận input"] --> B["🧠<br/>Cập nhật logic<br/>Update"]
B --> C["⚙️<br/>Cập nhật physics<br/>FixedUpdate"]
C --> D["🎥<br/>Camera và UI<br/>LateUpdate"]
D --> E["🖼️<br/>Render khung hình"]
E --> A
Mỗi vòng như vậy tạo ra một frame. Số frame trong một giây (frame rate, fps) không cố định: máy yếu chạy 30 fps, máy mạnh chạy 120 fps. Đây là lý do quan trọng nhất khiến bạn phải cẩn thận với thời gian, và cũng là lý do bài này có phần deltaTime.
Note: Đừng bao giờ giả định "mỗi frame là 1/60 giây". Đó là giả định đúng trên máy của bạn và sai trên máy người chơi.
Update, FixedUpdate và vòng đời của một script
Một script trong Unity không chạy từ trên xuống một lần rồi thôi. Nó được engine gọi qua các hàm vòng đời (lifecycle callbacks) theo đúng thứ tự dưới đây:
| Hàm | Khi nào chạy | Dùng cho việc gì |
|---|---|---|
Awake |
Một lần, trước Start, kể cả khi script đang bị disable |
Cache reference, khởi tạo dữ liệu nội bộ |
OnEnable |
Mỗi lần object được bật | Đăng ký event |
Start |
Một lần, ngay trước frame đầu tiên có Update |
Đọc giá trị của object khác đã Awake xong |
FixedUpdate |
Theo timestep cố định, mặc định 0.02 giây (50 lần/giây) | Di chuyển Rigidbody2D, lực, va chạm |
Update |
Mỗi frame, đúng một lần | Đọc input, logic game, đếm thời gian |
LateUpdate |
Mỗi frame, sau tất cả Update |
Camera follow, cập nhật UI theo vị trí nhân vật |
OnDestroy |
Khi object bị huỷ | Huỷ đăng ký, dọn dẹp |
Quy tắc dễ nhớ: input và logic trong Update, physics trong FixedUpdate, camera trong LateUpdate. Nếu đặt code di chuyển Rigidbody2D vào Update, nhân vật sẽ rung khi frame rate dao động, vì physics chỉ đồng bộ ở timestep cố định.
Bài tập nhanh 1. Bạn viết code làm coin xoay trong Update và code đẩy nhân vật bằng Rigidbody2D.AddForce trong Update. Chỗ nào sai?
Gợi ý: xoay coin bằng transform trong Update thì ổn, nhưng AddForce phải nằm trong FixedUpdate.
deltaTime: cách để tốc độ không phụ thuộc frame rate
Time.deltaTime là thời gian (tính bằng giây) của frame vừa kết thúc. Nếu bạn muốn nhân vật đi 5 unit mỗi giây, đừng cộng 5 vào vị trí mỗi frame, hãy nhân với Time.deltaTime:
void Update()
{
// 5 unit/giây, bất kể máy chạy 30 hay 120 fps
transform.position += Vector3.right * 5f * Time.deltaTime;
}
Với frame dài 0.1 giây, nhân vật dịch 0.5 unit; với frame 0.016 giây, nhân vật dịch 0.08 unit. Cộng lại trong một giây, cả hai đều ra 5 unit. Đó chính là "độc lập frame rate".
Vài chi tiết đi kèm:
- Trong
FixedUpdate, dùngTime.fixedDeltaTime(hoặc để Unity tự lo cho physics), không nhândeltaTime. Time.timeScalelàm chậm hoặc dừng game vàTime.deltaTimecũng bị ảnh hưởng theo. Menu pause muốn chạy thật thì dùngTime.unscaledDeltaTime.- Đừng nhân
deltaTimehai lần trong cùng một phép tính, đó là lỗi rất hay gặp khiến nhân vật bò rất chậm.
Bài tập nhanh 2. Đồng hồ đếm ngược của màn chơi nên trừ thời gian trong Update bằng Time.deltaTime hay Time.fixedDeltaTime? Vì sao?
Gợi ý: dùng Time.deltaTime trong Update, vì đồng hồ là logic game, không phải physics.
Dựng cấu trúc project 2D gọn gàng
Một project lộn xộn sẽ làm bạn mất thời gian đúng lúc cần nhất: lúc debug. Ngay từ đầu, hãy chọn template 2D khi tạo project trong Unity Hub, rồi giữ cấu trúc folder như sau trong thư mục Assets:
Assets/
Scenes/
Game.unity
Scripts/
Player/
PlayerController.cs
Core/
GameManager.cs
Prefabs/
Player.prefab
Coin.prefab
Art/
Sprites/
Audio/
Ba quy ước nên theo ngay từ frame đầu tiên:
- Mỗi loại asset một folder.
Scenes,Scripts,Prefabs,Art,Audio. Sau này thêmUIhaySettingscũng theo đúng mẫu đó. - Prefab cho object lặp lại. Nhân vật, coin, chướng ngại vật đều nên là prefab, vì bạn sẽ spawn chúng nhiều lần và muốn sửa một chỗ áp dụng cho tất cả.
- Chỉ thao tác file bên trong Unity. Mỗi asset đi kèm một file
.metagiữ thông tin liên kết. Di chuyển hay đổi tên file bằng File Explorer bên ngoài sẽ làm đứt liên kết đó, và bạn sẽ thấy các ôMissingtrong Inspector. Dùng cửa sổ Project của Unity là an toàn.
Bài tập nhanh 3. Bạn tạo một sprite đồng xu mới. Nó nên nằm ở đâu, và khi nào thì tạo thêm prefab từ nó?
Gợi ý: ảnh gốc để trong Art/Sprites/, còn prefab Coin.prefab trong Prefabs/ khi bạn cần gắn script, collider và spawn nó nhiều lần trong scene.
Bốn lỗi làm hỏng vòng lặp và cấu trúc project
- Quên
Time.deltaTime. Triệu chứng: game chạy nhanh trên máy mạnh, chậm trên máy yếu. Đây là lỗi số một của người mới. - Nhân
deltaTimehai lần. Triệu chứng: nhân vật di chuyển chậm bất thường, kiểu 5 unit/giây thành 0.08 unit/giây. - Đặt physics vào
Update. Triệu chứng: nhân vật rung, đôi khi xuyên tường khi frame rate tụt. Chuyển sangFixedUpdate. - Đổi tên hoặc di chuyển asset ngoài Unity. Triệu chứng: hàng loạt reference thành
Missing. Sửa được nhưng rất mất thời gian, nên tránh từ đầu.
Một mẹo nhỏ: khi cần kiểm tra tốc độ, bật chế độ xem fps trong Game view và thử chạy game ở độ phân giải khác. Nếu cảm giác di chuyển vẫn như cũ, tức là bạn đã dùng deltaTime đúng.
Chốt lại và chuẩn bị cho bài di chuyển nhân vật
Bạn vừa nắm được nhịp tim của mọi game Unity: vòng lặp frame, thứ tự các hàm vòng đời, và cách dùng Time.deltaTime để mọi thứ chạy đúng trên mọi thiết bị. Cộng thêm một cấu trúc project gọn gàng, bạn đã có nền đủ chắc để bắt đầu viết gameplay thật.
Điền nhanh để tự kiểm tra:
Unity gọi hàm _____ mỗi frame, còn physics được cập nhật trong _____. Muốn chuyển động độc lập frame rate, ta nhân tốc độ với _____. Fixed timestep mặc định của Unity là _____ giây, tức 50 lần mỗi giây. Camera follow nên đặt trong _____ để chạy sau khi nhân vật đã di chuyển. Mỗi asset trong Unity đi kèm một file _____ giữ thông tin liên kết.
Bài tiếp theo sẽ dùng chính Update và deltaTime vừa học để đọc input và cho nhân vật di chuyển, bước đầu tiên của gameplay trong project capstone.
Front: Vòng lặp game là gì? Back: Chu trình lặp liên tục: nhận input, cập nhật trạng thái, render, rồi lặp lại.
Front: Hàm Update chạy khi nào? Back: Mỗi frame, đúng một lần.
Front: FixedUpdate khác Update ở điểm nào? Back: FixedUpdate chạy theo timestep cố định và là nơi xử lý physics như Rigidbody2D.
Front: Time.deltaTime là gì? Back: Thời gian tính bằng giây của frame vừa kết thúc.
Front: Vì sao phải nhân tốc độ với Time.deltaTime? Back: Để tốc độ di chuyển không phụ thuộc frame rate của thiết bị.
Front: Thứ tự giữa Awake và Start? Back: Awake chạy trước Start; Awake vẫn chạy khi script bị disable, Start thì không.
Front: Hàm nào phù hợp để camera follow nhân vật? Back: LateUpdate, vì nó chạy sau khi mọi Update đã cập nhật vị trí.
Front: Time.timeScale ảnh hưởng đến điều gì? Back: Làm chậm hoặc dừng thời gian game và kéo theo Time.deltaTime; Time.unscaledDeltaTime thì không bị ảnh hưởng.
Front: Fixed timestep mặc định của Unity là bao nhiêu? Back: 0.02 giây, tức 50 lần mỗi giây.
Front: Unity lưu metadata của asset ở đâu? Back: Trong file .meta đi kèm mỗi asset.
Front: Vì sao không nên đổi tên hay di chuyển asset ngoài Unity? Back: File .meta không được cập nhật, các reference trong scene và prefab sẽ thành Missing.
Front: Prefab dùng để làm gì? Back: Làm template tái sử dụng cho object lặp lại như player, coin, chướng ngại vật.
Front: Nhân vật speed 5 unit/giây, một frame dài 0.1 giây thì dịch được bao nhiêu? Back: 0.5 unit, vì 5 nhân 0.1.
Front: Code đọc input nên đặt ở hàm nào? Back: Update, cùng với phần lớn logic game.