Examination project Sthlm Sunset

The final project at The Game Assembly where I worked as I AI programmer.

Content

Boss

The boss of the game is a complex AI with three diffrent phases. It is the last AI the player will be intruduced to. It is borrowing tricks from all the other AI in the game.

Boss Behaviour

Phase one

This phase starts with it dropping into the fight arena and releasing a shockwave, one of it’s borroed abilities from the machine gun AI, which was developed by another programmer. The shockwave creates a two quickly expanding circles with rocks shooting out from the ground between the two cirlces indicating the strike area.

Phase two

This phase starts when the boss health drops bellow seventy procent, making it fly away to the next fight arena. Before lifting it fires of a last shockwave then flies away. All of its weakpoints gets disabled at this stage and at the end its health get reset to seventy procent.

Phase three

This phase starts when the player enters the new arena at the top of the building. The boss has now changed its behaviour to an even more aggresive stance. Its new attack cycle involves it charging the player destroying columns in its way to launch two shockwaves in quick succesion. Then it starts firing away a long missile barrage. After that it repeates the cycle a second time but this time it ends it with firing its laser towards the player forcing it to take cover behind the remaining columns and sneak in a few shots. It will continue with this cycle for until it gets defeated by the player which ends with it going up in a big explosion.

Boss charge

void ChargePlayer::Init()
	{
		myPlayerID = gPlayerID;
		AIControllerComponent* ai = mySystem->GetComponent<AIControllerComponent>(myOwner);
		myMaxSpeed = ai->GetMaxSpeed();
		myCurrentSpeed = 1.0f;
		myMaxTorque = ai->GetMaxTorque();
		myCurrentTorque = myMaxTorque;
		myAcceleration = ai->GetAcceleration();
		myLoseOfControl = -1.001f;
		myDeAcceleration = myAcceleration * -1.0f;
		myHasRolledPastPlayer = false;
		mySystem->GetComponent<TransformComponent>(myOwner)->LookAtLocal(mySystem->GetComponent<TransformComponent>(myPlayerID)->GetPositionWorld());
		myCurrentTargetDirection = mySystem->GetComponent<TransformComponent>(myOwner)->GetForwardVector();

		mySystem->GetComponent<AIControllerComponent>(myOwner)->ActivateChargeCollider();
		mySystem->GetComponent<AnimatorComponent>(myOwner)->PlayAnimation(ANIM_TYPE_BOSS_CHARGESTART, ANIM_CAT_LOWERBODY);

		myLeftThrusterID = myBlackboard->GetObject<Entity>("LeftMissileLauncherID");
		myRightThrusterID = myBlackboard->GetObject<Entity>("RightMissileLauncherID");

		mySystem->GetComponent<ParticleEmitterComponent>(myLeftThrusterID)->SetVisible(true);
		mySystem->GetComponent<ParticleEmitterComponent>(myRightThrusterID)->SetVisible(true);

		mySystem->GetComponent<AudioComponent>(myOwner)->Play("BOSS_DRIVE");
	}
	Status ChargePlayer::Update(float const aDeltatime)
	{
		const Vector3<float> position = mySystem->GetComponent<TransformComponent>(myOwner)->GetPositionWorld();

		TransformComponent* targetTransform = mySystem->GetComponent<TransformComponent>(myPlayerID);
		if (!targetTransform)
		{
			DoneCharging();
			return Status::Success;
		}

		Vector3<float> playerPosition = targetTransform->GetPositionWorld();
		float distance = (playerPosition - position).Length();
		if (distance > 300.0f)
		{
			myTargetPos = playerPosition;
		}

		if (!myHasRolledPastPlayer)
		{
			Vector3f targetDir = (playerPosition - position).GetNormalized();
			float dotTarget = targetDir.Dot(myCurrentTargetDirection);

			myCurrentSpeed += myCurrentSpeed * myAcceleration * aDeltatime;
			if (myCurrentSpeed > myMaxSpeed)
				myCurrentSpeed = myMaxSpeed;

			myCurrentTorque += myCurrentTorque * myLoseOfControl * aDeltatime;
			if (myCurrentTorque <= 0.1f)
				myCurrentTorque = 0.0f;

			float stopDistance = (myCurrentSpeed * 250.0f) * -1.0f / (2.0f * myDeAcceleration);
			Vector3f currentPos = mySystem->GetComponent<TransformComponent>(myOwner)->GetPositionWorld();
			Vector3f currentDir = mySystem->GetComponent<TransformComponent>(myOwner)->GetForwardVector();
			Vector3f fencePos = mySystem->GetComponent<PathingComponent>(myOwner)->RaycastNavmeshFence(currentPos, currentDir, stopDistance);
			if (fencePos != Vector3f::Zero())
			{
				myHasRolledPastPlayer = true;
				mySystem->GetComponent<AudioComponent>(myOwner)->FadeStopSFX();
			}

			if (dotTarget < 0.0f)
			{
				myHasRolledPastPlayer = true;
				mySystem->GetComponent<AudioComponent>(myOwner)->FadeStopSFX();
			}
			myCurrentTargetDirection = mySystem->GetComponent<TransformComponent>(myOwner)->GetForwardVector();
		}
		else
		{
			myCurrentSpeed += myCurrentSpeed * myDeAcceleration * aDeltatime;
			if (myCurrentSpeed < 0.5f)
			{
				DoneCharging();
				return Status::Success;
			}
		}

		TransformComponent* fromTransform(mySystem->GetComponent<TransformComponent>(myOwner));

		const Vector3<float> desiredDirection((playerPosition - fromTransform->GetPositionWorld()).GetNormalized());
		const Vector3<float> currentDirection(fromTransform->GetForwardVector());

		Vector3<float> steering = (desiredDirection - currentDirection) * myCurrentTorque;
		Vector3<float> velocity = (currentDirection + steering * aDeltatime).GetNormalized() * myCurrentSpeed;

		myBlackboard->SetObject("Velocity", velocity);

		return Status::Running;
	}

