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) {
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()}");
}

View File

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

View File

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

View File

@ -6,24 +6,25 @@ using System.Threading.Tasks;
using UnityEngine;
[SerializeField]
public class Damageable : IDamageable {
public event Action<int, Vector2> OnTakeDamage;
public event Action OnDeath;
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 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; }

View File

@ -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<Rigidbody2D>();
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 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() { }
}
}

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_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: