Files
Colosseum/Assets/_Game/Scripts/Player/PlayerCamera.cs
dal4segno c88487ef4c feat: 카메라-플레이어 사이 장애물 숨김 처리
- SphereCast 충돌 당기기 로직 제거로 카메라가 항상 원래 거리 유지
- ObstacleTransparencyController 신규: 카메라-타겟 사이 장애물을 renderer.enabled로 숨김
- 플레이어 발 위치 이하의 바닥/지형은 숨김 대상에서 제외
- 파티클/트레일 렌더러는 숨김 스킵

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-04 08:20:35 +09:00

205 lines
7.0 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 LayerMask collisionMask;
[SerializeField] private float minHeightAboveGround = 0.3f; // 바닥 위 최소 카메라 높이
[Header("Smooth Distance")]
[Min(0.01f)] [SerializeField] private float distanceSmoothTime = 0.15f;
private Transform target;
private float yaw;
private float pitch;
private Camera cameraInstance;
private InputSystem_Actions inputActions;
private bool isSpectating = false;
private float currentDistance;
private float distanceSmoothVelocity;
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;
// 충돌 마스크 기본값 설정 (Default, Enemy, Ground 레이어만)
if (collisionMask == 0)
collisionMask = LayerMask.GetMask("Default", "Enemy", "Ground");
// 기존 메인 카메라 사용 또는 새로 생성
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;
// 부드러운 거리 보간 상태 초기화
currentDistance = distance;
distanceSmoothVelocity = 0f;
// 장애물 반투명 컨트롤러 연동
var transparencyController = cameraInstance.gameObject.GetComponent<ObstacleTransparencyController>();
if (transparencyController == null)
transparencyController = cameraInstance.gameObject.AddComponent<ObstacleTransparencyController>();
transparencyController.Initialize(target, collisionMask);
// 카메라 위치를 즉시 타겟 위치로 초기화
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;
// 부드러운 거리 보간 상태 리셋
currentDistance = distance;
distanceSmoothVelocity = 0f;
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;
// 부드러운 거리 보간
currentDistance = Mathf.SmoothDamp(currentDistance, distance, ref distanceSmoothVelocity, distanceSmoothTime);
// 보간된 거리로 최종 오프셋 재계산
offset = rotation * new Vector3(0f, 0f, -currentDistance);
offset.y += height;
desiredPos = target.position + offset;
// 카메라 아래에 지면이 너무 가까우면 위로 밀어올려 바닥 아래면이 보이지 않게 함
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);
}
}
}