Zombie Survival AI

An AI that survives the zombie apocalypse, written in C++ and using behaviourtrees. The purpose of the AI is to survive as long as possible. To survive, it has to collect food for energy, and the AI has to avoid or shoot the zombies!


Lessons learned

Learned about behaviour trees and how they work. I also learned about steering behaviours, and how to implement them. This project was also a great way to learn how to work with custom engines and their documentation.


Project Specifications

  • Engine: Custom Engine
  • Time spent: 1 Week

Code Snippets

//BehaviourTree
	m_pBehaviourTree = new BehaviourTree(m_pBlackboard,
		new BehaviourSelector
		({
			new BehaviourSequence
			({
				new BehaviourConditional(AreZombiesAround),
				new BehaviourSelector
				({
					new BehaviourSequence
					({
						new BehaviourConditional(HasGun),
						new BehaviourAction(ChangeToAim)
					}),
					new BehaviourAction(ChangeToAvoid)
				})
			}),
			new BehaviourSequence
			({
				new BehaviourSelector
				({
					new BehaviourConditional(HasEnergyPriority),
					new BehaviourConditional(HasHealthPriority),
					new BehaviourConditional(IsGridsearchDone)
				}),
				new BehaviourNotConditional(InHouse),
				new BehaviourConditional(HasHouseTarget),
				new BehaviourAction(DisableAutorotation),
				new BehaviourAction(ChangeToSeek)
			}),
			new BehaviourSelector
			({
				new BehaviourSequence
				({
					new BehaviourConditional(InHouse),
					new BehaviourSelector
					({
						new BehaviourSequence
						({
							new BehaviourConditional(HasItemsInHouse),
							new BehaviourConditional(HasItemTarget),
							new BehaviourNotConditional(NearItem),
							new BehaviourAction(EnableAutorotation),
							new BehaviourAction(ChangeToSeek)
						}),
						new BehaviourSequence
						({
							new BehaviourConditional(HasItemsInHouse),
							new BehaviourNotConditional(HasItemTarget),
							new BehaviourNotConditional(NearItem),
							new BehaviourAction(DisableAutorotation),
							new BehaviourAction(ChangeToSeek)
						}),
						new BehaviourSequence
						({
							new BehaviourConditional(HasItemsInHouse),
							new BehaviourConditional(NearItem),
							new BehaviourNotConditional(IsInventoryFull),
							new BehaviourAction(PickupItem),
							new BehaviourAction(DisableAutorotation)
						}),
						new BehaviourSequence
						({
							new BehaviourNotConditional(HasItemsInHouse),
							new BehaviourAction(DisableAutorotation),
							new BehaviourConditional(ChangeToSeek)
						})
					})
				})
			}),
			new BehaviourSequence
			({
				new BehaviourNotConditional(IsGridsearchDone),
				new BehaviourAction(DisableAutorotation),
				new BehaviourAction(ChangeToSeek)
			}),
			new BehaviourSequence
			({
				new BehaviourConditional(IsGridsearchDone),
				new BehaviourAction(DisableAutorotation),
				new BehaviourAction(ChangeToSeek)
			})
	}));
#pragma once
#include "IBehaviourPlugin.h"

#pragma region **ISTEERINGBEHAVIOUR** (BASE)
class ISteeringBehaviour
{
public:
	ISteeringBehaviour() {}
	virtual ~ISteeringBehaviour() {}

	virtual SteeringOutput CalculateSteering(float deltaT, AgentInfo agent) = 0;

	template<class T, typename std::enable_if<std::is_base_of<ISteeringBehaviour, T>::value>::type* = nullptr>
	T* As()
	{
		return static_cast<T*>(this);
	}
};
#pragma endregion

namespace SteeringBehaviours
{
	//SEEK
	//****
	class Seek : public ISteeringBehaviour
	{
	public:
		Seek() {};
		virtual ~Seek() {};

		//Seek Behaviour
		SteeringOutput CalculateSteering(float deltaT, AgentInfo agent) override;

