Team Projects

Spite: Frozen Hel

Action Role Playing Game

Contributions

  • AI for enemies - steering behaviors and group awareness, boss state machine

  • Audio programming with FMOD - setting up an interface to trigger spatialized sound-events via our custom engine.

  • Optimization of collision system using layers and masks

  • Scripted event for boss fight

  • Post processing graphics programming (with colleagues)

  • Component system set up (with colleagues)

  • Animatic cutscene support (with colleagues)

Things I could’ve done better

  • Smoother state and animation transitions for popcorn enemies

  • More optimized out of bounds-check for enemies in relation to NavMesh

Team work reflections

  • If some team members shows low attendance, be sure to address the problem a soon as possible. If their attendance does not get better, quickly redefine the scope of your project and redistribute the work load.

Project conditions

  • 16 people, 3 months, 20 hours a week, C++, Forge Engine (custom engine)

  • Reference game: Diablo 3

Download Spite: Frozen Hel

A seek steering behavior with built in arrive slowing, as to not make make actors over shoot their target when pathfinding on an intricate navmesh

Show Code

Tga::Vector3f CalculateSeekFromPositionsWithArrive(ActorComponent* aActor, const Tga::Vector3f& aPosition, 
const Tga::Vector3f& aOtherPosition, const float aForce, const float aTargetRadius, const float aSlowRadius)
{
	Tga::Vector3f toOther = aOtherPosition - aPosition;
	const float distanceToTarget = toOther.Length();

	if (distanceToTarget < aTargetRadius)
	{
		float const timeToTarget = ONE_TENTH;
		float targetSpeed        = 0.f;

		if (distanceToTarget > aSlowRadius)
		{
			targetSpeed = aActor->GetSpeed();
		}
		else
		{
			targetSpeed = aActor->GetSpeed() * distanceToTarget / aSlowRadius;
		}

		Tga::Vector3f targetVelocity = toOther.GetNormalized() * targetSpeed;
		Tga::Vector3f acceleration = targetVelocity - aActor->GetCurrentVelocity();
		acceleration /= timeToTarget;

		if (acceleration.Length() > aForce)
		{
			acceleration.Normalize();
			acceleration *= aForce;
		}
		return acceleration;
	}

	return toOther.GetNormalized() * aActor->GetForce();
}
    
    

Show Code

void CollisionManager::CheckCollisions(ColliderComponent* aFirstCollider, ColliderComponent* aSecondCollider)
{
        if (!aFirstCollider || !aSecondCollider)
        {
            return;
        }
	if (!CheckIfLayersCanCollide(aFirstCollider->GetLayer(), aSecondCollider->GetMask()))
	{
	    return;
	}
        // if collission was allowed the more expensive collision calculations could take place
        // rest of collision logic is not shown in this snippet ...
}

bool Forge::CheckIfLayersCanCollide(CollisionLayer aLayer, CollisionLayer anotherLayer)
{
	return static_cast<uint32_t>(aLayer) & static_cast<uint32_t>(anotherLayer);
}

inline void SetUpCollisionLayers(ObjectType aType, Forge::CollisionLayer& aOwnLayer, Forge::CollisionLayer& aMask)
{
	auto RaiseFlag = [](Forge::CollisionLayer aLayer)
		{
			return static_cast<uint32_t>(aLayer);
		};

	auto CreateLayer = [](uint32_t bitLayer)
		{
			return static_cast<Forge::CollisionLayer>(bitLayer);
		};

	using Layer = Forge::CollisionLayer;

switch (aType)
 {
		// Right now this is done only for attacks
   case ObjectType::Player:
   {
	aOwnLayer = CreateLayer(RaiseFlag(Layer::PlayerL));
	aMask = CreateLayer(RaiseFlag(Layer::EkkelL) | RaiseFlag(Layer::TrollL) | RaiseFlag(Layer::BossL) | RaiseFlag(Layer::HelFireL)
	| RaiseFlag(Layer::OrbL) | RaiseFlag(Layer::AttackL) | RaiseFlag(Layer::CheckpointL)
	| RaiseFlag(Layer::ShrineL) | RaiseFlag(Layer::DestructibleL) | RaiseFlag(Layer::LevelTransitionL)
	| RaiseFlag(Layer::ScriptedEventL) | RaiseFlag(Layer::HealthPickupL));

	break;
	}
	// ... etc ...
 }
}

    

Code showing the 'culling’ in the collision manager as well as setting up the collision layers. This optimized the game from doing 4500 collision calculations per frame, down to 96 calculations per frame.

Cargo Diver

Top Down Adventure

Contributions

  • A-star algorithm for enemy navigation

  • Spatial partition optimization using a grid data structure

  • Audio programming with BASS

  • Took on role of project coordinator

  • Smooth camera movement

