Innards is an action tower defense game about a nanobot and human cells getting rid of germs and viruses. In the game the player can order human cells to move to different parts of the level to eliminate germs and viruses or to destroy bushes to get access to more cell units. The game was a school project of half a year where I was part of a team of 4. We made the game in Unity.
A Windows version of the game can be downloaded via this link: https://innards.itch.io/
My main task was gameplay programming and some hightlights include:
- Player and unit movement. For the units I used Unity’s Navmesh
- A versatile projectile and attack system
- Unit behaviour
- Versatile wave spawn system
- Programmed lots of UI elements such as the spawn indicator
- Tweaking, iterating, digital prototyping and brainstorming gameplay
Code samples
Projectile.cs script that can be used to make all kinds of attacks
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Collider))]
[RequireComponent(typeof(Rigidbody))]
public class Projectile : MonoBehaviour {
public float range = 4f;
public float projectileLifeTime = 1f;
public int damage = 2;
public bool destroyAtFirstContact = true;
public bool damageOnlyAtFirstContact = true;
public GameObject castObjectWhenDestroyed;
public string[] canHitTheseTags;
[Header("Projectile lifetime animations (from x 0 to 1)")]
public AnimationCurve sidewaysVelocityAnimation = AnimationCurve.Linear(0f, 0f, 1f, 0f);
public AnimationCurve forwardVelocityAnimation = AnimationCurve.Linear(0f, 0f, 1f, 1f);
public AnimationCurve xScaleAnimation = AnimationCurve.Linear(0f, 1f, 1f, 1f);
public AnimationCurve yScaleAnimation = AnimationCurve.Linear(0f, 1f, 1f, 1f);
public AnimationCurve zScaleAnimation = AnimationCurve.Linear(0f, 1f, 1f, 1f);
private Rigidbody rb;
private bool isColliding;
private List<GameObject> collidingObjects = new List<GameObject>();
private List<Entity> collidingEntities = new List<Entity>();
private void Start() {
rb = GetComponent<Rigidbody>();
StartCoroutine("MoveProjectile");
}
IEnumerator MoveProjectile() {
float timer = 0f;
Vector3 startPosition = rb.position;
Vector3 endPosition = rb.position + transform.forward * range;
Vector3 startScale = transform.localScale;
Vector3 endScale = transform.localScale;
while (timer <= 1f) {
timer += Time.deltaTime * 1f / projectileLifeTime;
Vector3 nextPosition = Vector3.Lerp(startPosition, endPosition, forwardVelocityAnimation.Evaluate(timer));
nextPosition += transform.right * sidewaysVelocityAnimation.Evaluate(timer);
rb.MovePosition(nextPosition);
Vector3 nextScale = new Vector3(xScaleAnimation.Evaluate(timer), yScaleAnimation.Evaluate(timer), zScaleAnimation.Evaluate(timer));
nextScale = Vector3.Scale(nextScale, startScale);
transform.localScale = nextScale;
if (!destroyAtFirstContact) {
for (int i = 0; i < collidingEntities.Count; i++) {
if (collidingEntities[i] == null) {
collidingEntities.RemoveAt(i);
i++;
break;
}
if (!damageOnlyAtFirstContact)
collidingEntities[i].Damage(damage);
//collidingEntities[i].Damage(damage * Time.deltaTime);
}
}
yield return null;
}
if (castObjectWhenDestroyed)
Instantiate(castObjectWhenDestroyed, transform.position, transform.rotation, null);
Destroy(this.gameObject);
}
private void OnTriggerEnter(Collider other) {
if (CanHitThisTag(other.tag)){
if (!collidingObjects.Contains(other.gameObject)) {
collidingObjects.Add(other.gameObject);
}
isColliding = true;
} else {
return;
}
if (other.GetComponent<Entity>() != null) {
Entity otherEntity = other.GetComponent<Entity>();
collidingEntities.Add(otherEntity);
if (damageOnlyAtFirstContact)
otherEntity.Damage(damage);
}
if (castObjectWhenDestroyed && destroyAtFirstContact)
Instantiate(castObjectWhenDestroyed, transform.position, transform.rotation, null);
if (destroyAtFirstContact) {
StopAllCoroutines();
Destroy(this.gameObject);
}
}
private void OnTriggerExit(Collider other) {
if (collidingObjects.Contains(other.gameObject))
collidingObjects.Remove(other.gameObject);
if (other.gameObject.GetComponent<Entity>() != null) {
if (collidingEntities.Contains(other.gameObject.GetComponent<Entity>())) {
collidingEntities.Remove(other.gameObject.GetComponent<Entity>());
}
}
if (collidingObjects.Count <= 0)
isColliding = false;
}
private bool CanHitThisTag(string tag) {
for (int i = 0; i < canHitTheseTags.Length; i++) {
if (tag == canHitTheseTags[i]) return true;
}
return false;
}
}
The spawncontroller is a controller script that can generate spawners during tower defense. The spawner is just a simple spawner with an interval.
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
public class SpawnController : MonoBehaviour {
[Serializable]
public class SpawnWave{
public float delay = 10f;
public float spawnDuration;
public Spawner[] spawners;
}
public float fullLength;
public float timer;
public bool startSpawning = false;
public int currentWave;
public float waveTimeLeft;
public float waveStartedAtTime;
public bool finishedTowerDefense;
public bool isWaitingForWave;
private bool startedSpawning;
[SerializeField] public SpawnWave[] waves;
private List<Spawner> spawnersInUse = new List<Spawner>();
void Start()
{
float length = 0f;
for (int i = 0; i < waves.Length; i++) {
length += waves[i].delay;
length += waves[i].spawnDuration;
}
fullLength = length;
timer = 0f;
startedSpawning = false;
isWaitingForWave = true;
currentWave = 0;
finishedTowerDefense = false;
}
void Update() {
if (!startSpawning) {
timer = 0f;
return;
}
if (startSpawning && !startedSpawning) {
GameManager.Instance.activeSpawnController = this;
GameManager.Instance.currentGameState = GameState.TowerDefense;
startedSpawning = true;
}
if (startedSpawning && timer <= fullLength) {
timer += Time.deltaTime;
}
if (timer > fullLength) {
finishedTowerDefense = true;
currentWave = 0;
GameManager.Instance.currentGameState = GameState.Explore;
if (GameManager.Instance.activeSpawnController == this)
GameManager.Instance.activeSpawnController = null;
}
float stopLength = 0f;
float startLength = 0f;
spawnersInUse.Clear();
bool isDownTime = true;
bool isWaitingForNextWave = false;
for (int w = 0; w < waves.Length; w++) {
startLength += waves[w].delay;
if (timer < startLength && timer > stopLength) {
isWaitingForNextWave = true;
}
stopLength += waves[w].delay + waves[w].spawnDuration;
if (timer >= startLength && timer < stopLength) {
waveTimeLeft = stopLength - timer;
for (int s = 0; s < waves[w].spawners.Length; s++) {
spawnersInUse.Add(waves[w].spawners[s]);
}
} else {
isDownTime = false;
}
startLength = stopLength;
}
isWaitingForWave = isWaitingForNextWave;
stopLength = 0f;
startLength = 0f;
for (int w = 0; w < waves.Length; w++) {
stopLength += waves[w].delay + waves[w].spawnDuration;
if (timer >= startLength && timer < stopLength) {
currentWave = w;
waveStartedAtTime = startLength;
}
startLength += waves[w].delay;
if (timer >= startLength && timer < stopLength) {
//Enable wave with the current index w
for (int s = 0; s < waves[w].spawners.Length; s++) {
if (!waves[w].spawners[s].isSpawning) {
Debug.Log("Start spawning");
waves[w].spawners[s].StartSpawning();
waves[w].spawners[s].isSpawning = true;
Debug.Log(waves[w].spawners[s].isSpawning);
}
}
} else {
for (int s = 0; s < waves[w].spawners.Length; s++) {
if (waves[w].spawners[s].isSpawning && (!spawnersInUse.Contains(waves[w].spawners[s]) || isDownTime)) {
Debug.Log("Stop spawning spawner: " + w + s);
waves[w].spawners[s].StopSpawning();
waves[w].spawners[s].isSpawning = false;
}
}
}
startLength = stopLength;
}
}
public bool IsAnySpawnedUnitAlive() {
bool isTrue = false;
for (int w = 0; w < waves.Length; w++) {
for (int s = 0; s < waves[w].spawners.Length; s++) {
if (waves[w].spawners[s].transform.GetComponentsInChildren<ProjectileCastingUnit>().Length > 0) {
Debug.Log(waves[w].spawners[s].transform.GetComponentsInChildren<ProjectileCastingUnit>().Length);
isTrue = true;
return isTrue;
}
}
}
return isTrue;
}
public void StartTowerDefense() {
startSpawning = true;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spawner : MonoBehaviour {
public bool startSpawning = true;
public bool isSpawning;
public float spawnRate = 1f;
public GameObject objectToSpawn;
public Transform parent;
private void OnEnable() {
if (startSpawning)
StartCoroutine("Spawn");
}
private void OnDestroy() {
isSpawning = false;
StopAllCoroutines();
}
private void OnDisable() {
isSpawning = false;
StopAllCoroutines();
}
IEnumerator Spawn() {
//waiting to spawn
Instantiate(objectToSpawn, transform.position, Quaternion.identity, parent);
yield return new WaitForSeconds(spawnRate);
if (gameObject.activeInHierarchy)
StartCoroutine("Spawn");
}
public void StartSpawning() {
isSpawning = true;
StartCoroutine("Spawn");
}
public void StopSpawning() {
//isSpawning = false;
StopAllCoroutines();
}
}