		//Seek Functions
		virtual void SetTarget(const b2Vec2* target) { m_pTargetRef = target; }
		
	protected:
		const b2Vec2* m_pTargetRef = nullptr;
	};

	//FLEE
	//****
	class Flee : public Seek
	{
		public:
			Flee() {};
			virtual ~Flee() {};

			//Flee Behaviour
			SteeringOutput CalculateSteering(float deltaT, AgentInfo agent) override;
	};

	//AIM
	class Aim :public Seek
	{
		public:
			Aim() {};
			virtual ~Aim() {};

			//Aim behaviour
			SteeringOutput CalculateSteering(float deltaT, AgentInfo agent) override;

			void SetAimTarget(const b2Vec2* aimTarget) { m_pAimTargetRef = aimTarget; }
	protected:
		const b2Vec2* m_pAimTargetRef = nullptr;
	};
}

#pragma region ***Goal***
struct Goal
{
	//Four seperate controllable channels
	b2Vec2 Position = { 0.f, 0.f };
	bool PositionSet = false;

	float Orientation = 0.f;
	bool OrientationSet = false;

	b2Vec2 Velocity = { 0.f, 0.f };
	bool VelocitySet = false;

	float Rotation = 0.f;
	bool RotationSet = false;

	Goal() {};

	//Reset Goal's Channels
	void Clear()
	{
		Position.SetZero();
		PositionSet = false;

		Velocity.SetZero();
		VelocitySet = false;

		Orientation = 0.f;
		OrientationSet = false;

		Rotation = 0.f;
		RotationSet = false;
	}

	//Update Goal
	void UpdateGoal(const Goal& goal)
	{
		if (goal.PositionSet)
		{
			Position = goal.Position;
			PositionSet = true;
		}
		if (goal.OrientationSet)
		{
			Orientation = goal.Orientation;
			OrientationSet = true;
		}
		if (goal.VelocitySet)
		{
			Velocity = goal.Velocity;
			VelocitySet = true;
		}
		if (goal.RotationSet)
		{
			Rotation = goal.Rotation;
			RotationSet = true;
		}
	}

	//Merge Goal
	bool CanMergeGoal(const Goal& goal)
	{
		return !(
			PositionSet && goal.PositionSet ||
			OrientationSet && goal.OrientationSet ||
			VelocitySet && goal.VelocitySet ||
			RotationSet && goal.RotationSet
			);
	}

};
#pragma endregion

namespace CombinedSB
{
#pragma region ***Path***
	//SteeringPipeline Internal Path Segment (Path to (sub)goal)
	class Path
	{
	public:
		virtual ~Path() {}
		void SetGoal(Goal goal) { m_Goal = goal; }
		Goal GetGoal()const { return m_Goal; }
		void SetAgent(AgentInfo* pAgent) { m_pAgent = pAgent; }
		virtual float GetMaxPriority();

	protected:
		float m_MaxPriority = 50.f;
		Goal m_Goal = {};
		AgentInfo* m_pAgent = {};
	};
#pragma endregion

#pragma region ***[PIPELINE-PART] Targeter***
	class Targeter
	{
	public:
		virtual ~Targeter() {}

		//Targeter Functions
		virtual Goal GetGoal() = 0;
	};
#pragma endregion

#pragma region ***[PIPELINE-PART] Decomposer***
	class Decomposer
	{
	public:
		virtual ~Decomposer() {}

		//Decomposer Functions
		virtual Goal DecomposeGoal(const Goal& goal) = 0;
	};
#pragma endregion

#pragma region ***[PIPELINE-PART] Constraint***
	class Constraint
	{
	public:
		virtual ~Constraint() {}

		//Constraint Functions
		virtual float WillViolate(const Path* pPath, AgentInfo agent, float maxPriority) = 0;
		virtual Goal Suggest(const Path* pPath) = 0;
		void SetSuggestionUsed(bool val) { m_SuggestionUsed = val; }

