22 KiB
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.
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)
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.
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)
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).
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
- 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).
- Tạo Layer:
- Camera Culling:
- Camera của
Trappersẽ render cả LayerDefaultvàStealthTrap(để nhìn thấy bẫy của mình). - Camera của
Seekermặc định sẽ bỏ tích (Cull) LayerStealthTraptrong 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ý).
- Camera của
- 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ừ
StealthTrapsangDefaulthoặc bật Shader quét hồng ngoại để làm hiển thị bóng mờ bẫy.
- 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ừ
// 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)
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
StunhoặcFrozenđược cấu hình sẵn trong component UltimateCharacterLocomotion).
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:
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:
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)
- Tạo cấu trúc thư mục: Dựng sẵn các folder
Trap/Base,Trap/Concrete,Trap/ScriptableObjects. - Tạo file cấu hình: Viết class
TrapDataSO.cs. - Hoàn thiện Core Class: Viết
TrapBase.csxử lý trigger va chạm vật lý và vòng đời Fusion NetworkObject. - Tạo Prefab kiểm thử: Tạo 1 khối Cube gắn
NetworkObject,Collider(Is Trigger),NetworkTransformvà script kế thừa từTrapBase.
🏃 SPRINT 2: PLACEMENT & VISIBILITY (Dự kiến: 3 ngày)
- Hệ thống Ghost Preview: Viết
TrapPlacementController.csđể chiếu tia Raycast từ camera và hiển thị mô hình mờ. - Đồ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.
- Tàng hình & Phát hiện: Tạo các Layer
StealthTrapvà 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)
- 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).
- 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). - 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)
- 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ế trongOnTriggerEnterđể chống hack. - Ủy quyền trạng thái (State Authority): Mọi biến trạng thái như
TrapState StatehayTickTimerđề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 CallbackOnStateChangedđể kích hoạt VFX/SFX cục bộ. - 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).