using RuntimeSet; using UnityEngine; using UnityEngine.Assertions; namespace DefaultNamespace { 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; } } }