using System; using System.Threading; using Unity.XR.CoreUtils; using UnityEngine; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Movement; public class PlayerController : MonoBehaviour, ISceneInitializable { [Header("점프 설정")] [SerializeField] private float _jumpHeight = 1.2f; [Header("시야 가림 렌더러")] [SerializeField] private Renderer SightBlockingRenderer; [SerializeField] private float _fadeSightDuration = 1f; // 시야 가림 페이드에 걸리는 시간(초) private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); private static readonly int ColorId = Shader.PropertyToID("_Color"); private Material _sightMaterial; private CancellationTokenSource _fadeSightCts; private Vector3 _playerVelocity; private CharacterController _controller; private ContinuousMoveProvider _moveProvider; private XROrigin _xrOrigin; private float gravityValue = Physics.gravity.y; public bool IsSitting { get; private set; } // 앉아있는 동안 true (이동/점프 잠금) private float _standingHeight; // 앉기 전 카메라 높이(일어설 때 복원용) private void Awake() { _controller = GetComponentInChildren(true); _moveProvider = GetComponentInChildren(true); _xrOrigin = GetComponentInChildren(true); } public void OnSceneLoaded() { // 씬이 다시 로드돼도 한 번만 구독되도록 중복 제거 후 등록 InputManager.Instance.OnJump_Event -= this.OnJump; InputManager.Instance.OnJump_Event += this.OnJump; } public void SetHeight(float height) { _xrOrigin.CameraYOffset = height; } public void OnJump() { if (_controller == null) return; if (IsSitting) return; // 앉아있는 동안은 점프 금지 if (!_controller.isGrounded) return; _playerVelocity.y = Mathf.Sqrt(_jumpHeight * -2f * gravityValue); } // 앉기 시작: (좌석 지정 시) 그 위치로 이동 후 카메라 높이를 낮추고 이동을 잠근다. public void Sit(Transform sitPoint = null) { if (IsSitting) return; IsSitting = true; if (sitPoint != null) TeleportRig(sitPoint); // 좌석 위치/방향으로 순간이동 _standingHeight = _xrOrigin.CameraYOffset; // 현재 높이 기억 SetHeight(1.2f); if (_moveProvider != null) _moveProvider.enabled = false; // 이동 잠금 } // XR 리그(=플레이어 전체)를 target의 위치/방향으로 순간이동. // CharacterController는 켜진 채로 transform을 직접 바꾸면 되돌려지므로 잠시 끈다. private void TeleportRig(Transform target) { Transform rig = _xrOrigin != null ? _xrOrigin.transform : transform; bool ccWasEnabled = _controller != null && _controller.enabled; if (_controller != null) _controller.enabled = false; // 회전은 수평(yaw)만 적용해 기울어짐 방지 rig.SetPositionAndRotation(target.position, Quaternion.Euler(0f, target.eulerAngles.y, 0f)); if (_controller != null) _controller.enabled = ccWasEnabled; _playerVelocity = Vector3.zero; // 텔레포트 직후 누적 낙하속도 튐 방지 } // 일어서기: 높이 복원 + 이동 잠금 해제. public void StandUp() { if (!IsSitting) return; IsSitting = false; SetHeight(_standingHeight); if (_moveProvider != null) _moveProvider.enabled = true; // 이동 잠금 해제 } // 호출되면 시야 가림 렌더러의 _BaseColor 알파를 현재 값에서 1까지 서서히 올린다(시야 가림). public void FadeSight() => StartSightFade(1f); // 호출되면 알파를 현재 값에서 0까지 서서히 내린다(시야 회복). public void ClearSight() => StartSightFade(0f); private void StartSightFade(float targetAlpha) { if (SightBlockingRenderer == null) { Debug.LogWarning("[PlayerController] SightBlockingRenderer가 비어 있습니다."); return; } // .material 인스턴스를 써서 공유 머티리얼 에셋이 변형되지 않게 한다. if (_sightMaterial == null) _sightMaterial = SightBlockingRenderer.material; // 진행 중이던 페이드가 있으면 취소하고 새로 시작 _fadeSightCts?.Cancel(); _fadeSightCts?.Dispose(); _fadeSightCts = new CancellationTokenSource(); _ = SightFadeAsync(targetAlpha, _fadeSightCts.Token); } private async Awaitable SightFadeAsync(float targetAlpha, CancellationToken token) { int propId = _sightMaterial.HasProperty(BaseColorId) ? BaseColorId : _sightMaterial.HasProperty(ColorId) ? ColorId : BaseColorId; Color color = _sightMaterial.GetColor(propId); float startAlpha = color.a; float t = 0f; try { while (t < _fadeSightDuration) { t += Time.deltaTime; color.a = Mathf.Lerp(startAlpha, targetAlpha, t / _fadeSightDuration); _sightMaterial.SetColor(propId, color); await Awaitable.NextFrameAsync(token); } color.a = targetAlpha; _sightMaterial.SetColor(propId, color); } catch (OperationCanceledException) { } } // GameManager를 통해 게임을 종료한다. (UI 버튼·타임라인 시그널 등에서 호출) public void QuitGame() { if (GameManager.Instance == null) { Debug.LogWarning("[PlayerController] GameManager.Instance가 없습니다. 게임을 종료할 수 없습니다."); return; } GameManager.Instance.QuitGame(); } private void Update() { // 바닥 체크 및 Y축 속도 초기화 if (_controller.isGrounded && _playerVelocity.y < 0) { // 완전히 0으로 두면 바닥 감지가 불안정할 수 있으므로 약간의 음수 값을 유지 _playerVelocity.y = -2f; } _playerVelocity.y += gravityValue * Time.deltaTime; _controller.Move(_playerVelocity * Time.deltaTime); } private void OnDestroy() { _fadeSightCts?.Cancel(); _fadeSightCts?.Dispose(); } }