2026-05-18 버그 수정
This commit is contained in:
@@ -75,6 +75,7 @@ private void TryDamage(Collider2D other)
|
||||
if (_alreadyHit.Contains(target)) return;
|
||||
|
||||
_alreadyHit.Add(target);
|
||||
Debug.Log($"[Hitbox] t={Time.time:F3} damage={_damage} → {other.name} (parent={(target as MonoBehaviour)?.gameObject.name})");
|
||||
Vector2? targetPosition = GetCorrectionTargetPosition(other);
|
||||
target.TakeDamage(_damage, _hitVelocity, _hitReactionState, targetPosition, _correctHitTargetY, _hitPositionSolidMask, _hitPositionCorrectionDuration);
|
||||
OnHit?.Invoke(target);
|
||||
|
||||
@@ -19,6 +19,12 @@ public class Enemy : MonoBehaviour, IDamageable
|
||||
[SerializeField] private float _wallBounceMinXVelocity = 1f;
|
||||
[SerializeField] private float _wallBounceUpwardVelocity = 1.5f;
|
||||
|
||||
[Header("Separation")]
|
||||
[SerializeField] private float _separationRadius = 0.6f;
|
||||
[SerializeField] private float _separationStrength = 2f;
|
||||
[SerializeField] private LayerMask _separationLayer;
|
||||
private static readonly Collider2D[] _separationBuffer = new Collider2D[16];
|
||||
|
||||
private int _currentHealth;
|
||||
private Rigidbody2D _rb;
|
||||
private Animator _anim;
|
||||
@@ -58,7 +64,10 @@ private void Update()
|
||||
{
|
||||
_flashTimer -= Time.deltaTime;
|
||||
if (_flashTimer <= 0f && _spriteRenderer != null)
|
||||
{
|
||||
Debug.Log($"[Flash END] t={Time.time:F3} → revert to {_originalColor}");
|
||||
_spriteRenderer.color = _originalColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (_hitReactionTimer > 0f)
|
||||
@@ -78,10 +87,60 @@ private void FixedUpdate()
|
||||
}
|
||||
|
||||
ApplySmoothHitPositionCorrection();
|
||||
ApplySeparation();
|
||||
_lastVelocity = _rb.linearVelocity;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySeparation()
|
||||
{
|
||||
if (_separationRadius <= 0f || _separationStrength <= 0f) return;
|
||||
if (_separationLayer.value == 0) return;
|
||||
|
||||
ContactFilter2D filter = new ContactFilter2D
|
||||
{
|
||||
useLayerMask = true,
|
||||
layerMask = _separationLayer,
|
||||
useTriggers = false
|
||||
};
|
||||
|
||||
int count = Physics2D.OverlapCircle(_rb.position, _separationRadius, filter, _separationBuffer);
|
||||
Vector2 push = Vector2.zero;
|
||||
int contributors = 0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Collider2D other = _separationBuffer[i];
|
||||
if (other == null) continue;
|
||||
if (other.attachedRigidbody == _rb) continue;
|
||||
|
||||
Vector2 away = _rb.position - (Vector2)other.transform.position;
|
||||
float dist = away.magnitude;
|
||||
if (dist >= _separationRadius) continue;
|
||||
|
||||
Vector2 dir;
|
||||
if (dist < 0.001f)
|
||||
{
|
||||
// 완전히 같은 위치인 경우 무작위 수평 방향으로 분리
|
||||
dir = UnityEngine.Random.value < 0.5f ? Vector2.left : Vector2.right;
|
||||
}
|
||||
else
|
||||
{
|
||||
dir = away / dist;
|
||||
}
|
||||
|
||||
float strength = 1f - (dist / _separationRadius);
|
||||
push += dir * strength;
|
||||
contributors++;
|
||||
}
|
||||
|
||||
if (contributors == 0) return;
|
||||
|
||||
push /= contributors;
|
||||
// X축으로만 분리. Y는 중력에 맡겨서 바운스/공중부양 방지.
|
||||
_rb.position += new Vector2(push.x, 0f) * (_separationStrength * Time.fixedDeltaTime);
|
||||
}
|
||||
|
||||
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0f)
|
||||
{
|
||||
if (_currentHealth <= 0) return;
|
||||
@@ -93,6 +152,7 @@ public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReac
|
||||
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
Debug.Log($"[Flash START] t={Time.time:F3} duration={_hitFlashDuration:F3} (current color was {_spriteRenderer.color})");
|
||||
_spriteRenderer.color = _hitFlashColor;
|
||||
_flashTimer = _hitFlashDuration;
|
||||
}
|
||||
|
||||
132
Assets/02_Scripts/Enemy/EnemySpawner.cs
Normal file
132
Assets/02_Scripts/Enemy/EnemySpawner.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class EnemySpawner : MonoBehaviour
|
||||
{
|
||||
[Header("Spawn Configuration")]
|
||||
[SerializeField] private Enemy _enemyPrefab;
|
||||
[SerializeField] private int _maxAliveCount = 3;
|
||||
[SerializeField] private float _spawnInterval = 2f;
|
||||
[SerializeField] private float _initialDelay = 0f;
|
||||
|
||||
[Header("Spawn Position")]
|
||||
[SerializeField] private Transform[] _spawnPoints;
|
||||
[SerializeField] private float _spawnRadius = 0f;
|
||||
[SerializeField] private Transform _enemyParent;
|
||||
|
||||
[Header("Behavior")]
|
||||
[SerializeField] private bool _spawnOnStart = true;
|
||||
[SerializeField] private bool _respawnOnDeath = true;
|
||||
[SerializeField] private int _totalSpawnLimit = 0;
|
||||
|
||||
private readonly List<Enemy> _aliveEnemies = new();
|
||||
private float _nextSpawnTime;
|
||||
private int _totalSpawned;
|
||||
private bool _active;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_spawnOnStart) BeginSpawning();
|
||||
}
|
||||
|
||||
public void BeginSpawning()
|
||||
{
|
||||
_active = true;
|
||||
_nextSpawnTime = Time.time + _initialDelay;
|
||||
}
|
||||
|
||||
public void StopSpawning() => _active = false;
|
||||
|
||||
public void ResetSpawner()
|
||||
{
|
||||
for (int i = _aliveEnemies.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_aliveEnemies[i] != null)
|
||||
Destroy(_aliveEnemies[i].gameObject);
|
||||
}
|
||||
_aliveEnemies.Clear();
|
||||
_totalSpawned = 0;
|
||||
_nextSpawnTime = Time.time + _initialDelay;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_active) return;
|
||||
|
||||
_aliveEnemies.RemoveAll(e => e == null);
|
||||
|
||||
if (!_respawnOnDeath && _aliveEnemies.Count == 0 && _totalSpawned > 0) return;
|
||||
if (_totalSpawnLimit > 0 && _totalSpawned >= _totalSpawnLimit) return;
|
||||
if (_aliveEnemies.Count >= _maxAliveCount) return;
|
||||
if (Time.time < _nextSpawnTime) return;
|
||||
if (_enemyPrefab == null) return;
|
||||
|
||||
SpawnOne();
|
||||
_nextSpawnTime = Time.time + _spawnInterval;
|
||||
}
|
||||
|
||||
public Enemy SpawnOne()
|
||||
{
|
||||
if (_enemyPrefab == null) return null;
|
||||
|
||||
Vector3 spawnPos = GetSpawnPosition();
|
||||
Enemy enemy = Instantiate(_enemyPrefab, spawnPos, Quaternion.identity, _enemyParent);
|
||||
_aliveEnemies.Add(enemy);
|
||||
_totalSpawned++;
|
||||
return enemy;
|
||||
}
|
||||
|
||||
private Vector3 GetSpawnPosition()
|
||||
{
|
||||
Vector3 basePos;
|
||||
if (_spawnPoints != null && _spawnPoints.Length > 0)
|
||||
{
|
||||
Transform pick = _spawnPoints[Random.Range(0, _spawnPoints.Length)];
|
||||
basePos = pick != null ? pick.position : transform.position;
|
||||
}
|
||||
else
|
||||
{
|
||||
basePos = transform.position;
|
||||
}
|
||||
|
||||
if (_spawnRadius > 0f)
|
||||
{
|
||||
Vector2 offset = Random.insideUnitCircle * _spawnRadius;
|
||||
basePos.x += offset.x;
|
||||
basePos.y += offset.y;
|
||||
}
|
||||
|
||||
return basePos;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Color pointColor = Color.cyan;
|
||||
Color radiusColor = new Color(0f, 1f, 1f, 0.25f);
|
||||
|
||||
if (_spawnPoints != null && _spawnPoints.Length > 0)
|
||||
{
|
||||
foreach (var p in _spawnPoints)
|
||||
{
|
||||
if (p == null) continue;
|
||||
Gizmos.color = pointColor;
|
||||
Gizmos.DrawWireSphere(p.position, 0.3f);
|
||||
if (_spawnRadius > 0f)
|
||||
{
|
||||
Gizmos.color = radiusColor;
|
||||
Gizmos.DrawWireSphere(p.position, _spawnRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmos.color = pointColor;
|
||||
Gizmos.DrawWireSphere(transform.position, 0.3f);
|
||||
if (_spawnRadius > 0f)
|
||||
{
|
||||
Gizmos.color = radiusColor;
|
||||
Gizmos.DrawWireSphere(transform.position, _spawnRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Enemy/EnemySpawner.cs.meta
Normal file
2
Assets/02_Scripts/Enemy/EnemySpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ce7a44315e1034479529c22b51f84e7
|
||||
20
Assets/02_Scripts/Enemy/WaveData.cs
Normal file
20
Assets/02_Scripts/Enemy/WaveData.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(fileName = "WaveData", menuName = "Combat/WaveData")]
|
||||
public class WaveData : ScriptableObject
|
||||
{
|
||||
public string WaveName;
|
||||
public List<SpawnEntry> Spawns = new();
|
||||
public float TimeLimit = 30f;
|
||||
public float SpawnInterval = 0.3f;
|
||||
public float StartDelay = 0f;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SpawnEntry
|
||||
{
|
||||
public Enemy EnemyPrefab;
|
||||
public int Count = 1;
|
||||
}
|
||||
2
Assets/02_Scripts/Enemy/WaveData.cs.meta
Normal file
2
Assets/02_Scripts/Enemy/WaveData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 513c314684a222240a8b3d7f3e60b829
|
||||
276
Assets/02_Scripts/Enemy/WaveManager.cs
Normal file
276
Assets/02_Scripts/Enemy/WaveManager.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
public class WaveManager : MonoBehaviour
|
||||
{
|
||||
[Header("Waves")]
|
||||
[SerializeField] private List<WaveData> _waves = new();
|
||||
[SerializeField] private float _intermissionDuration = 2f;
|
||||
[SerializeField] private bool _startOnAwake = true;
|
||||
|
||||
[Header("Spawn Position")]
|
||||
[SerializeField] private Transform[] _spawnPoints;
|
||||
[SerializeField] private float _spawnRadius = 0f;
|
||||
[SerializeField] private Transform _enemyParent;
|
||||
|
||||
[Header("On Defeat")]
|
||||
[SerializeField] private bool _destroyAliveEnemiesOnDefeat = true;
|
||||
|
||||
public event Action<int, WaveData> OnWaveStart;
|
||||
public event Action<int> OnWaveCleared;
|
||||
public event Action OnAllWavesCleared;
|
||||
public event Action<int> OnDefeat;
|
||||
|
||||
public int CurrentWaveIndex { get; private set; }
|
||||
public int TotalWaveCount => _waves != null ? _waves.Count : 0;
|
||||
public WaveData CurrentWave =>
|
||||
_waves != null && CurrentWaveIndex >= 0 && CurrentWaveIndex < _waves.Count
|
||||
? _waves[CurrentWaveIndex]
|
||||
: null;
|
||||
public float TimeRemaining { get; private set; }
|
||||
public int AliveCount => _aliveEnemies.Count;
|
||||
public int RemainingToSpawn { get; private set; }
|
||||
public bool IsRunning { get; private set; }
|
||||
public bool IsDefeated { get; private set; }
|
||||
public bool IsVictory { get; private set; }
|
||||
|
||||
private readonly List<Enemy> _aliveEnemies = new();
|
||||
private CancellationTokenSource _waveCts;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_startOnAwake) StartWaves();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_waveCts?.Cancel();
|
||||
_waveCts?.Dispose();
|
||||
}
|
||||
|
||||
public void StartWaves()
|
||||
{
|
||||
if (_waves == null || _waves.Count == 0) return;
|
||||
RunAllWaves();
|
||||
}
|
||||
|
||||
public void StopWaves()
|
||||
{
|
||||
_waveCts?.Cancel();
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
private async void RunAllWaves()
|
||||
{
|
||||
_waveCts?.Cancel();
|
||||
_waveCts?.Dispose();
|
||||
_waveCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||
CancellationToken token = _waveCts.Token;
|
||||
|
||||
IsDefeated = false;
|
||||
IsVictory = false;
|
||||
|
||||
try
|
||||
{
|
||||
for (CurrentWaveIndex = 0; CurrentWaveIndex < _waves.Count; CurrentWaveIndex++)
|
||||
{
|
||||
WaveData wave = _waves[CurrentWaveIndex];
|
||||
if (wave == null) continue;
|
||||
|
||||
if (wave.StartDelay > 0f)
|
||||
await Awaitable.WaitForSecondsAsync(wave.StartDelay, token);
|
||||
|
||||
OnWaveStart?.Invoke(CurrentWaveIndex, wave);
|
||||
bool cleared = await RunWave(wave, token);
|
||||
|
||||
if (!cleared)
|
||||
{
|
||||
IsDefeated = true;
|
||||
OnDefeat?.Invoke(CurrentWaveIndex);
|
||||
if (_destroyAliveEnemiesOnDefeat)
|
||||
DestroyAliveEnemies();
|
||||
return;
|
||||
}
|
||||
|
||||
OnWaveCleared?.Invoke(CurrentWaveIndex);
|
||||
|
||||
if (CurrentWaveIndex < _waves.Count - 1 && _intermissionDuration > 0f)
|
||||
await Awaitable.WaitForSecondsAsync(_intermissionDuration, token);
|
||||
}
|
||||
|
||||
IsVictory = true;
|
||||
OnAllWavesCleared?.Invoke();
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private async Awaitable<bool> RunWave(WaveData wave, CancellationToken token)
|
||||
{
|
||||
IsRunning = true;
|
||||
TimeRemaining = wave.TimeLimit;
|
||||
_aliveEnemies.Clear();
|
||||
|
||||
int totalToSpawn = 0;
|
||||
foreach (var entry in wave.Spawns)
|
||||
if (entry != null) totalToSpawn += Mathf.Max(entry.Count, 0);
|
||||
RemainingToSpawn = totalToSpawn;
|
||||
|
||||
// 적 스폰은 백그라운드로 흘려보내고, 본 루프는 시간/클리어만 체크.
|
||||
SpawnWaveEnemiesAsync(wave, token);
|
||||
|
||||
while (true)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
_aliveEnemies.RemoveAll(e => e == null);
|
||||
|
||||
if (RemainingToSpawn == 0 && _aliveEnemies.Count == 0)
|
||||
{
|
||||
IsRunning = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
TimeRemaining -= Time.deltaTime;
|
||||
if (TimeRemaining <= 0f)
|
||||
{
|
||||
IsRunning = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
await Awaitable.NextFrameAsync(token);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SpawnWaveEnemiesAsync(WaveData wave, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var entry in wave.Spawns)
|
||||
{
|
||||
if (entry == null || entry.EnemyPrefab == null)
|
||||
{
|
||||
RemainingToSpawn -= entry != null ? entry.Count : 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < entry.Count; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
SpawnEnemy(entry.EnemyPrefab);
|
||||
RemainingToSpawn--;
|
||||
|
||||
if (wave.SpawnInterval > 0f)
|
||||
await Awaitable.WaitForSecondsAsync(wave.SpawnInterval, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
private void SpawnEnemy(Enemy prefab)
|
||||
{
|
||||
Vector3 position = GetSpawnPosition();
|
||||
Enemy enemy = Instantiate(prefab, position, Quaternion.identity, _enemyParent);
|
||||
_aliveEnemies.Add(enemy);
|
||||
}
|
||||
|
||||
private Vector3 GetSpawnPosition()
|
||||
{
|
||||
Vector3 basePos;
|
||||
if (_spawnPoints != null && _spawnPoints.Length > 0)
|
||||
{
|
||||
Transform pick = _spawnPoints[UnityEngine.Random.Range(0, _spawnPoints.Length)];
|
||||
basePos = pick != null ? pick.position : transform.position;
|
||||
}
|
||||
else
|
||||
{
|
||||
basePos = transform.position;
|
||||
}
|
||||
|
||||
if (_spawnRadius > 0f)
|
||||
{
|
||||
Vector2 offset = UnityEngine.Random.insideUnitCircle * _spawnRadius;
|
||||
basePos.x += offset.x;
|
||||
basePos.y += offset.y;
|
||||
}
|
||||
|
||||
return basePos;
|
||||
}
|
||||
|
||||
private void DestroyAliveEnemies()
|
||||
{
|
||||
for (int i = _aliveEnemies.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_aliveEnemies[i] != null)
|
||||
Destroy(_aliveEnemies[i].gameObject);
|
||||
}
|
||||
_aliveEnemies.Clear();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUIStyle style = new GUIStyle(GUI.skin.box)
|
||||
{
|
||||
fontSize = 22,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
richText = true,
|
||||
normal = { textColor = Color.white }
|
||||
};
|
||||
|
||||
string text;
|
||||
if (IsVictory)
|
||||
{
|
||||
text = "<color=#88ff88><b>VICTORY</b></color>";
|
||||
}
|
||||
else if (IsDefeated)
|
||||
{
|
||||
text = $"<color=#ff8888><b>DEFEAT</b></color>\nWave {CurrentWaveIndex + 1} timed out";
|
||||
}
|
||||
else if (IsRunning && CurrentWave != null)
|
||||
{
|
||||
string name = string.IsNullOrEmpty(CurrentWave.WaveName)
|
||||
? $"Wave {CurrentWaveIndex + 1}/{TotalWaveCount}"
|
||||
: CurrentWave.WaveName;
|
||||
|
||||
text = $"<b>{name}</b>\nTime: {TimeRemaining:F1}s\nEnemies: {AliveCount} (+{RemainingToSpawn} 대기)";
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GUI.Box(new Rect(Screen.width / 2f - 180f, 20f, 360f, 110f), text, style);
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Color pointColor = Color.cyan;
|
||||
Color radiusColor = new Color(0f, 1f, 1f, 0.25f);
|
||||
|
||||
if (_spawnPoints != null && _spawnPoints.Length > 0)
|
||||
{
|
||||
foreach (var p in _spawnPoints)
|
||||
{
|
||||
if (p == null) continue;
|
||||
Gizmos.color = pointColor;
|
||||
Gizmos.DrawWireSphere(p.position, 0.3f);
|
||||
if (_spawnRadius > 0f)
|
||||
{
|
||||
Gizmos.color = radiusColor;
|
||||
Gizmos.DrawWireSphere(p.position, _spawnRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Gizmos.color = pointColor;
|
||||
Gizmos.DrawWireSphere(transform.position, 0.3f);
|
||||
if (_spawnRadius > 0f)
|
||||
{
|
||||
Gizmos.color = radiusColor;
|
||||
Gizmos.DrawWireSphere(transform.position, _spawnRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Enemy/WaveManager.cs.meta
Normal file
2
Assets/02_Scripts/Enemy/WaveManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b4b77a885491744e8fc2316f85d4edc
|
||||
@@ -82,7 +82,9 @@ public class PlayerController : MonoBehaviour
|
||||
private CancellationTokenSource _motionCts;
|
||||
private CancellationTokenSource _animationSpeedCts;
|
||||
private CancellationTokenSource _actionVelocityCts;
|
||||
private bool _isAttackActive;
|
||||
private bool _isMotionActive;
|
||||
private float _actionDirection = 1f;
|
||||
private ActionData _lastAttackGizmoData;
|
||||
[SerializeField] private float _hitGizmoFadeDuration = 0.5f;
|
||||
private ActionData _lastHitData;
|
||||
@@ -161,10 +163,10 @@ private void FixedUpdate()
|
||||
ExecuteBufferedInputIfReady();
|
||||
TickComboWindow();
|
||||
|
||||
if (!IsMovementLocked())
|
||||
if (!IsMovementLocked() && !IsActionActive())
|
||||
_rb.linearVelocity = new Vector2(_moveInputX * _moveSpeed, _rb.linearVelocity.y);
|
||||
|
||||
if (!IsFacingLocked())
|
||||
if (!IsFacingLocked() && !IsActionActive())
|
||||
UpdateFacingFromMoveInput();
|
||||
|
||||
ApplyGravity();
|
||||
@@ -201,7 +203,7 @@ private void TickComboWindow()
|
||||
private void OnMoveInput(Vector2 value)
|
||||
{
|
||||
_moveInputX = value.x == 0f ? 0f : Mathf.Sign(value.x);
|
||||
if (_facingLockTimer <= 0f)
|
||||
if (_facingLockTimer <= 0f && !IsActionActive())
|
||||
UpdateFacingFromMoveInput();
|
||||
}
|
||||
|
||||
@@ -268,6 +270,7 @@ private void ExecuteComboInput(ComboInputType input)
|
||||
{
|
||||
if (transition.Trigger != input) continue;
|
||||
if (transition.Next == null || transition.Next.Action == null) continue;
|
||||
if (!CanPerformAction(transition.Next.Action)) continue;
|
||||
|
||||
ApplyForwardStep(transition.ForwardStep, transition.ForwardStepDuration);
|
||||
PerformAttack(transition.Next.Action, transition.ForwardStep > 0f);
|
||||
@@ -286,12 +289,21 @@ private void ExecuteComboInput(ComboInputType input)
|
||||
_ => null
|
||||
};
|
||||
if (root == null || root.Action == null) return;
|
||||
if (!CanPerformAction(root.Action)) return;
|
||||
|
||||
PerformAttack(root.Action);
|
||||
_currentNode = root;
|
||||
_comboWindowTimer = root.ComboWindow;
|
||||
}
|
||||
|
||||
private bool CanPerformAction(ActionData data)
|
||||
{
|
||||
if (data == null) return false;
|
||||
// 잡기 액션은 사정 거리 안에 적이 있어야만 실행. 없으면 입력 자체를 캔슬.
|
||||
if (data.IsGrab && FindGrabTarget(data) == null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ExecuteMotionNode(ComboNode root)
|
||||
{
|
||||
if (root == null || root.Action == null) return;
|
||||
@@ -336,14 +348,17 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
|
||||
CancelMotion();
|
||||
CancelAndDispose(ref _attackCts);
|
||||
_attackCts = CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
|
||||
CancellationTokenSource currentAttackCts = _attackCts;
|
||||
CancellationToken token = _attackCts.Token;
|
||||
|
||||
_attackCooldownTimer = data.Cooldown;
|
||||
_isAttackActive = true;
|
||||
_lastAttackGizmoData = data;
|
||||
_attackStartTime = Time.time;
|
||||
_hitFired = false;
|
||||
|
||||
ClearActionLocks();
|
||||
CaptureActionDirection(data);
|
||||
PlayActionAnimation(data);
|
||||
|
||||
PlayActionVelocity(data);
|
||||
@@ -362,6 +377,9 @@ private async void PerformAttack(ActionData data, bool preserveHorizontalVelocit
|
||||
{
|
||||
_attackHitbox?.Deactivate();
|
||||
}
|
||||
|
||||
if (_attackCts == currentAttackCts)
|
||||
_isAttackActive = false;
|
||||
}
|
||||
|
||||
private async void PerformMotion(ActionData data)
|
||||
@@ -375,12 +393,13 @@ private async void PerformMotion(ActionData data)
|
||||
CancellationToken token = _motionCts.Token;
|
||||
|
||||
SetMotionCooldown(data);
|
||||
_isMotionActive = true;
|
||||
ClearActionLocks();
|
||||
CaptureActionDirection(data);
|
||||
_isMotionActive = true;
|
||||
FaceMotionDirection(data);
|
||||
PlayActionAnimation(data);
|
||||
PlayActionVelocity(data);
|
||||
LockMovementIfNeeded(data);
|
||||
LockMovementIfNeeded(data, false, true);
|
||||
LockFacingIfNeeded(data);
|
||||
|
||||
bool completed = false;
|
||||
@@ -432,6 +451,7 @@ private async void PerformGroundPound()
|
||||
_hitFired = false;
|
||||
|
||||
ClearActionLocks();
|
||||
CaptureActionDirection(_groundPoundData);
|
||||
FaceMotionDirection(_groundPoundData);
|
||||
PlayActionAnimation(_groundPoundData);
|
||||
LockMovementIfNeeded(_groundPoundData);
|
||||
@@ -512,6 +532,7 @@ private async void PerformGroundPound()
|
||||
|
||||
private void CancelAttack()
|
||||
{
|
||||
_isAttackActive = false;
|
||||
CancelAndDispose(ref _attackCts);
|
||||
CancelAndDispose(ref _actionVelocityCts);
|
||||
_attackCooldownTimer = 0f;
|
||||
@@ -527,6 +548,11 @@ private void CancelMotion()
|
||||
CancelAndDispose(ref _actionVelocityCts);
|
||||
}
|
||||
|
||||
private bool IsActionActive()
|
||||
{
|
||||
return _isAttackActive || _isMotionActive || _isGroundPounding;
|
||||
}
|
||||
|
||||
private bool CanTransitionFromCurrentNode(ComboInputType input)
|
||||
{
|
||||
if (_comboWindowTimer <= 0f || _currentNode == null) return false;
|
||||
@@ -672,6 +698,7 @@ private void ActivateAttackHitbox(ActionData data)
|
||||
_lastHitTime = Time.time;
|
||||
_hitFired = true;
|
||||
|
||||
Debug.Log($"[Activate] t={Time.time:F3} action={GetActionName(data)} hitDuration={data.HitDuration:F3} motionActive={_isMotionActive} groundPounding={_isGroundPounding}");
|
||||
Vector2 sourcePosition = _rb != null ? _rb.position : (Vector2)transform.position;
|
||||
_attackHitbox.Activate(data, localPosition, hitVelocity, sourcePosition, hitTargetPosition, data.CorrectHitTargetY, _groundLayer.value, _enemyLayer);
|
||||
}
|
||||
@@ -737,8 +764,17 @@ private void LockMovementIfNeeded(ActionData data, bool preserveHorizontalVeloci
|
||||
if (!preserveHorizontalVelocity && data.Velocity == Vector2.zero)
|
||||
_rb.linearVelocity = new Vector2(0f, _rb.linearVelocity.y);
|
||||
|
||||
_inputLockTimer = GetActionLockDuration(data);
|
||||
_movementLockAction = ShouldUseAnimationLength(data) ? data : null;
|
||||
if (forceLock)
|
||||
{
|
||||
// 액션 길이 전체를 보장: 애니메이션 길이와 무관하게 MotionDuration만큼 잠금.
|
||||
_inputLockTimer = Mathf.Max(data.MotionDuration, 0.02f);
|
||||
_movementLockAction = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputLockTimer = GetActionLockDuration(data);
|
||||
_movementLockAction = ShouldUseAnimationLength(data) ? data : null;
|
||||
}
|
||||
}
|
||||
|
||||
private void LockFacingIfNeeded(ActionData data)
|
||||
@@ -889,21 +925,25 @@ private float GetAnimationSpeed(ActionData data, float normalizedTime)
|
||||
|
||||
private float GetMotionDirection(ActionData data)
|
||||
{
|
||||
if (data.UseInputDirection && _moveInputX != 0f)
|
||||
return _moveInputX;
|
||||
|
||||
return _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||
return _actionDirection;
|
||||
}
|
||||
|
||||
private void FaceMotionDirection(ActionData data)
|
||||
{
|
||||
if (_spriteRenderer == null) return;
|
||||
if (!data.UseInputDirection || _moveInputX == 0f) return;
|
||||
|
||||
_spriteRenderer.flipX = _moveInputX < 0f;
|
||||
_spriteRenderer.flipX = _actionDirection < 0f;
|
||||
_facingLockTimer = 0f;
|
||||
}
|
||||
|
||||
private void CaptureActionDirection(ActionData data)
|
||||
{
|
||||
if (data != null && data.UseInputDirection && _moveInputX != 0f)
|
||||
_actionDirection = _moveInputX;
|
||||
else
|
||||
_actionDirection = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||
}
|
||||
|
||||
private Vector2 GetHitVelocity(Vector2 hitVelocity)
|
||||
{
|
||||
float facing = _spriteRenderer != null && _spriteRenderer.flipX ? -1f : 1f;
|
||||
|
||||
Reference in New Issue
Block a user