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를 _meshRenderer = _indicatorObj.AddComponent(); // 추가할거에요 -> 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); // 파괴할거에요 -> 표시 오브젝트를 } }