Files
Northbound/Assets/Scripts/TeamGate.cs

388 lines
13 KiB
C#

using Unity.Netcode;
using UnityEngine;
using System.Collections.Generic;
namespace Northbound
{
/// <summary>
/// 특정 팀만 순간이동으로 통과할 수 있는 성문
/// 허용되지 않은 팀은 물리적으로 막힘
/// </summary>
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<GameObject, float> _lastTeleportTime = new Dictionary<GameObject, float>();
// 트리거별 진입 방향 추적
private Dictionary<Collider, bool> _triggerStates = new Dictionary<Collider, bool>();
private AudioSource _audioSource;
private void Awake()
{
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null && (teleportSound != null || blockedSound != null))
{
_audioSource = gameObject.AddComponent<AudioSource>();
_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<ITeamMember>();
if (teamMember == null)
{
teamMember = other.GetComponentInParent<ITeamMember>();
}
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($"<color=cyan>[TeamGate] {other.name} 텔레포트: {triggeredCollider.name} -> {exitPoint.name}</color>");
}
else
{
// 차단됨
if (blockedSound != null)
{
PlaySoundClientRpc(false);
}
Debug.Log($"<color=yellow>[TeamGate] {other.name} 차단됨 - 팀: {teamMember.GetTeam()}</color>");
}
_triggerStates[other] = false;
}
private void OnTriggerExit(Collider other)
{
// 트리거 상태 정리
if (_triggerStates.ContainsKey(other))
{
_triggerStates[other] = false;
}
}
/// <summary>
/// 팀원이 통과 가능한지 확인
/// </summary>
private bool CanPassThrough(ITeamMember teamMember)
{
if (allowAllTeams)
return true;
return teamMember.GetTeam() == allowedTeam;
}
/// <summary>
/// 오브젝트를 목적지로 텔레포트
/// </summary>
private void ExecuteTeleport(GameObject target, Vector3 destination)
{
// CharacterController를 사용하는 경우
CharacterController cc = target.GetComponent<CharacterController>();
if (cc != null)
{
cc.enabled = false;
target.transform.position = destination;
cc.enabled = true;
return;
}
// Rigidbody를 사용하는 경우
Rigidbody rb = target.GetComponent<Rigidbody>();
if (rb != null)
{
rb.position = destination;
rb.linearVelocity = Vector3.zero; // 속도 초기화
return;
}
// NavMeshAgent를 사용하는 경우 (적 AI)
UnityEngine.AI.NavMeshAgent agent = target.GetComponent<UnityEngine.AI.NavMeshAgent>();
if (agent != null)
{
agent.Warp(destination);
return;
}
// 기본 Transform 이동
target.transform.position = destination;
}
/// <summary>
/// 텔레포트 이펙트 재생 (모든 클라이언트)
/// </summary>
[Rpc(SendTo.ClientsAndHost)]
private void PlayTeleportEffectClientRpc(Vector3 position)
{
if (teleportEffectPrefab == null) return;
GameObject effect = Instantiate(teleportEffectPrefab, position, Quaternion.identity);
Destroy(effect, 3f);
}
/// <summary>
/// 사운드 재생 (모든 클라이언트)
/// </summary>
[Rpc(SendTo.ClientsAndHost)]
private void PlaySoundClientRpc(bool isTeleport)
{
if (_audioSource == null) return;
AudioClip clip = isTeleport ? teleportSound : blockedSound;
if (clip != null)
{
_audioSource.PlayOneShot(clip);
}
}
/// <summary>
/// 허용 팀 변경 (서버에서 호출)
/// </summary>
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void ChangeAllowedTeamServerRpc(TeamType newTeam)
{
allowedTeam = newTeam;
UpdateVisualClientRpc(newTeam);
Debug.Log($"<color=green>[TeamGate] 허용 팀 변경: {newTeam}</color>");
}
/// <summary>
/// 비주얼 업데이트 (모든 클라이언트)
/// </summary>
[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();
}
}
}