Create interfaces for IAttacker and IDamageable

Utilize interfaces to create new Enemy class inheriting them and further developing on them also encapsulating more enemy type functions
This commit is contained in:
Nico 2025-07-25 15:40:43 -07:00
parent d0349c8d99
commit 24b9ece93d
13 changed files with 355 additions and 200 deletions

View File

@ -4,109 +4,33 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using UnityEditor.Build;
using UnityEngine;
using UnityEngine.AI;
using static EnemySpawnerData;
using static UnityEngine.RuleTile.TilingRuleOutput;
public class Gobler : Alive {
[Header("Mechanics Attributes")]
[SerializeField] public float ChaseDistance = 10f;
[SerializeField] public float ChaseDistanceBuffer = 1f;
[SerializeField] public float AttackDistance = 1.5f;
[SerializeField] public float AttackMaskDiameter = 1f;
public class Gobler : Enemy {
[Header("Enemy Attributes")]
[SerializeField] public float Attack = 10f;
[SerializeField] public float Speed = 10f;
[SerializeField] public float Health = 10f;
[SerializeField] public float Energy = 10f;
protected FloatingTextSpawner TextPopUp;
private Player Player { get { return Player.Instance; } }
private Transform PlayerTransform { get { return Player.transform; } }
private Vector2 PlayerPos { get { return PlayerTransform.position; } }
private Vector2 CrystalPos { get { return GameManager.Crystal.transform.position; } }
private GameObject Owner;
private Rigidbody2D Rigidbody;
private Transform MyTransform { get { return Owner.transform; } }
private Vector2 MyPos { get { return MyTransform.position; } set { MyTransform.position = value; } }
private NavMeshAgent PathAgent;
//private List<Material> Materials;
private Material MaterialColorOverlay;
public Gobler(GameObject owner) {
Owner = owner;
PathAgent = Owner.GetComponent<NavMeshAgent>();
PathAgent.updatePosition = false;
PathAgent.updateRotation = false;
PathAgent.updateUpAxis = false;
PathAgent.speed = Speed;
IsUpdating = false;
MaxHealth = 100;
CurrentHealth = MaxHealth;
OnTakeDamage += (damage, direction) => SetState(State.TakeDamage);
OnDeath += () => SetState(State.Die);
Rigidbody = Owner.GetComponent<Rigidbody2D>();
foreach (var material in Owner.GetComponentsInChildren<SpriteRenderer>().Select(x => x.material).ToList()) {
switch (material.name.Split(' ')[0]) {
case "DamageFlashMat": MaterialColorOverlay = material; break;
}
}
TextPopUp = new FloatingTextSpawner(Owner.transform, 1);
public Gobler(GameObject owner) : base(owner) {
BaseDamage = 10;
AttackPreparationTime = 1;
AttackCooldownDuration = 1;
BaseHealth = 100;
CurrentHealth = BaseHealth;
IsKnockable = true;
SetState(State.GoToCrystal);
SubscribeToEvent();
//EnemyManagers[Enemies.Gobler].UpdateTick += () => PathAgentHandler.UpdatePositionIso(PathAgent, owner.transform);
//EnemyManagers[Enemies.Gobler].UpdateTick += Update;
}
private Action cachedUpdate;
private void SubscribeToEvent() {
cachedUpdate = () => {
if (PathAgent == null || Owner == null) return;
PathAgentHandler.UpdatePositionIso(PathAgent, Owner.transform);
Update();
};
EnemyManagers[Enemies.Gobler].UpdateTick += cachedUpdate;
EnemyManagers[Enemies.Gobler].GizmoTick += OnDrawGizmos;
}
private void UnsubscribeToEvent() {
EnemyManagers[Enemies.Gobler].UpdateTick -= cachedUpdate;
EnemyManagers[Enemies.Gobler].GizmoTick -= OnDrawGizmos;
}
public float DistFromPlayer { get; private set; }
public float DistFromCrystal { get; private set; }
public State CurrentState;
public enum State {
None,
GoToCrystal,
AttackCrystal,
ChasePlayer,
AttackPlayer,
PrepareAttack,
FinalizeAttack,
TakeDamage,
Die
}
protected void SetPriorityState() {
protected override void SetPriorityState() {
if (CurrentState == State.PrepareAttack) return;
if (CurrentState == State.FinalizeAttack) return;
if (CurrentState == State.ChasePlayer) return;
@ -116,17 +40,7 @@ public class Gobler : Alive {
SetState(State.ChasePlayer);
}
private bool IsUpdating;
protected void Update() {
if (Owner == null) return;
if (GameManager.Crystal == null) return;
if (IsUpdating) return;
IsUpdating = true;
DistFromPlayer = Vector2.Distance(MyPos, PlayerPos);
DistFromCrystal = Vector2.Distance(MyPos, CrystalPos);
SetPriorityState();
protected override void DoUpdate() {
switch (CurrentState) {
case State.None:
SetState(State.GoToCrystal);
@ -134,33 +48,27 @@ public class Gobler : Alive {
case State.GoToCrystal:
if (DistFromCrystal <= AttackDistance)
if (CanAttackCrystal)
SetState(State.PrepareAttack);
break;
case State.AttackCrystal:
break;
case State.ChasePlayer:
if (DistFromPlayer <= AttackDistance)
if (CanAttackPlayer)
SetState(State.PrepareAttack);
else if (DistFromPlayer > ChaseDistance + ChaseDistanceBuffer)
SetState(State.GoToCrystal);
SetState(State.None);
else
PathAgent.SetDestination(PlayerPos);
break;
case State.AttackPlayer:
break;
case State.TakeDamage:
if (IsInvincible) {
Rigidbody.linearVelocity = DirectionOfDamage * Knockback * 10 * InvincibilityLeft;
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);
}
if (!IsFrameFrozen) SetState(State.None);
@ -168,42 +76,34 @@ public class Gobler : Alive {
case State.PrepareAttack:
MaterialColorOverlay.SetFloat("_FlashAmount", 1 - FrameFreezeLeft);
MaterialColorOverlay.SetFloat("_FlashAmount", AttackPreparationTimeElapsedNormalized);
if (!IsFrameFrozen) SetState(State.FinalizeAttack);
break;
}
IsUpdating = false;
}
protected void SetState(State newState) {
protected override void SetState(State newState) {
CurrentState = newState;
switch (CurrentState) {
case State.None:
if (AggroOnPlayer) TextPopUp.SpawnFloatingText("?", Color.red);
PathAgent.isStopped = false;
AggroOnCrystal = false;
AggroOnPlayer = false;
break;
case State.GoToCrystal:
AggroOnCrystal = true;
PathAgent.SetDestination(CrystalPos);
break;
case State.AttackCrystal:
break;
case State.ChasePlayer:
break;
case State.AttackPlayer:
AggroOnPlayer = true;
TextPopUp.SpawnFloatingText("!", Color.red);
break;
@ -216,7 +116,7 @@ public class Gobler : Alive {
case State.PrepareAttack:
PathAgent.isStopped = true;
FrameFrozenUntil = Time.time + 1;
StartAttackPreparation();
MaterialColorOverlay.SetColor("_FlashColor", Color.purple);
break;
@ -225,28 +125,29 @@ public class Gobler : Alive {
Collider2D[] hits = Physics2D.OverlapCircleAll(MyPos + Vector2.down, 4);
foreach (var hit in hits) {
if (!hit.CompareTag("PlayerHurtBox") || !hit.CompareTag("SuperSpecialGem")) continue;
Debug.Log("Player hit");
Debug.Log($"{hit.tag} hit");
//GameObject parent = hit.transform.parent?.gameObject;
//Vector2 knockbakDirection = ActiveClass.HitBoxDraw.Directional ? PrevDirection : parent.transform.position - transform.position;
//EnemySpawnerData.EnemyMap[parent].Damage(ActiveClass.HitBoxDraw.Damage, knockbakDirection, ActiveClass.HitBoxDraw.Knockback);
}
SetState(State.None);
StartAttackCooldown();
SetState(AggroOnPlayer ? State.ChasePlayer : State.GoToCrystal);
break;
case State.Die:
UnsubscribeToEvent();
SpawnableEnemyInfo.DestroyEnemy(Owner, Enemies.Gobler);
SetState(State.None);
break;
}
if (PrevState != CurrentState) PrevState = CurrentState;
}
protected void OnDrawGizmos() {
protected override void OnDrawGizmos() {
if (Owner == null) return;
DrawChaseDistance();
@ -262,9 +163,11 @@ public class Gobler : Alive {
}
private void DrawAttackDistance() {
//Gizmos.color = CurrentState == (IState)AttackState ? Color.red : Color.green;
//Vector2 center = this.transform.position;
//Gizmos.DrawWireSphere(center, AttackDistance);
if (CurrentState != State.PrepareAttack) return;
Collider2D[] hits = Physics2D.OverlapCircleAll(MyPos + Vector2.down, 4);
Vector2 center = MyPos + Vector2.down;
DrawWireSphere(Color.salmon, center, 4);
}
private void DrawAttackReach() {
@ -277,59 +180,3 @@ public class Gobler : Alive {
DrawWireSphere(Color.red, point2D, AttackMaskDiameter);
}
}
public class Alive {
public int MaxHealth { get; protected set; }
public int CurrentHealth { get; protected set; }
public bool IsDead => CurrentHealth <= 0;
public bool IsInvincible => Time.time < InvincibleUntil;
protected float InvincibleUntil = 0f;
protected float InvincibilityLeft => Math.Max(0, InvincibleUntil - Time.time);
protected float InvincibilityDuration = 0.1f;
public bool IsFrameFrozen => Time.time < FrameFrozenUntil;
protected float FrameFrozenUntil = 0f;
protected float FrameFreezeLeft => Math.Max(0, FrameFrozenUntil - Time.time);
protected float FrameFreezeDuration = 0.3f;
public event Action<int, Vector2> OnTakeDamage;
public event Action OnDeath;
public event Action<int> OnHeal;
protected int DamageTaken;
protected Vector2 DirectionOfDamage;
protected int Knockback;
public void Damage(int amount, Vector2 hitDirection, int knockback = 1) {
if (IsDead || IsInvincible) return;
CurrentHealth -= amount;
CurrentHealth = Mathf.Max(CurrentHealth, 0);
InvincibleUntil = Time.time + InvincibilityDuration;
FrameFrozenUntil = Time.time + FrameFreezeDuration;
DamageTaken = amount;
Knockback = knockback;
hitDirection = hitDirection.normalized;
if (hitDirection == Vector2.zero) hitDirection = Vector2.up;
DirectionOfDamage = hitDirection;
OnTakeDamage?.Invoke(amount, hitDirection);
if (CurrentHealth == 0) OnDeath?.Invoke();
}
public void Heal(int amount) {
if (IsDead) return;
CurrentHealth += amount;
CurrentHealth = Mathf.Min(CurrentHealth, MaxHealth);
OnHeal?.Invoke(amount);
}
public void Reset() {
CurrentHealth = MaxHealth;
InvincibleUntil = 0f;
}
}

View File

@ -54,7 +54,7 @@ public class SpawnableEnemyInfo : MonoBehaviour {
if (NextSpawnableTime > Time.time) return;
NextSpawnableTime = Time.time + SpawnIntervalInSeconds;
foreach (var enemy in SpawnableEnemies){
foreach (var enemy in SpawnableEnemies) {
int currentSpawnCount = 0;
while (enemy.CanStartSpawning && enemy.CurrentSpawnCount < enemy.MaxSpawnCount && currentSpawnCount++ < enemy.MaxCountPerSpawn) {
var enemyObject = EnemyGameObjectPools[enemy.Type].Get(transform.position, Quaternion.identity);
@ -112,7 +112,7 @@ public static class EnemySpawnerData {
public static Dictionary<Enemies, EnemyManager> EnemyManagers = new Dictionary<Enemies, EnemyManager>();
public static Dictionary<Enemies, int> MaxSpawnCount = new Dictionary<Enemies, int>();
public static Dictionary<Enemies, int> CurrentSpawnCount = new Dictionary<Enemies, int>();
public static Dictionary<GameObject, Alive> EnemyMap = new Dictionary<GameObject, Alive>();
public static Dictionary<GameObject, Enemy> EnemyMap = new Dictionary<GameObject, Enemy>();
public static void RestartData() {
ObjectPrefabs = new Dictionary<Enemies, GameObject>();
@ -120,7 +120,7 @@ public static class EnemySpawnerData {
EnemyManagers = new Dictionary<Enemies, EnemyManager>();
MaxSpawnCount = new Dictionary<Enemies, int>();
CurrentSpawnCount = new Dictionary<Enemies, int>();
EnemyMap = new Dictionary<GameObject, Alive>();
EnemyMap = new Dictionary<GameObject, Enemy>();
}
public static void InitializeEnemyData(EnemySpawnInfo enemy) {
@ -130,5 +130,5 @@ public static class EnemySpawnerData {
MaxSpawnCount.Add(enemy.Type, 0);
CurrentSpawnCount.Add(enemy.Type, 0);
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a644663d39e949c44875ee37c76c1be6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5a1493ac8b3de2142a355b4e24cc4ed8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
public class Attacker {
public int BaseDamage = 0;
public bool CanAttack => Time.time < AttackCooldown;
public float AttackCooldown { get; protected set; } = 0f;
public float AttackCooldownLeft => Math.Max(0, AttackCooldown - Time.time);
public float AttackCooldownDuration { get; protected set; } = 1f;
public void StartAttackCooldown() => AttackCooldown = Time.time;
}
public interface IAttacker {
int BaseDamage { get; }
bool CanAttack => Time.time < AttackCooldown;
void StartAttackPreparation();
float AttackPreparationTime { get; }
float AttackPreparationTimeLeft { get; }
bool AttackIsReady { get; }
void StartAttackCooldown();
float AttackCooldown { get; }
float AttackCooldownLeft { get; }
float AttackCooldownDuration { get; }
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 90443507b543fbb45bd3a9f5c354596d

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
public class AttackerAndDamageable : Damageable, IAttacker {
public int BaseDamage { get; protected set; } = 0;
public bool CanAttack => Time.time > AttackCooldown;
public void StartAttackPreparation() => AttackReadyAt = Time.time + AttackPreparationTime;
public float AttackReadyAt { get; protected set; }
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 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 float AttackCooldownLeft => Math.Max(0, AttackCooldown - Time.time);
public float AttackCooldownDuration { get; protected set; } = 1f;
}

View File

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

View File

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
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; }
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;
public bool IsFrameFrozen => Time.time < FrameFrozenUntil;
public float FrameFrozenUntil { get; protected set; } = 0f;
public float FrameFreezeLeft => Math.Max(0, FrameFrozenUntil - Time.time);
public float FrameFreezeDuration { get; protected set; } = 0.3f;
public int DamageTaken { get; protected set; }
public Vector2 DirectionOfDamage { get; protected set; }
[SerializeField] public bool IsKnockable { get; protected set; }
public int Knockback { get; protected set; }
public void Damage(int amount, Vector2 hitDirection, int knockback = 1) {
if (IsDead || IsInvincible) return;
CurrentHealth -= amount;
CurrentHealth = Mathf.Max(CurrentHealth, 0);
InvincibleUntil = Time.time + InvincibilityDuration;
FrameFrozenUntil = Time.time + FrameFreezeDuration;
DamageTaken = amount;
if (IsKnockable) Knockback = knockback;
hitDirection = hitDirection.normalized;
if (hitDirection == Vector2.zero) hitDirection = Vector2.up;
DirectionOfDamage = hitDirection;
OnTakeDamage?.Invoke(amount, hitDirection);
if (CurrentHealth == 0) OnDeath?.Invoke();
}
public void Heal(int amount) {
if (IsDead) return;
CurrentHealth += amount;
CurrentHealth = Mathf.Min(CurrentHealth, BaseHealth);
OnHeal?.Invoke(amount);
}
public void Reset() {
CurrentHealth = BaseHealth;
InvincibleUntil = Time.time;
FrameFrozenUntil = Time.time;
}
}
public interface IDamageable {
int BaseHealth { get; }
int CurrentHealth { get; }
bool IsDead { get; }
bool IsInvincible { get; }
float InvincibleUntil { get; }
float InvincibilityLeft { get; }
float InvincibilityDuration { get; }
bool IsFrameFrozen { get; }
float FrameFrozenUntil { get; }
float FrameFreezeLeft { get; }
float FrameFreezeDuration { get; }
int DamageTaken { get; }
Vector2 DirectionOfDamage { get; }
bool IsKnockable { get; }
int Knockback { get; }
public void Damage(int amount, Vector2 hitDirection, int knockback = 1);
public void Heal(int amount);
public void Reset();
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 54a08dfa4b27c3d40bed84ba3e471ebb

View File

@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AI;
using static EnemySpawnerData;
[SerializeField]
public class Enemy: AttackerAndDamageable {
[Header("Mechanics Attributes")]
[SerializeField] public float ChaseDistance = 10f;
[SerializeField] public float ChaseDistanceBuffer = 1f;
[SerializeField] public float AttackDistance = 1.5f;
[SerializeField] public float AttackMaskDiameter = 1f;
[Header("Enemy Attributes")]
[SerializeField] public float Attack = 10f;
[SerializeField] public float Speed = 5f;
[SerializeField] public float Health = 10f;
[SerializeField] public float Energy = 10f;
protected FloatingTextSpawner TextPopUp;
protected Player Player { get { return Player.Instance; } }
protected Transform PlayerTransform { get { return Player.transform; } }
protected Vector2 PlayerPos { get { return PlayerTransform.position; } }
protected Vector2 CrystalPos { get { return GameManager.Crystal.transform.position; } }
protected GameObject Owner;
protected Rigidbody2D Rigidbody;
protected Transform MyTransform { get { return Owner.transform; } }
protected Vector2 MyPos { get { return MyTransform.position; } set { MyTransform.position = value; } }
protected NavMeshAgent PathAgent;
protected Material MaterialColorOverlay;
protected bool IsUpdating;
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 State CurrentState;
public State PrevState;
public enum State {
None,
GoToCrystal,
AttackCrystal,
ChasePlayer,
AttackPlayer,
PrepareAttack,
FinalizeAttack,
TakeDamage,
Die
}
public Enemy(GameObject owner) {
Owner = owner;
PathAgent = Owner.GetComponent<NavMeshAgent>();
PathAgent.updatePosition = false;
PathAgent.updateRotation = false;
PathAgent.updateUpAxis = false;
PathAgent.speed = Speed;
IsUpdating = false;
TextPopUp = new FloatingTextSpawner(Owner.transform, 1);
OnTakeDamage += (damage, direction) => SetState(State.TakeDamage);
OnDeath += () => SetState(State.Die);
OnDeath += () => UnsubscribeToEvent();
Rigidbody = Owner.GetComponent<Rigidbody2D>();
foreach (var material in Owner.GetComponentsInChildren<SpriteRenderer>().Select(x => x.material).ToList()) {
switch (material.name.Split(' ')[0]) {
case "DamageFlashMat": MaterialColorOverlay = material; break;
}
}
}
private Action cachedUpdate;
protected void SubscribeToEvent() {
cachedUpdate = () => {
if (PathAgent == null || Owner == null) return;
PathAgentHandler.UpdatePositionIso(PathAgent, Owner.transform);
Update();
};
EnemyManagers[Enemies.Gobler].UpdateTick += cachedUpdate;
EnemyManagers[Enemies.Gobler].GizmoTick += OnDrawGizmos;
}
protected void UnsubscribeToEvent() {
EnemyManagers[Enemies.Gobler].UpdateTick -= cachedUpdate;
EnemyManagers[Enemies.Gobler].GizmoTick -= OnDrawGizmos;
}
protected virtual void SetPriorityState() { }
protected virtual void DoUpdate() { }
protected void Update() {
if (Owner == null) return;
if (GameManager.Crystal == null) return;
if (IsUpdating) return;
IsUpdating = true;
DistFromPlayer = Vector2.Distance(MyPos, PlayerPos);
DistFromCrystal = Vector2.Distance(MyPos, CrystalPos);
SetPriorityState();
DoUpdate();
IsUpdating = false;
}
protected virtual void SetState(State newState) { }
protected virtual void OnDrawGizmos() { }
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 85104d36b352039479f65ed0b24a4768

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4ad93b11b89e04e40ab9a73f32005ec9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: