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