[전투 공간] - 단순 Plane 바닥 → Arena 계층 구조로 교체 (Floor, 벽 4개, Objects) - PolygonDarkFortress 외부 에셋 임포트 (전투 공간 디자인 적용) - 바닥 아래 검은 평면(FloorBase) 추가로 카메라 저각도 시 허공 노출 방지 - NavMesh 리베이크 [카메라] - PlayerCamera에 SphereCast 기반 지형 충돌 감지 추가 - 카메라가 바닥 아래를 비출 때 최소 높이 보장 [캐릭터] - 플레이어 CharacterController skinWidth 정상화 (0.0001 → 0.03) - 보스 NavMeshAgent baseOffset 조정으로 발 파묻힘 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
6.1 KiB
C#
184 lines
6.1 KiB
C#
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
namespace Colosseum.Player
|
|
{
|
|
/// <summary>
|
|
/// 3인칭 카메라 컨트롤러
|
|
/// </summary>
|
|
public class PlayerCamera : MonoBehaviour
|
|
{
|
|
[Header("Camera Settings")]
|
|
[SerializeField] private float distance = 5f;
|
|
[SerializeField] private float height = 2f;
|
|
[SerializeField] private float rotationSpeed = 2f;
|
|
[SerializeField] private float minPitch = -30f;
|
|
[SerializeField] private float maxPitch = 60f;
|
|
|
|
[Header("Collision")]
|
|
[SerializeField] private float collisionRadius = 0.2f;
|
|
[SerializeField] private LayerMask collisionMask = ~0; // 기본: 모든 레이어
|
|
[SerializeField] private float minHeightAboveGround = 0.3f; // 바닥 위 최소 카메라 높이
|
|
|
|
private Transform target;
|
|
private float yaw;
|
|
private float pitch;
|
|
private Camera cameraInstance;
|
|
private InputSystem_Actions inputActions;
|
|
private bool isSpectating = false;
|
|
|
|
public Transform Target => target;
|
|
public bool IsSpectating => isSpectating;
|
|
|
|
private void OnEnable()
|
|
{
|
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
}
|
|
|
|
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
|
{
|
|
// 씬 로드 시 카메라 참조 갱신
|
|
RefreshCamera();
|
|
SnapToTarget();
|
|
|
|
Debug.Log($"[PlayerCamera] Scene loaded, camera refreshed");
|
|
}
|
|
|
|
public void Initialize(Transform playerTransform, InputSystem_Actions actions)
|
|
{
|
|
target = playerTransform;
|
|
inputActions = actions;
|
|
isSpectating = false;
|
|
|
|
// 기존 메인 카메라 사용 또는 새로 생성
|
|
cameraInstance = Camera.main;
|
|
Debug.Log($"[PlayerCamera] Initialize: target={playerTransform?.name}, Camera.main={cameraInstance?.name ?? "NULL"}");
|
|
if (cameraInstance == null)
|
|
{
|
|
var cameraObject = new GameObject("PlayerCamera");
|
|
cameraInstance = cameraObject.AddComponent<Camera>();
|
|
cameraObject.tag = "MainCamera";
|
|
}
|
|
|
|
// 초기 각도
|
|
if (target != null)
|
|
{
|
|
yaw = target.eulerAngles.y;
|
|
}
|
|
pitch = 20f;
|
|
|
|
// 카메라 위치를 즉시 타겟 위치로 초기화
|
|
SnapToTarget();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 관전 대상 변경
|
|
/// </summary>
|
|
public void SetTarget(Transform newTarget)
|
|
{
|
|
if (newTarget == null) return;
|
|
|
|
target = newTarget;
|
|
isSpectating = true;
|
|
|
|
// 부드러운 전환을 위해 현재 카메라 위치에서 새 타겟으로
|
|
yaw = target.eulerAngles.y;
|
|
|
|
Debug.Log($"[PlayerCamera] Now spectating: {target.name}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 원래 플레이어로 복귀
|
|
/// </summary>
|
|
public void ResetToPlayer(Transform playerTransform)
|
|
{
|
|
target = playerTransform;
|
|
isSpectating = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 카메라 위치를 타겟 위치로 즉시 이동 (부드러운 전환 없이)
|
|
/// </summary>
|
|
public void SnapToTarget()
|
|
{
|
|
if (target == null || cameraInstance == null) return;
|
|
|
|
UpdateCameraPosition();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 카메라 참조 갱신 (씬 전환 후 호출)
|
|
/// </summary>
|
|
public void RefreshCamera()
|
|
{
|
|
// 씬 전환 시 항상 새 카메라 참조 획득
|
|
cameraInstance = Camera.main;
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
// 카메라 참조가 없으면 갱신 시도
|
|
if (cameraInstance == null)
|
|
{
|
|
RefreshCamera();
|
|
}
|
|
|
|
if (target == null || cameraInstance == null) return;
|
|
|
|
HandleRotation();
|
|
UpdateCameraPosition();
|
|
}
|
|
|
|
private void HandleRotation()
|
|
{
|
|
if (inputActions == null) return;
|
|
|
|
// Input Actions에서 Look 입력 받기
|
|
Vector2 lookInput = inputActions.Player.Look.ReadValue<Vector2>();
|
|
float mouseX = lookInput.x * rotationSpeed * 0.1f;
|
|
float mouseY = lookInput.y * rotationSpeed * 0.1f;
|
|
|
|
yaw += mouseX;
|
|
pitch -= mouseY;
|
|
pitch = Mathf.Clamp(pitch, minPitch, maxPitch);
|
|
}
|
|
|
|
private void UpdateCameraPosition()
|
|
{
|
|
// 구면 좌표로 카메라 위치 계산
|
|
Quaternion rotation = Quaternion.Euler(pitch, yaw, 0f);
|
|
Vector3 offset = rotation * new Vector3(0f, 0f, -distance);
|
|
offset.y += height;
|
|
|
|
Vector3 pivot = target.position + Vector3.up * height * 0.5f;
|
|
Vector3 desiredPos = target.position + offset;
|
|
|
|
// pivot → desiredPos 방향으로 SphereCast해서 지형 충돌 감지
|
|
Vector3 dir = desiredPos - pivot;
|
|
float maxDist = dir.magnitude;
|
|
if (Physics.SphereCast(pivot, collisionRadius, dir.normalized, out RaycastHit hit, maxDist, collisionMask, QueryTriggerInteraction.Ignore))
|
|
{
|
|
// 충돌 지점에서 collisionRadius만큼 pivot 쪽으로 당김
|
|
desiredPos = hit.point + dir.normalized * collisionRadius;
|
|
}
|
|
|
|
// 카메라 아래에 지면이 너무 가까우면 위로 밀어올려 바닥 아래면이 보이지 않게 함
|
|
if (Physics.Raycast(desiredPos, Vector3.down, out RaycastHit groundHit, minHeightAboveGround + 1f, collisionMask, QueryTriggerInteraction.Ignore))
|
|
{
|
|
float groundY = groundHit.point.y + minHeightAboveGround;
|
|
if (desiredPos.y < groundY)
|
|
desiredPos.y = groundY;
|
|
}
|
|
|
|
cameraInstance.transform.position = desiredPos;
|
|
cameraInstance.transform.LookAt(pivot);
|
|
}
|
|
}
|
|
}
|