Files
WhaleAdventure_VR/Assets/02_Scripts/Player/PlayerController.cs
2026-06-26 13:18:33 +09:00

183 lines
6.4 KiB
C#

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<CharacterController>(true);
_moveProvider = GetComponentInChildren<ContinuousMoveProvider>(true);
_xrOrigin = GetComponentInChildren<XROrigin>(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();
}
}