Projext/Assets/02_Scripts/Enemy/BossAI/BossAttackIndicator.cs
hydrozen e989d20668 카툰 쉐이더 추가 + 중복 스크립트 수정 + 전체 업데이트
- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP)
- ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트
- 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom)
- 씬, 프리팹, 애니메이션 등 전체 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:31:16 +09:00

309 lines
20 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; // 유니티 기능을 불러올거에요 -> UnityEngine을
// ══════════════════════════════════════════════════════════════
// BossAttackIndicator — 보스 범위 공격 바닥 표시기
// ══════════════════════════════════════════════════════════════
// XZ 평면(바닥)에 메쉬 생성, Y축 회전만 사용
// Sprites/Default 셰이더 → 반투명 보장
// 양면 렌더링 → 어떤 카메라 각도에서도 보임
// ══════════════════════════════════════════════════════════════
public class BossAttackIndicator : MonoBehaviour // 클래스를 선언할거에요 -> 범위 표시기를
{
// ── Inspector 설정 ──────────────────────────────────────
[Header("=== 표시 설정 ===")]
[SerializeField] private Color warningColor = new Color(1f, 0f, 0f, 0.25f); // 변수를 선언할거에요 -> 경고 색상을 (반투명 빨강)
[SerializeField] private Color impactColor = new Color(1f, 0f, 0f, 0.6f); // 변수를 선언할거에요 -> 임팩트 직전 색상을 (진한 빨강)
[SerializeField] private float groundOffset = 0.08f; // 변수를 선언할거에요 -> 바닥 위 높이를 (Z-fighting 방지)
[SerializeField] private int meshSegments = 40; // 변수를 선언할거에요 -> 메쉬 분할 수를
// ── 내부 참조 ───────────────────────────────────────────
private GameObject _indicatorObj; // 변수를 선언할거에요 -> 표시 오브젝트를
private MeshFilter _meshFilter; // 변수를 선언할거에요 -> 메쉬 필터를
private MeshRenderer _meshRenderer; // 변수를 선언할거에요 -> 메쉬 렌더러를
private Material _material; // 변수를 선언할거에요 -> 머티리얼을
// ── 페이드 상태 ────────────────────────────────────────
private bool _isShowing = false; // 변수를 선언할거에요 -> 현재 표시 중인지를
private float _fadeTimer = 0f; // 변수를 선언할거에요 -> 페이드 경과 시간을
private float _fadeDuration = 0.5f; // 변수를 선언할거에요 -> 페이드 소요 시간을
private Color _startColor; // 변수를 선언할거에요 -> 시작 색상을
private Color _endColor; // 변수를 선언할거에요 -> 끝 색상을
// ── 추적 모드 ──────────────────────────────────────────
private Transform _followTarget; // 변수를 선언할거에요 -> 따라갈 대상을
private float _forwardOffset; // 변수를 선언할거에요 -> 전방 오프셋을
private bool _followBoss = false; // 변수를 선언할거에요 -> 추적 모드인지를
private bool _isCircle = true; // 변수를 선언할거에요 -> 원형/부채꼴 구분을
// ══════════════════════════════════════════════════════════
// 초기화
// ══════════════════════════════════════════════════════════
private void Awake() // 함수를 실행할거에요 -> 컴포넌트 초기화를
{
CreateIndicatorObject(); // 실행할거에요 -> 표시 오브젝트 생성을
Hide(); // 실행할거에요 -> 초기 숨김
}
private void CreateIndicatorObject() // 함수를 선언할거에요 -> 표시 오브젝트를 생성하는
{
_indicatorObj = new GameObject("BossAttackIndicator_Mesh"); // 생성할거에요 -> 빈 게임오브젝트를
_indicatorObj.transform.SetParent(null); // 설정할거에요 -> 월드 루트로
_meshFilter = _indicatorObj.AddComponent<MeshFilter>(); // 추가할거에요 -> MeshFilter를
_meshRenderer = _indicatorObj.AddComponent<MeshRenderer>(); // 추가할거에요 -> MeshRenderer를
// ── 머티리얼: Sprites/Default 셰이더 사용 ───────────
// ⚠️ Standard 셰이더의 Transparent 모드는 코드 설정만으론 안 먹는 경우가 많아요
// Sprites/Default는 Unity 내장 셰이더로 반투명이 기본 지원됨 → 가장 안전한 선택
Shader spriteShader = Shader.Find("Sprites/Default"); // 찾을거에요 -> Sprites/Default 셰이더를
if (spriteShader == null) // 조건이 맞으면 실행할거에요 -> 못 찾으면 (거의 없는 경우)
spriteShader = Shader.Find("UI/Default"); // 대체할거에요 -> UI/Default로 폴백
if (spriteShader == null) // 조건이 맞으면 실행할거에요 -> 그것도 없으면
spriteShader = Shader.Find("Unlit/Color"); // 최후 대체할거에요
_material = new Material(spriteShader); // 생성할거에요 -> 머티리얼을 (반투명 보장 셰이더)
_material.color = warningColor; // 설정할거에요 -> 초기 색상을
_material.renderQueue = 3100; // 설정할거에요 -> 렌더 큐를 (다른 반투명 오브젝트 위에 그리기)
_meshRenderer.material = _material; // 적용할거에요 -> 머티리얼을
_meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; // 끌거에요 -> 그림자를
_meshRenderer.receiveShadows = false; // 끌거에요 -> 그림자 수신을
_indicatorObj.SetActive(false); // 끌거에요 -> 초기 비활성화
}
// ══════════════════════════════════════════════════════════
// Update: 페이드 + 보스 추적
// ══════════════════════════════════════════════════════════
private void Update() // 함수를 실행할거에요 -> 매 프레임을
{
if (!_isShowing) return; // 중단할거에요 -> 표시 중이 아니면
// ── 페이드: 경고색 → 임팩트색 ──────────────────────
_fadeTimer += Time.deltaTime; // 더할거에요 -> 경과 시간을
float t = Mathf.Clamp01(_fadeTimer / _fadeDuration); // 계산할거에요 -> 보간 비율을
_material.color = Color.Lerp(_startColor, _endColor, t); // 보간할거에요 -> 색상을
// ── 보스 추적 ──────────────────────────────────────
if (_followBoss && _followTarget != null) // 조건이 맞으면 실행할거에요 -> 추적 대상 있으면
{
Vector3 fwd = _followTarget.forward; // 가져올거에요 -> 보스 전방을
fwd.y = 0f; // 제거할거에요 -> 수직 성분을
if (fwd.sqrMagnitude < 0.001f) fwd = Vector3.forward; // 방어할거에요
fwd.Normalize(); // 정규화할거에요
Vector3 center = _followTarget.position + fwd * _forwardOffset; // 계산할거에요 -> 표시 중심을
center.y = _followTarget.position.y + groundOffset; // 설정할거에요 -> 바닥 높이를
_indicatorObj.transform.position = center; // 이동할거에요
// 부채꼴은 방향 회전 필요
if (!_isCircle) // 조건이 맞으면 실행할거에요 -> 부채꼴이면
{
float yAngle = Mathf.Atan2(fwd.x, fwd.z) * Mathf.Rad2Deg; // 계산할거에요 -> Y축 각도를
_indicatorObj.transform.rotation = Quaternion.Euler(0f, yAngle, 0f); // 회전할거에요 -> Y축만
}
}
}
// ══════════════════════════════════════════════════════════
// 공개 API — 원형 (Smash, DashSmash용)
// ══════════════════════════════════════════════════════════
public void ShowCircle(Transform bossTransform, float radius, float forwardOffset, float duration) // 함수를 선언할거에요 -> 원형 범위를 표시하는
{
if (_indicatorObj == null) return; // 중단할거에요
_isCircle = true; // 설정할거에요 -> 원형 모드
_meshFilter.mesh = CreateCircleMesh(radius); // 적용할거에요 -> 원형 메쉬를
_followTarget = bossTransform; // 저장할거에요
_forwardOffset = forwardOffset; // 저장할거에요
_followBoss = true; // 설정할거에요
Vector3 fwd = bossTransform.forward; // 가져올거에요
fwd.y = 0f; // 제거할거에요
if (fwd.sqrMagnitude < 0.001f) fwd = Vector3.forward; // 방어할거에요
fwd.Normalize(); // 정규화할거에요
Vector3 center = bossTransform.position + fwd * forwardOffset; // 계산할거에요 -> 중심을
center.y = bossTransform.position.y + groundOffset; // 설정할거에요 -> 바닥 높이를
_indicatorObj.transform.position = center; // 이동할거에요
_indicatorObj.transform.rotation = Quaternion.identity; // 원형이라 회전 불필요
StartFade(duration); // 실행할거에요 -> 페이드 시작
}
// ══════════════════════════════════════════════════════════
// 공개 API — 부채꼴 (Sweep용)
// ══════════════════════════════════════════════════════════
public void ShowFan(Transform bossTransform, float radius, float angle, float duration) // 함수를 선언할거에요 -> 부채꼴 범위를 표시하는
{
if (_indicatorObj == null) return; // 중단할거에요
_isCircle = false; // 설정할거에요 -> 부채꼴 모드
_meshFilter.mesh = CreateFanMesh(radius, angle); // 적용할거에요 -> 부채꼴 메쉬를
_followTarget = bossTransform; // 저장할거에요
_forwardOffset = 0f; // 설정할거에요
_followBoss = true; // 설정할거에요
Vector3 pos = bossTransform.position; // 가져올거에요
pos.y += groundOffset; // 올릴거에요
_indicatorObj.transform.position = pos; // 이동할거에요
Vector3 fwd = bossTransform.forward; // 가져올거에요
fwd.y = 0f; // 제거할거에요
if (fwd.sqrMagnitude < 0.001f) fwd = Vector3.forward; // 방어할거에요
fwd.Normalize(); // 정규화할거에요
float yAngle = Mathf.Atan2(fwd.x, fwd.z) * Mathf.Rad2Deg; // 계산할거에요
_indicatorObj.transform.rotation = Quaternion.Euler(0f, yAngle, 0f); // 회전할거에요 -> Y축만
StartFade(duration); // 실행할거에요
}
// ══════════════════════════════════════════════════════════
// 공개 API — 숨기기
// ══════════════════════════════════════════════════════════
public void Hide() // 함수를 선언할거에요 -> 범위 표시를 숨기는
{
_isShowing = false; // 설정할거에요
_followBoss = false; // 설정할거에요
if (_indicatorObj != null) // 조건이 맞으면 실행할거에요
_indicatorObj.SetActive(false); // 끌거에요
}
// ══════════════════════════════════════════════════════════
// 내부 — 페이드 시작
// ══════════════════════════════════════════════════════════
private void StartFade(float duration) // 함수를 선언할거에요 -> 페이드를 시작하는
{
_fadeDuration = Mathf.Max(duration, 0.1f); // 설정할거에요 -> 페이드 시간을
_fadeTimer = 0f; // 초기화할거에요 -> 타이머를
_startColor = warningColor; // 설정할거에요 -> 시작 색상을
_endColor = impactColor; // 설정할거에요 -> 끝 색상을
_material.color = _startColor; // 적용할거에요 -> 초기 색상을
_isShowing = true; // 설정할거에요 -> 표시 시작
_indicatorObj.SetActive(true); // 켤거에요 -> 표시 오브젝트를
}
// ══════════════════════════════════════════════════════════
// 원형 메쉬 생성 (XZ 평면, 양면)
// ══════════════════════════════════════════════════════════
// ⚠️ 양면 렌더링: 윗면 + 아랫면 삼각형 모두 생성
// → 카메라가 위에서 보든 아래서 보든 무조건 보임
// → 와인딩 문제 완전 해결
// ══════════════════════════════════════════════════════════
private Mesh CreateCircleMesh(float radius) // 함수를 선언할거에요 -> 원형 메쉬를 만드는
{
Mesh mesh = new Mesh(); // 생성할거에요 -> 새 메쉬를
mesh.name = "CircleIndicator"; // 이름을 지을거에요
int seg = meshSegments; // 가져올거에요 -> 분할 수를
Vector3[] verts = new Vector3[seg + 1]; // 생성할거에요 -> 정점 배열을
int[] tris = new int[seg * 3 * 2]; // 생성할거에요 -> 삼각형 배열을 (양면이라 ×2)
verts[0] = Vector3.zero; // 설정할거에요 -> 중심점을
float step = 360f / seg; // 계산할거에요 -> 분할 각도를
for (int i = 0; i < seg; i++) // 반복할거에요 -> 둘레 정점마다
{
float rad = i * step * Mathf.Deg2Rad; // 계산할거에요 -> 각도를 (라디안)
verts[i + 1] = new Vector3(
Mathf.Sin(rad) * radius, // X = sin × 반지름
0f, // Y = 0 (바닥 평면!)
Mathf.Cos(rad) * radius // Z = cos × 반지름
);
int next = (i + 1) % seg + 1; // 계산할거에요 -> 다음 둘레점 인덱스를
// ── 윗면 삼각형 (법선 위, 카메라에서 보임) ───────
int ti = i * 6; // 계산할거에요 -> 삼각형 시작 인덱스를 (양면이라 6개씩)
tris[ti] = 0; // 중심
tris[ti + 1] = i + 1; // 현재 둘레점
tris[ti + 2] = next; // 다음 둘레점 → 시계 방향(위에서 봤을 때) = 법선 UP
// ── 아랫면 삼각형 (법선 아래, 안전장치) ──────────
tris[ti + 3] = 0; // 중심
tris[ti + 4] = next; // 다음 둘레점
tris[ti + 5] = i + 1; // 현재 둘레점 → 반시계(위에서) = 법선 DOWN
}
mesh.vertices = verts; // 적용할거에요 -> 정점을
mesh.triangles = tris; // 적용할거에요 -> 삼각형을 (양면)
mesh.RecalculateNormals(); // 계산할거에요 -> 법선을
mesh.RecalculateBounds(); // 계산할거에요 -> 바운드를
return mesh; // 반환할거에요
}
// ══════════════════════════════════════════════════════════
// 부채꼴 메쉬 생성 (XZ 평면, 양면)
// ══════════════════════════════════════════════════════════
private Mesh CreateFanMesh(float radius, float angle) // 함수를 선언할거에요 -> 부채꼴 메쉬를 만드는
{
Mesh mesh = new Mesh(); // 생성할거에요 -> 새 메쉬를
mesh.name = "FanIndicator"; // 이름을 지을거에요
int seg = meshSegments; // 가져올거에요 -> 분할 수를
Vector3[] verts = new Vector3[seg + 2]; // 생성할거에요 -> 정점 배열을
int[] tris = new int[seg * 3 * 2]; // 생성할거에요 -> 삼각형 배열을 (양면 ×2)
verts[0] = Vector3.zero; // 설정할거에요 -> 중심점을
float halfAngle = angle * 0.5f; // 계산할거에요 -> 반각을
float startAng = -halfAngle; // 계산할거에요 -> 시작 각도를
float stepAng = angle / seg; // 계산할거에요 -> 분할 각도를
for (int i = 0; i <= seg; i++) // 반복할거에요 -> 호 정점마다
{
float rad = (startAng + i * stepAng) * Mathf.Deg2Rad; // 계산할거에요 -> 각도를 (라디안)
verts[i + 1] = new Vector3(
Mathf.Sin(rad) * radius, // X
0f, // Y = 0 (바닥!)
Mathf.Cos(rad) * radius // Z (forward 기준)
);
}
for (int i = 0; i < seg; i++) // 반복할거에요 -> 삼각형마다
{
// ── 윗면 (법선 UP) ──────────────────────────────
int ti = i * 6; // 계산할거에요 -> 인덱스를
tris[ti] = 0; // 중심
tris[ti + 1] = i + 1; // 현재 호점
tris[ti + 2] = i + 2; // 다음 호점 → 시계(위에서) = 법선 UP
// ── 아랫면 (법선 DOWN, 안전장치) ────────────────
tris[ti + 3] = 0; // 중심
tris[ti + 4] = i + 2; // 다음 호점
tris[ti + 5] = i + 1; // 현재 호점 → 반시계 = 법선 DOWN
}
mesh.vertices = verts; // 적용할거에요 -> 정점을
mesh.triangles = tris; // 적용할거에요 -> 삼각형을 (양면)
mesh.RecalculateNormals(); // 계산할거에요 -> 법선을
mesh.RecalculateBounds(); // 계산할거에요 -> 바운드를
return mesh; // 반환할거에요
}
// ══════════════════════════════════════════════════════════
// 정리
// ══════════════════════════════════════════════════════════
private void OnDestroy() // 함수를 실행할거에요 -> 파괴될 때
{
if (_material != null) Destroy(_material); // 파괴할거에요 -> 머티리얼 누수 방지
if (_indicatorObj != null) Destroy(_indicatorObj); // 파괴할거에요 -> 표시 오브젝트를
}
}