Things I could’ve done better

  • The grid could have been initialized in a way to have less cells, which would have made everything a bit faster.

Team work reflection

  • If you get stuck it is important not to wait to long with asking for help. Your need to feel independent and competent, can risk becoming a blocker for the group.

Project conditions

  • 16 people, 6 weeks, 20 hours a week, C++, TGE (custom school engine)

  • Reference game: Death’s Door

Show Code

bool Astar::FindPath()
{
  if (myStartPoint && myGoalPoint)
  {
	while (!myOpenWalkables.empty() && !myTargetFound)
	{
		Walkable* current = myOpenWalkables.top();
		myOpenWalkables.pop();


	if (current == myGoalPoint)
	{
		myGoalPoint->status = PathfindingStatus::Found;
		myStartPoint->status = PathfindingStatus::Start;
		myTargetFound = true;
		BuildPath();
		return true;
	}

	current->status = PathfindingStatus::Closed;

	for (Walkable* neighbour : current->GetReachableNeighbours())
	{
		if (neighbour->status == PathfindingStatus::Blocked || neighbour->status == PathfindingStatus::Closed)
		{
			continue;
		}

		if (neighbour->status == PathfindingStatus::Unvisited)
		{
			neighbour->status = PathfindingStatus::Open;

			if (neighbour->gCost > current->gCost + GetGCost(neighbour, current))
			{
                                // how many steps taken since start, plus one step, since that's the distance between neighbours
				neighbour->gCost = current->gCost + GetGCost(neighbour, current);
				neighbour->fCost = GetFCost(neighbour, myGoalPoint);
				myOpenWalkables.emplace(neighbour);
				myCameFrom[neighbour] = current;
			}
		}
	}
 }
 return false;
}

    

Robots pathfinding with A*.

Part of the A* class

Strings of Freedom

2.5D Platformer

Contributions

  • AI for grounded and air borne enemies

  • Implemented platformer compatible collision logic

  • Render culling and sprite batching, lowering draw calls by 90 percent

  • Collision culling, lowering collision checks by 99 percent

  • Platforming friendly camera movement

  • Input mapper design pattern

  • Xbox-controller support in custom engine

  • Audio programming with BASS

Things I could’ve done better

  • Could have been braver: diving deeper in more areas like UI and engine code.

Team work reflection

  • Always have group feedback sessions, to build team spirit and lessen unproductive behaviors.

  • Celebrate every finished sprint! Boosts moral and makes the team communicate better

Project conditions

  • 13 people, 10 weeks, 20 hours a week, C++, TGE (custom school engine)

  • Reference game: Hollow Knight

Download Strings of Freedom

Show Code

void CameraFollower::FollowPlayer()
{
	float const deltaTime = Tga::Engine::GetInstance()->GetDeltaTime();

	Tga::Vector2f playerPosition    = myPlayer->GetPositionVec2();
	Tga::Vector2f directionToPlayer = playerPosition - myPosition;
	float const distanceInX         = abs(directionToPlayer.x);
	float const distanceInY         = abs(directionToPlayer.y);

	if ((distanceInX >= myMinimumDistanceX) || (distanceInY >= myMinimumDistanceY))
	{
		float verticalSpeedScalar = distanceInY / myMinimumDistanceY;

		float horizontalSpeedScalar = distanceInX / myMinimumDistanceX;

		if (verticalSpeedScalar < TRIPLE)
		{
			verticalSpeedScalar = 1.f;
		}

		if (horizontalSpeedScalar < BIG_BOOST)
		{
			horizontalSpeedScalar = 1.f;
		}

		directionToPlayer.Normalize();

		if (myReturnToPlayer)
		{
			myMoveSpeed = RETURN_SPEED;
			horizontalSpeedScalar = SMALL_BOOST;
			verticalSpeedScalar = SMALL_BOOST;
		}
		else if (myPlayer->IsDashing())
		{
			myMoveSpeed = myPlayer->GetDashingSpeed();
		}
		else if (myPlayer->IsWallSliding())
		{
			myMoveSpeed = myPlayer->GetWalkingSpeed() * myWallSlideScalar;
		}
		else
		{
			myMoveSpeed = myPlayer->GetWalkingSpeed();
		}

		myPosition.x += directionToPlayer.x * myMoveSpeed * horizontalSpeedScalar * deltaTime;
		myPosition.y += directionToPlayer.y * myMoveSpeed * verticalSpeedScalar * deltaTime;
	}
	else if (myReturnToPlayer)
	{
		PostMaster::GetInstance()->PostEvent(GameEvent::CameraReachedDestination);
		myReturnToPlayer = false;
	}
}

    

Simple function smoothing the camera movement

Show Code

class ControllerInputManager
{
public:
	static ControllerInputManager* GetInstance();
	void UpdateEvents();
	void Update();
	bool IsButtonHeld(const ControllerButtonInputs aButtonCode) const;
	bool IsButtonPressed(const ControllerButtonInputs aButtonCode) const;
	bool IsButtonReleased(const ControllerButtonInputs aButtonCode) const;
	float IsThumbStickHeld(const WeightedInputs aWeightedButtonCode, ThumbstickDirection aDirection) const;
	float IsThumbStickPressed(const WeightedInputs aWeightedButtonCode, ThumbstickDirection aDirection) const;
	float IsThumbStickReleased(const WeightedInputs aWeightedButtonCode, ThumbstickDirection aDirection) const;
	bool IsThumbStickReleased(const WeightedInputs aWeightedButtonCode) const;
	bool IsTriggerReleased(const WeightedInputs aButtonCode);
	bool IsTriggerPressed(const WeightedInputs aButtonCode);
	bool IsTriggerHeld(const WeightedInputs aButtonCode);
	float GetTriggerWeight(const WeightedInputs aButtonCode);

	ControllerInputManager(ControllerInputManager& other) = delete;
	void operator=(const ControllerInputManager& other) = delete;

protected:
	ControllerInputManager();
	static ControllerInputManager* myInstance;

private:
	std::bitset<static_cast<size_t>(ControllerButtonInputs::Count)> myTentativeState{};
	std::bitset<static_cast<size_t>(ControllerButtonInputs::Count)> myCurrentState{};
	std::bitset<static_cast<size_t>(ControllerButtonInputs::Count)> myPreviousState{};

	Tga::Vector2<int16_t> myTentativeThumbStickLeftValue = {};
	Tga::Vector2<int16_t> myCurrentThumbStickLeftValue = {};
	Tga::Vector2<int16_t> myPreviousThumbStickLeftValue = {};

	Tga::Vector2<int16_t> myTentativeThumbStickRightValue = {};
	Tga::Vector2<int16_t> myCurrentThumbStickRightValue = {};
	Tga::Vector2<int16_t> myPreviousThumbStickRightValue = {};

	uint8_t myTentativeTriggerLeftValue = 0;
	uint8_t myCurrentTriggerLeftValue = 0;
	uint8_t myPreviousTriggerLeftValue = 0;

	uint8_t myTentativeTriggerRightValue = 0;
	uint8_t myCurrentTriggerRightValue = 0;
	uint8_t myPreviousTriggerRightValue = 0;

	int myLeftDeadZone = XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE;
	int myRightDeadZone = XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE;
};

enum class ControllerButtonInputs 
{
	ButtonA,//jump
	ButtonB,//heal
	ButtonX,//attack
	ButtonY,
	RightBumper,//dash
	LeftBumper,
	Start, //pause
	Back,
	DPadUp,
	DPadDown,
	DPadLeft,
	DPadRight,
	LeftThumb,
	RightThumb,
	Count
};
enum class WeightedInputs
{
	ThumbStickLeft,
	ThumbStickRight,
	TriggerLeft,
	TriggerRight,
	
};

enum class ThumbstickDirection
{
	DirectionUp,
	DirectionDown,
	DirectionLeft,
	DirectionRight,
};
    

The interface of xbox-controller support class.

Frogment

Mobile Puzzle Game

Contributions

  • Input controls for mobile devices

  • Gameplay logic for interactable obstacles such as pressure plates and elevators

  • Interface between player object and Unity’s animation system

  • Sound manager

  • Configuring optimization settings for Unity mobile games

Team work reflection

  • As a team it is important to remember what you have achieved, and not only what you have left to do, to avoid unnecessary stress and keep up spirit.

Project conditions

  • 13 people, 5 weeks, 20 hours a week, C#, Unity

  • Reference game: Monument Valley

Download Frogment

Show Code

public class PressurePlateBehavior : MonoBehaviour
{
    [SerializeField] bool myStaysDownForever = false;
    [SerializeField] bool myIsPressedDown = false;
    [SerializeField] UnityEvent onPressedDown;
    [SerializeField] UnityEvent onRelease;
    [SerializeField] MeshRenderer myPressurePlateRend;
    private float myDownMovementLength = -0.08f;

    [SerializeField] private GameObject myParticleComponent;
    void Start()
    {
        myParticleComponent.SetActive(true);
    }

    private void OnTriggerEnter(Collider other)
    {
        for (int i = 0; i < myPressurePlateRend.materials.Length; i++)
        {
            if(myPressurePlateRend.materials[i].name.Contains("M_EmissiveInteractable"))
            {
                myPressurePlateRend.materials[i].SetFloat("_Alive", 0);
            }
        }
        
        if (other.CompareTag("Player") && myIsPressedDown == false)
        {
            AudioManager.instance.PlaySoundEffect("MenuButton", 0.2f);

            GameObject particleInstance = Instantiate(myParticleComponent, transform.position, myParticleComponent.transform.rotation);
            ParticleSystem particleSystem = particleInstance.GetComponent<ParticleSystem>();

            if (particleSystem != null)
            {
                particleSystem.Play();
                Destroy(particleInstance, particleSystem.main.duration);
            }

            PushDown();
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Player") && myStaysDownForever == false)
        {
            ResetPlate();
        }
    }
    private void PushDown()
    {
        transform.position = new Vector3(transform.position.x, transform.position.y + myDownMovementLength, transform.position.z);
        myIsPressedDown = true;
        onPressedDown?.Invoke();
    }

    public bool CheckIfPressedDown()
    {
        return myIsPressedDown;
    }
    public void ResetPlate()
    {
        if (myIsPressedDown == true)
        {
            transform.position = new Vector3(transform.position.x, transform.position.y - myDownMovementLength, transform.position.z);
            myIsPressedDown = false;
            onRelease?.Invoke();
        }
    }
}

    

A pressure plate class, making use of the event system, making pressure plates a flexible asset, easy to use for many different type of events.

Tiny Shell Rough Sea

Endless Runner Racer

Contributions

  • Gameplay logic for pick ups: score, health, animations

  • Wrote an audio manager for the game

  • Configured Unity’s Cinemachine camera settings, to get a subtle under water feel

  • Scripted camera behavior on death and collisions

  • Created a health vs score multiplier-mechanic (with colleagues)

Things I could’ve done better

  • Gotten the camera metrics done quicker since it affects a lot how the game feels and looks, thus influencing how colleagues tweak their graphics and gameplay

Project conditions

  • 13 people, 7 weeks, 20 hours a week, C#, Unity

  • Reference game: Race Against The Sun

Download Tiny Shell Rough Sea

Show Code

public class Script_PlayTimeCameraManagement : MonoBehaviour
{
    public static Script_PlayTimeCameraManagement Instance { get; private set; }
    private CinemachineVirtualCamera playTimeCamera;
    private int lowPriority = 0;
    private int highPriority = 90;
    [SerializeField] private float myNormalWobbleSize = 0.7f;
    [SerializeField] private float myNormalWobbleSpeed = 0.1f;
    private bool myIsSideColiding = false;
    [SerializeField] private float mySideCollisionFrictionSize = 0.3f;
    [SerializeField] private float mySideCollisionFrictionSpeed = 3f;
    [SerializeField] private float frictionTime = 1f;
    private float myFrictionTimer;

    // Start is called before the first frame update
    void Start()
    {
        Instance = this;
        playTimeCamera = GetComponent<CinemachineVirtualCamera>();
        playTimeCamera.Priority = highPriority;
    }

    public void ToggleToLowPriority()
    {
       playTimeCamera.Priority = lowPriority;
    } 
    
    public void ToggleToHighPriority()
    {
       playTimeCamera.Priority = highPriority;
    }
    public void CreateSideCollisionCameraFriction()
    {
        myIsSideColiding = true;
        if (myIsSideColiding == true)
        {
            myFrictionTimer = frictionTime;
        }
        else
        {
            myFrictionTimer = 0;
        }
    }

    void Update()
    {
        CinemachineBasicMultiChannelPerlin cinemachineBasicMultiChannelPerlin =
        playTimeCamera.GetCinemachineComponent<CinemachineBasicMultiChannelPerlin>();
        if (myIsSideColiding == false)
        {
            cinemachineBasicMultiChannelPerlin.m_AmplitudeGain = myNormalWobbleSize;
            cinemachineBasicMultiChannelPerlin.m_FrequencyGain = myNormalWobbleSpeed;
        }
        else if (myIsSideColiding == true) 
        {
            if (myFrictionTimer > 0)
            {
                cinemachineBasicMultiChannelPerlin.m_AmplitudeGain = mySideCollisionFrictionSize;
                cinemachineBasicMultiChannelPerlin.m_FrequencyGain = mySideCollisionFrictionSpeed;
                myFrictionTimer -= Time.deltaTime;
                if (myFrictionTimer <= 0f)
                {
                    cinemachineBasicMultiChannelPerlin.m_AmplitudeGain = myNormalWobbleSize;
                    cinemachineBasicMultiChannelPerlin.m_FrequencyGain = myNormalWobbleSpeed;
                    myIsSideColiding = false;

                }
            }

        }

    }
    
}
    

Smaller part of the camera logic. Adding a sea-like bounce and enabling switching to a death state camera.