Boss laser

class LaserBlast : public BrainTree::Leaf
	{
	public:
		LaserBlast() = default;
		LaserBlast(ComponentManager* const aComponentManager,float aChargeDuration) :
			Leaf::Leaf(aComponentManager),myChargeDuration(aChargeDuration)
		{

		}
		using Ptr = std::shared_ptr<Node>;

		void Reset() override;
		void Init()override;
		Status Update(float const aDeltatime) override;
	private:
		float UpdateBeam(float aDeltaTime);
		void HandleDamage(float aDeltaTime); 
		void HandleImpact();
		void CreateBeam();
		void RayCast();
		void RemoveBeam();
	private:
		BeamData myBeamData;
		JointRotationInfo myJointOffset;
		Quaternion<float> myJointRotationOffset;
		Vector3f myBeamPosition;
		float myChargeDuration;
		float myChargeTimer;
		float myLaunchDuration;
		float myLaunchTimer;
		float myDamageIntervall;
		float myDamageTimer;
		float myDamage;
		float myBeamMaxLength;
		float myBeamLockOnSpeed;

		Entity myBeamLauncherID;
		int myEmitterID;
		int myDustEmitterID;
		int myBeamChargeID;
		int myBeamID;
		int myPlayerID;
		bool myIsHitting;
		bool myHasLaunched;
		bool myHasHit;
		bool myHasStartedCharging;
	};

Boss missiles

class BossLaunchMissiles : public BrainTree::Leaf
	{
	public:
		BossLaunchMissiles() = default;
		BossLaunchMissiles(ComponentManager* const aComponentManager) :
			Leaf::Leaf(aComponentManager)
		{

		}
		using Ptr = std::shared_ptr<Node>;

		void Init()override;
		Status Update(float const aDeltatime) override;
	private:
		void LaunchMissile(int aMissileLauncherID);
		MissileData myMissileData;
		Vector3f myShootDirection;

		float myLeftLaunchTimer;
		float myRightLaunchTimer;
		float myInitialMissileAscendHeight;
		int myLaunchedMissiles;
		int myPlayerID;
		Entity myLeftMissileID;
		Entity myRightMissileID;
	};

Boss shockwave

Roller

The second AI you will face inside the game. It one of the grunts in the game with a only one weakpoint, its core which is only visible when it launches its missles and wander around. This AI is a good source to obtain valuable arrows used by the player.

Roller Behaviour

It will try and roll over the player and fire missiles that aim at the ground of the player during launch sequence. It will do a cycle of rolling then firing or just firing if it can’t pathfind to the player. The roll over deals moderate damage and pushes the player away, making the player lose control for a moment. The missisles deal moderate damage but are easy to dodge if you keep moving.

Roller Charge

Roller Missiles

Laser Turret

The first AI you will face inside the game. It is the weakest of the AI with a very basic behaviour. Its an ambush enemy which waits for the player to get close to it without realising it. The laser deals low damage with each to tick but can deal a lot of damage it not dodged.

Laser Turret Behaviour

