chore: Assets 디렉토리 구조 정리 및 네이밍 컨벤션 적용

- Assets/_Game/ 하위로 게임 에셋 통합
- External/ 패키지 벤더별 분류 (Synty, Animations, UI)
- 에셋 네이밍 컨벤션 확립 및 적용
  (Data_Skill_, Data_SkillEffect_, Prefab_, Anim_, Model_, BT_ 등)
- pre-commit hook으로 네이밍 컨벤션 자동 검사 추가
- RESTRUCTURE_CHECKLIST.md 작성

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 19:08:27 +09:00
parent 309bf5f48b
commit c265f980db
17251 changed files with 2630777 additions and 206 deletions

View File

@@ -0,0 +1,76 @@
using System;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "ChaseTarget", story: "타겟 추적", category: "Action", id: "0889fbb015b8bf414ef569af08bb6868")]
public partial class ChaseTargetAction : Action
{
[SerializeReference]
public BlackboardVariable<GameObject> Target;
[SerializeReference]
public BlackboardVariable<float> Speed = new BlackboardVariable<float>(0f);
[SerializeReference]
public BlackboardVariable<float> StopDistance = new BlackboardVariable<float>(2f);
private UnityEngine.AI.NavMeshAgent agent;
protected override Status OnStart()
{
if (Target.Value == null)
{
return Status.Failure;
}
agent = GameObject.GetComponent<UnityEngine.AI.NavMeshAgent>();
if (agent == null)
{
Debug.LogWarning("[ChaseTarget] NavMeshAgent not found");
return Status.Failure;
}
// Speed가 0 이하면 NavMeshAgent의 기존 speed 유지 (EnemyData에서 설정한 값)
if (Speed.Value > 0f)
{
agent.speed = Speed.Value;
}
agent.stoppingDistance = StopDistance.Value;
agent.isStopped = false;
return Status.Running;
}
protected override Status OnUpdate()
{
if (Target.Value == null)
{
return Status.Failure;
}
// 이미 사거리 내에 있으면 성공
float distance = Vector3.Distance(GameObject.transform.position, Target.Value.transform.position);
if (distance <= StopDistance.Value)
{
agent.isStopped = true;
return Status.Success;
}
// 타겟 위치로 이동
agent.SetDestination(Target.Value.transform.position);
return Status.Running;
}
protected override void OnEnd()
{
if (agent != null)
{
agent.isStopped = true;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14296786101ccd742ac9f752f1fd3393

View File

@@ -0,0 +1,50 @@
using System;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
using Colosseum.Combat;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "FindTarget", story: "[타겟] ", category: "Action", id: "bb947540549026f3c5625c6d19213311")]
public partial class FindTargetAction : Action
{
[SerializeReference]
public BlackboardVariable<GameObject> Target;
[SerializeReference]
public BlackboardVariable<string> Tag = new BlackboardVariable<string>("Player");
protected override Status OnStart()
{
if (Tag.Value == null || string.IsNullOrEmpty(Tag.Value))
{
return Status.Failure;
}
// 모든 타겟 후보 검색
GameObject[] candidates = GameObject.FindGameObjectsWithTag(Tag.Value);
if (candidates == null || candidates.Length == 0)
{
return Status.Failure;
}
// 사망하지 않은 타겟 찾기
foreach (GameObject candidate in candidates)
{
IDamageable damageable = candidate.GetComponent<IDamageable>();
// IDamageable이 없거나 살아있는 경우 타겟으로 선택
if (damageable == null || !damageable.IsDead)
{
Target.Value = candidate;
return Status.Success;
}
}
// 살아있는 타겟이 없음
return Status.Failure;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f569cb803bceb7c4291d0f3074346741

View File

@@ -0,0 +1,22 @@
using System;
using Unity.Behavior;
using UnityEngine;
[Serializable, Unity.Properties.GeneratePropertyBag]
[Condition(name: "isDie", story: "죽었는지 확인", category: "Conditions", id: "8067176f9f490e7d974824f8087de448")]
public partial class IsDieCondition : Condition
{
public override bool IsTrue()
{
return true;
}
public override void OnStart()
{
}
public override void OnEnd()
{
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3d2c81704b2be4c4289bcf5555059b87

View File

@@ -0,0 +1,63 @@
using System;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "PlayAnimation", story: "[애니메이션] ", category: "Action", id: "b558ee0df6e00cd2e6bb2273b4f59cd2")]
public partial class PlayAnimationAction : Action
{
[SerializeReference]
public BlackboardVariable<string> AnimationName = new BlackboardVariable<string>("");
[SerializeReference]
public BlackboardVariable<bool> WaitForCompletion = new BlackboardVariable<bool>(true);
private Animator animator;
private bool animationStarted;
protected override Status OnStart()
{
animator = GameObject.GetComponentInChildren<Animator>();
if (animator == null || string.IsNullOrEmpty(AnimationName.Value))
{
return Status.Failure;
}
animator.Play(AnimationName.Value);
animationStarted = true;
if (!WaitForCompletion.Value)
{
return Status.Success;
}
return Status.Running;
}
protected override Status OnUpdate()
{
if (!WaitForCompletion.Value)
{
return Status.Success;
}
if (animator == null)
{
return Status.Failure;
}
var stateInfo = animator.GetCurrentAnimatorStateInfo(0);
// 애니메이션이 완료되었는지 확인
if (stateInfo.IsName(AnimationName.Value) && stateInfo.normalizedTime >= 1f)
{
return Status.Success;
}
return Status.Running;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3bc57afe07d75aa4e80ef5394b57f774

View File

@@ -0,0 +1,54 @@
using System;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "RotateToTarget", story: "[대상을] ", category: "Action", id: "30341f7e1af0565c0aca0253341b3e28")]
public partial class RotateToTargetAction : Action
{
[SerializeReference]
public BlackboardVariable<GameObject> Target;
[SerializeReference]
public BlackboardVariable<float> RotationSpeed = new BlackboardVariable<float>(10f);
[SerializeReference]
public BlackboardVariable<float> AngleThreshold = new BlackboardVariable<float>(5f);
protected override Status OnUpdate()
{
if (Target.Value == null)
{
return Status.Failure;
}
Vector3 direction = Target.Value.transform.position - GameObject.transform.position;
direction.y = 0f;
if (direction == Vector3.zero)
{
return Status.Success;
}
Quaternion targetRotation = Quaternion.LookRotation(direction);
float angleDifference = Quaternion.Angle(GameObject.transform.rotation, targetRotation);
// 임계값 이내면 성공
if (angleDifference <= AngleThreshold.Value)
{
return Status.Success;
}
// 부드럽게 회전
GameObject.transform.rotation = Quaternion.Slerp(
GameObject.transform.rotation,
targetRotation,
RotationSpeed.Value * Time.deltaTime
);
return Status.Running;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 33182146e0ade8443a539cf7780735e2

View File

@@ -0,0 +1,33 @@
using System;
using Unity.Behavior;
using UnityEngine;
using UnityEngine.EventSystems;
using Action = Unity.Behavior.Action;
using Unity.Properties;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "SetAnimatorTrigger", story: "애니메이션 [트리거] ", category: "Action", id: "bfef104cbd43df3d01f24570a4caa8ed")]
public partial class SetAnimatorTriggerAction : Action
{
[SerializeReference]
public BlackboardVariable<string> TriggerName = new BlackboardVariable<string>("");
protected override Status OnStart()
{
if (string.IsNullOrEmpty(TriggerName.Value))
{
return Status.Failure;
}
Animator animator = GameObject.GetComponentInChildren<Animator>();
if (animator == null)
{
return Status.Failure;
}
animator.SetTrigger(TriggerName.Value);
return Status.Success;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 77cf4f2ff35fe3040af16374f428a648

View File

@@ -0,0 +1,70 @@
using System;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
using Colosseum.Combat;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "SetTargetInRange", story: "[거리] [] ", category: "Action", id: "93b7a5d823a58618d5371c01ef894948")]
public partial class SetTargetInRangeAction : Action
{
[SerializeReference]
public BlackboardVariable<GameObject> Target;
[SerializeReference]
public BlackboardVariable<string> Tag = new BlackboardVariable<string>("Player");
[SerializeReference]
public BlackboardVariable<float> Range = new BlackboardVariable<float>(10f);
protected override Status OnStart()
{
if (string.IsNullOrEmpty(Tag.Value))
{
return Status.Failure;
}
// 모든 타겟 태그 오브젝트 찾기
GameObject[] targets = GameObject.FindGameObjectsWithTag(Tag.Value);
if (targets == null || targets.Length == 0)
{
return Status.Failure;
}
// 가장 가까운 살아있는 타겟 찾기
GameObject nearestTarget = null;
float nearestDistance = Range.Value; // Range 내에서만 검색
foreach (GameObject potentialTarget in targets)
{
// 사망한 타겟은 제외
IDamageable damageable = potentialTarget.GetComponent<IDamageable>();
if (damageable != null && damageable.IsDead)
{
continue;
}
float distance = Vector3.Distance(
GameObject.transform.position,
potentialTarget.transform.position
);
if (distance < nearestDistance)
{
nearestDistance = distance;
nearestTarget = potentialTarget;
}
}
if (nearestTarget == null)
{
return Status.Failure;
}
Target.Value = nearestTarget;
return Status.Success;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 02cd3ab41f67bf344b667b6a0c12a4d0

View File

@@ -0,0 +1,24 @@
using System;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "StopMovement", story: "이동 정지", category: "Action", id: "1a0e2eb87421ed94502031790df56f37")]
public partial class StopMovementAction : Action
{
protected override Status OnStart()
{
UnityEngine.AI.NavMeshAgent agent = GameObject.GetComponent<UnityEngine.AI.NavMeshAgent>();
if (agent != null)
{
agent.isStopped = true;
}
return Status.Success;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: afacb51ec60675d498bfc8b7ce942368

View File

@@ -0,0 +1,117 @@
using System;
using Colosseum.AI;
using Colosseum.Skills;
using Unity.Behavior;
using Unity.Properties;
using UnityEngine;
using Action = Unity.Behavior.Action;
/// <summary>
/// 보스 패턴을 실행하는 Behavior Tree Action.
/// 패턴 내 스텝(스킬 또는 대기)을 순서대로 실행하며, 패턴 쿨타임을 관리합니다.
/// </summary>
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "Use Pattern", story: "[Pattern] ", category: "Action", id: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6")]
public partial class UsePatternAction : Action
{
[SerializeReference] public BlackboardVariable<BossPatternData> Pattern;
private SkillController skillController;
private int currentStepIndex;
private float waitEndTime;
private bool isWaiting;
private float lastUsedTime = float.MinValue;
protected override Status OnStart()
{
if (Pattern?.Value == null)
{
Debug.LogWarning("[UsePatternAction] 패턴이 null입니다.");
return Status.Failure;
}
if (Time.time - lastUsedTime < Pattern.Value.Cooldown)
{
return Status.Failure;
}
if (Pattern.Value.Steps.Count == 0)
{
return Status.Failure;
}
skillController = GameObject.GetComponent<SkillController>();
if (skillController == null)
{
Debug.LogWarning($"[UsePatternAction] SkillController를 찾을 수 없습니다: {GameObject.name}");
return Status.Failure;
}
currentStepIndex = 0;
isWaiting = false;
return ExecuteCurrentStep();
}
protected override Status OnUpdate()
{
if (skillController == null)
return Status.Failure;
if (isWaiting)
{
if (Time.time < waitEndTime)
return Status.Running;
isWaiting = false;
}
else
{
if (skillController.IsPlayingAnimation)
return Status.Running;
}
currentStepIndex++;
if (currentStepIndex >= Pattern.Value.Steps.Count)
{
lastUsedTime = Time.time;
return Status.Success;
}
return ExecuteCurrentStep();
}
protected override void OnEnd()
{
skillController = null;
}
private Status ExecuteCurrentStep()
{
PatternStep step = Pattern.Value.Steps[currentStepIndex];
if (step.Type == PatternStepType.Wait)
{
isWaiting = true;
waitEndTime = Time.time + step.Duration;
return Status.Running;
}
// PatternStepType.Skill
if (step.Skill == null)
{
Debug.LogWarning($"[UsePatternAction] 스킬이 null입니다. (index {currentStepIndex})");
return Status.Failure;
}
bool success = skillController.ExecuteSkill(step.Skill);
if (!success)
{
Debug.LogWarning($"[UsePatternAction] 스킬 실행 실패: {step.Skill.SkillName} (index {currentStepIndex})");
return Status.Failure;
}
return Status.Running;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a696fff0581f7264d9491514e9aee277

View File

@@ -0,0 +1,69 @@
using System;
using Colosseum.Skills;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
/// <summary>
/// 지정된 스킬을 사용하는 Behavior Tree Action.
/// 스킬 실행이 완료될 때까지 Running 상태를 유지합니다.
/// </summary>
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "Use Skill", story: "[스킬] ", category: "Action", id: "799f1e8cfafa78b2d52ef61a6bbb29b9")]
public partial class UseSkillAction : Action
{
[SerializeReference] public BlackboardVariable<SkillData> ;
private SkillController skillController;
protected override Status OnStart()
{
// 스킬 데이터 확인
if (?.Value == null)
{
Debug.LogWarning("[UseSkillAction] 스킬이 null입니다.");
return Status.Failure;
}
// SkillController 컴포넌트 가져오기
skillController = GameObject.GetComponent<SkillController>();
if (skillController == null)
{
Debug.LogWarning($"[UseSkillAction] SkillController를 찾을 수 없습니다: {GameObject.name}");
return Status.Failure;
}
// 스킬 실행 시도
bool success = skillController.ExecuteSkill(.Value);
if (!success)
{
// 이미 다른 스킬 사용 중이거나 쿨타임
return Status.Failure;
}
return Status.Running;
}
protected override Status OnUpdate()
{
// SkillController가 해제된 경우
if (skillController == null)
{
return Status.Failure;
}
// 스킬 애니메이션이 종료되면 성공
if (!skillController.IsPlayingAnimation)
{
return Status.Success;
}
return Status.Running;
}
protected override void OnEnd()
{
skillController = null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cf140a11f5104674fb8367f4eb5702cb

View File

@@ -0,0 +1,31 @@
using System;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;
[Serializable, GeneratePropertyBag]
[NodeDescription(name: "Wait", story: "대기", category: "Action", id: "73b84bd4bc0eb61c998ac84c9853a69d")]
public partial class WaitAction : Action
{
[SerializeReference]
public BlackboardVariable<float> Duration = new BlackboardVariable<float>(1f);
private float startTime;
protected override Status OnStart()
{
startTime = Time.time;
return Status.Running;
}
protected override Status OnUpdate()
{
if (Time.time - startTime >= Duration.Value)
{
return Status.Success;
}
return Status.Running;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 324b69471491c9448ae5d71a426dd596