From db7559cd16d78d273c863358e3d509fe8d00784d Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Wed, 16 Aug 2023 14:56:18 -0500 Subject: [PATCH] initial work on enemy behavior --- Assets/Scripts/Enemy.cs | 184 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/Enemy.cs b/Assets/Scripts/Enemy.cs index f3c5bb1..cc133c2 100644 --- a/Assets/Scripts/Enemy.cs +++ b/Assets/Scripts/Enemy.cs @@ -1,11 +1,187 @@ +using RuntimeSet; using UnityEngine; +using UnityEngine.Assertions; namespace DefaultNamespace { - // TODO (Michael): Empty behavior until we decide more on how enemies should be structured. Mainly being used for - // other systems to have something to reference. public class Enemy : MonoBehaviour { - + private enum BehaviorState + { + Create, + Spawning, + Chasing, + Attacking, + Feared, + Dead, + } + + [SerializeField] private EnemyAttributesSO attributes; + [SerializeField] private EnemyRuntimeSetSO enemySet; + [SerializeField] private HeroUnitRuntimeSetSO heroSet; + [SerializeField] private GameObject target; + // TODO(zeph): Having behavior as a serialized member is convenient for + // experimentation early on so we can change it mid-play through the + // inspector, but later we'll more likely want it to be a property. + [SerializeField] private BehaviorState _behavior = BehaviorState.Spawning; + [SerializeField] private float _behaviorTime = 0; + [SerializeField] private float _fear = 0; + [SerializeField] private long _hp; + + private void Update() + { + // When the current behavior indicates movement, we do it in Update + // to ensure it is smooth at all framerates. + if (_behavior != BehaviorState.Chasing && _behavior != BehaviorState.Feared) + { + // No movement. + return; + } + if (target == null) + { + // No target. Find a new one. + Retarget(); + return; + } + // TODO(zeph): how do we actually,, get SPD attribute + var spd = SpaceSpeed(1000, Time.deltaTime); + var to = Vector3.MoveTowards(transform.position, target.transform.position, spd); + if (_behavior == BehaviorState.Feared) + { + // Move away rather than toward. + // TODO(zeph): this will limit speed moving away by distance + // to target if we're close + to = transform.position - 2 * (transform.position - to); + } + transform.position = to; + } + + private void FixedUpdate() + { + // We do behavior changes in the fixed update so that enemy actions + // do not depend on the framerate. Since movement happens in + // Update, the overall effect as framerate varies is that enemies + // might change their behaviors at slightly different distances, + // but they'll always be in a given state for the same duration. + // TODO(zeph): we could probably do this with coroutines instead, + // also i still haven't read anything about the actual fsm system + _behaviorTime += Time.fixedDeltaTime; + float dd; + switch (_behavior) + { + case BehaviorState.Create: + // TODO(zeph): calculate starting hp from CON attribute + _hp = 1; + SetBehavior(BehaviorState.Spawning); + break; + case BehaviorState.Spawning: + // TODO(zeph): move spawn time to an asset + if (_behaviorTime >= 0.5) + { + Retarget(); + SetBehavior(BehaviorState.Chasing); + } + break; + case BehaviorState.Chasing: + dd = SquareDistanceToTarget(); + // TODO(zeph): RNG attribute + if (dd <= SpaceRange(20000)) + { + SetBehavior(BehaviorState.Attacking); + } + break; + case BehaviorState.Attacking: + dd = SquareDistanceToTarget(); + if (dd > SpaceRange(20000)) + { + SetBehavior(BehaviorState.Chasing); + } + break; + case BehaviorState.Feared: + if (_behaviorTime > _fear) + { + SetBehavior(BehaviorState.Chasing); + } + break; + case BehaviorState.Dead: + // do nothing + break; + default: + // TODO(zeph): unreachable + break; + } + if (_behavior != BehaviorState.Dead && _hp == 0) + { + SetBehavior(BehaviorState.Dead); + // TODO(zeph): put a dead time constant somewhere + Destroy(gameObject, 10f); + } + } + + private void OnEnable() + { + Assert.IsNotNull(enemySet); + enemySet.Add(this); + } + + private void OnDisable() + { + enemySet.Remove(this); + } + + private void Retarget() + { + if (heroSet == null || heroSet.Count == 0) + { + // No heroes to target. + // TODO(zeph): switch to a special behavior? + target = null; + return; + } + // TODO(zeph): target based on threat, once threat exists + var k = Random.Range(0, heroSet.Count); + var it = heroSet.GetEnumerator(); + // This seems to be the best way to do this...? + for (var i = 0; i < k; i++) + { + it.MoveNext(); + } + target = it.Current.gameObject; + } + + private void SetBehavior(BehaviorState behavior) + { + _behavior = behavior; + _behaviorTime = 0; + } + + private float SquareDistanceToTarget() + { + return Vector3.Magnitude(transform.position - target.transform.position); + } + + public void Fear(float dur) + { + // If the enemy is already feared for a longer duration, don't + // shorten it. + if (_behavior == BehaviorState.Feared && _fear - _behaviorTime >= dur) + { + return; + } + _fear = dur; + SetBehavior(BehaviorState.Feared); + } + + private static float SpaceSpeed(long spd, float dt) + { + // For now, treat SPD as units of ten thousandths of a unit per second. + return (spd / 10000) * dt; + } + + private static float SpaceRange(long rng) + { + // Treat RNG as units of ten thousandths of a sqrt-meter. + return rng / 10000; + } } -} \ No newline at end of file +}