working on humbie still
This commit is contained in:
443
Assets/jelycho/Code/Enemies/Zombie/ZombieActor.cs
Normal file
443
Assets/jelycho/Code/Enemies/Zombie/ZombieActor.cs
Normal file
@@ -0,0 +1,443 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Animancer;
|
||||
using RebootKit.Engine.AI;
|
||||
using RebootKit.Engine.Extensions;
|
||||
using RebootKit.Engine.Main;
|
||||
using RebootKit.Engine.Network;
|
||||
using RebootKit.Engine.Simulation;
|
||||
using RebootReality.jelycho.Actors;
|
||||
using RebootReality.jelycho.Beacons;
|
||||
using RebootReality.jelycho.Main;
|
||||
using RebootReality.jelycho.Player;
|
||||
using TriInspector;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using Logger = RebootKit.Engine.Foundation.Logger;
|
||||
using Random = UnityEngine.Random;
|
||||
using UnityEvent = UnityEngine.Events.UnityEvent;
|
||||
|
||||
namespace RebootReality.jelycho.Enemies.Zombie {
|
||||
public class ZombieActorData : IActorData {
|
||||
public void Serialize(NetworkBufferWriter writer) {
|
||||
}
|
||||
|
||||
public void Deserialize(NetworkBufferReader reader) {
|
||||
}
|
||||
|
||||
public int GetMaxBytes() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ZombieBodyPartType {
|
||||
Body,
|
||||
Head,
|
||||
LeftArm,
|
||||
RightArm,
|
||||
LeftLeg,
|
||||
RightLeg
|
||||
}
|
||||
|
||||
// @TODO: weird naming
|
||||
[Serializable]
|
||||
public class ZombieBodyPart {
|
||||
public Transform root;
|
||||
public Transform[] wearables;
|
||||
public Collider[] colliders;
|
||||
public ParticleSystem bloodStreamParticles;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsAlive() {
|
||||
return root.gameObject.activeSelf;
|
||||
}
|
||||
|
||||
public void HideParts() {
|
||||
root.gameObject.SetActive(false);
|
||||
foreach (Transform wearable in wearables) {
|
||||
wearable.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
foreach (Collider collider in colliders) {
|
||||
collider.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ZombiePickVictim : IStrategy {
|
||||
public BehaviourNode.Status Process(Actor target, float dt) {
|
||||
if (target is not ZombieActor zombie) {
|
||||
return BehaviourNode.Status.Failure;
|
||||
}
|
||||
|
||||
Actor victim = zombie.FindNewVictim();
|
||||
if (victim == null) {
|
||||
return BehaviourNode.Status.Failure;
|
||||
}
|
||||
|
||||
return BehaviourNode.Status.Success;
|
||||
}
|
||||
}
|
||||
|
||||
class ZombieGoToPlayer : IStrategy {
|
||||
public BehaviourNode.Status Process(Actor target, float dt) {
|
||||
if (target is not ZombieActor zombie) {
|
||||
return BehaviourNode.Status.Failure;
|
||||
}
|
||||
|
||||
if (!zombie.HasTravelDestination) {
|
||||
float3 victimPos = zombie.Victim.transform.position;
|
||||
float dstToVictimSq = math.distancesq(victimPos, zombie.transform.position);
|
||||
|
||||
if (dstToVictimSq < 1.0f) {
|
||||
return BehaviourNode.Status.Success;
|
||||
}
|
||||
|
||||
zombie.GoTo(victimPos);
|
||||
return BehaviourNode.Status.Running;
|
||||
}
|
||||
|
||||
return BehaviourNode.Status.Running;
|
||||
}
|
||||
}
|
||||
|
||||
[DeclareBoxGroup("Body parts")]
|
||||
[DeclareBoxGroup("Animations")]
|
||||
public class ZombieActor : Actor, IHasHealth {
|
||||
static readonly Logger s_Logger = new Logger(nameof(ZombieActor));
|
||||
|
||||
[SerializeField] AnimancerComponent m_Animancer;
|
||||
|
||||
[SerializeField] NavMeshAgent m_NavAgent;
|
||||
public NavMeshAgent NavAgent {
|
||||
get {
|
||||
return m_NavAgent;
|
||||
}
|
||||
}
|
||||
|
||||
[SerializeField] Collider m_RootCollider;
|
||||
[SerializeField] Rigidbody[] m_RagdollRigidbodies;
|
||||
|
||||
[SerializeField] float m_MaxAttackDistance = 2.0f;
|
||||
[SerializeField] float m_LoseInterestMinDistance = 10.0f;
|
||||
[SerializeField] ulong m_BaseDamage = 10;
|
||||
[SerializeField] float m_AttackDelay = 1.0f;
|
||||
|
||||
[SerializeField, Group("Body parts")] ZombieBodyPart m_Head;
|
||||
[SerializeField, Group("Body parts")] ZombieBodyPart m_LeftArm;
|
||||
[SerializeField, Group("Body parts")] ZombieBodyPart m_RightArm;
|
||||
[SerializeField, Group("Body parts")] ZombieBodyPart m_LeftLeg;
|
||||
[SerializeField, Group("Body parts")] ZombieBodyPart m_RightLeg;
|
||||
|
||||
[SerializeField, Group("Animations")] TransitionAsset m_GroundLocomotion;
|
||||
[SerializeField, Group("Animations")] StringAsset m_GroundLocomotionPropertyRight;
|
||||
[SerializeField, Group("Animations")] StringAsset m_GroundLocomotionPropertyForward;
|
||||
SmoothedVector2Parameter m_SmoothLocomotionDirection;
|
||||
|
||||
[SerializeField, Group("Animations")] AnimationClip[] m_AttackClips;
|
||||
|
||||
BehaviourTree m_BehaviourTree;
|
||||
|
||||
public enum MindState {
|
||||
Normal,
|
||||
RunAway,
|
||||
Berserk
|
||||
}
|
||||
public bool IsRagdoll { get; private set; } = false;
|
||||
public MindState Mind { get; private set; } = MindState.Normal;
|
||||
public Actor Victim { get; private set; }
|
||||
|
||||
public bool HasTravelDestination { get; private set; }
|
||||
public float3 TravelDestination { get; private set; }
|
||||
|
||||
public UnityEvent died = new UnityEvent();
|
||||
|
||||
//
|
||||
// @MARK: Unity callbacks
|
||||
//
|
||||
void Awake() {
|
||||
SetRagdollLocal(IsRagdoll);
|
||||
|
||||
m_SmoothLocomotionDirection = new SmoothedVector2Parameter(m_Animancer,
|
||||
m_GroundLocomotionPropertyRight,
|
||||
m_GroundLocomotionPropertyForward,
|
||||
0.1f);
|
||||
m_Animancer.Play(m_GroundLocomotion);
|
||||
|
||||
m_BehaviourTree = new BehaviourTree("Zombie Behaviour");
|
||||
|
||||
var rootSelector = new Selector("Root");
|
||||
m_BehaviourTree.AddChild(rootSelector);
|
||||
|
||||
rootSelector.AddChild(CreateNormalSequence());
|
||||
}
|
||||
|
||||
BehaviourNode CreateNormalSequence() {
|
||||
var normalSequence = new Sequence("Normal", () => Mind == MindState.Normal);
|
||||
normalSequence.AddChild(new Leaf("Pick Victim", new ZombiePickVictim()));
|
||||
|
||||
var attackPlayerSequence = new Sequence("Attack Player", IsVictimPlayer);
|
||||
normalSequence.AddChild(attackPlayerSequence);
|
||||
attackPlayerSequence.AddChild(new Leaf("Go to Player", new ZombieGoToPlayer()));
|
||||
|
||||
var attackMotherSequence = new Sequence("Attack Mother", IsVictimMother);
|
||||
normalSequence.AddChild(attackMotherSequence);
|
||||
return normalSequence;
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Actor
|
||||
//
|
||||
public override void OnClientTick(float deltaTime) {
|
||||
base.OnClientTick(deltaTime);
|
||||
|
||||
if (!IsAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float3 vel = m_NavAgent.velocity;
|
||||
float forwardVelocity = math.dot(m_NavAgent.transform.forward, vel);
|
||||
float rightVelocity = math.dot(m_NavAgent.transform.right, vel);
|
||||
|
||||
m_SmoothLocomotionDirection.TargetValue = new Vector2(rightVelocity, forwardVelocity);
|
||||
}
|
||||
|
||||
public override void OnServerTick(float deltaTime) {
|
||||
base.OnServerTick(deltaTime);
|
||||
|
||||
if (RR.World.Context is not WorldContext world) {
|
||||
s_Logger.Error("Invalid world context");
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasTravelDestination) {
|
||||
float3 pos = transform.position;
|
||||
|
||||
if (math.distancesq(pos, TravelDestination) <= 1.0f) {
|
||||
HasTravelDestination = false;
|
||||
m_NavAgent.isStopped = true;
|
||||
}
|
||||
}
|
||||
|
||||
m_BehaviourTree.Process(this, deltaTime);
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Zombie
|
||||
//
|
||||
public bool GoTo(float3 pos) {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("Only server can call GoTo");
|
||||
return false;
|
||||
}
|
||||
|
||||
TravelDestination = pos;
|
||||
HasTravelDestination = true;
|
||||
m_NavAgent.isStopped = false;
|
||||
return m_NavAgent.SetDestination(TravelDestination);
|
||||
}
|
||||
|
||||
public bool IsVictimPlayer() {
|
||||
return Victim is PlayerActor;
|
||||
}
|
||||
|
||||
public bool IsVictimMother() {
|
||||
return Victim is MotherActor;
|
||||
}
|
||||
|
||||
public Actor FindNewVictim() {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("Only server can call FindNewVictim");
|
||||
return null;
|
||||
}
|
||||
|
||||
Victim = null;
|
||||
|
||||
(PlayerActor playerActor, float distSqToPlayer) = FindClosestPlayerActor(transform.position);
|
||||
if (playerActor != null && distSqToPlayer < m_LoseInterestMinDistance * m_LoseInterestMinDistance) {
|
||||
Victim = playerActor;
|
||||
return Victim;
|
||||
}
|
||||
|
||||
if (RR.World.Context is WorldContext ctx) {
|
||||
Victim = ctx.BaseManager.Mother;
|
||||
return Victim;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void PerformAttack() {
|
||||
m_Animancer.Play(m_AttackClips.Random());
|
||||
}
|
||||
|
||||
void Die() {
|
||||
EnableRagdoll();
|
||||
m_NavAgent.enabled = false;
|
||||
|
||||
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) {
|
||||
var zombieEvent = (ZombieActorEvents) actorEvent.EventID;
|
||||
|
||||
switch (zombieEvent) {
|
||||
case ZombieActorEvents.EnableRagdoll: {
|
||||
SetRagdollLocal(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: Ragdoll
|
||||
//
|
||||
void EnableRagdoll() {
|
||||
SendActorEvent((byte)ZombieActorEvents.EnableRagdoll);
|
||||
}
|
||||
|
||||
void SetRagdollLocal(bool active) {
|
||||
m_RootCollider.enabled = !active;
|
||||
|
||||
foreach (Rigidbody ragdollRigidbody in m_RagdollRigidbodies) {
|
||||
ragdollRigidbody.isKinematic = !active;
|
||||
}
|
||||
|
||||
m_Animancer.enabled = !active;
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: IHasHealth
|
||||
//
|
||||
public ulong Health { get; private set; } = 100;
|
||||
public ulong MaxHealth { get; private set; } = 100;
|
||||
|
||||
public bool IsAlive() {
|
||||
return Health > 0;
|
||||
}
|
||||
|
||||
//
|
||||
// @MARK: damage?
|
||||
//
|
||||
public void HitFeedback(float3 hitPosition) {
|
||||
if (RR.World.Context is WorldContext worldContext) {
|
||||
worldContext.FeedbacksManager.SpawnBloodSplash(hitPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public void ReceiveBodyPartDamage(ulong damage, ZombieBodyPartType bodyPartType) {
|
||||
if (!RR.IsServer()) {
|
||||
s_Logger.Error("ReceiveBodyPartDamage can only be called on the server.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ZombieBodyPart bodyPart = GetBodyParty(bodyPartType);
|
||||
if (!bodyPart.IsAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool isBodyPartDestroyed = Random.Range(0.0f, 1.0f) > 0.5f;
|
||||
|
||||
if (isBodyPartDestroyed) {
|
||||
bodyPart.HideParts();
|
||||
bodyPart.bloodStreamParticles.Play();
|
||||
|
||||
if (bodyPartType == ZombieBodyPartType.Head) {
|
||||
damage = Health;
|
||||
} else if (bodyPartType == ZombieBodyPartType.LeftLeg ||
|
||||
bodyPartType == ZombieBodyPartType.RightLeg) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
damage = math.min(damage, Health);
|
||||
|
||||
Health -= damage;
|
||||
if (Health <= 0) {
|
||||
Die();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasBodyPart(ZombieBodyPartType bodyPart) {
|
||||
if (Health <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bodyPart == ZombieBodyPartType.Body) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ZombieBodyPart part = GetBodyParty(bodyPart);
|
||||
if (part == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return part.IsAlive();
|
||||
}
|
||||
|
||||
ZombieBodyPart GetBodyParty(ZombieBodyPartType bodyPart) {
|
||||
return bodyPart switch {
|
||||
ZombieBodyPartType.Body => null,
|
||||
ZombieBodyPartType.Head => m_Head,
|
||||
ZombieBodyPartType.LeftArm => m_LeftArm,
|
||||
ZombieBodyPartType.RightArm => m_RightArm,
|
||||
ZombieBodyPartType.LeftLeg => m_LeftLeg,
|
||||
ZombieBodyPartType.RightLeg => m_RightLeg,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(bodyPart), bodyPart, null)
|
||||
};
|
||||
}
|
||||
|
||||
///
|
||||
/// @MARK: Editor Utility
|
||||
///
|
||||
#if UNITY_EDITOR
|
||||
[Button]
|
||||
void SetupHurtboxes() {
|
||||
foreach (ZombieHurtbox hurtbox in GetComponentsInChildren<ZombieHurtbox>()) {
|
||||
hurtbox.owner = this;
|
||||
UnityEditor.EditorUtility.SetDirty(hurtbox);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
enum ZombieActorEvents {
|
||||
None = 0x00,
|
||||
EnableRagdoll = 0x01
|
||||
}
|
||||
}
|
||||
3
Assets/jelycho/Code/Enemies/Zombie/ZombieActor.cs.meta
Normal file
3
Assets/jelycho/Code/Enemies/Zombie/ZombieActor.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82c11d5cbdd64c2ab63010901162299e
|
||||
timeCreated: 1752364002
|
||||
30
Assets/jelycho/Code/Enemies/Zombie/ZombieHurtbox.cs
Normal file
30
Assets/jelycho/Code/Enemies/Zombie/ZombieHurtbox.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using RebootKit.Engine.Main;
|
||||
using RebootKit.Engine.Simulation;
|
||||
using RebootReality.jelycho.Damage;
|
||||
using RebootReality.jelycho.Main;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
|
||||
namespace RebootReality.jelycho.Enemies.Zombie {
|
||||
public class ZombieHurtbox : MonoBehaviour, IHurtbox {
|
||||
public ZombieActor owner;
|
||||
[SerializeField] ZombieBodyPartType m_BodyPart = ZombieBodyPartType.Body;
|
||||
|
||||
void Awake() {
|
||||
Assert.IsNotNull(owner);
|
||||
}
|
||||
|
||||
public void ReceiveDamage(Actor attacker, ulong damage, float3 worldHitPos) {
|
||||
if (!owner.HasBodyPart(m_BodyPart)) {
|
||||
return;
|
||||
}
|
||||
|
||||
owner.ReceiveBodyPartDamage(damage, m_BodyPart);
|
||||
|
||||
if (RR.World.Context is WorldContext worldContext) {
|
||||
worldContext.FeedbacksManager.SpawnBloodSplash(worldHitPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/jelycho/Code/Enemies/Zombie/ZombieHurtbox.cs.meta
Normal file
3
Assets/jelycho/Code/Enemies/Zombie/ZombieHurtbox.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37d2b8f1679f41288194769e98584e0e
|
||||
timeCreated: 1756341610
|
||||
Reference in New Issue
Block a user