	private:
		bool m_SuggestionUsed = false;
	};
#pragma endregion

#pragma region ***[PIPELINE-PART] Actuator***
	class Actuator
	{
	public:
		virtual ~Actuator() {}

		//Actuator Functions
		virtual Path* CreatePath() = 0;
		virtual void UpdatePath(Path* pPath, AgentInfo agent, const Goal& goal) = 0;
		virtual SteeringOutput CalculateSteering(const Path* pPath, float deltaT, AgentInfo pAgent) = 0;
	};
#pragma endregion

	using namespace std;
#pragma region ***STEERING PIPELING***
	class SteeringPipeline : public ISteeringBehaviour
	{
	public:
		virtual ~SteeringPipeline() {};

		//SteeringPipeline Functions
		void SetActuator(Actuator* pActuator) { m_pActuator = pActuator; SafeDelete(m_pPath); }
		void SetTargeters(vector<Targeter*> targeters) { m_Targeters = targeters; }
		void SetDecomposers(vector<Decomposer*> decomposers) { m_Decomposers = decomposers; }
		void SetConstraints(vector<Constraint*> constraints) { m_Constraints = constraints; }
		void SetFallBack(ISteeringBehaviour* pFallback) { m_pFallbackBehaviour = pFallback; }

		//SteeringPipeline Behaviour
		SteeringOutput CalculateSteering(float deltaT, AgentInfo agent) override;

	private:

		Path* m_pPath = nullptr;
		Actuator* m_pActuator = nullptr;
		vector<Targeter*> m_Targeters = {};
		vector<Decomposer*> m_Decomposers = {};
		vector<Constraint*> m_Constraints = {};

		UINT m_MaxConstraintSteps = 10;
		ISteeringBehaviour* m_pFallbackBehaviour = nullptr;
	};
#pragma endregion

	//***********************************************************************************
	//1. TARGETER (FixedGoalTargeter)
	//Basic Implementation
	class FixedGoalTargeter : public Targeter
	{
	public:
		Goal& GetGoalRef();
		Goal GetGoal() override;

	private:
		Goal m_Goal = {};
	};

	//***********************************************************************************
	//2. DECOMPOSER (Path Planner)
	//Could be combined with NavMesh Graph
	//Skip for now...


	//***********************************************************************************
	//3. CONSTRAINT
	//Simple constraint that checks if the character isn't walking into a SphereObstacle

	struct ZombieObstacle
	{
		b2Vec2 Position;
		float Radius;
	};

	class AvoidZombiesConstraint : public Constraint
	{
	public:
		AvoidZombiesConstraint() {}

		float WillViolate(const Path* pPath, AgentInfo agent, float maxPriority) override;
		Goal Suggest(const Path* pPath) override;
		void SetZombies(const vector<ZombieObstacle>& zombies) { m_ZombiesObstacles = zombies; }

	private:
		float m_AvoidMargin = 0.5f;
		vector<ZombieObstacle> m_ZombiesObstacles = {};
		float WillViolate(const Path* pPath, AgentInfo agent, float maxPriority, const ZombieObstacle &zombie); //Internal single sphere check

		Goal m_SuggestedGoal = {};
	};

	//***********************************************************************************
	//4. ACTUATOR (Basic Actuator, Seek path goal)
	class BasicActuator : public Actuator
	{
	public:
		BasicActuator(SteeringBehaviours::Seek* pSeekBehaviour) :m_pSeekBehaviour(pSeekBehaviour) {}
		~BasicActuator() { SafeDelete(m_pPath); }

		Path* CreatePath() override;
		void UpdatePath(Path* pPath, AgentInfo agent, const Goal& goal) override;
		SteeringOutput CalculateSteering(const Path* pPath, float deltaT, AgentInfo agent) override;

	private:
		Path* m_pPath = nullptr;
		SteeringBehaviours::Seek* m_pSeekBehaviour = nullptr;
	};
};

Project Showcase