Dự án này là bản clone game 2048 viết bằng Unity + C#. Mình làm dự án này để luyện:
- tư duy tách lớp/kiến trúc trong Unity (MonoBehaviour, prefab, ScriptableObject)
- thuật toán dịch/ghép ô (merge) của 2048
- quản lý state game (new game / game over / score / best score)
- Unity:
2022.3.62f2(LTS)
(xemProjectSettings/ProjectVersion.txt) - Ngôn ngữ: C#
- UI: uGUI + TextMeshPro
- Render Pipeline: Universal Render Pipeline (URP)
- Package chính (xem
Packages/manifest.json):com.unity.render-pipelines.universal:14.0.12com.unity.textmeshpro:3.0.7com.unity.ugui:1.0.0com.unity.test-framework:1.1.33- IDE integration:
com.unity.ide.rider(3.0.36),com.unity.ide.visualstudio(2.0.22)
- Mở bằng Unity Hub → Open trỏ vào thư mục project này.
- Mở scene chính:
Assets/Scenes/2048.unity - Nhấn Play để chạy.
- W / ↑: đi lên
- S / ↓: đi xuống
- A / ←: sang trái
- D / →: sang phải
Scene build đã được cấu hình trong ProjectSettings/EditorBuildSettings.asset:
Assets/Scenes/2048.unity
Vào File → Build Settings… → chọn platform → Build.
Assets/Scenes/2048.unity: scene chínhAssets/Prefabs/Tiles.prefab: prefab tile (ô số)Assets/Tiles/: cácTileState(ScriptableObject) cho màu sắc theo sốAssets/Scipts/: code chính (lưu ý thư mục đang đặt tên Scipts)GameManager.cs: vòng đời game, score, best score, game over UITileBoard.cs: input + thuật toán move/merge + spawn tile + check game overTileGrid.cs,TileRow.cs,TileCell.cs: lưới 4x4, tọa độ, ô trống/kínTile.cs: state của tile, animation move/mergeTileState.cs: ScriptableObject màu nền/màu chữ
GameManager.Start()gọiNewGame()NewGame():- reset score
- đọc best score từ
PlayerPrefs - ẩn UI game over (
CanvasGroup) - gọi
board.ClearBoard() - tạo 2 tile đầu (
board.CreateTile()x2) - bật
board.enabled = true
- Khi thua,
GameManager.GameOver()sẽ:- disable
TileBoard - bật tương tác game over
- fade-in bằng coroutine
- disable
TileGrid: biết tất cả cell, gán Coordinates, tìm cell kề và cell trống ngẫu nhiênTileBoard: chứa danh sách_tiles, xử lý input mỗi frame, quyết định thứ tự quét để move đúng, merge, spawn tile mới, check game overTile: biết cell hiện tại, number/state, tự animate move/merge, và self-destroy khi mergeTileState(ScriptableObject): data-driven theme màu (tách data khỏi code)
Khi người chơi bấm 1 hướng:
- mỗi tile “trượt” xa nhất có thể theo hướng đó
- nếu gặp tile cùng số và tile đó chưa merge trong lượt này → merge
- sau khi có thay đổi → spawn 1 tile mới (mặc định số 2)
- kiểm tra thua: full grid và không còn merge được theo 4 hướng
- Thứ tự quét (scan order) theo hướng:
MoveTiles(direction, startX, incrementX, startY, incrementY)- đi lên: quét từ hàng trên xuống để tránh kéo “đè” sai
- đi xuống: quét từ hàng dưới lên
- trái/phải: tương tự với cột
- Với mỗi cell đang có tile →
MoveTile(tile, direction):- duyệt cell kề theo hướng cho tới khi:
- gặp ô trống: ghi nhớ
newCell(ô trống xa nhất) - gặp ô có tile:
- nếu
CanMerge(a,b)(cùng số và!b.Locked) →Merge(a,b)rồi kết thúc - nếu không merge được → dừng
- nếu
- gặp ô trống: ghi nhớ
- nếu có
newCell→ move tile sang đó
- duyệt cell kề theo hướng cho tới khi:
Khi merge a vào b, tile b được set Locked = true để tránh trường hợp:
2 2 4 khi đi trái mà tile 4 mới tạo lại merge tiếp trong cùng lượt.
Sau khi kết thúc lượt (đợi animation ~0.12s), coroutine WaitForChanges() sẽ:
- mở lock cho toàn bộ tile (
Locked = false) - nếu còn ô trống → spawn tile mới
- kiểm tra game over
Với grid 4x4, mỗi tile di chuyển tối đa 3 bước nên thực tế rất nhỏ. Tổng quát:
- quét (N \times N) ô, mỗi ô đi tối đa (N) bước → khoảng (O(N^3)) theo cách duyệt tuyến tính, nhưng với N=4 thì coi như hằng số; tối ưu thêm có thể làm theo “compress + merge” theo từng hàng/cột để (O(N^2)).
- MonoBehaviour lifecycle:
Awake,Start,Update - Prefab workflow:
Instantiate(tilePrefab, parent),Destroy(gameObject) - Coroutine: animation move/merge (
Tile.Animate), fade UI game over (GameManager.Fade), delay giữa lượt (TileBoard.WaitForChanges) - UI:
CanvasGroupđể fade + block inputTextMeshProUGUIđể hiển thị score/best score/numberImageđổi màu nền tile theoTileState
- ScriptableObject:
TileStateđể lưu màu nền/màu chữ theo từng mức số (data-driven)
- Lưu dữ liệu:
PlayerPrefslưuBestScore
- Input:
Input.GetKeyDowncho WASD/Arrow (dễ mở rộng sang swipe/mobile)
- “Em implement logic merge 2048 như nào để không merge 2 lần?” → dùng scan order + Locked
- “Tại sao scan order quan trọng?” → tránh case kéo/merge sai vì update theo thứ tự
- “Tách class như nào cho dễ maintain?” →
GameManager(state),TileBoard(rule),Grid/Cell(data structure),Tile(view/animation),TileState(config) - “Lưu best score làm sao?” →
PlayerPrefs - “Animation trong Unity?” → coroutine + Lerp/SmoothStep
- spawn ngẫu nhiên 2 hoặc 4 (vd 90% là 2, 10% là 4)
- hỗ trợ swipe (mobile) và hold để lặp hướng
- thêm Undo/Replay (lưu stack state grid)
- lưu/restore toàn bộ board (không chỉ best score)
- viết Unit Test cho thuật toán merge (tách logic thuần C# khỏi MonoBehaviour)
- polish UX: scale pop khi merge, âm thanh, rung nhẹ, highlight move hợp lệ
- Repo này đang có thư mục
Library/,Temp/,Logs/…; bình thường nên để.gitignoreloại ra (project đã có.gitignoremẫu Unity/Rider).