코드 리팩토링

재사용성 및 확장성을 고려하여 코드 전반을 리팩토링함
This commit is contained in:
2026-01-21 01:45:15 +09:00
parent b4ac8f600f
commit db5db4b106
45 changed files with 2775 additions and 248 deletions

View File

@@ -0,0 +1,215 @@
using System;
using System.Collections;
using UnityEngine;
/// <summary>
/// Reusable action execution system with busy state management.
/// Replaces repeated coroutine busy-state patterns across the codebase.
/// </summary>
public class ActionExecutor : MonoBehaviour
{
public bool IsBusy { get; private set; }
private Coroutine _currentAction;
/// <summary>
/// Event fired when an action starts.
/// </summary>
public event Action OnActionStarted;
/// <summary>
/// Event fired when an action completes.
/// </summary>
public event Action OnActionCompleted;
/// <summary>
/// Event fired when an action is cancelled.
/// </summary>
public event Action OnActionCancelled;
/// <summary>
/// Try to execute an action. Returns false if already busy.
/// </summary>
/// <param name="request">The action request to execute</param>
/// <returns>True if action started, false if busy</returns>
public bool TryExecute(ActionRequest request)
{
if (IsBusy) return false;
_currentAction = StartCoroutine(ExecuteRoutine(request));
return true;
}
/// <summary>
/// Cancel the current action if one is running.
/// </summary>
public void Cancel()
{
if (_currentAction != null)
{
StopCoroutine(_currentAction);
_currentAction = null;
IsBusy = false;
OnActionCancelled?.Invoke();
}
}
/// <summary>
/// Force reset the busy state. Use with caution.
/// </summary>
public void ForceReset()
{
if (_currentAction != null)
{
StopCoroutine(_currentAction);
_currentAction = null;
}
IsBusy = false;
}
private IEnumerator ExecuteRoutine(ActionRequest request)
{
IsBusy = true;
OnActionStarted?.Invoke();
// Pre-action callback
request.OnStart?.Invoke();
// Animation trigger
if (request.Animator != null && !string.IsNullOrEmpty(request.AnimTrigger))
{
if (request.AnimSpeed > 0)
{
request.Animator.SetFloat("ActionSpeed", request.AnimSpeed);
}
request.Animator.SetTrigger(request.AnimTrigger);
}
// Wait for impact point
if (request.ImpactDelay > 0)
{
float adjustedDelay = request.AnimSpeed > 0
? request.ImpactDelay / request.AnimSpeed
: request.ImpactDelay;
yield return new WaitForSeconds(adjustedDelay);
}
// Execute main effect
request.OnImpact?.Invoke();
// Wait for remaining duration
float remainingTime = request.TotalDuration - request.ImpactDelay;
if (request.AnimSpeed > 0)
{
remainingTime /= request.AnimSpeed;
}
if (remainingTime > 0)
{
yield return new WaitForSeconds(remainingTime);
}
// Completion callback
request.OnComplete?.Invoke();
IsBusy = false;
_currentAction = null;
OnActionCompleted?.Invoke();
}
}
/// <summary>
/// Configuration for an action to be executed by ActionExecutor.
/// </summary>
[Serializable]
public struct ActionRequest
{
/// <summary>
/// Animator to trigger animations on.
/// </summary>
public Animator Animator;
/// <summary>
/// Animation trigger name.
/// </summary>
public string AnimTrigger;
/// <summary>
/// Animation playback speed multiplier.
/// </summary>
public float AnimSpeed;
/// <summary>
/// Time delay before the impact/effect happens (for syncing with animation).
/// </summary>
public float ImpactDelay;
/// <summary>
/// Total duration of the action.
/// </summary>
public float TotalDuration;
/// <summary>
/// Callback invoked when action starts.
/// </summary>
public Action OnStart;
/// <summary>
/// Callback invoked at the impact moment.
/// </summary>
public Action OnImpact;
/// <summary>
/// Callback invoked when action completes.
/// </summary>
public Action OnComplete;
/// <summary>
/// Create a simple action request with just timing.
/// </summary>
public static ActionRequest Simple(float duration, Action onComplete)
{
return new ActionRequest
{
TotalDuration = duration,
AnimSpeed = 1f,
OnComplete = onComplete
};
}
/// <summary>
/// Create an animated action request.
/// </summary>
public static ActionRequest Animated(Animator animator, string trigger, float duration,
float impactDelay, Action onImpact, float speed = 1f)
{
return new ActionRequest
{
Animator = animator,
AnimTrigger = trigger,
AnimSpeed = speed,
ImpactDelay = impactDelay,
TotalDuration = duration,
OnImpact = onImpact
};
}
/// <summary>
/// Create a full action request with all callbacks.
/// </summary>
public static ActionRequest Full(Animator animator, string trigger, float duration,
float impactDelay, float speed, Action onStart, Action onImpact, Action onComplete)
{
return new ActionRequest
{
Animator = animator,
AnimTrigger = trigger,
AnimSpeed = speed,
ImpactDelay = impactDelay,
TotalDuration = duration,
OnStart = onStart,
OnImpact = onImpact,
OnComplete = onComplete
};
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 27e07922480ecf44faf5135ad4872531

View File

@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Utility class for common physics queries.
/// Eliminates repeated OverlapSphere patterns across the codebase.
/// </summary>
public static class PhysicsQueryUtility
{
// Reusable buffer to avoid allocations (32 should be enough for most cases)
private static readonly Collider[] OverlapBuffer = new Collider[32];
/// <summary>
/// Find the closest object implementing interface T within radius.
/// </summary>
/// <typeparam name="T">Interface or component type to search for</typeparam>
/// <param name="origin">Center point of the search</param>
/// <param name="radius">Search radius</param>
/// <param name="layerMask">Layer mask to filter objects</param>
/// <returns>Closest object of type T, or null if none found</returns>
public static T FindClosest<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
{
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
T closest = null;
float minDistSqr = float.MaxValue;
for (int i = 0; i < count; i++)
{
T component = OverlapBuffer[i].GetComponentInParent<T>();
if (component != null)
{
float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude;
if (distSqr < minDistSqr)
{
minDistSqr = distSqr;
closest = component;
}
}
}
return closest;
}
/// <summary>
/// Find the closest object implementing interface T within radius, also returning distance.
/// </summary>
public static T FindClosest<T>(Vector3 origin, float radius, LayerMask layerMask, out float distance) where T : class
{
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
T closest = null;
float minDistSqr = float.MaxValue;
for (int i = 0; i < count; i++)
{
T component = OverlapBuffer[i].GetComponentInParent<T>();
if (component != null)
{
float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude;
if (distSqr < minDistSqr)
{
minDistSqr = distSqr;
closest = component;
}
}
}
distance = closest != null ? Mathf.Sqrt(minDistSqr) : 0f;
return closest;
}
/// <summary>
/// Execute action on all objects of type T within radius.
/// </summary>
/// <typeparam name="T">Interface or component type to search for</typeparam>
/// <param name="origin">Center point of the search</param>
/// <param name="radius">Search radius</param>
/// <param name="layerMask">Layer mask to filter objects</param>
/// <param name="action">Action to execute on each found object</param>
public static void ForEachInRadius<T>(Vector3 origin, float radius, LayerMask layerMask, Action<T> action) where T : class
{
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
// Track processed objects to avoid duplicates (same object hit by multiple colliders)
var processed = new HashSet<T>();
for (int i = 0; i < count; i++)
{
T component = OverlapBuffer[i].GetComponentInParent<T>();
if (component != null && !processed.Contains(component))
{
processed.Add(component);
action(component);
}
}
}
/// <summary>
/// Execute action on all objects of type T within radius, with additional context.
/// </summary>
public static void ForEachInRadius<T>(Vector3 origin, float radius, LayerMask layerMask,
Action<T, Collider> action) where T : class
{
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
var processed = new HashSet<T>();
for (int i = 0; i < count; i++)
{
T component = OverlapBuffer[i].GetComponentInParent<T>();
if (component != null && !processed.Contains(component))
{
processed.Add(component);
action(component, OverlapBuffer[i]);
}
}
}
/// <summary>
/// Get all objects of type T within radius.
/// </summary>
/// <typeparam name="T">Interface or component type to search for</typeparam>
/// <param name="origin">Center point of the search</param>
/// <param name="radius">Search radius</param>
/// <param name="layerMask">Layer mask to filter objects</param>
/// <returns>List of all objects of type T within radius</returns>
public static List<T> FindAllInRadius<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
{
var results = new List<T>();
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
for (int i = 0; i < count; i++)
{
T component = OverlapBuffer[i].GetComponentInParent<T>();
if (component != null && !results.Contains(component))
{
results.Add(component);
}
}
return results;
}
/// <summary>
/// Find all objects of type T within radius, sorted by distance (closest first).
/// </summary>
public static List<T> FindAllInRadiusSorted<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
{
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
var resultsWithDistance = new List<(T component, float distSqr)>();
for (int i = 0; i < count; i++)
{
T component = OverlapBuffer[i].GetComponentInParent<T>();
if (component != null)
{
bool alreadyAdded = false;
foreach (var item in resultsWithDistance)
{
if (ReferenceEquals(item.component, component))
{
alreadyAdded = true;
break;
}
}
if (!alreadyAdded)
{
float distSqr = (origin - OverlapBuffer[i].transform.position).sqrMagnitude;
resultsWithDistance.Add((component, distSqr));
}
}
}
// Sort by distance
resultsWithDistance.Sort((a, b) => a.distSqr.CompareTo(b.distSqr));
var results = new List<T>(resultsWithDistance.Count);
foreach (var item in resultsWithDistance)
{
results.Add(item.component);
}
return results;
}
/// <summary>
/// Check if any object of type T exists within radius.
/// </summary>
public static bool AnyInRadius<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
{
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
for (int i = 0; i < count; i++)
{
if (OverlapBuffer[i].GetComponentInParent<T>() != null)
{
return true;
}
}
return false;
}
/// <summary>
/// Count objects of type T within radius.
/// </summary>
public static int CountInRadius<T>(Vector3 origin, float radius, LayerMask layerMask) where T : class
{
int count = Physics.OverlapSphereNonAlloc(origin, radius, OverlapBuffer, layerMask);
var processed = new HashSet<T>();
for (int i = 0; i < count; i++)
{
T component = OverlapBuffer[i].GetComponentInParent<T>();
if (component != null)
{
processed.Add(component);
}
}
return processed.Count;
}
}

View File

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