working on enemies
This commit is contained in:
10
Assets/jelycho/Code/Enemies/IKillable.cs
Normal file
10
Assets/jelycho/Code/Enemies/IKillable.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using RebootKit.Engine.Simulation;
|
||||
|
||||
namespace RebootReality.jelycho.Enemies {
|
||||
public interface IKillable {
|
||||
bool IsAlive();
|
||||
|
||||
// @NOTE: Returns damage dealt
|
||||
ulong OnHit(Actor attacker, ulong damage);
|
||||
}
|
||||
}
|
||||
3
Assets/jelycho/Code/Enemies/IKillable.cs.meta
Normal file
3
Assets/jelycho/Code/Enemies/IKillable.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5449b1adab2f474b9d7a25cb59c66e6d
|
||||
timeCreated: 1754955332
|
||||
68
Assets/jelycho/Code/Enemies/WavesManagerActor.cs
Normal file
68
Assets/jelycho/Code/Enemies/WavesManagerActor.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Collections.Generic;
|
||||
using RebootKit.Engine.Extensions;
|
||||
using RebootKit.Engine.Main;
|
||||
using RebootKit.Engine.Simulation;
|
||||
using Unity.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
|
||||
namespace RebootReality.jelycho.Enemies {
|
||||
public class WavesManagerActor : Actor {
|
||||
[SerializeField] AssetReferenceGameObject[] m_ZombiesPrefabs;
|
||||
[SerializeField] Transform[] m_SpawnPoints;
|
||||
|
||||
[SerializeField] float m_SpawnRate = 32;
|
||||
[SerializeField] int m_MaxZombies = 1000;
|
||||
|
||||
int m_Stage;
|
||||
int m_ZombiesToSpawn;
|
||||
float m_SpawnTimer;
|
||||
|
||||
readonly List<ZombieActor> m_AliveZombies = new List<ZombieActor>(128);
|
||||
|
||||
//
|
||||
// @MARK: Waves Manager
|
||||
//
|
||||
public void StartWave(int stage) {
|
||||
m_Stage = stage;
|
||||
m_ZombiesToSpawn = stage * 10;
|
||||
m_SpawnTimer = 1.0f;
|
||||
}
|
||||
|
||||
void SpawnZombieAtRandomPosition() {
|
||||
Transform spawnPoint = m_SpawnPoints.Random();
|
||||
ZombieActor zombieActor = (ZombieActor) RR.SpawnActor(m_ZombiesPrefabs.Random(), spawnPoint.position, spawnPoint.rotation);
|
||||
zombieActor.died.AddListener(() => {
|
||||
m_AliveZombies.RemoveSwapBack(zombieActor);
|
||||
});
|
||||
|
||||
m_AliveZombies.Add(zombieActor);
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: IActor
|
||||
//
|
||||
protected override IActorData CreateActorData() {
|
||||
return new NoActorData();
|
||||
}
|
||||
|
||||
public override void OnServerTick(float deltaTime) {
|
||||
base.OnServerTick(deltaTime);
|
||||
|
||||
if (m_ZombiesToSpawn <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_AliveZombies.Count >= m_MaxZombies) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_SpawnTimer -= deltaTime;
|
||||
if (m_SpawnTimer <= 0.0f) {
|
||||
SpawnZombieAtRandomPosition();
|
||||
m_ZombiesToSpawn -= 1;
|
||||
m_SpawnTimer = 1.0f / m_SpawnRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/jelycho/Code/Enemies/WavesManagerActor.cs.meta
Normal file
3
Assets/jelycho/Code/Enemies/WavesManagerActor.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 554400bcdd2649daa241c7ba9c4884dd
|
||||
timeCreated: 1755566821
|
||||
@@ -1,8 +1,13 @@
|
||||
using System;
|
||||
using RebootKit.Engine.Extensions;
|
||||
using RebootKit.Engine.Main;
|
||||
using RebootKit.Engine.Network;
|
||||
using RebootKit.Engine.Simulation;
|
||||
using RebootReality.jelycho.Player;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using UnityEngine.Events;
|
||||
using Logger = RebootKit.Engine.Foundation.Logger;
|
||||
|
||||
namespace RebootReality.jelycho.Enemies {
|
||||
@@ -18,21 +23,39 @@ namespace RebootReality.jelycho.Enemies {
|
||||
}
|
||||
}
|
||||
|
||||
public interface IKillable {
|
||||
bool IsAlive();
|
||||
|
||||
float OnHit(Actor attacker, float damage);
|
||||
}
|
||||
|
||||
public class ZombieActor : Actor, IKillable {
|
||||
static readonly Logger s_Logger = new Logger(nameof(ZombieActor));
|
||||
|
||||
static readonly int s_MovementSpeedHash = Animator.StringToHash("MovementSpeed");
|
||||
|
||||
enum AIState {
|
||||
Idle,
|
||||
Dead,
|
||||
AttackBase,
|
||||
AttackCharacter,
|
||||
PanicEscape,
|
||||
Berserk
|
||||
}
|
||||
|
||||
[SerializeField] Animator m_Animator;
|
||||
|
||||
[SerializeField] NavMeshAgent m_NavAgent;
|
||||
|
||||
[SerializeField] Collider[] m_RagdollColliders;
|
||||
[SerializeField] Rigidbody[] m_RagdollRigidbodies;
|
||||
|
||||
[SerializeField] Collider[] m_Hitboxes;
|
||||
|
||||
[SerializeField] float m_MaxAttackDistance = 1.0f;
|
||||
[SerializeField] float m_LoseInterestMinDistance = 10.0f;
|
||||
[SerializeField] ulong m_BaseDamage = 10;
|
||||
[SerializeField] float m_AttackDelay = 1.0f;
|
||||
|
||||
AIState m_State = AIState.Idle;
|
||||
|
||||
PlayerActor m_PlayerTarget;
|
||||
float m_NextAttackTimer;
|
||||
|
||||
public UnityEvent died = new UnityEvent();
|
||||
|
||||
//
|
||||
// @MARK: Unity callbacks
|
||||
@@ -40,6 +63,151 @@ namespace RebootReality.jelycho.Enemies {
|
||||
void Awake() {
|
||||
SetRagdollLocal(false);
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Actor
|
||||
//
|
||||
public override void OnClientTick(float deltaTime) {
|
||||
base.OnClientTick(deltaTime);
|
||||
|
||||
if (!IsAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float velXZ = m_NavAgent.velocity.With(y: 0).magnitude;
|
||||
m_Animator.SetFloat(s_MovementSpeedHash, velXZ);
|
||||
}
|
||||
|
||||
public override void OnServerTick(float deltaTime) {
|
||||
base.OnServerTick(deltaTime);
|
||||
|
||||
if (RR.World.Context is not WorldContext world) {
|
||||
s_Logger.Error("Invalid world context");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (m_State) {
|
||||
case AIState.Idle: {
|
||||
ServerTickIdle(deltaTime);
|
||||
break;
|
||||
}
|
||||
|
||||
case AIState.AttackBase: {
|
||||
ServerTickAttackBase(deltaTime);
|
||||
break;
|
||||
}
|
||||
|
||||
case AIState.AttackCharacter: {
|
||||
ServerTickAttackCharacter(deltaTime);
|
||||
break;
|
||||
}
|
||||
|
||||
case AIState.PanicEscape: {
|
||||
break;
|
||||
}
|
||||
|
||||
case AIState.Berserk: {
|
||||
ServerTickBerserk(deltaTime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Zombie
|
||||
//
|
||||
void ServerTickIdle(float dt) {
|
||||
(PlayerActor playerActor, float distSqToPlayer) = FindClosestPlayerActor(transform.position);
|
||||
if (playerActor == null || distSqToPlayer >= m_LoseInterestMinDistance * m_LoseInterestMinDistance) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_State = AIState.AttackCharacter;
|
||||
m_PlayerTarget = playerActor;
|
||||
|
||||
s_Logger.Info($"Found player actor to attack: {m_PlayerTarget}");
|
||||
m_NavAgent.SetDestination(m_PlayerTarget.transform.position);
|
||||
m_NavAgent.isStopped = false;
|
||||
}
|
||||
|
||||
void ServerTickAttackCharacter(float dt) {
|
||||
if (m_PlayerTarget == null || !m_PlayerTarget.IsAlive()) {
|
||||
SetIdleState();
|
||||
return;
|
||||
}
|
||||
|
||||
float3 playerPos = m_PlayerTarget.transform.position;
|
||||
float3 zombiePos = transform.position;
|
||||
|
||||
float distToPlayerSq = math.distancesq(playerPos, zombiePos);
|
||||
if (distToPlayerSq >= m_LoseInterestMinDistance * m_LoseInterestMinDistance) {
|
||||
SetIdleState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (distToPlayerSq <= m_MaxAttackDistance * m_MaxAttackDistance) {
|
||||
m_NextAttackTimer -= dt;
|
||||
if (m_NextAttackTimer <= 0.0f) {
|
||||
m_Animator.CrossFade("Attack_0", 0.0f, 0);
|
||||
m_NextAttackTimer = m_AttackDelay;
|
||||
}
|
||||
|
||||
if (!m_NavAgent.isStopped) {
|
||||
m_NavAgent.isStopped = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float distFromDstToTargetSq = math.distancesq(playerPos, m_NavAgent.destination);
|
||||
if (distFromDstToTargetSq > 1.0f) {
|
||||
m_NavAgent.isStopped = false;
|
||||
m_NavAgent.SetDestination(m_PlayerTarget.transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
void ServerTickAttackBase(float dt) {
|
||||
|
||||
}
|
||||
|
||||
void ServerTickBerserk(float dt) {
|
||||
|
||||
}
|
||||
|
||||
void SetIdleState() {
|
||||
m_PlayerTarget = null;
|
||||
m_State = AIState.Idle;
|
||||
}
|
||||
|
||||
void Die() {
|
||||
s_Logger.Info("Die");
|
||||
EnableRagdoll();
|
||||
|
||||
m_State = AIState.Dead;
|
||||
died.Invoke();
|
||||
}
|
||||
|
||||
(PlayerActor, float) FindClosestPlayerActor(float3 origin) {
|
||||
if (RR.World.Context is not WorldContext context) {
|
||||
return (null, -1.0f);
|
||||
}
|
||||
|
||||
PlayerActor res = null;
|
||||
float closestDistanceSq = float.MaxValue;
|
||||
|
||||
foreach (Actor actor in RR.Actors()) {
|
||||
if (actor is not PlayerActor playerActor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float distSq = math.distancesq(actor.transform.position, origin);
|
||||
if (distSq < closestDistanceSq) {
|
||||
res = playerActor;
|
||||
closestDistanceSq = distSq;
|
||||
}
|
||||
}
|
||||
|
||||
return (res, closestDistanceSq);
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Actor
|
||||
@@ -85,29 +253,29 @@ namespace RebootReality.jelycho.Enemies {
|
||||
//
|
||||
// @MARK: IKillable
|
||||
//
|
||||
public float Health { get; private set; } = 100.0f;
|
||||
public ulong Health { get; private set; } = 100;
|
||||
|
||||
public bool IsAlive() {
|
||||
return Health > 0.0f;
|
||||
return Health > 0;
|
||||
}
|
||||
|
||||
public float OnHit(Actor attacker, float damage) {
|
||||
public ulong OnHit(Actor attacker, ulong damage) {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("OnHit can only be called on the server.");
|
||||
return 0.0f;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!IsAlive()) {
|
||||
return 0.0f;
|
||||
return 0;
|
||||
}
|
||||
|
||||
s_Logger.Info($"Hit: {damage}");
|
||||
|
||||
damage = math.min(damage, Health);
|
||||
Health -= damage;
|
||||
if (Health <= 0.0f) {
|
||||
s_Logger.Info("Die");
|
||||
EnableRagdoll();
|
||||
return damage - Mathf.Abs(Health);
|
||||
if (Health <= 0) {
|
||||
Die();
|
||||
return damage;
|
||||
}
|
||||
|
||||
return damage;
|
||||
|
||||
Reference in New Issue
Block a user