using Unity.Netcode; using UnityEngine; using System.Collections.Generic; namespace Northbound { /// /// 특정 팀만 순간이동으로 통과할 수 있는 성문 /// 허용되지 않은 팀은 물리적으로 막힘 /// public class TeamGate : NetworkBehaviour { [Header("Gate Settings")] [Tooltip("통과를 허용할 팀")] public TeamType allowedTeam = TeamType.Player; [Tooltip("모든 팀 통과 허용 (공용 문)")] public bool allowAllTeams = false; [Header("Teleport Settings")] [Tooltip("트리거 콜라이더 (문 앞면)")] public Collider frontTrigger; [Tooltip("트리거 콜라이더 (문 뒷면)")] public Collider backTrigger; [Tooltip("앞면 텔레포트 목적지 (문 뒤)")] public Transform frontExitPoint; [Tooltip("뒷면 텔레포트 목적지 (문 앞)")] public Transform backExitPoint; [Tooltip("텔레포트 쿨다운 (초) - 연속 텔레포트 방지")] public float teleportCooldown = 1f; [Header("Visual Feedback")] public GameObject teleportEffectPrefab; public AudioClip teleportSound; public AudioClip blockedSound; [Header("Team Indicator")] [Tooltip("팀 표시용 렌더러 (깃발, 조명 등)")] public Renderer teamIndicatorRenderer; [Tooltip("허용 팀 머티리얼")] public Material allowedTeamMaterial; [Tooltip("차단 표시 머티리얼")] public Material blockedMaterial; // 쿨다운 추적 private Dictionary _lastTeleportTime = new Dictionary(); // 트리거별 진입 방향 추적 private Dictionary _triggerStates = new Dictionary(); private AudioSource _audioSource; private void Awake() { _audioSource = GetComponent(); if (_audioSource == null && (teleportSound != null || blockedSound != null)) { _audioSource = gameObject.AddComponent(); _audioSource.spatialBlend = 1f; _audioSource.maxDistance = 20f; } // 트리거 설정 확인 if (frontTrigger != null) { frontTrigger.isTrigger = true; } else { Debug.LogError($"[TeamGate] {gameObject.name}: Front Trigger가 설정되지 않았습니다!"); } if (backTrigger != null) { backTrigger.isTrigger = true; } else { Debug.LogError($"[TeamGate] {gameObject.name}: Back Trigger가 설정되지 않았습니다!"); } // 출구 지점 확인 if (frontExitPoint == null || backExitPoint == null) { Debug.LogError($"[TeamGate] {gameObject.name}: Exit Points가 설정되지 않았습니다!"); } UpdateVisual(); } private void OnTriggerEnter(Collider other) { // 서버에서만 텔레포트 처리 if (!IsServer) return; // 어느 트리거에 진입했는지 확인 Collider triggeredCollider = null; Transform exitPoint = null; if (other.bounds.Intersects(frontTrigger.bounds)) { triggeredCollider = frontTrigger; exitPoint = frontExitPoint; } else if (other.bounds.Intersects(backTrigger.bounds)) { triggeredCollider = backTrigger; exitPoint = backExitPoint; } if (triggeredCollider == null || exitPoint == null) return; // 이미 트리거 안에 있으면 무시 (연속 텔레포트 방지) if (_triggerStates.ContainsKey(other) && _triggerStates[other]) return; _triggerStates[other] = true; // 팀 정보 가져오기 ITeamMember teamMember = other.GetComponent(); if (teamMember == null) { teamMember = other.GetComponentInParent(); } if (teamMember == null) { _triggerStates[other] = false; return; } // 쿨다운 체크 GameObject targetObject = teamMember as MonoBehaviour != null ? (teamMember as MonoBehaviour).gameObject : other.gameObject; if (_lastTeleportTime.ContainsKey(targetObject)) { float timeSinceLastTeleport = Time.time - _lastTeleportTime[targetObject]; if (timeSinceLastTeleport < teleportCooldown) { _triggerStates[other] = false; return; // 쿨다운 중 } } // 통과 가능 여부 확인 bool canPass = CanPassThrough(teamMember); if (canPass) { // 텔레포트 실행 ExecuteTeleport(other.gameObject, exitPoint.position); _lastTeleportTime[targetObject] = Time.time; // 이펙트 및 사운드 PlayTeleportEffectClientRpc(other.transform.position); PlayTeleportEffectClientRpc(exitPoint.position); if (teleportSound != null) { PlaySoundClientRpc(true); } // Debug.Log($"[TeamGate] {other.name} 텔레포트: {triggeredCollider.name} -> {exitPoint.name}"); } else { // 차단됨 if (blockedSound != null) { PlaySoundClientRpc(false); } // Debug.Log($"[TeamGate] {other.name} 차단됨 - 팀: {teamMember.GetTeam()}"); } _triggerStates[other] = false; } private void OnTriggerExit(Collider other) { // 트리거 상태 정리 if (_triggerStates.ContainsKey(other)) { _triggerStates[other] = false; } } /// /// 팀원이 통과 가능한지 확인 /// private bool CanPassThrough(ITeamMember teamMember) { if (allowAllTeams) return true; return teamMember.GetTeam() == allowedTeam; } /// /// 오브젝트를 목적지로 텔레포트 /// private void ExecuteTeleport(GameObject target, Vector3 destination) { // CharacterController를 사용하는 경우 CharacterController cc = target.GetComponent(); if (cc != null) { cc.enabled = false; target.transform.position = destination; cc.enabled = true; return; } // Rigidbody를 사용하는 경우 Rigidbody rb = target.GetComponent(); if (rb != null) { rb.position = destination; rb.linearVelocity = Vector3.zero; // 속도 초기화 return; } // NavMeshAgent를 사용하는 경우 (적 AI) UnityEngine.AI.NavMeshAgent agent = target.GetComponent(); if (agent != null) { agent.Warp(destination); return; } // 기본 Transform 이동 target.transform.position = destination; } /// /// 텔레포트 이펙트 재생 (모든 클라이언트) /// [Rpc(SendTo.ClientsAndHost)] private void PlayTeleportEffectClientRpc(Vector3 position) { if (teleportEffectPrefab == null) return; GameObject effect = Instantiate(teleportEffectPrefab, position, Quaternion.identity); Destroy(effect, 3f); } /// /// 사운드 재생 (모든 클라이언트) /// [Rpc(SendTo.ClientsAndHost)] private void PlaySoundClientRpc(bool isTeleport) { if (_audioSource == null) return; AudioClip clip = isTeleport ? teleportSound : blockedSound; if (clip != null) { _audioSource.PlayOneShot(clip); } } /// /// 허용 팀 변경 (서버에서 호출) /// [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] public void ChangeAllowedTeamServerRpc(TeamType newTeam) { allowedTeam = newTeam; UpdateVisualClientRpc(newTeam); Debug.Log($"[TeamGate] 허용 팀 변경: {newTeam}"); } /// /// 비주얼 업데이트 (모든 클라이언트) /// [Rpc(SendTo.ClientsAndHost)] private void UpdateVisualClientRpc(TeamType team) { allowedTeam = team; UpdateVisual(); } private void UpdateVisual() { if (teamIndicatorRenderer == null) return; if (allowAllTeams) { if (allowedTeamMaterial != null) teamIndicatorRenderer.material = allowedTeamMaterial; } else { // 팀별 색상 설정 (옵션) switch (allowedTeam) { case TeamType.Player: if (allowedTeamMaterial != null) teamIndicatorRenderer.material = allowedTeamMaterial; break; default: if (blockedMaterial != null) teamIndicatorRenderer.material = blockedMaterial; break; } } } private void OnDrawGizmos() { // 트리거 영역 시각화 Gizmos.color = new Color(0f, 1f, 1f, 0.3f); if (frontTrigger != null) { DrawTriggerGizmo(frontTrigger, Color.cyan); } if (backTrigger != null) { DrawTriggerGizmo(backTrigger, Color.magenta); } // 출구 지점 시각화 if (frontExitPoint != null) { Gizmos.color = Color.green; Gizmos.DrawWireSphere(frontExitPoint.position, 0.5f); Gizmos.DrawLine(frontExitPoint.position, frontExitPoint.position + frontExitPoint.forward * 1f); } if (backExitPoint != null) { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(backExitPoint.position, 0.5f); Gizmos.DrawLine(backExitPoint.position, backExitPoint.position + backExitPoint.forward * 1f); } // 텔레포트 경로 표시 if (frontTrigger != null && frontExitPoint != null) { Gizmos.color = Color.cyan; Gizmos.DrawLine(frontTrigger.bounds.center, frontExitPoint.position); } if (backTrigger != null && backExitPoint != null) { Gizmos.color = Color.magenta; Gizmos.DrawLine(backTrigger.bounds.center, backExitPoint.position); } } private void DrawTriggerGizmo(Collider col, Color color) { Gizmos.color = color; if (col is BoxCollider box) { Matrix4x4 oldMatrix = Gizmos.matrix; Gizmos.matrix = col.transform.localToWorldMatrix; Gizmos.DrawWireCube(box.center, box.size); Gizmos.matrix = oldMatrix; } else if (col is SphereCollider sphere) { Gizmos.DrawWireSphere(col.transform.position + sphere.center, sphere.radius); } } public override void OnDestroy() { // 정리 _lastTeleportTime.Clear(); _triggerStates.Clear(); base.OnDestroy(); } } }