309 lines
20 KiB
C#
309 lines
20 KiB
C#
|
|
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); // 파괴할거에요 -> 표시 오브젝트를
|
|||
|
|
}
|
|||
|
|
}
|