working on enemies

This commit is contained in:
2025-08-20 05:06:00 +02:00
parent f6a4db7b4d
commit fb1c3f8290
619 changed files with 46709 additions and 668 deletions

View 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);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5449b1adab2f474b9d7a25cb59c66e6d
timeCreated: 1754955332

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 554400bcdd2649daa241c7ba9c4884dd
timeCreated: 1755566821

View File

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