289 lines
8.2 KiB
C#
289 lines
8.2 KiB
C#
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 {
|
|
public class ZombieActorData : IActorData {
|
|
public void Serialize(NetworkBufferWriter writer) {
|
|
}
|
|
|
|
public void Deserialize(NetworkBufferReader reader) {
|
|
}
|
|
|
|
public int GetMaxBytes() {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
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
|
|
//
|
|
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
|
|
//
|
|
protected override IActorData CreateActorData() {
|
|
return new ZombieActorData();
|
|
}
|
|
|
|
protected override void OnActorEventClient(ActorEvent actorEvent) {
|
|
ZombieActorEvents zombieEvent = (ZombieActorEvents) actorEvent.EventID;
|
|
|
|
switch (zombieEvent) {
|
|
case ZombieActorEvents.EnableRagdoll: {
|
|
SetRagdollLocal(true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// @MARK: Ragdoll
|
|
//
|
|
void EnableRagdoll() {
|
|
SendActorEvent((byte)ZombieActorEvents.EnableRagdoll);
|
|
}
|
|
|
|
void SetRagdollLocal(bool active) {
|
|
foreach (Collider ragdollCollider in m_RagdollColliders) {
|
|
ragdollCollider.enabled = active;
|
|
}
|
|
|
|
foreach (Rigidbody ragdollRigidbody in m_RagdollRigidbodies) {
|
|
ragdollRigidbody.isKinematic = !active;
|
|
}
|
|
|
|
m_Animator.enabled = !active;
|
|
|
|
foreach (Collider hitbox in m_Hitboxes) {
|
|
hitbox.enabled = !active;
|
|
}
|
|
}
|
|
|
|
//
|
|
// @MARK: IKillable
|
|
//
|
|
public ulong Health { get; private set; } = 100;
|
|
|
|
public bool IsAlive() {
|
|
return Health > 0;
|
|
}
|
|
|
|
public ulong OnHit(Actor attacker, ulong damage) {
|
|
if (!RR.IsServer()) {
|
|
s_Logger.Error("OnHit can only be called on the server.");
|
|
return 0;
|
|
}
|
|
|
|
if (!IsAlive()) {
|
|
return 0;
|
|
}
|
|
|
|
s_Logger.Info($"Hit: {damage}");
|
|
|
|
damage = math.min(damage, Health);
|
|
Health -= damage;
|
|
if (Health <= 0) {
|
|
Die();
|
|
return damage;
|
|
}
|
|
|
|
return damage;
|
|
}
|
|
}
|
|
|
|
enum ZombieActorEvents {
|
|
None = 0x00,
|
|
EnableRagdoll = 0x01
|
|
}
|
|
} |