Hướng dẫn làm game Unity: 5 chặng, đúng một project
Hướng dẫn làm game Unity cho người mới bắt đầu?
Làm một game hành động 3D — Knight, Mage và Archer trong một sân đấu — qua năm chặng, từ project trống tới file APK trên điện thoại, không đòi biết code từ trước. Mỗi chặng kết bằng một thứ bấm được và thử được, nên project đi tới chỗ xong thay vì làm lại từ đầu.
Đây là toàn bộ lộ trình: một đầu là project Unity trống, đầu kia là một game hành động 3D — Knight, Mage và Archer đánh nhau trong một sân đấu, cài ngay trên chiếc điện thoại trong túi bạn. Năm chặng, đúng một project, và không đòi bạn phải biết code từ trước. Mỗi chặng kết bằng một thứ nhìn thấy và bấm được, nên không lúc nào bạn phải tự đoán liệu cái mình làm có chạy thật không.
Lý do game đầu tiên không bao giờ xong hiếm khi là độ khó. Thường là vì project trên màn hình không giống cái bạn muốn, nên bạn làm lại — bằng engine khác, với ý tưởng lớn hơn, ở trang ba của một chuỗi tutorial mà chưa ai đi tới cuối. Một project làm xong dạy nhiều hơn mười project bỏ giữa đường, vì một lý do rất bình thường: chỉ project xong hẳn mới kéo bạn qua cái đuôi phiền phức — build settings, cái độ phân giải chưa ai thử, khung hình rớt xuống 30 khi ba kẻ địch cùng xuất hiện.
60 giây chuẩn bị
- 1
Unity Hub > Installs > Install Unity, chọn bản Unity 6 LTS. Trong danh sách module thì tick Android Build Support và Microsoft Visual Studio Community: Hub cài luôn editor kèm IDE, nên không còn bước cấu hình Visual Studio riêng để mà sai.
- 2
New project, mẫu Universal 3D, đặt trên ổ SSD, tên ngắn và không có dấu cách. Xin module Android ngay từ đây thay vì năm chặng sau mới mở — thêm module muộn nghĩa là cài lại, và chặng 5 là chỗ rất tệ để phát hiện mình đã quên.
- 3
Đăng nhập bằng Unity ID miễn phí khi editor hỏi. Thứ được kích hoạt là licence Personal, và nó đủ cho cả năm chặng — không phải mua gì, không phải cài gì thêm. Chưa đăng nhập thì Unity không mở, nên đây là bước ba chứ không phải dòng chú thích.
Bản đồ: 5 chặng, vẫn một cái scene
Mỗi chặng là một tab của khoá học, và tên bên dưới link thẳng tới tab đó kèm danh sách bài. Số bài và số phút lấy trực tiếp từ khoá học nên không thể nào cũ ở đây.
Chặng 1 · Căn bản — 11 bài, 100 phút
Một cái sàn đã có vật liệu, và một lưới khối do code sinh ra chứ không phải do chuột đặt — nhờ vậy sau này đổi thành lưới khác cũng được.
Ba nhân vật — Knight, Mage, Archer — đổi bằng phím 1, 2, 3, thêm màn hình intro có nhạc nạp vào sân đấu.
Chặng 2 · Nhân vật — 9 bài, 114 phút
Knight đã có hình hài: CharacterController, trọng lực, cú nhảy đáp đúng lên các khối của chặng 1, và animation đi kèm đúng việc nó đang làm.
Ba skill ở phím Q/E/R có hồi chiêu, đạn lấy lại từ pool thay vì sinh mới, và kẻ địch biết đuổi, biết trúng đòn, biết chết.
Chặng 3 · Camera — 6 bài, 67 phút
Camera góc thứ ba xoay quanh mà không xuyên qua tường sân, và rung lên khi có đòn trúng — sau khi bạn tự tay viết một lần camera bám theo, nên biết package đang làm gì hộ mình.
Một input map nuôi cả bàn phím, tay cầm lẫn cần gạt trên màn hình: vẫn trận đánh đó, chơi bằng một ngón cái.
Chặng 4 · Thế giới — 7 bài, 83 phút
Cái hộp xám thành một nơi thật: địa hình, đèn bake cùng skybox và sương mù, đạo cụ ghép từ bộ module 1×1 m, tiếng chân và một mixer — và frame rate vẫn giữ nguyên trong lúc làm.
Bản demo thành game: từng đợt địch từ bốn điểm sinh, một panel thắng, một panel thua, và nút chơi lại mà không cần tắt app.
Chặng 5 · Điện thoại — 7 bài, 83 phút
Player Settings kiểu đem đi phát hành được: package name, IL2CPP và ARM64, một keystore, và file APK đưa cho ai là người đó cài được luôn.
File sống sót sau khi tắt app, URP chỉnh cho điện thoại chứ không phải cho editor, và 60 FPS trên chính cái máy đang cầm.
Chặng 1: sàn, khối và ba nhân vật
Chặng 1 cố tình nhỏ và không phải khởi động: nó là sân đấu mà phần còn lại của khoá học diễn ra trên đó. Kết chặng là một cái sàn, một lưới khối do hai vòng lặp sinh ra, ba nhân vật bạn đi lại được, và màn hình intro có nhạc nạp vào trận đấu. Tất cả những thứ đó tồn tại trước khi bất cứ thứ gì trông đẹp.
Thứ tự có chủ đích: bốn bài C# ngắn, rồi tới Editor, rồi mới tới scene. Không phải để dạy lập trình — mà để một script Unity đọc được. Biến chứa gì, vì sao Update chạy mỗi khung hình, một class thành cái gì khi bạn đã thấy nó nằm trong Inspector, và game đầu cần bao nhiêu if, for, List — ít hơn bạn sợ. Sau đó tới năm cửa sổ bạn thật sự sống trong đó. Và chỉ khi đó một object nằm trên sàn. C# trước Unity, Unity trước game, mỗi bước tốn một buổi chứ không một tháng.
using UnityEngine;
public class ArenaBuilder : MonoBehaviour
{
public GameObject block;
public Vector2Int size = new Vector2Int(6, 6);
public float spacing = 2f;
void Start()
{
for (int x = 0; x < size.x; x++)
{
for (int z = 0; z < size.y; z++)
{
Vector3 pos = transform.position + new Vector3(x * spacing, 0f, z * spacing);
// Tham số thứ tư biến block thành object con của object này.
Instantiate(block, pos, Quaternion.identity, transform);
}
}
}
}Ba mươi sáu khối từ một vòng lặp đặt trong một vòng lặp khác. Đó là lý do phải sinh ra thay vì đặt: tới chặng 4 cần sân rộng hơn thì lưới đổi bằng một con số, và không ai phải kéo lại từng cái cube.
Chặng 2, 3 và 4: cú đánh đầu tiên, camera, và một nơi để đánh
Chặng 2: những cơ chế tự trả tiền cho chính nó
Nhân vật có CharacterController, trọng lực và cú nhảy đáp thật sự lên các khối của chặng 1; rồi máy trạng thái Animator lấy đôi chân khỏi script, nên idle, run, jump, attack đến từ một blend tree cùng các transition chạy bằng đúng những biến mà code di chuyển đã ghi sẵn. Bài đáng gọi tên to lên là skill: mỗi skill là một asset ScriptableObject chứa cooldown, damage và projectile của nó.
using UnityEngine;
[CreateAssetMenu(menuName = "Game/Skill")]
public class SkillData : ScriptableObject
{
public string skillName;
public float cooldown = 1f;
public int damage = 10;
public GameObject projectile;
}
// Caster giữ một cổng cho mỗi ô; asset chỉ cung cấp con số.
public class SkillCaster : MonoBehaviour
{
public SkillData[] skills;
float[] nextReadyAt;
void Awake() => nextReadyAt = new float[skills.Length];
public bool TryCast(int slot)
{
if (Time.time < nextReadyAt[slot]) return false;
nextReadyAt[slot] = Time.time + skills[slot].cooldown;
return true;
}
}Chính cái tách đó trả tiền về sau. Con số người ta muốn chỉnh nằm trong Inspector chứ không nằm trong class chứa luôn logic bắn đạn, nên cân bằng lại Slash không phá được thứ bắn ra nó. Skill thứ tư là một asset mới trong folder cộng một phần tử vào mảng đã có — đó là lý do một giờ với ScriptableObject đáng giá hơn một tuần toàn if-chain.
Chặng 3: phần mà người ta hay bỏ qua
Chặng 3 quyết định có ai khác ngoài bạn chơi được nó không. Bạn tự tay viết camera bám theo một lần — Lerp tới một offset trong LateUpdate — rồi mới làm quen Cinemachine, nên biết chính xác virtual camera và brain của nó đang làm gì hộ mình, và nên đổ lỗi cho đâu khi tầm nhìn xuyên qua tường. Sau đó một action map của Input System thay cho mọi chỗ kiểm phím trong dự án: một action Move có tên, được nuôi bởi bàn phím, cần gạt tay cầm và một joystick ảo trên Canvas. Cái binding cuối cùng chính là khoảng cách giữa một project Unity và một game trên điện thoại.
Chặng 4: từ demo thành game
Chặng 4 làm hai việc trên đúng cái Arena.unity. Nó mặc đồ cho sân — địa hình, đèn mặt trời bake cùng skybox và sương mù, đạo cụ từ bộ module 1×1 m và prefab variant, tiếng chân và một mixer — và giữ cảnh đã mặc đồ chạy nhanh bằng LOD group, batching và occlusion, vì sân đẹp mà 20 FPS thì không chơi được. Rồi GameDirector biến nó thành game: từng đợt địch từ bốn điểm sinh, panel thắng, panel thua, và nút chơi lại. Không có gì ở đó phải nghĩ từ số không; đó là các hệ thống của chặng 1 tới 3 nối với nhau, và cái nối đó chính là điều người ta gọi bằng vòng lặp game.
Sát thương đi qua một event trên CharacterStats thay vì tham chiếu tới kẻ vừa bị đánh, nên thanh máu, số sát thương và chớp trắng cùng đăng ký trên đúng một sự kiện. Đó là pattern mà phần còn lại của chặng tựa vào.
Kẻ địch đi trên NavMesh đã bake thay vì code đuổi theo viết tay. Đó là cách đáng để dành thêm mười lăm phút: một lộ trình bạn nhìn thấy trong Scene view, và bake lại khi chặng 4 dời mấy hòn đá.
Chặng 5: lên điện thoại thật
Chặng cuối là vài tiếng chỉnh cấu hình và một khoảnh khắc trả tiền cho cả khoá: game chạy trên một thiết bị không phải máy tính của bạn. Chuyển build target sang Android, khai báo danh sách scene, đặt package name, chọn IL2CPP và ARM64, rồi bấm Build. Chỉ vậy là bạn đã có file APK cài qua USB hoặc gửi cho ai đó, họ cài rồi trả lại máy. Còn upload lên Google Play cần AAB, tạo bằng cách tick một ô trong đúng cửa sổ đó.
- 1
Chuyển nền tảng khiến Unity nhập lại mọi texture sang định dạng Android đọc được, nên hãy bấm rồi đi pha cà phê. Sau đó mở và bấm Add Open Scenes: scene không có trong danh sách đó không tồn tại trong game, và đây là lý do số một khiến bản build mở ra màn hình đen.
- 2
Bản debug tự ký nên thử thì không cần keystore; bản release đã ký mới là thứ Google Play nhận. Giữ kỹ file keystore và mật khẩu của nó — để mất là mất luôn định danh của app, chứ không phải mất một cửa sổ cấu hình.
- 3
Khi nó hành xử lạ trên máy thật, adb logcat là cuộc trò chuyện mà điện thoại đang nói. Một lệnh đã lọc, và lý do game tự đóng hiện ra ngay trên màn hình — bài build có sẵn lệnh đó cùng chuyện IL2CPP cắt code gây ra phần lớn vụ văng khi khởi động.
- 4
Frame rate là con boss cuối: Application.targetFrameRate không tự động bằng 60 trên mọi điện thoại, và FPS trong editor chẳng nói gì về máy thật. Cách sửa là những việc nhàm ở bài hiệu năng — texture nén, ít draw call hơn, LOD cho đạo cụ, URP thay vì pipeline dựng sẵn — đo trên chính cái điện thoại, chứ không phải đo gần nó.
Năm sai lầm khiến game đầu tiên không bao giờ xong
Phạm vi. Ý tưởng đầu tiên bao giờ cũng quá lớn, vì đó là thứ bạn biết rõ nhất. Một game sinh tồn thế giới mở cần cả trăm hệ thống; một sân đấu với ba nhân vật và một vòng lặp đợt địch cần cỡ mười, và mười cái đó là mười cái mọi game đều cần. Cắt tới khi bản kế hoạch khiến bạn hơi ngượng — cỡ đó là vừa.
Đổi engine giữa dự án. Cỏ không xanh hơn ở bên kia, nó chỉ là một bộ vấn đề khác. Engine nào cũng có một bức tường vào khoảng tháng thứ hai, và chuyển đi không chở bạn qua nó — nó chỉ reset lại phép đếm. Đang cầm thứ gì thì hãy xong một thứ trong đó đã.
Không bao giờ phát hành. Bản build không ai chơi giữ mọi quyết định còn bỏ ngỏ, nên bạn không phải trả lời cái nào sai. Phát hành mới là phép thử duy nhất tính: một người không phải bạn cầm máy và bối rối khi bạn không đứng bên. Rất khó chịu, và chính cái khó chịu đó là toàn bộ sản phẩm.
Chờ art. Giai đoạn hộp xám không phải phần xấu để bỏ qua, nó là phần game phải hay. Một phòng toàn capsule và cube mà không chơi được thì nhét nhân vật đẹp vào cũng vẫn không chơi được, chỉ tốn thêm tiền sửa. Làm cho cú nhảy của cube đúng cảm giác trước, rồi thả model vào mà không đụng code.
Học vòng tròn. Hướng dẫn thứ chín về CharacterController cho cảm giác tiến bộ nhưng chẳng được gì, vì điều thứ mười bạn chưa biết chỉ hiện ra khi bạn vấp điều thứ chín. Đọc tài liệu là học thuộc; làm cho một capsule bước qua khối mà không rơi xuyên sàn là làm kỹ thuật. Hãy làm cái sau, và tra cứu lúc dự án đòi.
Thế nào là xong
- ✓Game mở ở menu, chơi một đợt, và thua được — rồi chơi lại bằng một nút, không phải bằng cách tắt app.
- ✓Bản build nằm trên điện thoại còn editor đã tắt: game chạy mà không cắm vào máy tính nào cả.
- ✓Một người chưa từng thấy dự án chơi hai phút mà không hỏi bạn bấm gì.
- ✓Nó giữ 60 FPS trên đúng cái máy đó, hoặc bạn chỉ được chỗ nó rớt và vì sao.
- ✓Tắt app rồi mở lại là tiến trình trở về — file save sống sót, và cái setting bạn đã đổi cũng vậy.
- ✓Nói được trong một câu thứ vẫn đang thiếu. Câu đó là thứ đầu tiên bạn sửa ở project sau — và đó chính là mục đích: project sau bắt đầu nhanh hơn vì project này đã xong.
Bước kế tiếp chỉ dài một trang: mở tab đầu tiên, Căn bản, và bắt đầu từ bài thứ nhất. Nó giả định bạn chưa từng viết code, và kết thúc bằng một cái sàn trong scene cùng ba nhân vật đang đứng trên đó.
bài học làm ra thứ này
C# trong 60 giây: biến
Bài này là một công thức đứng riêng. Trong khoá học, cùng thứ đó được dựng như một phần của dự án xuyên suốt năm tab — Căn bản, bài 01.