Files
MyCharacterAgent/Assets/02_Scripts/Character/PlatformOccluder.cs
2026-09-01 21:36:23 +09:00

137 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
/// <summary>
/// 발판 아래에 깊이만 기록하는 사각형을 깔아 캐릭터를 가린다.
/// "발판(=실제 창) 뒤에 서 있는" 그림을 만들 때 쓴다. 예: 창 밑에서 머리만 내미는 포즈.
///
/// 왜 렌더 순서로 해결되지 않는가:
/// 발판은 Unity 오브젝트가 아니라 진짜 데스크톱 창이고, 우리 창은 그 위에 떠 있는
/// 최상위 오버레이다. 그래서 "창 뒤에 그린다"가 물리적으로 불가능하다.
/// 대신 그 영역에서 캐릭터를 아예 그리지 않으면, 우리 창은 그 자리에 투명한 채로
/// 남고 진짜 창이 비쳐 보인다. 결과가 같다.
///
/// 방법은 깊이 마스크다. 색을 안 쓰고(ColorMask 0) 깊이만 쓰는 사각형을
/// 캐릭터보다 카메라 가까이, 캐릭터보다 먼저(Queue Geometry-100) 그리면
/// 그 뒤의 캐릭터 픽셀이 깊이 테스트에서 탈락한다. 알파를 0 으로 덮는 방법과 달리
/// 같은 자리에 그려진 다른 것(채팅창 등)을 건드리지 않는다.
/// </summary>
[DefaultExecutionOrder(100)] // WindowClimber(LateUpdate) 가 위치를 정한 뒤에 따라간다
public class PlatformOccluder : MonoBehaviour
{
[Header("참조 (비우면 씬에서 탐색)")]
[SerializeField] WindowClimber climber;
[SerializeField] Camera viewCamera;
[Tooltip("MyCharacterAgent/PlatformOccluder 셰이더. " +
"직접 참조해야 빌드에서 셰이더가 빠지지 않는다")]
[SerializeField] Shader occluderShader;
[Header("동작")]
[Tooltip("캐릭터보다 카메라 쪽으로 얼마나 앞에 둘지(월드 단위). " +
"너무 작으면 깊이 정밀도 문제로 지글거리고, 너무 크면 다른 것까지 가린다")]
[SerializeField] float depthBias = 0.05f;
[Tooltip("발판 위쪽 경계를 얼마나 내릴지(픽셀). 양수면 발판선보다 아래부터 가린다. " +
"머리가 살짝 더 보이게 하고 싶을 때 조절한다")]
[SerializeField] float topOffset = 0f;
/// <summary>지금 가려야 하는지. CharacterMotionDirector 가 포즈에 따라 켜고 끈다.</summary>
public bool Active { get; set; }
MeshFilter meshFilter;
MeshRenderer meshRenderer;
Mesh mesh;
readonly Vector3[] corners = new Vector3[4];
void Awake()
{
if (climber == null) climber = FindFirstObjectByType<WindowClimber>();
if (viewCamera == null) viewCamera = Camera.main;
BuildQuad();
SetVisible(false);
}
void OnDestroy()
{
if (mesh != null) Destroy(mesh);
if (meshRenderer != null && meshRenderer.material != null) Destroy(meshRenderer.material);
}
void LateUpdate()
{
if (meshRenderer == null) return;
if (!Active || climber == null || viewCamera == null)
{
SetVisible(false);
return;
}
var platform = climber.CurrentPlatform;
if (platform == null)
{
// 공중에 있는 동안은 가릴 발판이 없다.
SetVisible(false);
return;
}
UpdateQuad(platform.Value);
SetVisible(true);
}
/// <summary>발판의 X 범위 × (발판선 아래 ~ 화면 아래)를 덮는 사각형으로 갱신한다.</summary>
void UpdateQuad(DesktopPlatform platform)
{
// 캐릭터와 같은 평면보다 살짝 앞. 여기서 깊이를 써야 캐릭터가 탈락한다.
float d = Mathf.Max(0.01f, climber.CharacterDepth - depthBias);
float top = platform.Y - topOffset;
corners[0] = viewCamera.ScreenToWorldPoint(new Vector3(platform.XMin, 0f, d));
corners[1] = viewCamera.ScreenToWorldPoint(new Vector3(platform.XMax, 0f, d));
corners[2] = viewCamera.ScreenToWorldPoint(new Vector3(platform.XMax, top, d));
corners[3] = viewCamera.ScreenToWorldPoint(new Vector3(platform.XMin, top, d));
// 정점을 월드 좌표로 넣으므로 트랜스폼은 원점/무회전으로 둔다.
mesh.SetVertices(corners);
mesh.RecalculateBounds();
}
void BuildQuad()
{
transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
transform.localScale = Vector3.one;
mesh = new Mesh { name = "PlatformOccluderQuad" };
// 매 프레임 정점이 바뀌므로 표시해 둔다.
mesh.MarkDynamic();
mesh.SetVertices(corners);
mesh.SetTriangles(new[] { 0, 2, 1, 0, 3, 2 }, 0);
meshFilter = gameObject.GetComponent<MeshFilter>();
if (meshFilter == null) meshFilter = gameObject.AddComponent<MeshFilter>();
meshFilter.sharedMesh = mesh;
meshRenderer = gameObject.GetComponent<MeshRenderer>();
if (meshRenderer == null) meshRenderer = gameObject.AddComponent<MeshRenderer>();
meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
meshRenderer.receiveShadows = false;
meshRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
meshRenderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
if (occluderShader == null)
{
Debug.LogWarning("[PlatformOccluder] 셰이더가 비어 있어 가림이 동작하지 않습니다. " +
"MyCharacterAgent/PlatformOccluder 셰이더를 연결하세요.");
return;
}
meshRenderer.material = new Material(occluderShader) { name = "PlatformOccluder (runtime)" };
}
void SetVisible(bool value)
{
if (meshRenderer != null && meshRenderer.enabled != value) meshRenderer.enabled = value;
}
}