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
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
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
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
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.