Skip to content

Instantly share code, notes, and snippets.

@polygonalcube
Last active January 17, 2026 21:01
Show Gist options
  • Select an option

  • Save polygonalcube/471e644f2e7de521d81c95446a60309d to your computer and use it in GitHub Desktop.

Select an option

Save polygonalcube/471e644f2e7de521d81c95446a60309d to your computer and use it in GitHub Desktop.
Senior Project: Kin Tandem (Unity, C#)

Kin Tandem is a two-player cooperative adventure game made in Unity by a team of 18. I served as the main enemy programmer & worked on the game's 5 enemies & 2 minor bosses, using reusable components & state machines.

The full repo isn't available, as the project was made using Perforce.

using UnityEngine;
public abstract class Enemy : MonoBehaviour
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// The base class for enemies. Contains variables referenced by the Enemy Hive Mind.
///
/// </summary>
private bool isAttacking;
private bool isHovering;
[HideInInspector] public Transform currentTarget;
[HideInInspector] public Vector3 hoverTarget;
[HideInInspector] public Vector2 idealHoverDistances;
//protected EnemyHiveMind enemyHiveMind;
LazyDependency<EnemyHiveMind> enemyHiveMind;
private void OnDestroy()
{
if (!EnemyHiveMindIsPresent())
{
return;
}
//enemyHiveMind?.UpdateEnemyList();
enemyHiveMind.Value?.enemies.Remove(gameObject);
if (enemyHiveMind.Value.hoveringEnemies.Contains(gameObject))
{
enemyHiveMind.Value?.hoveringEnemies.Remove(gameObject);
}
if (enemyHiveMind.Value.attackingEnemies.Contains(gameObject))
{
enemyHiveMind.Value?.attackingEnemies.Remove(gameObject);
}
}
protected virtual void Awake()
{
//enemyHiveMind = GameObject.FindGameObjectWithTag("Enemy Hive Mind").GetComponent<EnemyHiveMind>();
//enemyHiveMind?.UpdateEnemyList();
//enemyHiveMind.Value?.enemies.Add(gameObject);
}
protected virtual void Start()
{
enemyHiveMind.Value?.enemies.Add(gameObject);
}
/// <summary>
/// Returns true if the Enemy Hive Mind is present.
/// </summary>
/// <param></param>
/// <returns> True: the Enemy Hive Mind exists. False: the Enemy Hive Mind does not exist. </returns>
protected bool EnemyHiveMindIsPresent()
{
bool result = enemyHiveMind.IsResolvable();//enemyHiveMind.Value != null;
if (!result)
{
Debug.LogWarning("Enemy Hive Mind not present in this scene. Enemies won't function correctly without one.");
}
return result;
}
/// <summary>
/// Returns the value of the Enemy Hive Mind lazy dependency.
/// </summary>
/// <param></param>
/// <returns> The value of the Enemy Hive Mind lazy dependency. </returns>
protected EnemyHiveMind GetEnemyHiveMindValue()
{
return enemyHiveMind.Value;
}
/// <summary>
/// Returns true if this enemy is attacking.
/// </summary>
/// <param></param>
/// <returns> True: this enemy is attacking. False: this enemy is not attacking. </returns>
public bool GetIsAttacking()
{
return isAttacking;
}
/// <summary>
/// Set the value of isAttacking.
/// </summary>
/// <param name="newValue"> The new value of isAttacking. </param>
/// <returns></returns>
protected void SetIsAttacking(bool newValue)
{
isAttacking = newValue;
if (isAttacking)
{
if (!enemyHiveMind.Value.attackingEnemies.Contains(gameObject))
{
enemyHiveMind.Value?.attackingEnemies.Add(gameObject);
}
}
else
{
enemyHiveMind.Value?.attackingEnemies.Remove(gameObject);
}
/*if (EnemyHiveMindIsPresent())
{
enemyHiveMind.Value.attackingEnemies.Count = enemyHiveMind.Value.attackingEnemies.Count;
}*/
}
/// <summary>
/// Returns true if this enemy is hovering (staying close to the player without yet attacking).
/// </summary>
/// <param></param>
/// <returns> True: this enemy is hovering. False: this enemy is not hovering. </returns>
public bool GetIsHovering()
{
return isHovering;
}
/// <summary>
/// Set the value of isHovering.
/// </summary>
/// <param name="newValue"> The new value of isHovering. </param>
/// <returns></returns>
protected void SetIsHovering(bool newValue)
{
isHovering = newValue;
if (isHovering)
{
if (!enemyHiveMind.Value.hoveringEnemies.Contains(gameObject))
{
enemyHiveMind.Value?.hoveringEnemies.Add(gameObject);
}
}
else
{
enemyHiveMind.Value?.hoveringEnemies.Remove(gameObject);
}
}
}
using System.Collections.Generic;
using UnityEngine;
public class EnemyHiveMind : SingletonMonobehavior<EnemyHiveMind>
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// The collective brain of all the enemies. Has functionality for determining target positions & limiting the amount of attackers.
///
/// </summary>
//[SerializeField] private GameObject[] enemies = new GameObject[0];
public List<GameObject> enemies = new();
public List<GameObject> hoveringEnemies = new();
public List<GameObject> attackingEnemies = new();
//[HideInInspector] public int numberOfAttackers;
public int maximumNumberOfAttackers = 3;
//[SerializeField] private float closeHoverDistance = 4f;
//[SerializeField] private float farHoverDistance = 5f;
[SerializeField] private Transform enemyTarget;
//[SerializeField] private string enemyTag = "Enemy";
//public static EnemyHiveMind ehm;
//private float mindStateTimer;
//private bool mindWantsAttacks;
protected override void OnDestroy()
{
base.OnDestroy();
}
protected override void Awake()
{
base.Awake();
/*if (ehm != null && ehm != this)
{
Destroy(gameObject);
}
else
{
ehm = this;
}
DontDestroyOnLoad(gameObject);*/
enemyTarget = GameObject.FindGameObjectWithTag("Player").transform;
}
void Update()
{
//UpdateEnemyList();
//FindAllHoveringEnemies();
DetermineHoverPositions();
//FindAllAttackingEnemies();
/*mindStateTimer -= Time.deltaTime;
if (mindStateTimer <= 0f)
{
mindWantsAttacks = !mindWantsAttacks;
if (mindWantsAttacks)
{
mindStateTimer = 0.1f;
}
else
{
mindStateTimer = 4.9f;
}
}*/
}
/// <summary>
/// Returns true if there aren't too many attacking enemies to allow more enemies to attack.
/// </summary>
/// <param></param>
/// <returns> False: too many attackers to allow more attackers. True: not too many attackers to allow more attackers. </returns>
public bool CanAttack()
{
return (NumberOfAttackers() < maximumNumberOfAttackers);// && mindWantsAttacks;
}
/// <summary>
/// Given the list of hovering enemies, determine the hover targets for each of them. If the index of an enemy in
/// the list is prime, the enemy will stand closer to the player.
/// </summary>
/// <param></param>
/// <returns></returns>
void DetermineHoverPositions()
{
int amountOfHoveringEnemies = hoveringEnemies.Count;
if (amountOfHoveringEnemies <= 0)
{
return;
}
for (int i = 0; i < amountOfHoveringEnemies; i++)
{
Enemy enemyScript = hoveringEnemies[i].GetComponent<Enemy>();
float angle = (TwoPIRadians() / (float)(amountOfHoveringEnemies)) * (float)(i);
float vectorMagnitude = enemyScript.idealHoverDistances.y;
if (IsPrime(i))
{
vectorMagnitude = enemyScript.idealHoverDistances.x;
}
Vector3 newHoverTarget = enemyScript.currentTarget.position +
(new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * vectorMagnitude);
hoveringEnemies[i].GetComponent<Enemy>().hoverTarget = newHoverTarget;
//Debug.Log(newHoverTarget);
}
}
/// <summary>
/// Finds all enemies marked as attacking.
/// </summary>
/// <param></param>
/// <returns></returns>
/*void FindAllAttackingEnemies()
{
attackingEnemies.Clear();
foreach (GameObject enemy in enemies)
{
Enemy enemyScript = enemy.GetComponent<Enemy>();
if (enemyScript != null && enemyScript.GetIsAttacking())
{
attackingEnemies.Add(enemy);
}
}
numberofAttackers = attackingEnemies.Count;
}*/
/// <summary>
/// Finds all enemies marked as hovering.
/// </summary>
/// <param></param>
/// <returns></returns>
/*void FindAllHoveringEnemies()
{
hoveringEnemies.Clear();
foreach (GameObject enemy in enemies)
{
Enemy enemyScript = enemy.GetComponent<Enemy>();
if (enemyScript != null && enemyScript.GetIsHovering())
{
hoveringEnemies.Add(enemy);
}
}
}*/
/// <summary>
/// Funny boolean. Returns true if the number given is one of the first few prime numbers.
/// </summary>
/// <param name="number"> Will get checked for being prime. </param>
/// <returns> True: is one of the first few prime numbers. False: is not one of the first few prime numbers. </returns>
bool IsPrime(int number)
{
return (number == 2 || number == 3 || number == 5 || number == 7 || number == 11 || number == 13 || number == 15);
}
/// <summary>
/// Returns attackingEnemies.Count.
/// </summary>
/// <param></param>
/// <returns> attackingEnemies.Count. </returns>
public int NumberOfAttackers()
{
return attackingEnemies.Count;
}
/// <summary>
/// More convenient than typing "Mathf.PI * 2f".
/// </summary>
/// <param></param>
/// <returns> Mathf.PI * 2f. </returns>
float TwoPIRadians()
{
return Mathf.PI * 2f;
}
/// <summary>
/// Updates the enemy list.
/// </summary>
/// <param></param>
/// <returns></returns>
/*public void UpdateEnemyList()
{
enemies = GameObject.FindGameObjectsWithTag(enemyTag);
}*/
}
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityHFSM;
[RequireComponent(typeof(GroundCheckComponent))]
[RequireComponent(typeof(MoveComponent))]
[RequireComponent(typeof(NavigateComponent))]
public class EnemyMeleeTurtle : Enemy
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// The logic for the melee enemy. It starts out idle, then approaches if the player gets close enough,
/// then hovers around the player before eventually attacking.
///
/// </summary>
private StateMachine finiteStateMachine;
private GroundCheckComponent groundChecker;
private HealthComponent health;
private ModelEffectComponent modelEffects;
private MoveComponent mover;
private NavigateComponent navigator;
private NavMeshAgent navMeshAgent;
private GameObject dummyGameObject;
//private Transform[] targets;
private List<Transform> targets = new();
//public Transform currentTarget;
[SerializeField] private float approachDistance = 15f;
[SerializeField] private float attackDistance = 0.75f;
[SerializeField] private float hoverDistance = 6f;
[SerializeField] private float playerLossDistance = 18f;
[SerializeField] private Vector2 idealHoverDistancesSet = new(4f, 5f);
private float stateChangeTimer;
[SerializeField] private float flickerTime = 0.55f;
enum Age
{
Adult,
Baby
};
protected override void Awake()
{
base.Awake();
groundChecker = GetComponent<GroundCheckComponent>();
health = GetComponent<HealthComponent>();
modelEffects = GetComponent<ModelEffectComponent>();
mover = GetComponent<MoveComponent>();
navigator = GetComponent<NavigateComponent>();
navMeshAgent = GetComponent<NavMeshAgent>();
dummyGameObject = new GameObject();
GameObject[] targetGameObjects = GameObject.FindGameObjectsWithTag("Player");
foreach (GameObject gameObject in targetGameObjects)
{
targets.Add(gameObject.transform);
}
currentTarget = targets[0];
finiteStateMachine = new StateMachine();
}
protected override void Start()
{
base.Start();
Action<float> Flicker = flicker => modelEffects.FlickerForSecondsEvent(flickerTime);
health.OnDamaged += Flicker;
idealHoverDistances = idealHoverDistancesSet;
// Add states.
finiteStateMachine.AddState
(
"Idle", new State
(
onEnter: state => EnterIdle(),
onLogic: state => Idle(),
onExit: state => ExitIdle()
)
);
finiteStateMachine.AddState
(
"Approach", new State
(
onEnter: state => EnterApproach(),
onLogic: state => Approach(),
onExit: state => ExitApproach()
)
);
finiteStateMachine.AddState
(
"Hover", new State
(
onEnter: state => EnterHover(),
onLogic: state => Hover(),
onExit: state => ExitHover()
)
);
finiteStateMachine.AddState
(
"Wait", new State
(
onEnter: state => EnterWait(),
onLogic: state => Wait(),
onExit: state => ExitWait()
)
);
finiteStateMachine.AddState
(
"Attack", new State
(
onEnter: state => EnterAttack(),
onLogic: state => Attack(),
onExit: state => ExitAttack()
)
);
finiteStateMachine.AddState("Approach", new State(onLogic: state => Approach()));
finiteStateMachine.AddState("EnterHover", new State(onLogic: state => EnterHover()));
finiteStateMachine.AddState("Hover", new State(onLogic: state => Hover()));
finiteStateMachine.AddState("EnterWait", new State(onLogic: state => EnterWait()));
finiteStateMachine.AddState("Wait", new State(onLogic: state => Wait()));
finiteStateMachine.AddState("PreAttack", new State(onLogic: state => PreAttack()));
finiteStateMachine.AddState("EnterAttack", new State(onLogic: state => EnterAttack()));
finiteStateMachine.AddState("Attack", new State(onLogic: state => Attack()));
// Base state is Idle.
finiteStateMachine.SetStartState("Idle");
// Logic for transitioning between states
// First state is the from state, second is the to state, third is the condition
/*fsm.AddTwoWayTransition(
"Patrol",
"Approach",
transition => InApproachingRange()
);*/
finiteStateMachine.AddTransition(new Transition(
"Idle", "Approach", transition => (InApproachingRange() && EnemyHiveMindIsPresent())));
finiteStateMachine.AddTransition(new Transition(
"Approach", "Idle", transition => HasLostThePlayer()));
finiteStateMachine.AddTransition(new Transition(
"Approach", "EnterHover", transition => InHoveringRange()));
finiteStateMachine.AddTransition(new Transition(
"EnterHover", "Hover", transition => true));
finiteStateMachine.AddTransition(new Transition(
"Hover", "EnterWait", transition => (stateChangeTimer <= 0f && !GetEnemyHiveMindValue().CanAttack() && CloseToHoverTarget())));
finiteStateMachine.AddTransition(new Transition(
"Hover", "PreAttack", transition => (stateChangeTimer <= 0f && GetEnemyHiveMindValue().CanAttack())));
finiteStateMachine.AddTransition(new Transition(
"EnterWait", "Wait", transition => true));
finiteStateMachine.AddTransition(new Transition(
"Wait", "EnterHover", transition => (stateChangeTimer <= 0f && !GetEnemyHiveMindValue().CanAttack())));
finiteStateMachine.AddTransition(new Transition(
"Wait", "PreAttack", transition => (stateChangeTimer <= 0f && GetEnemyHiveMindValue().CanAttack())));
finiteStateMachine.AddTransition(new Transition(
"PreAttack", "Approach", transition => OutsideOfHoverDistance()));
finiteStateMachine.AddTransition(new Transition(
"PreAttack", "EnterAttack", transition => InAttackingRange()));
finiteStateMachine.AddTransition(new Transition(
"EnterAttack", "Attack", transition => true));
finiteStateMachine.AddTransition(new Transition(
"Attack", "EnterHover", transition => stateChangeTimer <= 0f));
finiteStateMachine.Init();
}
void FixedUpdate()
{
finiteStateMachine.OnLogic();
}
/// <summary>
/// The approach state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Approach()
{
SetIsAttacking(false);
mover.moveDirection = navigator.DecideDirection(dummyGameObject.transform, currentTarget.position, navMeshAgent);
mover.Move(groundChecker.IsGrounded());
}
/// <summary>
/// The attack state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Attack()
{
mover.moveDirection = Vector3.zero;
mover.Move(groundChecker.IsGrounded());
stateChangeTimer -= Time.deltaTime;
}
/// <summary>
/// Returns true if the enemy & player are close enough together, while the enemy is hovering.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is close enough to the player. False: the enemy is not close enough to the player. </returns>
bool CloseToHoverTarget()
{
return navigator.DisplacementFromTarget(transform.position, hoverTarget) <= 1.0f;
}
/// <summary>
/// Determines the closest target out of a list of targets.
/// </summary>
/// <param></param>
/// <returns></returns>
void DetermineCurrentTarget()
{
if (targets.Count <= 0)
{
return;
}
float lowestDistance = float.MaxValue;
foreach (Transform target in targets)
{
float targetDistance = navigator.DisplacementFromTarget(transform.position, target.position);
if (targetDistance < lowestDistance)
{
lowestDistance = targetDistance;
currentTarget = target;
}
}
}
/*
/// <summary>
/// Given a position, decides the direction to go.
/// </summary>
/// <param></param>
/// <returns></returns>
void DecideDirection(Vector3 targetToFaceTowards)
{
dummyGameObject.transform.position = new Vector3(transform.position.x, targetToFaceTowards.y, transform.position.z);
bool willNavigate = willPathfind && navMeshAgent != null;
if (willNavigate)
{
navigator.SetDestination(targetToFaceTowards);
NavMeshHit navMeshHit;
navigator.SamplePathPosition(NavMesh.AllAreas, 1f, out navMeshHit);
dummyGameObject.transform.LookAt(navMeshHit.position);
direction = dummyGameObject.transform.forward;
direction = new Vector3(direction.x, 0f, direction.z);
}
else
{
//dummyGameObject.transform.position = new Vector3(transform.position.x, targetToFaceTowards.y, transform.position.z);
dummyGameObject.transform.LookAt(targetToFaceTowards);
//model.transform.rotation = dummyGameObject.transform.rotation;
//model.transform.localPosition = Vector3.zero;
//Debug.Log(dummyGameObject.transform.forward);
//Debug.Log(dummyGameObject.transform.TransformDirection(dummyGameObject.transform.forward));
direction = dummyGameObject.transform.forward;
direction = new Vector3(direction.x, 0f, direction.z);
//direction.Normalize();
}
}
*//*
/// <summary>
/// Given a position, determines the horizontal (Y-axis omitted) displacement from self.
/// </summary>
/// <param name="targetToCheck"> The target to check. </param>
/// <returns> Distance from target. </returns>
float DisplacementFromTarget(Vector3 targetToCheck)
{
return Vector2.Distance(new Vector2(transform.position.x, transform.position.z), new Vector2(targetToCheck.x, targetToCheck.z));
}
*/
/*LazyDependency<EnemyHiveMind> EnemyHiveMind()
{
LazyDependency<EnemyHiveMind> ehm;
if
}*/
/// <summary>
/// The enter attack state.
/// </summary>
/// <param></param>
/// <returns></returns>
void EnterAttack()
{
//SetIsAttacking(true);
SetIsHovering(false);
stateChangeTimer = 0.6f;
//animator.SetTrigger("Attack");
}
/// <summary>
/// The enter hover state.
/// </summary>
/// <param></param>
/// <returns></returns>
void EnterHover()
{
//SetIsAttacking(false);
SetIsHovering(true);
stateChangeTimer = 0.7f;//1.2f;
DetermineCurrentTarget();
}
/// <summary>
/// The enter wait state.
/// </summary>
/// <param></param>
/// <returns></returns>
void EnterWait()
{
//SetIsHovering(false);
SetIsAttacking(false);
stateChangeTimer = 2.0f;//0.3f;
DetermineCurrentTarget();
}
/// <summary>
/// Returns true if the enemy & player are far enough apart.
/// </summary>
/// <param></param>
/// <returns> True: the enemy has lost the player by being too far away. False: the enemy has not lost the player by being too far away. </returns>
bool HasLostThePlayer()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) > playerLossDistance;
}
/// <summary>
/// The hover state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Hover()
{
if (navigator.DisplacementFromTarget(transform.position, hoverTarget) > 0.2f)
{
//navigator.DecideDirection(hoverTarget);
mover.moveDirection = navigator.DecideDirection(dummyGameObject.transform, hoverTarget, navMeshAgent);
//mover.moveDirection = direction;
}
else
{
mover.moveDirection = Vector3.zero;
}
mover.Move(groundChecker.IsGrounded());
if (navigator.DisplacementFromTarget(transform.position, currentTarget.position) < hoverDistance)
{
stateChangeTimer -= Time.deltaTime;
}
}
/// <summary>
/// The idle state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Idle()
{
mover.moveDirection = Vector3.zero;
mover.Move(groundChecker.IsGrounded());
DetermineCurrentTarget();
}
/// <summary>
/// Returns true if the enemy is in approaching range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is in approaching range of the player. False: the enemy is not in approaching range of the player. </returns>
bool InApproachingRange()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) < approachDistance;
}
// <summary>
/// Returns true if the enemy is in attacking range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is in attacking range of the player. False: the enemy is not in attacking range of the player. </returns>
bool InAttackingRange()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) < attackDistance;
}
// <summary>
/// Returns true if the enemy is in hovering range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is in hovering range of the player. False: the enemy is not in hovering range of the player. </returns>
bool InHoveringRange()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) < hoverDistance;
}
// <summary>
/// Returns true if the enemy is not in hovering range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is not in hovering range of the player. False: the enemy is in hovering range of the player. </returns>
bool OutsideOfHoverDistance()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) > hoverDistance;
}
/// <summary>
/// The pre-attack state.
/// </summary>
/// <param></param>
/// <returns></returns>
void PreAttack()
{
SetIsHovering(false);
SetIsAttacking(true);
//navigator.DecideDirection(target.position);
mover.moveDirection = navigator.DecideDirection(dummyGameObject.transform, currentTarget.position, navMeshAgent);
//mover.moveDirection = direction;
mover.Move(groundChecker.IsGrounded());
}
/// <summary>
/// The wait state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Wait()
{
mover.moveDirection = Vector3.zero;
mover.Move(groundChecker.IsGrounded());
stateChangeTimer -= Time.deltaTime;
}
}
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityHFSM;
[RequireComponent(typeof(GroundCheckComponent))]
[RequireComponent(typeof(MoveComponent))]
[RequireComponent(typeof(NavigateComponent))]
//[RequireComponent(typeof(ShooterComponent))]
public class EnemySpellOffenseLizard : Enemy
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// The logic for the melee enemy. It starts out idle, then approaches if the player gets close enough,
/// then hovers around the player before eventually sending projectiles the player's way.
///
/// </summary>
private StateMachine finiteStateMachine;
private GroundCheckComponent groundChecker;
private HealthComponent health;
private ModelEffectComponent modelEffects;
private MoveComponent mover;
private NavigateComponent navigator;
[SerializeField] private ShooterComponent shooter;
private NavMeshAgent navMeshAgent;
private GameObject dummyGameObject;
private List<Transform> targets = new();
[SerializeField] private float approachDistance = 20f;
[SerializeField] private float attackDistance = 13f;
[SerializeField] private float hoverDistance = 12f;
[SerializeField] private float playerLossDistance = 25f;
[SerializeField] private Vector2 idealHoverDistancesSet = new(9f, 11f);
private float stateChangeTimer;
[SerializeField] private float flickerTime = 0.55f;
[SerializeField] private LayerMask playerLayers;
protected override void Awake()
{
base.Awake();
groundChecker = GetComponent<GroundCheckComponent>();
health = GetComponent<HealthComponent>();
modelEffects = GetComponent<ModelEffectComponent>();
mover = GetComponent<MoveComponent>();
navigator = GetComponent<NavigateComponent>();
if (shooter == null)
{
shooter = GetComponent<ShooterComponent>();
if (shooter == null)
{
Debug.LogWarning("No ShooterComponent found on this ranged enemy. Expect incorrect behavior.");
}
}
navMeshAgent = GetComponent<NavMeshAgent>();
dummyGameObject = new GameObject();
GameObject[] targetGameObjects = GameObject.FindGameObjectsWithTag("Player");
foreach (GameObject gameObject in targetGameObjects)
{
targets.Add(gameObject.transform);
}
currentTarget = targets[0];
finiteStateMachine = new StateMachine();
}
protected override void Start()
{
base.Start();
Action<float> Flicker = flicker => modelEffects.FlickerForSecondsEvent(flickerTime);
health.OnDamaged += Flicker;
idealHoverDistances = idealHoverDistancesSet;
// Add states.
finiteStateMachine.AddState
(
"Idle", new State
(
onEnter: state => EnterIdle(),
onLogic: state => Idle(),
onExit: state => ExitIdle()
)
);
finiteStateMachine.AddState
(
"Approach", new State
(
onEnter: state => EnterApproach(),
onLogic: state => Approach(),
onExit: state => ExitApproach()
)
);
(
"Cast", new State
(
onEnter: state => EnterCast(),
onLogic: state => Cast(),
onExit: state => ExitCast()
)
);
finiteStateMachine.AddState("Idle", new State(onLogic: state => Idle()));
finiteStateMachine.AddState("Approach", new State(onLogic: state => Approach()));
finiteStateMachine.AddState("EnterHover", new State(onLogic: state => EnterHover()));
finiteStateMachine.AddState("Hover", new State(onLogic: state => Hover()));
finiteStateMachine.AddState("EnterWait", new State(onLogic: state => EnterWait()));
finiteStateMachine.AddState("Wait", new State(onLogic: state => Wait()));
finiteStateMachine.AddState("EnterAttack", new State(onLogic: state => EnterAttack()));
finiteStateMachine.AddState("Attack", new State(onLogic: state => Attack()));
// Base state is Idle.
finiteStateMachine.SetStartState("Idle");
// Logic for transitioning between states
// First state is the from state, second is the to state, third is the condition
/*fsm.AddTwoWayTransition(
"Patrol",
"Approach",
transition => InApproachingRange()
);*/
finiteStateMachine.AddTransition(new Transition(
"Idle", "Approach", transition => (InApproachingRange() && EnemyHiveMindIsPresent())));
finiteStateMachine.AddTransition(new Transition(
"Approach", "Idle", transition => HasLostThePlayer()));
finiteStateMachine.AddTransition(new Transition(
"Approach", "EnterHover", transition => InHoveringRange()));
finiteStateMachine.AddTransition(new Transition(
"EnterHover", "Hover", transition => true));
finiteStateMachine.AddTransition(new Transition(
"Hover", "EnterWait", transition => (stateChangeTimer <= 0f && !GetEnemyHiveMindValue().CanAttack() && CloseToHoverTarget())));
finiteStateMachine.AddTransition(new Transition(
"Hover", "EnterAttack", transition => (stateChangeTimer <= 0f && GetEnemyHiveMindValue().CanAttack())));
finiteStateMachine.AddTransition(new Transition(
"EnterWait", "Wait", transition => true));
finiteStateMachine.AddTransition(new Transition(
"Wait", "EnterHover", transition => (stateChangeTimer <= 0f && !GetEnemyHiveMindValue().CanAttack())));
finiteStateMachine.AddTransition(new Transition(
"Wait", "EnterAttack", transition => (stateChangeTimer <= 0f && GetEnemyHiveMindValue().CanAttack())));
finiteStateMachine.AddTransition(new Transition(
"EnterAttack", "Attack", transition => true));
finiteStateMachine.AddTransition(new Transition(
"Attack", "EnterHover", transition => OutsideOfAttackDistance()));
finiteStateMachine.Init();
}
void FixedUpdate()
{
finiteStateMachine.OnLogic();
}
/// <summary>
/// The approach state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Approach()
{
SetIsAttacking(false);
mover.moveDirection = navigator.DecideDirection(dummyGameObject.transform, currentTarget.position, navMeshAgent);
mover.Move(groundChecker.IsGrounded());
}
/// <summary>
/// The attack state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Attack()
{
mover.moveDirection = Vector3.zero;
mover.Move(groundChecker.IsGrounded());
stateChangeTimer -= Time.deltaTime;
shooter.Shoot();
}
/// <summary>
/// Returns true if the enemy & player are close enough together, while the enemy is hovering.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is close enough to the player. False: the enemy is not close enough to the player. </returns>
bool CloseToHoverTarget()
{
return navigator.DisplacementFromTarget(transform.position, hoverTarget) <= 1.0f;
}
/// <summary>
/// Determines the closest target out of a list of targets.
/// </summary>
/// <param></param>
/// <returns></returns>
void DetermineCurrentTarget()
{
if (targets.Count <= 0)
{
return;
}
//int closestTargetIndex = 0;
float lowestDistance = float.MaxValue;
foreach (Transform target in targets)
{
float targetDistance = navigator.DisplacementFromTarget(transform.position, target.position);
if (targetDistance < lowestDistance)
{
lowestDistance = targetDistance;
currentTarget = target;
}
}
}
/// <summary>
/// The enter attack state.
/// </summary>
/// <param></param>
/// <returns></returns>
void EnterAttack()
{
SetIsHovering(false);
stateChangeTimer = 0.6f;
shooter.SetTarget(currentTarget);
}
/// <summary>
/// The enter hover state.
/// </summary>
/// <param></param>
/// <returns></returns>
void EnterHover()
{
SetIsHovering(true);
stateChangeTimer = 1.2f;
DetermineCurrentTarget();
}
/// <summary>
/// The enter wait state.
/// </summary>
/// <param></param>
/// <returns></returns>
void EnterWait()
{
SetIsHovering(false);
stateChangeTimer = 0.3f;
DetermineCurrentTarget();
}
/// <summary>
/// Returns true if the enemy & player are far enough apart.
/// </summary>
/// <param></param>
/// <returns> True: the enemy has lost the player by being too far away. False: the enemy has not lost the player by being too far away. </returns>
bool HasLostThePlayer()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) > playerLossDistance;
}
/// <summary>
/// The hover state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Hover()
{
if (navigator.DisplacementFromTarget(transform.position, hoverTarget) > 0.2f)
{
mover.moveDirection = navigator.DecideDirection(dummyGameObject.transform, hoverTarget, navMeshAgent);
}
else
{
mover.moveDirection = Vector3.zero;
}
mover.Move(groundChecker.IsGrounded());
if (navigator.DisplacementFromTarget(transform.position, currentTarget.position) < hoverDistance)
{
stateChangeTimer -= Time.deltaTime;
}
}
/// <summary>
/// The idle state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Idle()
{
mover.moveDirection = Vector3.zero;
mover.Move(groundChecker.IsGrounded());
DetermineCurrentTarget();
}
/// <summary>
/// Returns true if the enemy is in approaching range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is in approaching range of the player. False: the enemy is not in approaching range of the player. </returns>
bool InApproachingRange()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) < approachDistance;
}
// <summary>
/// Returns true if the enemy is in attacking range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is in attacking range of the player. False: the enemy is not in attacking range of the player. </returns>
bool InAttackingRange()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) < attackDistance;
}
// <summary>
/// Returns true if the enemy is in hovering range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is in hovering range of the player. False: the enemy is not in hovering range of the player. </returns>
bool InHoveringRange()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) < hoverDistance;
}
// <summary>
/// Returns true if the enemy is not in hovering range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is not in hovering range of the player. False: the enemy is in hovering range of the player. </returns>
bool OutsideOfAttackDistance()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) > attackDistance;
}
// <summary>
/// Returns true if the enemy is not in hovering range of the player.
/// </summary>
/// <param></param>
/// <returns> True: the enemy is not in hovering range of the player. False: the enemy is in hovering range of the player. </returns>
bool OutsideOfHoverDistance()
{
return navigator.DisplacementFromTarget(transform.position, currentTarget.position) > hoverDistance;
}
/// <summary>
/// The wait state.
/// </summary>
/// <param></param>
/// <returns></returns>
void Wait()
{
mover.moveDirection = Vector3.zero;
mover.Move(groundChecker.IsGrounded());
stateChangeTimer -= Time.deltaTime;
}
}
using UnityEngine;
[RequireComponent(typeof(GroundCheckComponent))]
[RequireComponent(typeof(MoveComponent))]
public class EnemyStationary : Enemy
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// The bare minimum enemy. Potentially good for training dummies in a tutorial level.
///
/// </summary>
private GroundCheckComponent groundChecker;
private MoveComponent mover;
void Awake()
{
groundChecker = GetComponent<GroundCheckComponent>();
mover = GetComponent<MoveComponent>();
}
void Update()
{
//Movement();
mover.moveDirection = Vector3.zero;
mover.Move(groundChecker.IsGrounded());
}
/*void Movement()
{
mover.moveDirection = Vector3.zero;
mover.Move(groundChecker.IsGrounded());
}*/
}
using UnityEngine;
public class GroundCheckComponent : MonoBehaviour
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// Checks if the ground is present.
///
/// </summary>
[SerializeField] private Transform groundCheckTransform;
[SerializeField] private float groundCheckRadius = 0.5f;
[SerializeField] private LayerMask groundLayer;
/// <summary>
/// Returns true if the entity is grounded (the sphere overlaps with an object in the ground layer).
/// </summary>
/// <param></param>
/// <returns> True: currently grounded. False: not currently grounded. </returns>
public bool IsGrounded()
{
return Physics.OverlapSphere(groundCheckTransform.position, groundCheckRadius, groundLayer).Length > 0;
}
}
using System;
using System.Collections;
using UnityEngine;
public class ModelEffectComponent : MonoBehaviour
{
[SerializeField] private GameObject model;
/// <summary>
/// Flips the visibility of the model each time it's called. Call each frame for a convincing damaged effect.
/// </summary>
/// <param></param>
/// <returns></returns>
public void Flicker()
{
SetVisibility(!model.activeSelf);
}
/// <summary>
/// Calls the coroutine that flickers the model for some amount of seconds. The purpose of this wrapper is to
/// prevent users of this method from having to care how flickering is implemented. If, later, this coroutine
/// implementation is changed for something else, only the component needs updating, instead of having to change
/// every instance of "StartCoroutine(FlickerForSeconds(...)".
/// </summary>
/// <param name="seconds"> The amount of time, in seconds, the model will flicker for. </param>
/// <returns></returns>
public void FlickerForSeconds(float seconds)
{
StartCoroutine(Flickering(seconds));
}
/// <summary>
/// A version of FlickerForSeconds() compatible with Action<float>.
/// </summary>
/// <param name="seconds"> The amount of time, in seconds, the model will flicker for. </param>
/// <returns> An empty delegate. </returns>
public Action<float> FlickerForSecondsEvent(float seconds)
{
StartCoroutine(Flickering(seconds));
return delegate { };
}
/// <summary>
/// Coroutine that flickers the model for some amount of seconds.
/// </summary>
/// <param name="seconds"> The amount of time, in seconds, the model will flicker for. </param>
/// <returns></returns>
IEnumerator Flickering(float seconds)
{
float timeElapsed = 0f;
while (timeElapsed <= seconds)
{
Flicker();
yield return null;
timeElapsed += Time.deltaTime;
}
SetVisibility(true);
}
/// <summary>
/// Sets the visibility of the model by enabling/disabling the MeshRenderer.
/// </summary>
/// <param name="newValue"> The new value of model.enabled. ></param>
/// <returns></returns>
public void SetVisibility(bool newValue)
{
model.SetActive(newValue);
}
}
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class MoveComponent : MonoBehaviour
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// A reusable component for entity movement.
///
/// </summary>
private CharacterController characterController;
public bool alwaysMoving;
public bool affectedByGravity = true;
public float jumpHeight = 2f;
public float jumpGravity = -1f;
public float fallGravity = -1f;
public float acceleration = 1f;
public float deceleration = 1f;
private float currentSpeedX, currentSpeedY, currentSpeedZ;
public Vector3 maximumSpeed = new(10f, 999f, 10f);
public Vector3 moveDirection;
void Awake()
{
characterController = GetComponent<CharacterController>();
}
void Update()
{
if (alwaysMoving)
{
Move();
}
}
/// <summary>
/// A helper function for Move(). Applies acceleration to a variable.
/// </summary>
/// <param name="speedVar"> The variable getting acceleration applied to it. </param>
/// <param name="direction"> The direction of the acceleration. </param>
/// <returns></returns>
public void Accelerate(ref float speedVar, float direction)
{
speedVar += (acceleration * 100f) * direction;
}
/// <summary>
/// A helper function for Move(). Applies deceleration to a variable.
/// </summary>
/// <param name="speedVar"> The variable getting deceleration applied to it. </param>
/// <returns></returns>
public void Decelerate(ref float speedVar)
{
speedVar += (deceleration * 100f) * Mathf.Sign(-speedVar);
if (Mathf.Abs(speedVar) <= (deceleration * 100f))
{
speedVar = 0f;
}
}
/// <summary>
/// A helper function for Move(). Clamps variables according to maximum movement speed.
/// </summary>
/// <param name="speedVar"> The variable getting capped. </param>
/// <param name="speedCap"> The relevant maximum speed variable. </param>
/// <param name="direction"> The speed direction. </param>
/// <returns></returns>
public void Cap(ref float speedVar, float speedCap, float direction)
{
bool maxSpeedExceeded = Mathf.Abs(speedVar) > speedCap;
bool maxSpeedWithDirectionalLimiterExceeded = (Mathf.Abs(speedVar) > speedCap * Mathf.Abs(direction)) &&
(direction != 0f);
if (maxSpeedWithDirectionalLimiterExceeded)
{
speedVar = speedCap * direction;
}
else if (maxSpeedExceeded)
{
speedVar = speedCap * Mathf.Sign(direction);
}
}
/// <summary>
/// A helper function for Move(). Sets the Y speed variable to an appropriate jumping force.
/// </summary>
/// <param></param>
/// <returns></returns>
public void Jump()
{
currentSpeedY = Mathf.Abs(Mathf.Sqrt((jumpHeight * 100f) * -2f * jumpGravity));
}
/// <summary>
/// When called by an entity, uses the components speed variables to move the entity.
/// </summary>
/// <param name="isGrounded"> Is the entity grounded. </param>
/// <returns></returns>
public void Move( /*Vector3 moveDirection = moveDirection, */ bool isGrounded = false)
{
if (Mathf.Abs(moveDirection.x) != 0f)
{
Accelerate(ref currentSpeedX, moveDirection.x);
}
else
{
Decelerate(ref currentSpeedX);
}
if (!affectedByGravity)
{
if (Mathf.Abs(moveDirection.y) != 0f)
{
Accelerate(ref currentSpeedY, moveDirection.y);
}
else
{
Decelerate(ref currentSpeedY);
}
}
if (Mathf.Abs(moveDirection.z) != 0f)
{
Accelerate(ref currentSpeedZ, moveDirection.z);
}
else
{
Decelerate(ref currentSpeedZ);
}
if (affectedByGravity)
{
if (currentSpeedY > 0)
{
currentSpeedY += (jumpGravity * 100f) * Time.deltaTime;
}
else
{
currentSpeedY += (fallGravity * 100f) * Time.deltaTime;
}
}
if (isGrounded)
{
currentSpeedY = 0f;
}
if (moveDirection.y >= 1f && affectedByGravity && isGrounded)
{
Jump();
}
Cap(ref currentSpeedX, maximumSpeed.x, moveDirection.x);
if (affectedByGravity)
{
Cap(ref currentSpeedY, maximumSpeed.y, -1f);
}
else
{
Cap(ref currentSpeedY, maximumSpeed.y, moveDirection.y);
}
Cap(ref currentSpeedZ, maximumSpeed.z, moveDirection.z);
if (characterController.enabled == false)
{
return;
}
if (characterController != null)
{
characterController.Move(new Vector3(currentSpeedX, currentSpeedY, currentSpeedZ) * Time.deltaTime);
}
else
{
transform.position += new Vector3(currentSpeedX, currentSpeedY, currentSpeedZ) * Time.deltaTime;
}
}
}
using UnityEngine;
using UnityEngine.AI;
public class NavigateComponent : MonoBehaviour
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// A reusable component that contains methods for judging distances to targets & facing towards targets (Vector3s).
///
/// </summary>
/// <summary>
/// Given a position, decides the direction to go.
/// </summary>
/// <param></param>
/// <returns></returns>
public Vector3 DecideDirection(Transform looker, Vector3 targetToFaceTowards, NavMeshAgent navMeshAgent = null)
{
Vector3 direction = Vector3.zero;
looker.position = new Vector3(transform.position.x, targetToFaceTowards.y, transform.position.z);
if (navMeshAgent != null)
{
navMeshAgent.SetDestination(targetToFaceTowards);
NavMeshHit navMeshHit;
navMeshAgent.SamplePathPosition(NavMesh.AllAreas, 1f, out navMeshHit);
looker.LookAt(navMeshHit.position);
direction = looker.forward;
direction = new Vector3(direction.x, 0f, direction.z);
}
else
{
//dummyGameObject.transform.position = new Vector3(transform.position.x, targetToFaceTowards.y, transform.position.z);
looker.LookAt(targetToFaceTowards);
//model.transform.rotation = dummyGameObject.transform.rotation;
//model.transform.localPosition = Vector3.zero;
//Debug.Log(dummyGameObject.transform.forward);
//Debug.Log(dummyGameObject.transform.TransformDirection(dummyGameObject.transform.forward));
direction = looker.forward;
direction = new Vector3(direction.x, 0f, direction.z);
//direction.Normalize();
}
return direction;
}
/// <summary>
/// Given a position, determines the horizontal (Y-axis omitted) displacement from self.
/// </summary>
/// <param name="targetToCheck"> The target to check. </param>
/// <returns> Distance from target. </returns>
public float DisplacementFromTarget(Vector3 origin, Vector3 targetToCheck)
{
return Vector2.Distance(new Vector2(origin.x, origin.z), new Vector2(targetToCheck.x, targetToCheck.z));
}
}
using UnityEngine;
[RequireComponent(typeof(SpawnerComponent))]
public class ShooterComponent : MonoBehaviour
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// A reusable component that spawns GameObjects & gives them velocity.
///
/// </summary>
[SerializeField] private SpawnerComponent spawner;
private float shotDelay;
public float shotDelaySet;
public Transform target;
void Start()
{
shotDelay = 0f;
}
void Update()
{
shotDelay -= Time.deltaTime;
}
/// <summary>
/// Spawns a GameObject, then gives it velocity.
/// </summary>
/// <param></param>
/// <returns> The newly created projectile. </returns>
public GameObject Shoot()
{
if (shotDelay > 0f)
{
return null;
}
shotDelay = shotDelaySet;
GameObject newProjectile = spawner.Spawn(transform.position);
if (newProjectile == null)
{
return null;
}
if (newProjectile.TryGetComponent<MoveComponent>(out MoveComponent mover))
{
mover.alwaysMoving = true;
newProjectile.transform.LookAt(target);
mover.moveDirection = newProjectile.transform.forward;
//mover.moveDirection = transform.forward;
}
return newProjectile;
}
/// <summary>
/// Set the value of target.
/// </summary>
/// <param name="newTarget"> The new value of target. </param>
/// <returns></returns>
public void SetTarget(Transform newTarget)
{
target = newTarget;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnerComponent : MonoBehaviour
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// A reusable component that spawns GameObjects.
///
/// </summary>
[SerializeField] private GameObject spawnee;
[SerializeField] private LayerMask spawneeLayer;
[SerializeField] private float destroyAfterSeconds = 0f;
private List<GameObject> objectsToDespawn = new();
/*void OnDestroy()
{
for (int index = objectsToDespawn.Count - 1; index >= 0; index--)
{
GameObject despawnee = objectsToDespawn[index];
objectsToDespawn.Remove(despawnee);
Destroy(despawnee);
}
}*/
/// <summary>
/// Spawns a GameObject, then optionally destroys it after a certain amount of time has passed.
/// </summary>
/// <param></param>
/// <returns> The newly created projectile. </returns>
public GameObject Spawn(Vector3 spawnPosition, GameObject parent = null)
{
//Spawns spawnee at spawnPosition, returns the gameObject Instance
GameObject newObject = Instantiate(spawnee, spawnPosition, Quaternion.identity);
if (parent != null)
{
newObject.transform.SetParent(parent.transform);
}
if (destroyAfterSeconds > 0f)
{
//StartCoroutine(Despawn(newObject));
Destroy(newObject, destroyAfterSeconds);
}
return newObject;
}
/// <summary>
/// Coroutine that despawns a GameObject after a certain amount of time.
/// </summary>
/// <param name="despawnee"> The object about to get despawned. </param>
/// <returns></returns>
/*IEnumerator Despawn(GameObject despawnee)
{
objectsToDespawn.Add(despawnee);
yield return new WaitForSeconds(destroyAfterSeconds);
if (despawnee != null)
{
Destroy(despawnee);
}
}*/
}
using UnityEngine;
public static class UnityExtensions
{
/// <summary>
/// Authors list:
/// Joshua Walcott
///
/// Summary:
/// With this, you can add methods to built-in classes.
///
/// </summary>
/// <summary>
/// Returns true if this LayerMasks contains this layer.
/// </summary>
/// <param name="layerMask"> Not actually a parameter, but the object that calls this method. </param>
/// <param name="layer"> The index of the layer according to Unity's Layers tab. </param>
/// <returns> True: this LayerMask does contain the layer. False: this LayerMask does not contain the layer. </returns>
public static bool Contains(this LayerMask layerMask, int layer)
{
return (layerMask.value & (1 << layer)) != 0;
}
/// <summary>
/// Returns true if this LayerMasks contains this GameObject.
/// </summary>
/// <param name="layerMask"> Not actually a parameter, but the object that calls this method. </param>
/// <param name="gameObject"> The GameObject that might be in the LayerMask. </param>
/// <returns> True: this LayerMask does contain the GameObject. False: this LayerMask does not contain the GameObject. </returns>
public static bool Contains(this LayerMask layerMask, GameObject gameObject)
{
return (layerMask.value & (1 << gameObject.layer)) != 0;
}
/// <summary>
/// Returns true if this LayerMasks contains this GameObject.
/// </summary>
/// <param name="layerMask"> Not actually a parameter, but the object that calls this method. </param>
/// <param name="collider"> The collider that might be in the LayerMask. </param>
/// <returns> True: this LayerMask does contain the collider. False: this LayerMask does not contain the collider. </returns>
public static bool Contains(this LayerMask layerMask, Collider collider)
{
return (layerMask.value & (1 << collider.gameObject.layer)) != 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment