feat: 카메라-플레이어 사이 장애물 숨김 처리

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

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-04 08:20:35 +09:00
parent f3189a1810
commit c88487ef4c
3 changed files with 114 additions and 14 deletions

View File

@@ -0,0 +1,105 @@
using System.Collections.Generic;
using UnityEngine;
namespace Colosseum.Player
{
/// <summary>
/// 카메라와 타겟 사이의 장애물을 숨기는 컨트롤러
/// </summary>
public class ObstacleTransparencyController : MonoBehaviour
{
[Header("Occlusion Settings")]
[Min(0.01f)] [SerializeField] private float checkRadius = 0.3f;
private Transform cameraTransform;
private Transform targetTransform;
private LayerMask occlusionMask;
private readonly HashSet<Renderer> hiddenRenderers = new();
public void Initialize(Transform target, LayerMask mask)
{
cameraTransform = GetComponent<Camera>().transform;
targetTransform = target;
occlusionMask = mask;
}
private void LateUpdate()
{
if (cameraTransform == null || targetTransform == null) return;
Vector3 origin = cameraTransform.position;
Vector3 direction = targetTransform.position - origin;
float maxDistance = direction.magnitude;
if (maxDistance < 0.01f) return;
direction.Normalize();
HashSet<Renderer> currentHits = CollectHits(origin, direction, maxDistance);
foreach (Renderer renderer in currentHits)
{
if (!hiddenRenderers.Contains(renderer))
{
renderer.enabled = false;
hiddenRenderers.Add(renderer);
}
}
List<Renderer> toShow = null;
foreach (Renderer renderer in hiddenRenderers)
{
if (renderer == null || !currentHits.Contains(renderer))
{
if (renderer != null)
renderer.enabled = true;
(toShow ??= new List<Renderer>()).Add(renderer);
}
}
if (toShow != null)
{
foreach (Renderer renderer in toShow)
hiddenRenderers.Remove(renderer);
}
}
private HashSet<Renderer> CollectHits(Vector3 origin, Vector3 direction, float maxDistance)
{
var hits = new HashSet<Renderer>();
RaycastHit[] rayHits = Physics.SphereCastAll(origin, checkRadius, direction, maxDistance, occlusionMask, QueryTriggerInteraction.Ignore);
float targetY = targetTransform.position.y;
foreach (RaycastHit hit in rayHits)
{
if (hit.point.y <= targetY) continue;
Renderer renderer = hit.collider.GetComponent<Renderer>();
if (renderer == null) continue;
if (renderer is ParticleSystemRenderer || renderer is TrailRenderer) continue;
hits.Add(renderer);
}
return hits;
}
public void ClearHidden()
{
foreach (Renderer renderer in hiddenRenderers)
{
if (renderer != null)
renderer.enabled = true;
}
hiddenRenderers.Clear();
}
private void OnDestroy()
{
ClearHidden();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2e94a9a820149c5589b02955f59d465e

View File

@@ -17,9 +17,7 @@ namespace Colosseum.Player
[SerializeField] private float maxPitch = 60f; [SerializeField] private float maxPitch = 60f;
[Header("Collision")] [Header("Collision")]
[SerializeField] private float collisionRadius = 0.3f;
[SerializeField] private LayerMask collisionMask; [SerializeField] private LayerMask collisionMask;
[SerializeField] private float minDistance = 0.5f;
[SerializeField] private float minHeightAboveGround = 0.3f; // 바닥 위 최소 카메라 높이 [SerializeField] private float minHeightAboveGround = 0.3f; // 바닥 위 최소 카메라 높이
[Header("Smooth Distance")] [Header("Smooth Distance")]
@@ -87,6 +85,12 @@ namespace Colosseum.Player
currentDistance = distance; currentDistance = distance;
distanceSmoothVelocity = 0f; distanceSmoothVelocity = 0f;
// 장애물 반투명 컨트롤러 연동
var transparencyController = cameraInstance.gameObject.GetComponent<ObstacleTransparencyController>();
if (transparencyController == null)
transparencyController = cameraInstance.gameObject.AddComponent<ObstacleTransparencyController>();
transparencyController.Initialize(target, collisionMask);
// 카메라 위치를 즉시 타겟 위치로 초기화 // 카메라 위치를 즉시 타겟 위치로 초기화
SnapToTarget(); SnapToTarget();
} }
@@ -177,19 +181,8 @@ namespace Colosseum.Player
Vector3 pivot = target.position + Vector3.up * height * 0.5f; Vector3 pivot = target.position + Vector3.up * height * 0.5f;
Vector3 desiredPos = target.position + offset; Vector3 desiredPos = target.position + offset;
// pivot → desiredPos 방향으로 SphereCast해서 지형 충돌 감지
Vector3 dir = desiredPos - pivot;
float maxDist = dir.magnitude;
float targetDistance = distance;
if (Physics.SphereCast(pivot, collisionRadius, dir.normalized, out RaycastHit hit, maxDist, collisionMask, QueryTriggerInteraction.Ignore))
{
// 충돌 지점에서 collisionRadius만큼 pivot 쪽으로 당긴 거리
targetDistance = Mathf.Max(minDistance, hit.distance - collisionRadius);
}
// 부드러운 거리 보간 // 부드러운 거리 보간
currentDistance = Mathf.SmoothDamp(currentDistance, targetDistance, ref distanceSmoothVelocity, distanceSmoothTime); currentDistance = Mathf.SmoothDamp(currentDistance, distance, ref distanceSmoothVelocity, distanceSmoothTime);
currentDistance = Mathf.Max(minDistance, currentDistance);
// 보간된 거리로 최종 오프셋 재계산 // 보간된 거리로 최종 오프셋 재계산
offset = rotation * new Vector3(0f, 0f, -currentDistance); offset = rotation * new Vector3(0f, 0f, -currentDistance);