Chương 3: Kiến trúc tổng quan
Kiến trúc template được thiết kế xoay quanh một mục tiêu cốt lõi: Độ độc lập tuyệt đối giữa hạ tầng (Core) và sản phẩm (Game). Khi bạn làm game mới, bạn chỉ viết code vào thư mục Game/ và cấu hình CoreConfig.asset, giữ cho tầng Core/ nguyên vẹn như một package thư viện.
1. Hai tầng kiến trúc: Core Layer vs Game Layer
Hệ thống được chia thành 2 phân tầng tách biệt với ranh giới rõ ràng:
flowchart TD
subgraph GameLayer["TẦNG GAME (Assembly-CSharp) - Namespace: TapBrain.Game"]
direction TB
GEntry["GameEntry (Composition Root phía Game)"]
GApp["GameApp Facade (Wallet, Progress)"]
GMeta["Meta: GameWallet · LevelProgress"]
GPlay["Gameplay: GameplayController · LevelSession"]
GUI["UI: HomeScreen · MainScreen · ResultDialog"]
end
subgraph FacadeLayer["GIAO DIỆN TRUY CẬP (Stateless Facades)"]
direction LR
AppF["App.Ads · App.Iap · App.Analytics · App.Dialogs · App.Scenes..."]
SLoc["ServiceLocator (Register / Get / TryGet)"]
end
subgraph CoreLayer["TẦNG CORE (TapBrain.Core.asmdef) - Namespace: TapBrain.Core"]
direction TB
Boot["Bootstrapper (BeforeSceneLoad) + InitPipeline"]
Services["Services: Ads · IAP · Analytics · Firebase · RTDB · BI · Platform"]
Infra["Infra: SceneLoader · DialogService · UIOverlay · Audio · Storage"]
end
subgraph SDKLayer["SDKs & HỆ ĐIỀU HÀNH BÊN THỨ BA"]
direction LR
SDKs["AppLovin MAX · Unity IAP 5 · Firebase · GameAnalytics · AppMetrica · iOS/Android"]
end
GameLayer -->|"chỉ gọi qua"| AppF
AppF --> SLoc
SLoc --> CoreLayer
CoreLayer --> SDKLayer
style CoreLayer fill:#1e293b,stroke:#3b82f6,stroke-width:2px,color:#fff
style GameLayer fill:#0f172a,stroke:#10b981,stroke-width:2px,color:#fff
style FacadeLayer fill:#334155,stroke:#f59e0b,stroke-width:1px,color:#fff
style SDKLayer fill:#1e1e2e,stroke:#94a3b8,stroke-width:1px,color:#fff
Nguyên tắc vàng: R1 & R2
- Quy tắc R1: Toàn bộ code gameplay, logic game, coin, level, inventory và UI của game chỉ được nằm trong thư mục
Assets/_Project/Game/. Không bao giờ đưa class game vàoCore/. - Quy tắc R2: Core không bao giờ được biết Game. Ranh giới này được đảm bảo bằng file
TapBrain.Core.asmdef. Vì assembly của Core không tham chiếu tớiAssembly-CSharp, nếu bạn vô tình gõ bất kỳ class nào của game trong Core, compiler sẽ báo lỗi lập tức!
Nếu Core không biết Game, làm sao Core báo cho Game biết khi user mua hàng IAP thành công hoặc khi app pause?
Câu trả lời: Core giao tiếp thông qua Interface, Delegate và Event:
- Với IAP: Core cung cấp delegate public delegate bool PurchaseGrantHandler(string productId). Phía Game (trong GameEntry) đăng ký hàm cấp quà: App.Iap.SetGrantHandler(wallet.GrantProduct).
- Với Notification: Core phát event NotificationService.ScheduleRequested khi app pause để Game tự lên lịch nhắc nhở.
2. Bản đồ cấu trúc thư mục (Repository Map)
Dưới đây là cấu trúc chi tiết toàn bộ project trong Assets/_Project/:
Assets/_Project/
├── Core/ # TapBrain.Core.asmdef (Namespace: TapBrain.Core)
│ ├── Bootstrap/
│ │ ├── Bootstrapper.cs # Composition root của Core, tự kích hoạt BeforeSceneLoad
│ │ ├── InitPipeline.cs # Quản lý 10 bước khởi động có timeout
│ │ └── LoadingScreen.cs # Màn hình thanh tiến trình trong scene Loading
│ ├── Config/
│ │ └── CoreConfig.cs # ScriptableObject chứa mọi key và cấu hình module
│ ├── Infra/
│ │ ├── App.cs # Facade tĩnh gọi nhanh dịch vụ hàng ngày
│ │ ├── ServiceLocator.cs # Bảng đăng ký và truy xuất service
│ │ ├── CoreHost.cs # MonoBehaviour vĩnh viễn chạy coroutine cho class C# thuần
│ │ ├── CoreLog.cs # Ghi log chuẩn hóa có tag [Boot], [Ads], [IAP]...
│ │ ├── LocalStorage.cs # Bọc PlayerPrefs: hỗ trợ long, double, json, prefix
│ │ ├── SceneLoader.cs # Chuyển scene an toàn theo SceneId kèm fade màn hình
│ │ ├── UserIdentity.cs # Sinh và lưu giữ UserId dùng chung cho mọi SDK
│ │ ├── AudioService.cs # Quản lý nhạc nền (loop) và SFX (one-shot), tự mute khi xem ads
│ │ └── UI/
│ │ ├── DialogBase.cs # Lớp cơ sở cho mọi popup (hiệu ứng scale/fade, nút Back)
│ │ ├── DialogService.cs # Quản lý ngăn xếp popup (stack) trên canvas DontDestroyOnLoad
│ │ └── UIOverlay.cs # Canvas trên cùng: fade chuyển scene, blocker, toast
│ ├── Services/
│ │ ├── Ads/ # IAdsService, MaxAdsService, AdsPolicy (chống spam)
│ │ ├── Analytics/ # AnalyticsService (hub) + 3 sink (Firebase, GA, AppMetrica)
│ │ ├── Firebase/ # FirebaseService, RemoteConfigService, RealtimeDbService
│ │ ├── IAP/ # IIapService, IapService, PendingOrderStore (crash-safe)
│ │ ├── BI/ # BIService (đẩy user profile telemetry)
│ │ └── Platform/ # AttService, SearchAdsService, NotificationService,
│ │ # StoreServices (AppUpdate, Review), HapticService
│ ├── Plugins/iOS/ # Code native iOS (.m): ATT, SearchAds, Haptics, Review
│ └── Editor/ # TapBrain.Core.Editor.asmdef: CoreConfigTools, Build Guard
│
├── Game/ # KHÔNG asmdef (biên dịch vào Assembly-CSharp mặc định)
│ ├── GameEntry.cs # Composition root phía game (tự chạy AfterSceneLoad)
│ ├── GameApp.cs # Lối tắt dịch vụ game: GameApp.Wallet, GameApp.Progress
│ ├── GameKeys.cs # Tập trung toàn bộ key: storage, Remote Config, ads, IAP
│ ├── Meta/
│ │ ├── GameWallet.cs # Dịch vụ quản lý soft currency (Coin), lưu trữ tức thì
│ │ └── LevelProgress.cs # Dịch vụ quản lý số thứ tự màn chơi hiện tại
│ ├── Gameplay/
│ │ ├── GameplayController.cs # ĐIỂM BẮT ĐẦU GAMEPLAY tại scene Main
│ │ └── LevelSession.cs # Luật 1 lượt chơi: thưởng coin, đếm giờ, gửi analytics
│ └── UI/
│ ├── HomeScreen.cs # Giao diện màn Home (nút Play, cài đặt)
│ ├── MainScreen.cs # HUD trong màn chơi (hiển thị coin, level, sự kiện nút)
│ ├── ResultDialog.cs # Popup kết quả thắng/thua kèm nút xem ads x2 coin
│ ├── SettingsDialog.cs # Popup cài đặt âm thanh, nhạc nền, rung
│ └── DebugServicePanel.cs # Bảng test nhanh F1 (test ads, IAP, event ngay trong Editor)
│
├── Scenes/
│ ├── Loading.unity # Scene index 0 (chạy boot pipeline)
│ ├── Home.unity # Scene index 1 (sảnh chính)
│ └── Main.unity # Scene index 2 (scene chơi game)
│
└── Resources/
├── CoreConfig.asset # File cấu hình duy nhất của toàn bộ project
└── Dialogs/
├── ResultDialog.prefab # Prefab popup kết quả
└── SettingsDialog.prefab # Prefab popup cài đặt
3. Sơ đồ luồng gọi dịch vụ (Call Path Diagram)
Khi bạn viết một dòng code gameplay trong GameplayController, luồng thực thi đi qua các tầng như thế nào? Hãy quan sát sơ đồ dưới đây:
sequenceDiagram
autonumber
actor Player as Người chơi
participant GC as GameplayController
participant AppFacade as App Facade (Static)
participant Loc as ServiceLocator
participant MaxSvc as MaxAdsService (IAdsService)
participant Policy as AdsPolicy
participant SDK as AppLovin MAX SDK
Player->>GC: Bấm nút "x2 Coins"
GC->>AppFacade: App.Ads.ShowRewarded("double_reward", callback)
AppFacade->>Loc: ServiceLocator.Get()
Loc-->>AppFacade: Trả về instance MaxAdsService
AppFacade->>MaxSvc: ShowRewarded("double_reward", callback)
MaxSvc->>Policy: Kiểm tra tính khả dụng & trạng thái
MaxSvc->>SDK: MaxSdk.ShowRewardedAd(adUnitId, placement)
Note over SDK,Player: Người chơi xem video quảng cáo
SDK-->>MaxSvc: OnRewardedAdReceivedRewardEvent
MaxSvc-->>GC: callback(RewardedResult.Rewarded)
GC->>GC: GameApp.Wallet.Add(bonusCoins)
Các mắt xích trong chuỗi gọi:
- Code Game (Caller):
GameplayControllermuốn hiện quảng cáo nhận thưởng, gọiApp.Ads.ShowRewarded(...). - Facade (App.Ads): Lớp tĩnh cực mỏng, chỉ thực hiện lệnh chuyển tiếp:
ServiceLocator.Get<IAdsService>(). Nó không chứa biến, không chứa state. - ServiceLocator: Tra cứu trong từ điển
Dictionary<Type, object>đã được đăng ký từ lúc boot, trả về thể hiện đang chạy làMaxAdsService. - Triển khai dịch vụ (Service Implementation):
MaxAdsServicekiểm tra chính sách, tắt âm thanh game, rồi chuyển lệnh sang SDK gốc. - SDK bên thứ 3: AppLovin MAX hiển thị quảng cáo lên màn hình thiết bị và gửi callback trả về khi hoàn tất.
Giả sử studio quyết định chuyển toàn bộ quảng cáo từ AppLovin MAX sang Google AdMob Mediation:
- Bạn có cần phải sửa bất kỳ dòng code nào trong GameplayController.cs hay MainScreen.cs không?
- Bạn cần tạo class mới nào và đăng ký nó ở đâu trong thư mục Core/?