Files
BABA_YAGA/Assets/Scripts/Trap/Trap Plan Detail.md

576 lines
22 KiB
Markdown
Raw Normal View History

2026-07-02 08:59:23 +07:00
# 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<Collider>();
animator = GetComponentInChildren<Animator>();
audioSource = GetComponent<AudioSource>();
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<NetworkObject>();
if (networkInfo != null)
{
// Kiểm tra PlayerData của target
var playerData = target.GetComponent<PlayerData>();
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<TrapBase> 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<Renderer>();
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<TrapBase>();
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``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<Renderer>();
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<Health>();
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<UltimateCharacterLocomotion>();
if (locomotion != null)
{
// Tìm ability "Stun" hoặc "Frozen" đã được thiết lập sẵn trong Opsive Editor
Ability stunAbility = locomotion.GetAbility<StunAbility>(); // 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<NetworkObject>().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).