This AI will hide under ground for the most part when the player isen’t withing its sight. The AI has two lines of sight depending if it above or below ground. The attack cycle is charing its laser then firing at the place the player is when fully charged then following the player in an attempt to keep the laser hiting the player. If it loses sight of the player it will go back into hiding. When it hides you cannot deal any damage to it.

Laser Turret laser

Weak points

Every AI in the game has body colliders attached to it’s bones which in turn is how the AI receive damage. Each bodycollider has its own damage multiplier which determines if its a weak point or not. The weakpoints are highlighted through emmisive areas on the AI model which gets highlighten when you aim at them.

struct BodyColliderInfo
{
	Transform myBaseTransform;
	std::string myToBone;
	Entity myParent;
	BodyPart myBodyPart = BodyPart::Null;
	float myDamageMultiplier = 1.f;
	COLLISION_FLAG myCollisionFlag;
};

class BodyColliderComponent : public Component, public Subscriber
{
public:
	BodyColliderComponent(const int aID, const int aObjectID, ComponentManager* aManager);
	~BodyColliderComponent();
	
	void Update(const float aDeltaTime) override;
	void Create(const BodyColliderInfo& anInfo, PhysicsShape aShape);

	const std::string& GetBoneName() const;

	void AttachEmitter(const std::string& anEmitterName,const Vector3f& anOffset,const Quaternion<float>& aRotation);
	void AttachDamageEmitter(const std::string& anEmitterName);
	void SetMaxHeath(const int aHealthValue);
	void SetDestructionDamage(const int aDamageValue);
	void SetDamage(const float aDamage);
	void SetDamageRange(const float aRange);
	void SetTargetID(const int aID);

	void ReceiveMessage(const Message& aMessage) override;
	void RecieveForwardMessage(const ComponentMessage aMessage) override;

	void OnAdd() override;
	void OnActivate() override;
	void OnDeactivate() override;
	void OnRelease() override;

	const Entity GetParentID() const;
	const Transform& GetTransform() const;
	void Detach();
	void SetKnockBackSpeed(float aSpeed);
private:
	void OnDamage(int aDamage);
	void OnDestruction(int, ComponentManagerProxy& aProxy);
	BodyColliderInfo myInfo;
	Vector3<float> myBoneOffset;
	Transform myTransform;
	uint32_t myModelInstanceToSubscribe;
	std::string myBoneName;
	float myDamage;
	float myDamageTimer;
	float myDamageReset;
	float myRange;
	float myKnockBackSpeed;
	float myDestructionDamage;
	int myTargetID;
};

Wander area

Each AI in the game has a spawn point then a wander radius which wil determine how far away from the spawn point it is willing to chase the player. The further away from the center the more force it’s wander functionality will put into moving back against the center, thus keeping it inside a specific area.

	Status Wander::Update(float const aDeltatime)
	{
		aDeltatime;

		myDesiredDirection = Steering::Wander(mySystem, myOwner, ourRng, myCurrentAngle, aDeltatime);

		float distance = (myCenterPoint - mySystem->GetComponent<TransformComponent>(myOwner)->GetPositionWorld()).Length();
		float pullstrength = (distance - myComfortRange) / 10.0f;
		if (pullstrength > 0)
		{
			Vector3f pullDirection = (myCenterPoint - mySystem->GetComponent<TransformComponent>(myOwner)->GetPositionWorld()).GetNormalized();
			myDesiredDirection += pullDirection * pullstrength;
		}

		Vector3f currentPos = mySystem->GetComponent<TransformComponent>(myOwner)->GetPositionWorld();
		Vector3f currentDir = mySystem->GetComponent<TransformComponent>(myOwner)->GetForwardVector();

		Vector3f fencePos = mySystem->GetComponent<PathingComponent>(myOwner)->RaycastNavmeshFence(currentPos, currentDir, 500.0f);
		if (fencePos != Vector3f::Zero())
		{
			float fenceRepulsion = 500 - (currentPos - fencePos).Length();
			Vector3f fenceDirection = (currentPos - fencePos).GetNormalized();
			myDesiredDirection += fenceDirection * fenceRepulsion;
		}

		const Vector3<float> currentDirection(mySystem->GetComponent<TransformComponent>(myOwner)->GetForwardVector());

		Vector3<float> steering = (myDesiredDirection - currentDirection) * mySystem->GetComponent<AIControllerComponent>(myOwner)->GetMaxTorque();
		Vector3<float> velocity = currentDirection + steering * aDeltatime;

		velocity = velocity.GetNormalized() * myWanderSpeed;
		myBlackboard->SetObject("Velocity", velocity);

		return Status::Running;
	}

Trailer