Chương 2: Kiến thức nền cần học
Nếu bạn mới tốt nghiệp hoặc có khoảng 6 tháng làm quen với Unity, bạn thường chỉ quen viết code mọi thứ vào trong hàm Update() của MonoBehaviour. Chương này sẽ trang bị cho bạn 5 nhóm kiến thức sống còn mà template sử dụng. Với mỗi mục, chúng tôi giải thích: Tại sao template cần, Kiến thức tối thiểu phải nắm, Ví dụ trích từ chính codebase này, và Tài liệu chính thống để đọc thêm.
Phần 1: Ngôn ngữ C# hiện đại
1.1. Class vs Struct vs Readonly Struct
Tại sao template cần: Trong game mobile, việc cấp phát đối tượng liên tục trên Heap (bộ nhớ heap) sẽ khiến bộ thu gom rác (Garbage Collector - GC) phải hoạt động thường xuyên, gây giật khung hình (frame drop). Template dùng readonly struct cho các gói dữ liệu thuần túy (data transfer object).
Kiến thức tối thiểu:
class: Kiểu tham chiếu (Reference Type), nằm trên Heap, được quản lý bởi GC. Thích hợp cho các đối tượng có trạng thái thay đổi theo thời gian và có danh tính (Service, Controller).struct: Kiểu tham trị (Value Type), thường nằm trên Stack hoặc nội tuyến trong đối tượng chứa nó, không gây áp lực cho GC.readonly struct: Struct bất biến (immutable), mọi field đều làreadonly. C# tối ưu không tạo bản copy phòng thủ khi truyền dữ liệu.
Ví dụ trong codebase:
// File: Assets/_Project/Game/Gameplay/LevelSession.cs
public readonly struct LevelResult
{
public readonly int Level;
public readonly LevelOutcome Outcome;
public readonly int Coins;
public LevelResult(int level, LevelOutcome outcome, int coins)
{
Level = level;
Outcome = outcome;
Coins = coins;
}
public bool Won => Outcome == LevelOutcome.Won;
}
Link học tập:
1.2. Interface (Giao diện)
Tại sao template cần: Giúp Core che giấu chi tiết triển khai cụ thể của SDK bên thứ ba. Nhờ có IAdsService, code game chỉ biết gọi hàm hiển thị quảng cáo mà không cần biết bên dưới đang dùng AppLovin MAX hay Google AdMob hay một mạng nào khác.
Kiến thức tối thiểu: Interface là một bản hợp đồng định nghĩa các method, property, event mà class kế thừa bắt buộc phải thực hiện. Không chứa biến và code logic.
Ví dụ trong codebase:
// File: Assets/_Project/Core/Services/Ads/IAdsService.cs
public interface IAdsService
{
bool IsInitialized { get; }
bool IsRewardedReady { get; }
bool IsInterstitialReady { get; }
event Action Initialized;
void ShowBanner();
void HideBanner();
bool TryShowInterstitial(string placement, Action onClosed = null);
void ShowRewarded(string placement, Action onResult);
}
// MaxAdsService.cs triển khai interface này:
public sealed class MaxAdsService : IAdsService { ... }
Tương tự, IAnalyticsSink được triển khai bởi 3 sink khác nhau: FirebaseAnalyticsSink, GameAnalyticsSink, và AppMetricaSink.
Link học tập:
1.3. Delegates, Action<T>, Lambdas và Closures
Tại sao template cần: Mọi thao tác mobile (chờ xem hết video quảng cáo, chờ người chơi xác nhận thanh toán IAP, chờ mạng trả lời) đều là bất đồng bộ (asynchronous). Ta dùng delegate / callback để nhận kết quả khi tác vụ hoàn thành.
Kiến thức tối thiểu:
Action: Đại diện cho hàm không có tham số và không trả về giá trị (void).Action<T>: Đại diện cho hàm nhận 1 tham số kiểuT.Lambda (=>): Cú pháp viết hàm ẩn danh nhanh chóng:result => { ... }.Closure: Khả năng của lambda "bắt giữ" (capture) biến cục bộ bên ngoài. Lưu ý: Closure có thể tạo ra heap allocation nhỏ, không nên lạm dụng trong Update().
Ví dụ trong codebase:
// File: Assets/_Project/Game/Gameplay/GameplayController.cs
App.Ads.ShowRewarded(GameKeys.PlacementDoubleReward, adResult =>
{
// Callback chạy bất đồng bộ khi user đóng quảng cáo
if (adResult == RewardedResult.Rewarded)
{
_wallet.Add(result.Coins);
App.Overlay.Toast($"+{result.Coins} bonus coins");
}
});
Link học tập:
1.4. Events (+=, -=) và hiểm họa Memory Leaks
Tại sao template cần: Dùng để thông báo thay đổi trạng thái: coin thay đổi, popup đóng, scene load xong. Tuy nhiên, nếu không hủy đăng ký (unsubscribe), game sẽ bị rò rỉ bộ nhớ nghiêm trọng!
Kiến thức tối thiểu:
Service như GameWallet sống suốt vòng đời app. Trong khi đó, component MainScreen hay GameplayController gắn trên scene chỉ sống trong scene Main. Khi đổi sang scene khác, nếu bạn không dùng -=, GameWallet vẫn giữ tham chiếu tới hàm của controller cũ trong bộ nhớ. Lần sau coin thay đổi, nó sẽ gọi vào một đối tượng đã chết → Crash hoặc ném ngoại lệ MissingReferenceException!
Ví dụ trong codebase:
// File: Assets/_Project/Game/Gameplay/GameplayController.cs
private void Start()
{
_wallet = GameApp.Wallet;
// BẮT BUỘC: Đăng ký lắng nghe sự kiện
_wallet.Changed += screen.SetCoins;
screen.BackClicked += GoHome;
}
private void OnDestroy()
{
// BẮT BUỘC: Luôn gỡ đăng ký khi MonoBehaviour bị tiêu hủy!
if (_wallet != null) _wallet.Changed -= screen.SetCoins;
if (screen != null) screen.BackClicked -= GoHome;
}
Link học tập:
1.5. Generics
Tại sao template cần: Cho phép lấy Service hoặc mở Dialog với đúng kiểu dữ liệu (type-safe) mà không cần ép kiểu (SettingsDialog)dialog.
Ví dụ trong codebase:
// File: Assets/_Project/Core/Infra/UI/DialogService.cs
public T Show() where T : DialogBase
{
var prefab = LoadPrefab();
var dialog = Object.Instantiate(prefab, _root, false);
dialog.Open();
return dialog;
}
// Gọi mở popup cực kỳ gọn gàng và chuẩn kiểu:
SettingsDialog dlg = App.Dialogs.Show();
Link học tập:
1.6. Static Classes & Properties (Lớp tĩnh & Lối tắt App Facade)
Tại sao template cần: Thay vì tạo Singleton có state, template dùng các lớp tĩnh App và GameApp làm "cổng ngắm" (facade) dẫn tới ServiceLocator.
Ví dụ trong codebase:
// File: Assets/_Project/Core/Infra/App.cs
public static class App
{
// Không chứa biến, chỉ là property chuyển tiếp (forwarder)
public static IAdsService Ads => ServiceLocator.Get();
public static IIapService Iap => ServiceLocator.Get();
public static AnalyticsService Analytics => ServiceLocator.Get();
}
Link học tập:
1.7. Enums, Nullable, Switch, Const vs Readonly, Namespaces
- Enums: Định nghĩa tập giá trị trạng thái rõ ràng như
RewardedResult(Rewarded, Skipped, NotReady, Failed). - Nullable Types (
T?): Cho phép struct có giá trịnull. Ví dụ hàmLevelSession.Finish()trả vềLevelResult?: nếu màn chơi đã kết thúc trước đó rồi thì trả vềnull. - Const vs Readonly:
constxác định hằng số lúc biên dịch (compile-time, ví dụGameKeys.Coins = "game.coins");readonlyxác định lúc chạy (runtime, ví dụprivate readonly CoreHost _host;). - Namespaces: Ngăn ngừa trùng tên class. Mọi thứ trong Core nằm trong
TapBrain.CorevàTapBrain.Core.Services; game code nằm trongTapBrain.Game.
Link học tập:
- Enumeration types (Microsoft Docs)
- Nullable value types (Microsoft Docs)
- const keyword (Microsoft Docs)
- Namespaces (Microsoft Docs)
Phần 2: Kiến trúc Unity Engine chuyên sâu
2.1. Vòng đời MonoBehaviour (Lifecycle)
Thứ tự chạy quen thuộc: Awake() → OnEnable() → Start() → Update() → OnDisable() → OnDestroy().
Quy tắc trong template:
Awake(): Chỉ khởi tạo biến nội bộ của component (ví dụGetComponent<CanvasGroup>()trongDialogBase).Start(): Đăng ký event với các service bên ngoài (_wallet.Changed += ...) và bắt đầu logic.OnDestroy(): Bắt buộc hủy đăng ký event để tránh rò rỉ bộ nhớ.
Link học tập:
2.2. RuntimeInitializeOnLoadMethod & DontDestroyOnLoad
Tại sao template cần: Để loại bỏ hoàn toàn các prefab khởi động đặt trong scene!
Thuộc tính [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] bảo với Unity rằng: Hãy thực thi hàm này ngay khi game khởi động, trước khi bất kỳ Scene nào được nạp. Nhờ đó, Bootstrapper.Boot() tạo ra GameObject [Core] mang DontDestroyOnLoad chứa toàn bộ services. Bạn có thể mở trực tiếp scene Main.unity và ấn Play để test gameplay mà không bị lỗi thiếu service!
Link học tập:
- RuntimeInitializeOnLoadMethodAttribute (Unity ScriptReference)
- Object.DontDestroyOnLoad (Unity ScriptReference)
2.3. Assembly Definitions (asmdef) - Tại sao Core có mà Game thì không?
Trong template, bạn sẽ thấy file TapBrain.Core.asmdef tại thư mục Assets/_Project/Core/. Trong khi đó thư mục Game/ không có file asmdef nào.
- Core có asmdef: Để ép buộc Core là một thư viện độc lập. Nếu ai đó vô tình gọi class của Game bên trong Core, Unity compiler sẽ báo lỗi ngay lập tức!
- Game KHÔNG có asmdef: Code của game sẽ tự động được biên dịch vào assembly mặc định là
Assembly-CSharp. Quyết định thiết kế này giúp các bạn mới có thể thoải mái import các asset/plugin từ Unity Asset Store (vốn thường không có asmdef) vào thư mục Game mà không bao giờ bị lỗi biên dịch không tìm thấy class!
Link học tập:
2.4. ScriptableObject (CoreConfig) & Thư mục Resources
CoreConfig là một ScriptableObject nằm tại Assets/_Project/Resources/CoreConfig.asset. Toàn bộ key SDK, ID quảng cáo, cờ bật tắt module được lưu tập trung tại đây. Khi khởi động, Bootstrapper chỉ việc gọi Resources.Load<CoreConfig>("CoreConfig") là có ngay dữ liệu mà không cần phụ thuộc scene.
Link học tập:
2.5. Coroutines và Quy tắc Stop Condition
Coroutine dùng để chạy tác vụ qua nhiều frame (như đếm thời gian tải, fade màn hình, gửi request HTTP). Trong template, class C# thuần túy (không phải MonoBehaviour) có thể chạy Coroutine thông qua CoreHost.Run(routine).
Mọi coroutine trong template đều phải ghi rõ điều kiện dừng (Stop Condition) trong comment và trong code. Tuyệt đối không dùng while(true) mà không có lối thoát (break hoặc timeout), vì coroutine đó sẽ chạy vĩnh viễn trong background làm hao pin thiết bị.
Link học tập:
2.6. uGUI, CanvasScaler, TextMeshPro, PlayerPrefs
- CanvasScaler: Cấu hình
Scale With Screen Size, độ phân giải chuẩn1080 x 1920, tỷ lệMatch = 0.5để UI co giãn đẹp mắt trên cả iPhone tai thỏ lẫn tablet iPad. - TextMeshPro (TMP): Render font bằng công nghệ Signed Distance Field (SDF), chữ cực nét ở mọi độ phân giải.
- LocalStorage (bọc PlayerPrefs): Hỗ trợ lưu trữ type-safe các kiểu
int,float,string,long,doublevà cảJSON.
Link học tập:
Phần 3: Design Patterns thực chiến
| Pattern | Ý nghĩa trong Template | Vị trí trong Code | Tài liệu đọc thêm |
|---|---|---|---|
| Composition Root | Nơi duy nhất khởi tạo, cấu hình và kết nối toàn bộ hệ thống dependencies. | Bootstrapper.cs (cho Core) và GameEntry.cs (cho Game). |
Game Programming Patterns |
| Service Locator | Nơi lưu trữ và tra cứu dịch vụ tập trung. Thay thế cho việc dùng hàng chục Singleton rải rác. | ServiceLocator.Register<T>() và ServiceLocator.Get<T>(). |
Service Locator Pattern |
| Facade Pattern | Cung cấp một giao diện truy cập ngắn gọn, thuận tiện cho các dịch vụ thường dùng hàng ngày. | App.Ads, App.Iap, App.Dialogs, GameApp.Wallet. |
Refactoring Guru: Facade |
| Observer Pattern | Tách rời bên phát tín hiệu và bên nhận. Thay vì gọi trực tiếp nhau, các bên giao tiếp qua Event. | GameWallet.Changed, InitPipeline.Completed. |
Observer Pattern |
| Dependency Inversion | Module cấp cao không phụ thuộc module cấp thấp; cả hai phụ thuộc vào Interface trừu tượng. | Game gọi IAdsService, không gọi trực tiếp SDK MaxSdk. |
Design Patterns (Refactoring Guru) |
| Separation of View & Logic | Tách biệt hoàn toàn giữa giao diện (View) và luật chơi (Logic). | MainScreen (View) ↔ GameplayController (Điều phối) ↔ LevelSession (Luật thuần C#). |
Game Programming Patterns |
1. Gây dính chặt phụ thuộc (Tight Coupling): Khi bạn gõ SoundManager.Instance.Play(), code của bạn đã bị dính chặt vào class cụ thể đó, không thể viết Unit Test và không thể đổi bản triển khai khác.
2. Khó kiểm soát vòng đời: Ai là người tạo ra nó? Khi nào nó bị hủy? Nếu bấm Play ở scene khác, liệu Instance có bị null không?
3. Dễ gây rò rỉ bộ nhớ: GameObject mang singleton sống vĩnh viễn, vô tình kéo theo các texture, clip âm thanh cũ không thể giải phóng.
Phần 4: Kiến thức ngành Game Mobile (Mobile Business)
4.1. Quảng cáo Mobile & Ad Mediation
- Định dạng quảng cáo:
- Banner: Dải quảng cáo nhỏ cố định ở đáy hoặc đỉnh màn hình.
- Interstitial (Quảng cáo xen kẽ): Quảng cáo toàn màn hình xuất hiện ngắt quãng giữa các màn chơi (sau khi thắng/thua). Có thể bấm bỏ qua (skip) sau 5s.
- Rewarded (Quảng cáo nhận thưởng): Người chơi chủ động bấm xem để nhận quà (x2 coin, hồi sinh). Chỉ được cấp thưởng khi user xem hết (
RewardedResult.Rewarded). - App Open: Quảng cáo hiển thị khi mở app hoặc quay lại app từ màn hình home.
- Mediation (Hòa giải quảng cáo): Thay vì tích hợp riêng lẻ từng mạng quảng cáo (Google AdMob, Unity Ads, Mintegral, Liftoff, v.v.), template sử dụng AppLovin MAX làm nền tảng trung gian. MAX sẽ tổ chức đấu giá (bidding) và phân phối quảng cáo của mạng trả tiền cao nhất.
- eCPM (effective Cost Per Mille): Số tiền studio kiếm được trên mỗi 1.000 lượt xem quảng cáo.
- ILRD (Impression-Level Revenue Data): Dữ liệu doanh thu chi tiết đến từng lượt xem, được MAX gửi tự động sang GameAnalytics và AppMetrica để đo chỉ số LTV/ROAS.
- Frequency Capping: Quy định giới hạn tần suất quảng cáo để không gây khó chịu cho người chơi (được quản lý bởi
AdsPolicy: khoảng cách tối thiểu giữa 2 quảng cáointer_interval_sec = 30s, không hiện tronginter_first_delay_sec = 60sđầu).
Link học tập:
4.2. In-App Purchase (IAP) & Xử lý giao dịch an toàn
- Consumable (Tiêu hao): Các vật phẩm mua được nhiều lần (gói Coin, Gem, mạng chơi).
- Non-consumable (Vĩnh viễn): Các gói mua một lần dùng mãi mãi (như gói Remove Ads, mở khóa full game). Bắt buộc phải có tính năng Restore Purchases trên iOS theo quy định của Apple.
- Receipt Validation (Xác thực biên lai): Kiểm tra biên lai ký điện tử từ Apple App Store hoặc Google Play Store để chống việc dùng công cụ hack/cheat IAP ảo. Template sử dụng Receipt Validation Obfuscator của Unity IAP.
- Pending / Deferred Purchases: Các đơn hàng chưa thể trừ tiền ngay (ví dụ tài khoản trẻ em chờ phụ huynh phê duyệt qua Ask-to-Buy). Template xử lý trạng thái này bằng
PurchaseResult.Pending.
Link học tập:
4.3. Analytics, Funnels & Remote Config
- Analytics Hub: Template gửi sự kiện đồng thời tới:
- Firebase Analytics: Chuẩn GA4 cho sự kiện hành vi và phân tích retention D1/D7.
- GameAnalytics: Đo đạc luồng tiến trình game (progression: start, complete, fail) và doanh thu quảng cáo ILRD.
- AppMetrica: Đo lường sự kiện chi tiết và phân bổ người dùng khu vực châu Âu/CIS.
- Funnel (Phễu chuyển đổi): Tập hợp các sự kiện theo trình tự:
main_open→level_start→level_completeđể phát hiện màn chơi nào quá khó khiến người chơi bỏ game. - Remote Config: Cho phép PM điều chỉnh các biến số game (ví dụ: thưởng bao nhiêu coin, thời gian giãn cách quảng cáo) trực tiếp trên Firebase Console mà không cần lập trình viên phải build và update app lên Store.
Link học tập:
4.4. Quyền riêng tư: ATT, IDFA & GDPR
- ATT (App Tracking Transparency): Quy định bắt buộc của Apple từ iOS 14.5. Ứng dụng phải hiện popup xin phép người dùng trước khi được quyền lấy mã định danh quảng cáo
IDFA. Template đặt bước xin quyền ATT tại bước 1 của Boot flow. - GDPR / Consent Flow: Quy định bảo vệ quyền riêng tư người dùng tại Liên minh Châu Âu (EU), được cấu hình qua AppLovin Integration Manager.
Link học tập:
Phần 5: Công cụ Build & Quản lý phụ thuộc
- EDM4U (External Dependency Manager for Unity): Công cụ tự động giải quyết xung đột thư viện Android (AAR/Gradle dependencies) và iOS (CocoaPods). Khi gặp lỗi build Android liên quan tới thư viện, chỉ cần chạy menu
Assets > External Dependency Manager > Android Resolver > Force Resolve. - Gradle Templates: File
mainTemplate.gradlevàgradleTemplate.propertiestrongAssets/Plugins/Android/được EDM quản lý tự động. Không sửa tay các dependency vào các file này! - Xcode & CocoaPods: Khi build iOS, Unity xuất ra Xcode project. Thư viện CocoaPods sẽ tự động chạy lệnh
pod installđể tích hợp Firebase và MAX. - Git Branching: Khi clone template để làm game mới, hãy tạo một branch mới hoàn toàn. Tuyệt đối không commit các file nhạy cảm như keystore hay private key lên Git.
Link học tập:
Hãy trả lời 3 câu hỏi sau mà không nhìn lại tài liệu:
1. Tại sao component trong scene không bao giờ được quên -= khi lắng nghe event từ GameWallet?
2. Tại sao tầng Core có file TapBrain.Core.asmdef nhưng tầng Game thì không?
3. Sự khác nhau giữa lệnh TryShowInterstitial và ShowRewarded trong IAdsService là gì?