Add knockbock gaurd against being pushed out of bounds

Further develop on attacking and taking damage states
This commit is contained in:
Nico 2025-07-25 17:52:08 -07:00
parent 24b9ece93d
commit 62b981f440
8 changed files with 123 additions and 53 deletions

View File

@ -16,10 +16,13 @@ public class Gobler : Enemy {
public Gobler(GameObject owner) : base(owner) { public Gobler(GameObject owner) : base(owner) {
BaseDamage = 10; BaseDamage = 10;
AttackPreparationTime = 1; AttackPreparationTime = 0.5f;
AttackCooldownDuration = 1; AttackCooldownDuration = 1;
BaseHealth = 100; BaseHealth = 100;
CurrentHealth = BaseHealth; CurrentHealth = BaseHealth;
InvincibilityDuration = 0.11f;
FrameFreezeDuration = 0.3f;
IsKnockable = true; IsKnockable = true;
SetState(State.GoToCrystal); SetState(State.GoToCrystal);
@ -56,9 +59,11 @@ public class Gobler : Enemy {
case State.ChasePlayer: case State.ChasePlayer:
if (CanAttackPlayer) if (CanAttackPlayer)
SetState(State.PrepareAttack); SetState(State.PrepareAttack);
else if (DistFromPlayer > ChaseDistance + ChaseDistanceBuffer) else if (DistFromPlayer > ChaseDistance + ChaseDistanceBuffer) {
if (AggroOnPlayer) TextPopUp.SpawnFloatingText("?", Color.red);
AggroOnPlayer = false;
SetState(State.None); SetState(State.None);
else } else
PathAgent.SetDestination(PlayerPos); PathAgent.SetDestination(PlayerPos);
break; break;
@ -66,48 +71,59 @@ public class Gobler : Enemy {
case State.TakeDamage: case State.TakeDamage:
if (IsInvincible) { if (IsInvincible) {
var velocity = DirectionOfDamage * Knockback * 10 * InvincibilityLeft; var velocity = DirectionOfDamage * Knockback * 10 * InvincibilityLeft;
//var isWalkable = NavMeshUtils.IsWalkable(MyPos, DirectionOfDamage); var isWalkable = NavMeshUtils.IsWalkable(MyPos, DirectionOfDamage);
//Rigidbody.linearVelocity = isWalkable ? velocity : Vector2.zero; Rigidbody.linearVelocity = isWalkable ? velocity : Vector2.zero;
Rigidbody.linearVelocity = velocity; //Rigidbody.linearVelocity = velocity;
MaterialColorOverlay.SetFloat("_FlashAmount", InvincibilityLeft); MaterialColorOverlay.SetFloat("_FlashAmount", InvincibilityLeft / InvincibilityDuration);
} else {
MaterialColorOverlay.SetFloat("_FlashAmount", 0);
Rigidbody.linearVelocity = Vector2.zero;
} }
if (!IsFrameFrozen) SetState(State.None); if (!IsFrameFrozen) SetState(State.None);
break; break;
case State.PrepareAttack: case State.PrepareAttack:
MaterialColorOverlay.SetFloat("_FlashAmount", AttackPreparationTimeElapsedNormalized); 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; break;
} }
} }
protected override void SetState(State newState) { protected override void DoSetState(State newState) {
Debug.Log($"New State: {newState.ToString()}");
CurrentState = newState; CurrentState = newState;
switch (CurrentState) { switch (newState) {
case State.None: case State.None:
if (AggroOnPlayer) TextPopUp.SpawnFloatingText("?", Color.red); PathAgent.isStopped = true;
PathAgent.isStopped = false;
AggroOnCrystal = false;
AggroOnPlayer = false;
break; break;
case State.GoToCrystal: case State.GoToCrystal:
AggroOnCrystal = true; AggroOnCrystal = true;
PathAgent.isStopped = false;
PathAgent.SetDestination(CrystalPos); PathAgent.SetDestination(CrystalPos);
break; break;
case State.ChasePlayer: case State.ChasePlayer:
AggroOnPlayer = true; AggroOnPlayer = true;
PathAgent.isStopped = false;
TextPopUp.SpawnFloatingText("!", Color.red); TextPopUp.SpawnFloatingText("!", Color.red);
break; break;
case State.TakeDamage: case State.TakeDamage:
AggroOnCrystal = false;
AggroOnPlayer = false;
PathAgent.isStopped = true; PathAgent.isStopped = true;
TextPopUp.SpawnFloatingText(DamageTaken.ToString(), Color.red, 3); TextPopUp.SpawnFloatingText(DamageTaken.ToString(), Color.red, 3);
MaterialColorOverlay.SetColor("_FlashColor", Color.white); MaterialColorOverlay.SetColor("_FlashColor", Color.white);
@ -131,8 +147,8 @@ public class Gobler : Enemy {
//EnemySpawnerData.EnemyMap[parent].Damage(ActiveClass.HitBoxDraw.Damage, knockbakDirection, ActiveClass.HitBoxDraw.Knockback); //EnemySpawnerData.EnemyMap[parent].Damage(ActiveClass.HitBoxDraw.Damage, knockbakDirection, ActiveClass.HitBoxDraw.Knockback);
} }
MaterialColorOverlay.SetFloat("_FlashAmount", 0);
StartAttackCooldown(); StartAttackCooldown();
SetState(AggroOnPlayer ? State.ChasePlayer : State.GoToCrystal);
break; break;
@ -143,6 +159,7 @@ public class Gobler : Enemy {
} }
if (PrevState != CurrentState) PrevState = CurrentState; if (PrevState != CurrentState) PrevState = CurrentState;
Debug.Log($"State Done: {newState.ToString()}");
} }

View File

@ -5,29 +5,43 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
[SerializeField]
public class Attacker { public class Attacker {
public int BaseDamage = 0; [SerializeField] public int BaseDamage { get; protected set; } = 0;
public bool CanAttack => Time.time < AttackCooldown;
public float AttackCooldown { get; protected set; } = 0f; 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 AttackCooldownLeft => Math.Max(0, AttackCooldown - Time.time);
public float AttackCooldownDuration { get; protected set; } = 1f; [SerializeField] public float AttackCooldownDuration { get; protected set; } = 1f;
public void StartAttackCooldown() => AttackCooldown = Time.time;
} }
public interface IAttacker { public interface IAttacker {
int BaseDamage { get; } public int BaseDamage { get;}
bool CanAttack => Time.time < AttackCooldown;
void StartAttackPreparation(); public void StartAttackPreparation() ;
float AttackPreparationTime { get; } public float AttackReadyAt { get; }
float AttackPreparationTimeLeft { get; } public float AttackPreparationTime { get; }
bool AttackIsReady { get; } public float AttackPreparationTimeLeft { get; }
public float AttackPreparationTimeLeftNormalized { get; }
public float AttackPreparationTimeElapsed { get; }
public float AttackPreparationTimeElapsedNormalized { get; }
public bool AttackIsReady => AttackPreparationTimeLeft == 0;
void StartAttackCooldown(); public void StartAttackCooldown() ;
float AttackCooldown { get; } public bool CanAttack => Time.time > AttackCooldown;
float AttackCooldownLeft { get; } public float AttackCooldown { get; }
float AttackCooldownDuration { get; } public float AttackCooldownLeft { get; }
public float AttackCooldownDuration { get; }
} }

View File

@ -5,21 +5,22 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
[SerializeField]
public class AttackerAndDamageable : Damageable, IAttacker { public class AttackerAndDamageable : Damageable, IAttacker {
public int BaseDamage { get; protected set; } = 0; [SerializeField] public int BaseDamage { get; protected set; } = 0;
public bool CanAttack => Time.time > AttackCooldown;
public void StartAttackPreparation() => AttackReadyAt = Time.time + AttackPreparationTime; public void StartAttackPreparation() => AttackReadyAt = Time.time + AttackPreparationTime;
public bool AttackIsReady => AttackPreparationTimeLeft == 0;
public float AttackReadyAt { get; protected set; } 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 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 AttackPreparationTimeElapsed => AttackPreparationTime - AttackPreparationTimeLeft;
public float AttackPreparationTimeElapsedNormalized => 1 - AttackPreparationTimeLeftNormalized; public float AttackPreparationTimeElapsedNormalized => 1 - AttackPreparationTimeLeftNormalized;
public bool AttackIsReady => AttackPreparationTimeLeft == 0;
public void StartAttackCooldown() => AttackCooldown = Time.time + AttackCooldownDuration; 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 AttackCooldownLeft => Math.Max(0, AttackCooldown - Time.time);
public float AttackCooldownDuration { get; protected set; } = 1f; [SerializeField] public float AttackCooldownDuration { get; protected set; } = 1f;
} }

View File

@ -6,24 +6,25 @@ using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
[SerializeField]
public class Damageable : IDamageable { public class Damageable : IDamageable {
public event Action<int, Vector2> OnTakeDamage; public event Action<int, Vector2> OnTakeDamage;
public event Action OnDeath; public event Action OnDeath;
public event Action<int> OnHeal; public event Action<int> OnHeal;
public int BaseHealth { get; protected set; } [SerializeField] public int BaseHealth { get; protected set; }
public int CurrentHealth { get; protected set; } public int CurrentHealth { get; protected set; }
public bool IsDead => CurrentHealth <= 0; public bool IsDead => CurrentHealth <= 0;
public bool IsInvincible => Time.time < InvincibleUntil; public bool IsInvincible => Time.time < InvincibleUntil;
public float InvincibleUntil { get; protected set; } = 0f; public float InvincibleUntil { get; protected set; } = 0f;
public float InvincibilityLeft => Math.Max(0, InvincibleUntil - Time.time); 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 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 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 int DamageTaken { get; protected set; }
public Vector2 DirectionOfDamage { get; protected set; } public Vector2 DirectionOfDamage { get; protected set; }

View File

@ -4,13 +4,14 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Unity.VisualScripting; using Unity.VisualScripting;
using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.AI; using UnityEngine.AI;
using static EnemySpawnerData; using static EnemySpawnerData;
[SerializeField] [SerializeField]
public class Enemy: AttackerAndDamageable { public class Enemy : AttackerAndDamageable {
[Header("Mechanics Attributes")] [Header("Mechanics Attributes")]
[SerializeField] public float ChaseDistance = 10f; [SerializeField] public float ChaseDistance = 10f;
[SerializeField] public float ChaseDistanceBuffer = 1f; [SerializeField] public float ChaseDistanceBuffer = 1f;
@ -41,13 +42,14 @@ public class Enemy: AttackerAndDamageable {
protected Material MaterialColorOverlay; protected Material MaterialColorOverlay;
protected bool IsUpdating; protected bool IsUpdating;
protected bool IsSettingNewState;
public float DistFromPlayer { get; protected set; } public float DistFromPlayer { get; protected set; }
public float DistFromCrystal { get; protected set; } public float DistFromCrystal { get; protected set; }
protected bool CanAttackPlayer => DistFromPlayer <= AttackDistance && CanAttack; protected bool CanAttackPlayer => DistFromPlayer <= AttackDistance && CanAttack;
protected bool CanAttackCrystal => DistFromCrystal <= AttackDistance && CanAttack; protected bool CanAttackCrystal => DistFromCrystal <= AttackDistance && CanAttack;
public bool AggroOnPlayer { get; protected set; } public bool AggroOnPlayer { get; protected set; }
public bool AggroOnCrystal{ get; protected set; } public bool AggroOnCrystal { get; protected set; }
public State CurrentState; public State CurrentState;
public State PrevState; public State PrevState;
@ -72,12 +74,12 @@ public class Enemy: AttackerAndDamageable {
PathAgent.updateUpAxis = false; PathAgent.updateUpAxis = false;
PathAgent.speed = Speed; PathAgent.speed = Speed;
IsUpdating = false; IsUpdating = false;
IsSettingNewState = false;
TextPopUp = new FloatingTextSpawner(Owner.transform, 1); TextPopUp = new FloatingTextSpawner(Owner.transform, 1);
OnTakeDamage += (damage, direction) => SetState(State.TakeDamage); OnTakeDamage += (damage, direction) => { QueueUpdate(); SetState(State.TakeDamage); IsUpdating = false; };
OnDeath += () => SetState(State.Die); OnDeath += () => { QueueUpdate(); SetState(State.Die); UnsubscribeToEvent(); IsUpdating = false; };
OnDeath += () => UnsubscribeToEvent();
Rigidbody = Owner.GetComponent<Rigidbody2D>(); Rigidbody = Owner.GetComponent<Rigidbody2D>();
foreach (var material in Owner.GetComponentsInChildren<SpriteRenderer>().Select(x => x.material).ToList()) { foreach (var material in Owner.GetComponentsInChildren<SpriteRenderer>().Select(x => x.material).ToList()) {
@ -106,10 +108,16 @@ public class Enemy: AttackerAndDamageable {
protected virtual void SetPriorityState() { } protected virtual void SetPriorityState() { }
protected void QueueUpdate() {
while (IsUpdating) System.Threading.Thread.Sleep(100);
IsUpdating = true;
}
protected virtual void DoUpdate() { } protected virtual void DoUpdate() { }
protected void Update() { protected void Update() {
if (Owner == null) return; if (Owner == null) return;
if (GameManager.Crystal == null) return; if (GameManager.Crystal == null) return;
if (IsSettingNewState) return;
if (IsUpdating) return; if (IsUpdating) return;
IsUpdating = true; IsUpdating = true;
@ -122,7 +130,13 @@ public class Enemy: AttackerAndDamageable {
IsUpdating = false; 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() { } protected virtual void OnDrawGizmos() { }
} }

View File

@ -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;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 43b1282d8b6e3cf4d9b06a56faa9eed0

View File

@ -1325,7 +1325,7 @@ MonoBehaviour:
m_AudioPlay: 0 m_AudioPlay: 0
m_DebugDrawModesUseInteractiveLightBakingData: 0 m_DebugDrawModesUseInteractiveLightBakingData: 0
m_Position: 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 speed: 2
m_Value: {x: -66.720314, y: -10.525324, z: -0.0061987927} m_Value: {x: -66.720314, y: -10.525324, z: -0.0061987927}
m_RenderMode: 0 m_RenderMode: 0
@ -1377,7 +1377,7 @@ MonoBehaviour:
speed: 2 speed: 2
m_Value: {x: 0, y: 0, z: 0, w: 1} m_Value: {x: 0, y: 0, z: 0, w: 1}
m_Size: m_Size:
m_Target: 7.788484 m_Target: 1.3985817
speed: 2 speed: 2
m_Value: 7.788484 m_Value: 7.788484
m_Ortho: m_Ortho:
@ -1842,7 +1842,7 @@ MonoBehaviour:
scrollPos: {x: 0, y: 255} scrollPos: {x: 0, y: 255}
m_SelectedIDs: 28ce0000 m_SelectedIDs: 28ce0000
m_LastClickedID: 52776 m_LastClickedID: 52776
m_ExpandedIDs: 00000000f8cd0000facd0000fccd0000fecd000000ce000002ce000004ce000006ce000008ce00000ace00000cce00000ece000010ce000012ce000014ce000016ce000018ce00001ace00001cce00001ece000020ce000022ce000024ce000026ce000028ce00002ace00002cce00002ece000030ce000032ce000034ce000036ce000038ce00003ace00003cce00003ece0000 m_ExpandedIDs: 0000000028ce00002ace00002cce00002ece000030ce000032ce000034ce000036ce000038ce00003ace00003cce00003ece000040ce000042ce000044ce000046ce000048ce00004ace00004cce00004ece000050ce000052ce000054ce000056ce000058ce00005ace00005cce00005ece000060ce000062ce000064ce000066ce000068ce00006ace00006cce00006ece0000
m_RenameOverlay: m_RenameOverlay:
m_UserAcceptedRename: 0 m_UserAcceptedRename: 0
m_Name: m_Name:
@ -1871,7 +1871,7 @@ MonoBehaviour:
scrollPos: {x: 0, y: 0} scrollPos: {x: 0, y: 0}
m_SelectedIDs: m_SelectedIDs:
m_LastClickedID: 0 m_LastClickedID: 0
m_ExpandedIDs: 00000000f8cd0000facd0000fccd0000fecd000000ce000002ce000004ce000006ce000008ce00000ace00000cce00000ece000010ce000012ce000014ce000016ce000018ce00001ace00001cce00001ece000020ce000022ce000024ce000026ce000028ce00002ace00002cce00002ece000030ce000032ce000034ce000036ce000038ce00003ace00003cce00003ece0000 m_ExpandedIDs: 0000000028ce00002ace00002cce00002ece000030ce000032ce000034ce000036ce000038ce00003ace00003cce00003ece000040ce000042ce000044ce000046ce000048ce00004ace00004cce00004ece000050ce000052ce000054ce000056ce000058ce00005ace00005cce00005ece000060ce000062ce000064ce000066ce000068ce00006ace00006cce00006ece0000
m_RenameOverlay: m_RenameOverlay:
m_UserAcceptedRename: 0 m_UserAcceptedRename: 0
m_Name: m_Name: