Project Description
The game pictured in Projectile Arcing in Unity with Animation Curves, this prototype was the result of wanting to try out and learn some Unity Programming Patterns after my first full semester in AAU’s Game Design MA program.
The object of the game is to build the enemy path in order to create the most optimized killbox(es); however, the path has a chance to branch after the player’s tile laying phase ends, throwing a monkey wrench in the player’s carefully planned route. Similar to Isle of Arrows (whose beta came out about a month or so into me working on this project), the idea is to eventually give the player enemy path tiles with further attributes that will automatically Slow/Poison/Stun/etc. any enemy who steps on them.
If I ever return to this project, the next hurdle is figuring out the enemy wave progression.
Programming Notes
- Uses all cutting/bleeding edge Unity features: AI Navigation Runtime Navmesh Generation, Unity 2021+ Object Pooling, UI Toolkit layouts, modern Input System.
- One-Draw Healthbars and ScriptableObject Events, as usual.
- The tech tree & achievements systems take advantage of the fact that ScriptableObjects can register OnEnable and OnDisable methods, and therefore can subscribe to events. Each unique UnlockCondition listens to an event; can filter on the results for more complicated events, and marks itself as unlocked once the Event has been recorded enough times. (The ISerializationCallbackReceiver methods are used to reset the runtime values to their defaults each play session):
using System;
using UnityEngine;
[CreateAssetMenu (fileName = "UnlockCondition_", menuName = "Scriptable Objects/Tech Tree/Unlock Condition", order = 52)]
public class UnlockCondition : ScriptableObject, ISerializationCallbackReceiver
{
#if UNITY_EDITOR
[TextArea] public string developerDescription = "";
#endif
public string DisplayName;
public EventObject ConditionEvent;
public EventObjectInt ConditionIntEvent;
public EventObjectGameObject ConditionGameObjectEvent;
public ConditionFilter ConditionFilter;
public int ConditionGoal;
[SerializeField] private int ConditionCountDefault;
[NonSerialized] public int ConditionCount;
[SerializeField] private bool ConditionMetDefault;
[NonSerialized] public bool ConditionMet;
#region Event Suubscriptions
private void OnEnable()
{
if(ConditionEvent) ConditionEvent.RegisterListener(OnConditionEvent);
if(ConditionGameObjectEvent) ConditionGameObjectEvent.RegisterListener(OnConditionGameObjectEvent);
if(ConditionIntEvent) ConditionIntEvent.RegisterListener(OnConditionIntEvent);
}
private void OnDisable()
{
if(ConditionGameObjectEvent) ConditionGameObjectEvent.UnregisterListener(OnConditionGameObjectEvent);
}
#endregion
public void OnBeforeSerialize() { }
// reset count and bool after each play session
public void OnAfterDeserialize()
{
ConditionCount = ConditionCountDefault;
ConditionMet = ConditionMetDefault;
}
protected virtual void OnConditionEvent()
{
// don't bother if we already met our condition
if (ConditionMet) return;
// increase our count and check if we're done
ConditionCount++;
CheckCondition();
Debug.Log(name + " condition " + ConditionCount + " of " + ConditionGoal);
}
protected virtual void OnConditionIntEvent(int conditionInt)
{
// don't bother if we already met our condition
if (ConditionMet) return;
// don't fire on zero parameter
if (conditionInt == 0) return;
// increase our count and check if we're done
ConditionCount+= conditionInt;
CheckCondition();
Debug.Log(name + " condition " + ConditionCount + " of " + ConditionGoal);
}
protected virtual void OnConditionGameObjectEvent(GameObject conditionObject)
{
// don't bother if we already met our condition
if (ConditionMet) return;
// don't fire on null parameter
if (!conditionObject) return;
// if we have a filter, make sure we match it before continuing
if (ConditionFilter!=null)
{
if (!ConditionFilter.CheckFilter(conditionObject)) return;
}
// increase our count and check if we're done
ConditionCount++;
CheckCondition();
Debug.Log(name + " condition " + ConditionCount + " of " + ConditionGoal);
}
protected void CheckCondition()
{
if (ConditionCount < ConditionGoal) return;
ConditionMet = true;
}
}
- The setup for an actual TechTreeUnlock is simple:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[CreateAssetMenu (fileName = "TechUnlock_", menuName = "Scriptable Objects/Tech Unlock", order = 51)]
public class TechUnlock : ScriptableObject
{
public string Name;
public List<UnlockCondition> UnlockConditions;
public List<BuildableObject> Unlocks;
public List<BuildableObject> Hides;
public bool Unlocked => UnlockConditions.All(condition => condition.ConditionMet);
}
- The Inspector setup for unlocking Kinetic Weapons II after building 10 Kinetic Towers (Hides can be used if the new building unlocked replaces an existing Buildable):

