# KẾ HOẠCH TRIỂN KHAI CHI TIẾT: HỆ THỐNG BẪY (TRAP SYSTEM) **Dự án:** BABA_YAGA **Công nghệ:** Unity, Photon Fusion (Shared/Host mode), Opsive Ultimate Character Controller (UCC) **Tác giả:** Antigravity AI Pair Programmer **Ngày lập:** 30/06/2026 --- ## 📌 1. KIẾN TRÚC TỔNG QUAN (ARCHITECTURE OVERVIEW) Hệ thống bẫy được thiết kế theo mô hình **Server-Authoritative (Host-authoritative)** kết hợp với **Client-side Prediction/Visualization**. Điều này đảm bảo tính bảo mật (kẻ địch không thể cheat vị trí bẫy hoặc tự ý gỡ bẫy) và trải nghiệm mượt mà của người chơi. ```mermaid graph TD Trapper[Trapper Client] -->|1. Di chuyển & Ngắm| Ghost[Ghost Trap Preview - Local] Trapper -->|2. Click Đặt Bẫy| RPC[RPC_RequestPlaceTrap] RPC -->|3. Nhận yêu cầu| Host[Host / Server] Host -->|4. Kiểm tra hợp lệ| Spawn[Runner.Spawn TrapPrefab] Spawn -->|5. Đồng bộ hóa| AllClients[All Clients Spawned] AllClients -->|Trapper| ShowFull[Hiển thị rõ + Outline] AllClients -->|Seeker| HideStealth[Ẩn bẫy/Dùng Stealth Shader] ``` ### Các lớp đối tượng cốt lõi (Core Classes) | Tên File | Loại | Vai trò chính | | :--- | :--- | :--- | | `TrapBase.cs` | `NetworkBehaviour` | Lớp cha trừu tượng quản lý vòng đời, trạng thái mạng, va chạm và kích hoạt bẫy. | | `TrapPlacementController.cs` | `MonoBehaviour` | Xử lý logic hiển thị bóng mờ (Ghost Mesh) và gửi RPC đặt bẫy từ Trapper. | | `TrapVisibilityHandler.cs` | `MonoBehaviour` | Xử lý việc ẩn/hiện bẫy đối với từng Role (`Seeker` ẩn, `Trapper` hiện). | | `TrapDataSO.cs` | `ScriptableObject` | Lưu trữ cấu hình bẫy (Thời gian hồi, sát thương, tầm quét, prefab, icon UI). | --- ## 🛠️ 2. CHI TIẾT CÁC GIAI ĐOẠN TRIỂN KHAI --- ### GIAI ĐOẠN 1: XÂY DỰNG CORE ARCHITECTURE & MẠNG (PHOTON FUSION) *Mục tiêu: Đảm bảo bẫy được sinh ra đồng bộ trên tất cả các Client và có cơ chế quản lý trạng thái mạng.* #### 1. Định nghĩa enum trạng thái bẫy (`TrapState`) ```csharp public enum TrapState { Arming, // Đang thiết lập (chờ kích hoạt để tránh nổ ngay khi đặt) Armed, // Đã sẵn sàng hoạt động (đang quét kẻ địch) Triggered, // Đã bị dẫm trúng và đang chạy hiệu ứng Despawning // Đang bị hủy / dọn dẹp } ``` #### 2. Xây dựng lớp Base `TrapBase.cs` Lớp này sẽ kế thừa từ `NetworkBehaviour` của Fusion. ```csharp using Fusion; using UnityEngine; [RequireComponent(typeof(NetworkObject))] public abstract class TrapBase : NetworkBehaviour { [Header("Trap Config")] [SerializeField] protected TrapDataSO trapData; [Networked] public PlayerRef Owner { get; set; } [Networked, ChangedOnFields(nameof(OnStateChanged))] public TrapState State { get; set; } [Networked] protected TickTimer ArmingTimer { get; set; } [Networked] protected TickTimer LifetimeTimer { get; set; } protected Collider trapCollider; protected Animator animator; protected AudioSource audioSource; public override void Spawned() { trapCollider = GetComponent(); animator = GetComponentInChildren(); audioSource = GetComponent(); if (Object.HasStateAuthority) { State = TrapState.Arming; ArmingTimer = TickTimer.CreateFromSeconds(Runner, trapData.ArmingDelay); LifetimeTimer = TickTimer.CreateFromSeconds(Runner, trapData.Lifetime); } // Tự động cấu hình hiển thị (Tàng hình với Seeker, Hiện với Trapper) ConfigureVisibility(); } public override void FixedUpdateNetwork() { if (!Object.HasStateAuthority) return; // Xử lý đếm ngược Arming if (State == TrapState.Arming && ArmingTimer.Expired(Runner)) { State = TrapState.Armed; } // Tự hủy nếu hết hạn tồn tại if (LifetimeTimer.Expired(Runner)) { DespawnTrap(); } } protected virtual void OnTriggerEnter(Collider other) { if (!Object.HasStateAuthority) return; if (State != TrapState.Armed) return; // Kiểm tra xem đối tượng chạm vào có phải là Seeker không if (IsTargetValid(other.gameObject)) { TriggerTrap(other.gameObject); } } protected bool IsTargetValid(GameObject target) { // Tích hợp kiểm tra Role của đối tượng va chạm (không tự kích hoạt bẫy của chính mình/đồng đội) var networkInfo = target.GetComponent(); if (networkInfo != null) { // Kiểm tra PlayerData của target var playerData = target.GetComponent(); if (playerData != null && playerData.PlayerRole == _Role.Seeker) { return true; } } return false; } protected void TriggerTrap(GameObject victim) { State = TrapState.Triggered; OnTriggered(victim); } // Các hàm ảo để các bẫy cụ thể ghi đè (Override) protected abstract void OnTriggered(GameObject victim); protected static void OnStateChanged(Changed changed) { changed.Behaviour.HandleStateVisuals(); } protected virtual void HandleStateVisuals() { switch (State) { case TrapState.Arming: // Play đặt bẫy SFX/VFX break; case TrapState.Armed: // Bật đèn tín hiệu/nháy nhẹ (chỉ Trapper thấy rõ) break; case TrapState.Triggered: if (animator != null) animator.SetTrigger("Trigger"); if (audioSource != null && trapData.TriggerSFX != null) audioSource.PlayOneShot(trapData.TriggerSFX); // Sinh VFX vụ nổ/sập bẫy break; } } protected void DespawnTrap() { State = TrapState.Despawning; Runner.Despawn(Object); } protected abstract void ConfigureVisibility(); } ``` --- ### GIAI ĐOẠN 2: HỆ THỐNG ĐẶT BẪY (PLACEMENT MECHANICS) *Mục tiêu: Người chơi Trapper có thể rê chuột chọn vị trí (Ghost Preview) và click để Spawn bẫy qua RPC mạng.* #### 1. ScriptableObject cấu hình bẫy (`TrapDataSO.cs`) ```csharp using UnityEngine; [CreateAssetMenu(fileName = "NewTrapData", menuName = "BabaYaga/TrapData")] public class TrapDataSO : ScriptableObject { public string TrapName; public TrapType Type; public NetworkPrefabRef TrapPrefab; public GameObject GhostPrefab; // Mô hình mờ để xem trước public float Cooldown = 5f; public float ArmingDelay = 1.5f; public float Lifetime = 60f; public float PlacementMaxDistance = 5f; [Header("Visuals & Audio")] public Sprite Icon; public AudioClip PlaceSFX; public AudioClip TriggerSFX; public GameObject TriggerVFX; } ``` #### 2. Xử lý đặt bẫy `TrapPlacementController.cs` Script này gắn trên nhân vật người chơi (chỉ kích hoạt với `Trapper` có Input Authority). ```csharp using Fusion; using UnityEngine; public class TrapPlacementController : NetworkBehaviour { [SerializeField] private TrapDataSO[] availableTraps; [SerializeField] private LayerMask placementLayerMask; private int selectedTrapIndex = 0; private GameObject currentGhostInstance; private bool isPreviewing = false; private float lastPlaceTime; void Update() { if (!Object.HasInputAuthority) return; // Nhấn nút để kích hoạt Preview (ví dụ: Phím Q hoặc số 1,2,3) if (Input.GetKeyDown(KeyCode.Q)) { TogglePreview(); } if (isPreviewing) { UpdateGhostPosition(); if (Input.GetMouseButtonDown(0)) { TryPlaceTrap(); } } } void TogglePreview() { isPreviewing = !isPreviewing; if (isPreviewing) { if (currentGhostInstance == null) { currentGhostInstance = Instantiate(availableTraps[selectedTrapIndex].GhostPrefab); } } else { if (currentGhostInstance != null) { Destroy(currentGhostInstance); } } } void UpdateGhostPosition() { Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); // Raycast từ tâm màn hình if (Physics.Raycast(ray, out RaycastHit hit, availableTraps[selectedTrapIndex].PlacementMaxDistance, placementLayerMask)) { currentGhostInstance.SetActive(true); currentGhostInstance.transform.position = hit.point; // Xoay bẫy theo mặt phẳng tiếp xúc (Normal của bề mặt) currentGhostInstance.transform.rotation = Quaternion.FromToRotation(Vector3.up, hit.normal); // Kiểm tra xem vị trí có hợp lệ không (ví dụ: không đè tường, không quá dốc) bool isValid = ValidatePlacement(hit.point); UpdateGhostVisuals(isValid); } else { currentGhostInstance.SetActive(false); // Ẩn nếu chỉ vào khoảng không } } bool ValidatePlacement(Vector3 position) { // Check overlap sphere để đảm bảo không đè lên bẫy khác hoặc tường Collider[] colliders = Physics.OverlapSphere(position, 0.5f, LayerMask.GetMask("Walls", "Trap")); return colliders.Length == 0; } void UpdateGhostVisuals(bool isValid) { // Thay đổi màu Material của Ghost (Xanh = Đặt được, Đỏ = Không đặt được) Renderer[] renderers = currentGhostInstance.GetComponentsInChildren(); Color color = isValid ? new Color(0f, 1f, 0f, 0.4f) : new Color(1f, 0f, 0f, 0.4f); foreach (var r in renderers) { r.material.color = color; } } void TryPlaceTrap() { if (Time.time - lastPlaceTime < availableTraps[selectedTrapIndex].Cooldown) { Debug.Log("Trap is on Cooldown!"); return; } Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); if (Physics.Raycast(ray, out RaycastHit hit, availableTraps[selectedTrapIndex].PlacementMaxDistance, placementLayerMask)) { if (ValidatePlacement(hit.point)) { // Gửi RPC yêu cầu Server spawn bẫy thực sự RPC_RequestPlaceTrap(hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal), selectedTrapIndex); lastPlaceTime = Time.time; TogglePreview(); // Tắt preview sau khi đặt } } } [Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)] private void RPC_RequestPlaceTrap(Vector3 position, Quaternion rotation, int trapIndex) { if (trapIndex < 0 || trapIndex >= availableTraps.Length) return; TrapDataSO data = availableTraps[trapIndex]; // Host sinh bẫy trên mạng NetworkObject spawnedTrap = Runner.Spawn(data.TrapPrefab, position, rotation, Object.InputAuthority); // Thiết lập thuộc tính bẫy TrapBase trapScript = spawnedTrap.GetComponent(); if (trapScript != null) { trapScript.Owner = Object.InputAuthority; } } } ``` --- ### GIAI ĐOẠN 3: CƠ CHẾ TẦM NHÌN & KHÔNG GIAN (STEALTH MECHANICS) *Mục tiêu: Giữ bẫy tàng hình trước phe Seeker nhưng đồng đội/Trapper vẫn nhìn thấy.* #### Giải pháp Kỹ thuật: Layer & Camera Culling + Shader 1. **Thiết lập Layers**: - Tạo Layer: `TrapperOnly` (Ví dụ Layer 10). - Tạo Layer: `SeekerOnly` (Ví dụ Layer 11). - Tạo Layer: `StealthTrap` (Ví dụ Layer 12). 2. **Camera Culling**: - Camera của `Trapper` sẽ render cả Layer `Default` và `StealthTrap` (để nhìn thấy bẫy của mình). - Camera của `Seeker` mặc định **sẽ bỏ tích (Cull)** Layer `StealthTrap` trong thuộc tính *Culling Mask*. Điều này giúp bẫy tàng hình tuyệt đối trên màn hình của Seeker mà không cần ẩn GameObject (giúp Collider vẫn hoạt động vật lý). 3. **Cơ chế Reveal (Bị phát hiện)**: - Khi Seeker ở gần bẫy trong bán kính 3m, hoặc dùng kỹ năng dò quét, script cục bộ trên Seeker sẽ chuyển đổi Layer của bẫy từ `StealthTrap` sang `Default` hoặc bật Shader quét hồng ngoại để làm hiển thị bóng mờ bẫy. ```csharp // Trong script TrapBase.cs hoặc script bổ trợ TrapVisibilityHandler.cs: protected override void ConfigureVisibility() { var localPlayer = Runner.LocalPlayer; PlayerDataManager pdm = PlayerDataManager.Instance; if (pdm != null && pdm.TryGetPlayerMetaData(localPlayer, out var meta)) { if (meta.Role == _Role.Trapper) { // Trapper nhìn thấy rõ gameObject.layer = LayerMask.NameToLayer("Default"); SetTrapAlpha(0.8f); // Hơi trong suốt để nhận biết là bẫy } else { // Seeker: Đưa vào layer ẩn gameObject.layer = LayerMask.NameToLayer("StealthTrap"); } } } private void SetTrapAlpha(float alpha) { Renderer[] renderers = GetComponentsInChildren(); foreach (var r in renderers) { foreach (var mat in r.materials) { // Đảm bảo Shader hỗ trợ chế độ trong suốt (Transparent) Color col = mat.color; col.a = alpha; mat.color = col; } } } ``` --- ### GIAI ĐOẠN 4: LIÊN KẾT VỚI OPSIVE UCC (INTEGRATION WITH OPSIVE UCC) *Mục tiêu: Bẫy tác động trực tiếp vào các chỉ số Máu (Health) và Khống chế (State/Abilities) của Opsive Character.* Opsive UCC có các lớp built-in rất mạnh mẽ để xử lý sát thương và hiệu ứng: #### 1. Gây sát thương lên người chơi (Apply Damage) ```csharp using Opsive.Shared.Inventory; using Opsive.UltimateCharacterController.Traits; // Namespace chứa Health protected void ApplyDamageToUCC(GameObject target, float damageAmount) { Health victimHealth = target.GetComponent(); if (victimHealth != null && victimHealth.IsAlive()) { // Gây sát thương thông qua API của Opsive victimHealth.Damage(damageAmount, transform.position, Vector3.zero, 0); } } ``` #### 2. Trói chân / Khống chế di chuyển (Root / Stun Mechanism) Có 2 cách tích hợp khống chế với UCC: * **Cách A (Đơn giản):** Can thiệp trực tiếp vào CharacterLocomotion tốc độ di chuyển. * **Cách B (Khuyên Dùng):** Kích hoạt/Tắt một custom Ability trong UCC (ví dụ: Ability `Stun` hoặc `Frozen` được cấu hình sẵn trong component *UltimateCharacterLocomotion*). ```csharp using Opsive.UltimateCharacterController.Character; using Opsive.UltimateCharacterController.Character.Abilities; protected void ApplyStunToUCC(GameObject target, float duration) { UltimateCharacterLocomotion locomotion = target.GetComponent(); if (locomotion != null) { // Tìm ability "Stun" hoặc "Frozen" đã được thiết lập sẵn trong Opsive Editor Ability stunAbility = locomotion.GetAbility(); // Cần tạo class StunAbility kế thừa từ Ability if (stunAbility != null) { locomotion.TryStartAbility(stunAbility); // Lên lịch tắt ability sau duration giây StartCoroutine(RemoveStunCoroutine(locomotion, stunAbility, duration)); } } } private System.Collections.IEnumerator RemoveStunCoroutine(UltimateCharacterLocomotion loco, Ability ability, float delay) { yield return new WaitForSeconds(delay); if (loco != null && ability.IsActive) { loco.TryStopAbility(ability); } } ``` --- ### GIAI ĐOẠN 5: HIỆN THỰC HÓA CÁC LOẠI BẪY CỤ THỂ (CONCRETE TRAPS) Dưới đây là thiết kế chi tiết cho 4 bẫy quan trọng nhất để bắt đầu: #### 1. Bear Trap (Bẫy kẹp sắt) - Sát thương đơn mục tiêu + Trói chân * **Mô tả:** Khi Seeker chạm phải, kẹp sắt sập lại, gây 30 sát thương và trói chân tại chỗ trong 3 giây. * **Mã nguồn:** ```csharp public class BearTrap : TrapBase { [Header("Bear Trap Settings")] [SerializeField] private float damage = 30f; [SerializeField] private float stunDuration = 3f; protected override void OnTriggered(GameObject victim) { // Gây sát thương Opsive Health ApplyDamageToUCC(victim, damage); // Trói chân Opsive Locomotion ApplyStunToUCC(victim, stunDuration); // Đợi kết thúc Animation sập bẫy trước khi biến mất StartCoroutine(DespawnAfterDelay(2f)); } private System.Collections.IEnumerator DespawnAfterDelay(float delay) { yield return new WaitForSeconds(delay); DespawnTrap(); } protected override void ConfigureVisibility() { // Logic tàng hình mặc định } } ``` #### 2. Gas Poison Trap (Bẫy độc hại) - Sát thương theo thời gian (DoT) + Giảm tầm nhìn * **Mô tả:** Khi nổ, kích hoạt Particle System khí độc diện rộng (bán kính 4m). Bất kỳ Seeker nào đứng bên trong sẽ chịu 5 sát thương mỗi giây và bị che mờ camera bởi UI khói độc. * **Mã nguồn:** ```csharp public class PoisonGasTrap : TrapBase { [SerializeField] private float damagePerSecond = 5f; [SerializeField] private float gasDuration = 8f; [SerializeField] private float gasRadius = 4f; protected override void OnTriggered(GameObject victim) { // Bật Particle System độc (đồng bộ qua State thay đổi hiệu ứng) // Bắt đầu quét vùng gây sát thương định kỳ trên Server StartCoroutine(ApplyGasDamageArea()); } private System.Collections.IEnumerator ApplyGasDamageArea() { float elapsed = 0f; while (elapsed < gasDuration) { Collider[] targets = Physics.OverlapSphere(transform.position, gasRadius, LayerMask.GetMask("Player")); foreach (var col in targets) { if (IsTargetValid(col.gameObject)) { ApplyDamageToUCC(col.gameObject, damagePerSecond); // Rpc_TriggerPoisonOverlayOnClient(col.gameObject.GetComponent().InputAuthority); } } yield return new WaitForSeconds(1f); elapsed += 1f; } DespawnTrap(); } } ``` #### 3. Alarm Trap (Bẫy còi báo động) - Tiết lộ vị trí Seeker * **Mô tả:** Khi chạm vào, không gây sát thương nhưng hú còi cực to và ping vị trí Seeker lên bản đồ nhỏ (Minimap) của Trapper trong 5 giây. #### 4. Flashbang Trap (Bẫy mù) - Làm mù màn hình Seeker * **Mô tả:** Khi dẫm phải sẽ phát nổ chói lóa. Gửi một RPC làm trắng màn hình UI của nạn nhân, giảm âm lượng âm thanh game về 0 và phục hồi dần trong 4 giây. --- ## 📈 3. KẾ HOẠCH BẮT TAY THỰC HIỆN TỪNG BƯỚC (ROADMAP TO ACTION) Để bắt tay vào làm ngay một cách khoa học, chúng ta sẽ chia thành các Task nhỏ trong **3 Sprint chính**: ### 🏃 SPRINT 1: CORE INFRASTRUCTURE (Dự kiến: 2-3 ngày) 1. [ ] **Tạo cấu trúc thư mục**: Dựng sẵn các folder `Trap/Base`, `Trap/Concrete`, `Trap/ScriptableObjects`. 2. [ ] **Tạo file cấu hình**: Viết class `TrapDataSO.cs`. 3. [ ] **Hoàn thiện Core Class**: Viết `TrapBase.cs` xử lý trigger va chạm vật lý và vòng đời Fusion NetworkObject. 4. [ ] **Tạo Prefab kiểm thử**: Tạo 1 khối Cube gắn `NetworkObject`, `Collider` (Is Trigger), `NetworkTransform` và script kế thừa từ `TrapBase`. ### 🏃 SPRINT 2: PLACEMENT & VISIBILITY (Dự kiến: 3 ngày) 1. [ ] **Hệ thống Ghost Preview**: Viết `TrapPlacementController.cs` để chiếu tia Raycast từ camera và hiển thị mô hình mờ. 2. [ ] **Đồng bộ đặt bẫy**: Viết hàm RPC gửi tọa độ đặt bẫy lên Host để Host sinh bẫy thật. 3. [ ] **Tàng hình & Phát hiện**: Tạo các Layer `StealthTrap` và cấu hình Camera Culling Mask cho Seeker. Viết logic chuyển đổi shader/layer trên client của Trapper. ### 🏃 SPRINT 3: OPSIVE INTEGRATION & CONCRETE TRAPS (Dự kiến: 3 ngày) 1. [ ] **Kết nối Opsive UCC**: Thực hiện code gây sát thương (Health) và viết custom Ability trói chân (Stun/Freeze). 2. [ ] **Hoàn thiện các bẫy cụ thể**: Viết `BearTrap.cs` (Kẹp sắt) và `PoisonGasTrap.cs` (Bẫy ga độc). 3. [ ] **Kiểm thử Multiplayer**: Build game ra 2 màn hình để test đồng bộ: - Trapper đặt bẫy -> Có hao tốn Cooldown không? Có sinh bẫy đồng bộ không? - Seeker đi qua bẫy -> Bẫy có nổ không? Seeker có bị trừ máu và đứng im không? - Camera của Seeker có nhìn thấy bẫy trước khi nổ không? (Phải tàng hình). --- ## ⚠️ 4. CÁC ĐIỂM CẦN LƯU Ý KHI LẬP TRÌNH (CRITICAL TIPS) 1. **Collider Network**: Hãy đảm bảo bẫy có Collider được cấu hình `Is Trigger = True`. Chỉ có Server mới được thực hiện hàm xử lý sát thương hay khống chế trong `OnTriggerEnter` để chống hack. 2. **Ủy quyền trạng thái (State Authority)**: Mọi biến trạng thái như `TrapState State` hay `TickTimer` đều phải được gắn nhãn `[Networked]`. Khi thay đổi các thuộc tính này trên Server, Fusion sẽ tự động đồng bộ xuống Client và gọi hàm Callback `OnStateChanged` để kích hoạt VFX/SFX cục bộ. 3. **Clean Up**: Luôn kiểm tra việc hủy bẫy (`Runner.Despawn`) để tránh tràn bộ nhớ khi đặt quá nhiều bẫy trong Maze (mê cung).