From 62b981f4406088f1d7d41136513df08cd45bf344 Mon Sep 17 00:00:00 2001 From: Nico Date: Fri, 25 Jul 2025 17:52:08 -0700 Subject: [PATCH] Add knockbock gaurd against being pushed out of bounds Further develop on attacking and taking damage states --- .../Scripts/Runtime/AI/EnemyManager/Gobler.cs | 47 +++++++++++++------ .../Scripts/Runtime/Core/Combat/Attacker.cs | 46 +++++++++++------- .../Core/Combat/AttackerAndDamageable.cs | 15 +++--- .../Scripts/Runtime/Core/Combat/Damageable.cs | 9 ++-- .../Runtime/Core/Combat/EnemyCombat.cs | 28 ++++++++--- .../Runtime/Core/Utils/NavMeshUtils.cs | 21 +++++++++ .../Runtime/Core/Utils/NavMeshUtils.cs.meta | 2 + UserSettings/Layouts/default-6000.dwlt | 8 ++-- 8 files changed, 123 insertions(+), 53 deletions(-) create mode 100644 Assets/Scripts/Runtime/Core/Utils/NavMeshUtils.cs create mode 100644 Assets/Scripts/Runtime/Core/Utils/NavMeshUtils.cs.meta diff --git a/Assets/Scripts/Runtime/AI/EnemyManager/Gobler.cs b/Assets/Scripts/Runtime/AI/EnemyManager/Gobler.cs index bdcfea5..018bb17 100644 --- a/Assets/Scripts/Runtime/AI/EnemyManager/Gobler.cs +++ b/Assets/Scripts/Runtime/AI/EnemyManager/Gobler.cs @@ -16,10 +16,13 @@ public class Gobler : Enemy { public Gobler(GameObject owner) : base(owner) { BaseDamage = 10; - AttackPreparationTime = 1; + AttackPreparationTime = 0.5f; AttackCooldownDuration = 1; + BaseHealth = 100; CurrentHealth = BaseHealth; + InvincibilityDuration = 0.11f; + FrameFreezeDuration = 0.3f; IsKnockable = true; SetState(State.GoToCrystal); @@ -56,9 +59,11 @@ public class Gobler : Enemy { case State.ChasePlayer: if (CanAttackPlayer) SetState(State.PrepareAttack); - else if (DistFromPlayer > ChaseDistance + ChaseDistanceBuffer) + else if (DistFromPlayer > ChaseDistance + ChaseDistanceBuffer) { + if (AggroOnPlayer) TextPopUp.SpawnFloatingText("?", Color.red); + AggroOnPlayer = false; SetState(State.None); - else + } else PathAgent.SetDestination(PlayerPos); break; @@ -66,48 +71,59 @@ public class Gobler : Enemy { case State.TakeDamage: if (IsInvincible) { var velocity = DirectionOfDamage * Knockback * 10 * InvincibilityLeft; - //var isWalkable = NavMeshUtils.IsWalkable(MyPos, DirectionOfDamage); - //Rigidbody.linearVelocity = isWalkable ? velocity : Vector2.zero; - Rigidbody.linearVelocity = velocity; - MaterialColorOverlay.SetFloat("_FlashAmount", InvincibilityLeft); + var isWalkable = NavMeshUtils.IsWalkable(MyPos, DirectionOfDamage); + Rigidbody.linearVelocity = isWalkable ? velocity : Vector2.zero; + //Rigidbody.linearVelocity = velocity; + MaterialColorOverlay.SetFloat("_FlashAmount", InvincibilityLeft / InvincibilityDuration); + } else { + MaterialColorOverlay.SetFloat("_FlashAmount", 0); + Rigidbody.linearVelocity = Vector2.zero; } + if (!IsFrameFrozen) SetState(State.None); break; case State.PrepareAttack: MaterialColorOverlay.SetFloat("_FlashAmount", AttackPreparationTimeElapsedNormalized); - if (!IsFrameFrozen) SetState(State.FinalizeAttack); + if (AttackIsReady) SetState(State.FinalizeAttack); + break; + + + case State.FinalizeAttack: + SetState(AggroOnPlayer ? State.ChasePlayer : State.GoToCrystal); break; } } - protected override void SetState(State newState) { + protected override void DoSetState(State newState) { + Debug.Log($"New State: {newState.ToString()}"); CurrentState = newState; - switch (CurrentState) { + switch (newState) { case State.None: - if (AggroOnPlayer) TextPopUp.SpawnFloatingText("?", Color.red); - PathAgent.isStopped = false; - AggroOnCrystal = false; - AggroOnPlayer = false; + PathAgent.isStopped = true; break; case State.GoToCrystal: AggroOnCrystal = true; + PathAgent.isStopped = false; PathAgent.SetDestination(CrystalPos); break; case State.ChasePlayer: AggroOnPlayer = true; + PathAgent.isStopped = false; TextPopUp.SpawnFloatingText("!", Color.red); break; case State.TakeDamage: + AggroOnCrystal = false; + AggroOnPlayer = false; PathAgent.isStopped = true; TextPopUp.SpawnFloatingText(DamageTaken.ToString(), Color.red, 3); MaterialColorOverlay.SetColor("_FlashColor", Color.white); @@ -131,8 +147,8 @@ public class Gobler : Enemy { //EnemySpawnerData.EnemyMap[parent].Damage(ActiveClass.HitBoxDraw.Damage, knockbakDirection, ActiveClass.HitBoxDraw.Knockback); } + MaterialColorOverlay.SetFloat("_FlashAmount", 0); StartAttackCooldown(); - SetState(AggroOnPlayer ? State.ChasePlayer : State.GoToCrystal); break; @@ -143,6 +159,7 @@ public class Gobler : Enemy { } if (PrevState != CurrentState) PrevState = CurrentState; + Debug.Log($"State Done: {newState.ToString()}"); } diff --git a/Assets/Scripts/Runtime/Core/Combat/Attacker.cs b/Assets/Scripts/Runtime/Core/Combat/Attacker.cs index 46b1722..80b893f 100644 --- a/Assets/Scripts/Runtime/Core/Combat/Attacker.cs +++ b/Assets/Scripts/Runtime/Core/Combat/Attacker.cs @@ -5,29 +5,43 @@ using System.Text; using System.Threading.Tasks; using UnityEngine; - +[SerializeField] public class Attacker { - public int BaseDamage = 0; - public bool CanAttack => Time.time < AttackCooldown; - public float AttackCooldown { get; protected set; } = 0f; + [SerializeField] public int BaseDamage { get; protected set; } = 0; + + public void StartAttackPreparation() => AttackReadyAt = Time.time + AttackPreparationTime; + public bool AttackIsReady => AttackPreparationTimeLeft == 0; + public float AttackReadyAt { get; protected set; } + [SerializeField] public float AttackPreparationTime { get; protected set; } = 0; + public float AttackPreparationTimeLeft => Math.Max(0, AttackReadyAt - Time.time); + public float AttackPreparationTimeLeftNormalized => Math.Max(0, (AttackReadyAt - Time.time) / AttackPreparationTime); + public float AttackPreparationTimeElapsed => AttackPreparationTime - AttackPreparationTimeLeft; + public float AttackPreparationTimeElapsedNormalized => 1 - AttackPreparationTimeLeftNormalized; + + public void StartAttackCooldown() => AttackCooldown = Time.time + AttackCooldownDuration; + public bool CanAttack => Time.time > AttackCooldown; + public float AttackCooldown { get; protected set; } public float AttackCooldownLeft => Math.Max(0, AttackCooldown - Time.time); - public float AttackCooldownDuration { get; protected set; } = 1f; - public void StartAttackCooldown() => AttackCooldown = Time.time; + [SerializeField] public float AttackCooldownDuration { get; protected set; } = 1f; } public interface IAttacker { - int BaseDamage { get; } - bool CanAttack => Time.time < AttackCooldown; + public int BaseDamage { get;} - void StartAttackPreparation(); - float AttackPreparationTime { get; } - float AttackPreparationTimeLeft { get; } - bool AttackIsReady { get; } + public void StartAttackPreparation() ; + public float AttackReadyAt { get; } + public float AttackPreparationTime { get; } + public float AttackPreparationTimeLeft { get; } + public float AttackPreparationTimeLeftNormalized { get; } + public float AttackPreparationTimeElapsed { get; } + public float AttackPreparationTimeElapsedNormalized { get; } + public bool AttackIsReady => AttackPreparationTimeLeft == 0; - void StartAttackCooldown(); - float AttackCooldown { get; } - float AttackCooldownLeft { get; } - float AttackCooldownDuration { get; } + public void StartAttackCooldown() ; + public bool CanAttack => Time.time > AttackCooldown; + public float AttackCooldown { get; } + public float AttackCooldownLeft { get; } + public float AttackCooldownDuration { get; } } diff --git a/Assets/Scripts/Runtime/Core/Combat/AttackerAndDamageable.cs b/Assets/Scripts/Runtime/Core/Combat/AttackerAndDamageable.cs index 5cedbe3..a4e34a8 100644 --- a/Assets/Scripts/Runtime/Core/Combat/AttackerAndDamageable.cs +++ b/Assets/Scripts/Runtime/Core/Combat/AttackerAndDamageable.cs @@ -5,21 +5,22 @@ using System.Text; using System.Threading.Tasks; using UnityEngine; +[SerializeField] public class AttackerAndDamageable : Damageable, IAttacker { - public int BaseDamage { get; protected set; } = 0; - public bool CanAttack => Time.time > AttackCooldown; + [SerializeField] public int BaseDamage { get; protected set; } = 0; public void StartAttackPreparation() => AttackReadyAt = Time.time + AttackPreparationTime; + public bool AttackIsReady => AttackPreparationTimeLeft == 0; public float AttackReadyAt { get; protected set; } - public float AttackPreparationTime { get; protected set; } = 0; + [SerializeField] public float AttackPreparationTime { get; protected set; } = 0; public float AttackPreparationTimeLeft => Math.Max(0, AttackReadyAt - Time.time); - public float AttackPreparationTimeLeftNormalized => Math.Max(0, (AttackReadyAt - Time.time) / AttackReadyAt); + public float AttackPreparationTimeLeftNormalized => Math.Max(0, (AttackReadyAt - Time.time) / AttackPreparationTime); public float AttackPreparationTimeElapsed => AttackPreparationTime - AttackPreparationTimeLeft; public float AttackPreparationTimeElapsedNormalized => 1 - AttackPreparationTimeLeftNormalized; - public bool AttackIsReady => AttackPreparationTimeLeft == 0; public void StartAttackCooldown() => AttackCooldown = Time.time + AttackCooldownDuration; - public float AttackCooldown { get; protected set; } = 0f; + public bool CanAttack => Time.time > AttackCooldown; + public float AttackCooldown { get; protected set; } public float AttackCooldownLeft => Math.Max(0, AttackCooldown - Time.time); - public float AttackCooldownDuration { get; protected set; } = 1f; + [SerializeField] public float AttackCooldownDuration { get; protected set; } = 1f; } diff --git a/Assets/Scripts/Runtime/Core/Combat/Damageable.cs b/Assets/Scripts/Runtime/Core/Combat/Damageable.cs index e1d88c8..dd84d67 100644 --- a/Assets/Scripts/Runtime/Core/Combat/Damageable.cs +++ b/Assets/Scripts/Runtime/Core/Combat/Damageable.cs @@ -6,24 +6,25 @@ using System.Threading.Tasks; using UnityEngine; +[SerializeField] public class Damageable : IDamageable { public event Action OnTakeDamage; public event Action OnDeath; public event Action OnHeal; - public int BaseHealth { get; protected set; } + [SerializeField] public int BaseHealth { get; protected set; } public int CurrentHealth { get; protected set; } public bool IsDead => CurrentHealth <= 0; public bool IsInvincible => Time.time < InvincibleUntil; public float InvincibleUntil { get; protected set; } = 0f; public float InvincibilityLeft => Math.Max(0, InvincibleUntil - Time.time); - public float InvincibilityDuration { get; protected set; } = 0.1f; + [SerializeField] public float InvincibilityDuration { get; protected set; } = 0; public bool IsFrameFrozen => Time.time < FrameFrozenUntil; - public float FrameFrozenUntil { get; protected set; } = 0f; + public float FrameFrozenUntil { get; protected set; } public float FrameFreezeLeft => Math.Max(0, FrameFrozenUntil - Time.time); - public float FrameFreezeDuration { get; protected set; } = 0.3f; + [SerializeField] public float FrameFreezeDuration { get; protected set; } = 0; public int DamageTaken { get; protected set; } public Vector2 DirectionOfDamage { get; protected set; } diff --git a/Assets/Scripts/Runtime/Core/Combat/EnemyCombat.cs b/Assets/Scripts/Runtime/Core/Combat/EnemyCombat.cs index 70e24e2..ab19e02 100644 --- a/Assets/Scripts/Runtime/Core/Combat/EnemyCombat.cs +++ b/Assets/Scripts/Runtime/Core/Combat/EnemyCombat.cs @@ -4,13 +4,14 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Unity.VisualScripting; +using UnityEditor; using UnityEngine; using UnityEngine.AI; using static EnemySpawnerData; [SerializeField] -public class Enemy: AttackerAndDamageable { +public class Enemy : AttackerAndDamageable { [Header("Mechanics Attributes")] [SerializeField] public float ChaseDistance = 10f; [SerializeField] public float ChaseDistanceBuffer = 1f; @@ -41,13 +42,14 @@ public class Enemy: AttackerAndDamageable { protected Material MaterialColorOverlay; protected bool IsUpdating; + protected bool IsSettingNewState; public float DistFromPlayer { get; protected set; } public float DistFromCrystal { get; protected set; } protected bool CanAttackPlayer => DistFromPlayer <= AttackDistance && CanAttack; protected bool CanAttackCrystal => DistFromCrystal <= AttackDistance && CanAttack; public bool AggroOnPlayer { get; protected set; } - public bool AggroOnCrystal{ get; protected set; } + public bool AggroOnCrystal { get; protected set; } public State CurrentState; public State PrevState; @@ -72,12 +74,12 @@ public class Enemy: AttackerAndDamageable { PathAgent.updateUpAxis = false; PathAgent.speed = Speed; IsUpdating = false; + IsSettingNewState = false; TextPopUp = new FloatingTextSpawner(Owner.transform, 1); - OnTakeDamage += (damage, direction) => SetState(State.TakeDamage); - OnDeath += () => SetState(State.Die); - OnDeath += () => UnsubscribeToEvent(); + OnTakeDamage += (damage, direction) => { QueueUpdate(); SetState(State.TakeDamage); IsUpdating = false; }; + OnDeath += () => { QueueUpdate(); SetState(State.Die); UnsubscribeToEvent(); IsUpdating = false; }; Rigidbody = Owner.GetComponent(); foreach (var material in Owner.GetComponentsInChildren().Select(x => x.material).ToList()) { @@ -106,10 +108,16 @@ public class Enemy: AttackerAndDamageable { protected virtual void SetPriorityState() { } + protected void QueueUpdate() { + while (IsUpdating) System.Threading.Thread.Sleep(100); + IsUpdating = true; + } + protected virtual void DoUpdate() { } protected void Update() { if (Owner == null) return; if (GameManager.Crystal == null) return; + if (IsSettingNewState) return; if (IsUpdating) return; IsUpdating = true; @@ -122,7 +130,13 @@ public class Enemy: AttackerAndDamageable { IsUpdating = false; } - protected virtual void SetState(State newState) { } + protected void SetState(State newState) { + IsSettingNewState = true; + DoSetState(newState); + IsSettingNewState = false; + } + + protected virtual void DoSetState(State newState) { } protected virtual void OnDrawGizmos() { } - } +} diff --git a/Assets/Scripts/Runtime/Core/Utils/NavMeshUtils.cs b/Assets/Scripts/Runtime/Core/Utils/NavMeshUtils.cs new file mode 100644 index 0000000..d3c47e5 --- /dev/null +++ b/Assets/Scripts/Runtime/Core/Utils/NavMeshUtils.cs @@ -0,0 +1,21 @@ +using UnityEngine; +using UnityEngine.AI; +using static UnityEditor.PlayerSettings; + +public static class NavMeshUtils { + public static bool IsWalkable(Vector3 origin, Vector3 velocity, float navMeshCheckRadius = 0.2f) { + + Vector3 target = origin + velocity * Time.fixedDeltaTime; + + //// 1. Physics obstacle check (optional, but good to avoid wall collisions) + //if (Physics.Linecast(origin, target, obstacleMask)) + // return false; + + // 2. Check if point is still on the NavMesh + if (!NavMesh.SamplePosition(target, out var hit, navMeshCheckRadius, NavMesh.AllAreas)) + return false; + + return true; + return (hit.position - target).sqrMagnitude < 0.04f; + } +} \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Core/Utils/NavMeshUtils.cs.meta b/Assets/Scripts/Runtime/Core/Utils/NavMeshUtils.cs.meta new file mode 100644 index 0000000..ebeb16f --- /dev/null +++ b/Assets/Scripts/Runtime/Core/Utils/NavMeshUtils.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 43b1282d8b6e3cf4d9b06a56faa9eed0 \ No newline at end of file diff --git a/UserSettings/Layouts/default-6000.dwlt b/UserSettings/Layouts/default-6000.dwlt index b7a1f22..7d41cd8 100644 --- a/UserSettings/Layouts/default-6000.dwlt +++ b/UserSettings/Layouts/default-6000.dwlt @@ -1325,7 +1325,7 @@ MonoBehaviour: m_AudioPlay: 0 m_DebugDrawModesUseInteractiveLightBakingData: 0 m_Position: - m_Target: {x: -66.720314, y: -10.525324, z: -0.0061987927} + m_Target: {x: 16.61, y: -15.860004, z: -0.14518732} speed: 2 m_Value: {x: -66.720314, y: -10.525324, z: -0.0061987927} m_RenderMode: 0 @@ -1377,7 +1377,7 @@ MonoBehaviour: speed: 2 m_Value: {x: 0, y: 0, z: 0, w: 1} m_Size: - m_Target: 7.788484 + m_Target: 1.3985817 speed: 2 m_Value: 7.788484 m_Ortho: @@ -1842,7 +1842,7 @@ MonoBehaviour: scrollPos: {x: 0, y: 255} m_SelectedIDs: 28ce0000 m_LastClickedID: 52776 - m_ExpandedIDs: 00000000f8cd0000facd0000fccd0000fecd000000ce000002ce000004ce000006ce000008ce00000ace00000cce00000ece000010ce000012ce000014ce000016ce000018ce00001ace00001cce00001ece000020ce000022ce000024ce000026ce000028ce00002ace00002cce00002ece000030ce000032ce000034ce000036ce000038ce00003ace00003cce00003ece0000 + m_ExpandedIDs: 0000000028ce00002ace00002cce00002ece000030ce000032ce000034ce000036ce000038ce00003ace00003cce00003ece000040ce000042ce000044ce000046ce000048ce00004ace00004cce00004ece000050ce000052ce000054ce000056ce000058ce00005ace00005cce00005ece000060ce000062ce000064ce000066ce000068ce00006ace00006cce00006ece0000 m_RenameOverlay: m_UserAcceptedRename: 0 m_Name: @@ -1871,7 +1871,7 @@ MonoBehaviour: scrollPos: {x: 0, y: 0} m_SelectedIDs: m_LastClickedID: 0 - m_ExpandedIDs: 00000000f8cd0000facd0000fccd0000fecd000000ce000002ce000004ce000006ce000008ce00000ace00000cce00000ece000010ce000012ce000014ce000016ce000018ce00001ace00001cce00001ece000020ce000022ce000024ce000026ce000028ce00002ace00002cce00002ece000030ce000032ce000034ce000036ce000038ce00003ace00003cce00003ece0000 + m_ExpandedIDs: 0000000028ce00002ace00002cce00002ece000030ce000032ce000034ce000036ce000038ce00003ace00003cce00003ece000040ce000042ce000044ce000046ce000048ce00004ace00004cce00004ece000050ce000052ce000054ce000056ce000058ce00005ace00005cce00005ece000060ce000062ce000064ce000066ce000068ce00006ace00006cce00006ece0000 m_RenameOverlay: m_UserAcceptedRename: 0 m_Name: