576 lines
22 KiB
Markdown
576 lines
22 KiB
Markdown
|
|
# 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` 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<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).
|