Files
Colosseum/Assets/_Game/Scripts/Editor/AllyTargetingDebugTest.cs
dal4segno 8cd2623163 feat: 아군 타게팅 시스템 구현 — SingleAlly 투사체형 치유/보호막
- 치유/보호막 스킬을 즉발 자가시전에서 투사체형 아군 1인 타겟팅으로 전환

- TargetType.SingleAlly 추가, targetOverride 매개변수로 외부 타겟 주입 지원

- PlayerSkillInput: 카메라 레이캐스트 기반 아군 탐지, 서버 검증, RPC 타겟 ID 전달

- AllyTargetIndicator: 호버 아군 위에 디스크 인디케이터 표시, 사거리/초과 색상 변경

- SpawnEffect: 타겟 방향 회전 보정

- 투사체 스폰 이펙트 에셋 생성 (치유/보호막 각각)

- 인디케이터 프리팹 + URP/Unlit 머티리얼 생성

- Player 프리팹에 AllyTargetIndicator 컴포넌트 추가 및 설정

- Input.mousePosition → Mouse.current.position.ReadValue() 수정 (Input System 호환)
2026-03-31 23:06:13 +09:00

288 lines
11 KiB
C#

using System;
using System.Collections;
using System.Reflection;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Colosseum.Player
{
/// <summary>
/// 아군 타게팅 시스템의 시각적 데모 + 설정 검증.
/// BalanceDummy 씬에서 TrainingDummy를 임시 아군으로 만들고,
/// 인디케이터 프리팹 표시 → 치유 스킬 투사체 발사를 실제로 보여줍니다.
/// </summary>
public class AllyTargetingDebugTest : MonoBehaviour
{
[Header("테스트 설정")]
[Tooltip("가짜 아군으로 사용할 GameObject 이름")]
[SerializeField] private string allyTargetName = "TrainingDummy";
[Tooltip("치유 스킬 에셋 경로")]
[SerializeField] private string healSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset";
[Tooltip("보호막 스킬 에셋 경로")]
[SerializeField] private string shieldSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset";
[Tooltip("인디케이터 높이 오프셋")]
[Min(0f)] [SerializeField] private float indicatorHeight = 2.2f;
[Tooltip("시작 지연 (초)")]
[Min(0.5f)] [SerializeField] private float startDelay = 2f;
// 복원용
private TeamType originalTeamType;
private GameObject spawnedIndicator;
private GameObject allyObject;
private void Start()
{
Debug.Log("═══════════════════════════════════════════════════");
Debug.Log("[AllyTargetDemo] 아군 타게팅 시각적 데모 시작");
Debug.Log("═══════════════════════════════════════════════════");
StartCoroutine(RunDemo());
}
private void OnDestroy()
{
// 플레이 종료 시 복원
if (allyObject != null)
{
RestoreTeam();
DestroyIndicator();
}
}
private IEnumerator RunDemo()
{
yield return new WaitForSeconds(startDelay);
var player = FindPlayer();
if (player == null)
{
Debug.LogError("[AllyTargetDemo] 플레이어를 찾을 수 없습니다.");
EndDemo();
yield break;
}
allyObject = GameObject.Find(allyTargetName);
if (allyObject == null)
{
Debug.LogError($"[AllyTargetDemo] '{allyTargetName}'을(를) 찾을 수 없습니다.");
EndDemo();
yield break;
}
// ── 1. TrainingDummy 팀을 임시 Player로 변경 ──
var allyTeam = allyObject.GetComponent<Team>();
if (allyTeam == null)
{
Debug.LogError($"[AllyTargetDemo] {allyTargetName}에 Team 컴포넌트가 없습니다.");
EndDemo();
yield break;
}
originalTeamType = allyTeam.TeamType;
SetTeamType(allyTeam, TeamType.Player);
Debug.Log($"[AllyTargetDemo] {allyTargetName} 팀 변경: {originalTeamType} → Player");
// 거리 로그
float dist = Vector3.Distance(player.transform.position, allyObject.transform.position);
Debug.Log($"[AllyTargetDemo] 플레이어 ↔ 아군 거리: {dist:F1}m");
// ── 2. 인디케이터 프리팹 표시 (초록색) ──
yield return ShowIndicator(player, allyObject);
// ── 3. 2초 대기 (인디케이터 관찰 시간) ──
Debug.Log("[AllyTargetDemo] ▶ 인디케이터 표시 중... 2초 대기");
yield return new WaitForSeconds(2f);
// ── 4. 인디케이터 색상 빨간색으로 변경 (거리 초과 시뮬레이션) ──
yield return ChangeIndicatorColor(new Color(1f, 0.3f, 0.3f, 0.8f));
Debug.Log("[AllyTargetDemo] ▶ 인디케이터 빨간색으로 변경 (거리 초과 시뮬) — 2초 대기");
yield return new WaitForSeconds(2f);
// ── 5. 인디케이터 제거 후 잠시 대기 ──
DestroyIndicator();
Debug.Log("[AllyTargetDemo] ▶ 인디케이터 제거");
yield return new WaitForSeconds(1f);
// ── 6. 치유 스킬 실행 (투사체 발사) ──
Debug.Log("[AllyTargetDemo] ▶ 치유 스킬 투사체 발사...");
yield return ExecuteSkillVisual(player, allyObject, healSkillPath, "치유");
yield return new WaitForSeconds(3f);
// ── 7. 보호막 스킬 실행 (투사체 발사) ──
Debug.Log("[AllyTargetDemo] ▶ 보호막 스킬 투사체 발사...");
yield return ExecuteSkillVisual(player, allyObject, shieldSkillPath, "보호막");
yield return new WaitForSeconds(3f);
// ── 8. 인디케이터 다시 표시 (복원 데모) ──
yield return ShowIndicator(player, allyObject);
Debug.Log("[AllyTargetDemo] ▶ 인디케이터 최종 표시 — 3초 후 종료");
yield return new WaitForSeconds(3f);
// ── 9. 정리 ──
DestroyIndicator();
RestoreTeam();
Debug.Log("[AllyTargetDemo] 팀 복원 완료");
Debug.Log("═══════════════════════════════════════════════════");
Debug.Log("<color=green>[AllyTargetDemo] 데모 완료</color>");
Debug.Log("═══════════════════════════════════════════════════");
EndDemo();
}
#region
private IEnumerator ShowIndicator(GameObject player, GameObject ally)
{
// 플레이어의 AllyTargetIndicator에서 프리팹 참조 가져오기
var indicator = player.GetComponent<UI.AllyTargetIndicator>();
if (indicator == null)
{
Debug.LogError("[AllyTargetDemo] AllyTargetIndicator 컴포넌트가 없습니다.");
yield break;
}
var prefabField = typeof(UI.AllyTargetIndicator).GetField("indicatorPrefab",
BindingFlags.NonPublic | BindingFlags.Instance);
if (prefabField == null)
{
Debug.LogError("[AllyTargetDemo] indicatorPrefab 필드 접근 실패.");
yield break;
}
var prefab = prefabField.GetValue(indicator) as GameObject;
if (prefab == null)
{
Debug.LogError("[AllyTargetDemo] indicatorPrefab이 null입니다.");
yield break;
}
// 아군 머리 위에 인스턴스화
Vector3 pos = ally.transform.position;
pos.y += indicatorHeight;
spawnedIndicator = Instantiate(prefab, pos, Quaternion.Euler(90f, 0f, 0f));
// 초록색으로 설정
var renderer = spawnedIndicator.GetComponent<Renderer>();
if (renderer != null)
{
renderer.material.color = new Color(0.2f, 1f, 0.2f, 0.8f);
}
Debug.Log($"[AllyTargetDemo] 인디케이터 표시: {prefab.name} @ {ally.name} (y+{indicatorHeight})");
yield return null;
}
private IEnumerator ChangeIndicatorColor(Color color)
{
if (spawnedIndicator == null) yield break;
var renderer = spawnedIndicator.GetComponent<Renderer>();
if (renderer != null)
{
renderer.material.color = color;
Debug.Log($"[AllyTargetDemo] 인디케이터 색상 변경: ({color.r:F1}, {color.g:F1}, {color.b:F1}, {color.a:F1})");
}
yield return null;
}
private IEnumerator ExecuteSkillVisual(GameObject player, GameObject ally, string skillPath, string skillLabel)
{
#if UNITY_EDITOR
var skillData = AssetDatabase.LoadAssetAtPath<Skills.SkillData>(skillPath);
if (skillData == null)
{
Debug.LogError($"[AllyTargetDemo] 스킬 로드 실패: {skillPath}");
yield break;
}
var entry = Skills.SkillLoadoutEntry.CreateTemporary(skillData);
bool hasSingleAlly = entry.HasEffectWithTargetType(Skills.TargetType.SingleAlly);
Debug.Log($"[AllyTargetDemo] {skillLabel}: SingleAlly 효과 있음={hasSingleAlly}");
var skillController = player.GetComponent<Skills.SkillController>();
if (skillController == null)
{
Debug.LogError("[AllyTargetDemo] SkillController가 없습니다.");
yield break;
}
if (skillController.IsExecutingSkill)
{
Debug.LogWarning($"[AllyTargetDemo] 스킬 실행 중 — 대기");
yield return new WaitUntil(() => !skillController.IsExecutingSkill);
}
// 플레이어를 아군 방향으로 회전
Vector3 direction = (ally.transform.position - player.transform.position);
direction.y = 0;
if (direction.sqrMagnitude > 0.001f)
{
player.transform.rotation = Quaternion.LookRotation(direction);
}
Debug.Log($"[AllyTargetDemo] {skillLabel} 실행 → 타겟: {ally.name}");
bool success = skillController.ExecuteSkill(entry, ally);
if (success)
Debug.Log($"<color=green>[AllyTargetDemo] {skillLabel} 스킬 발동 성공 — 투사체 확인!</color>");
else
Debug.LogError($"<AllyTargetDemo] {skillLabel} 스킬 발동 실패");
#else
Debug.LogError("[AllyTargetDemo] 에디터 전용");
#endif
yield return null;
}
#endregion
#region
private static GameObject FindPlayer()
{
var controllers = FindObjectsOfType<PlayerNetworkController>();
return controllers.Length > 0 ? controllers[0].gameObject : null;
}
private static void SetTeamType(Team team, TeamType type)
{
var field = typeof(Team).GetField("teamType",
BindingFlags.NonPublic | BindingFlags.Instance);
field?.SetValue(team, type);
}
private void RestoreTeam()
{
if (allyObject == null) return;
var allyTeam = allyObject.GetComponent<Team>();
if (allyTeam != null)
{
SetTeamType(allyTeam, originalTeamType);
Debug.Log($"[AllyTargetDemo] 팀 복원: {allyObject.name} → {originalTeamType}");
}
}
private void DestroyIndicator()
{
if (spawnedIndicator != null)
{
Destroy(spawnedIndicator);
spawnedIndicator = null;
}
}
private static void EndDemo()
{
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#endif
}
#endregion
}
}