[{"content":" Overview This post covers my journey in building a four-legged robotic spider that can walk on uneven surfaces using inverse kinematics in C++ by using F.A.B.R.I.K, an industry standard method implemented in engines such as Unreal and Unity.\nYour browser does not support the video tag. Goal The spider has several legs, and each leg should behave like a small chain of joints. The body needed to move independently, but the feet should remain planted on the terrain even at differing heights. To do that, I used FABRIK, which is a positional inverse kinematics solver, and is conceptually very simple and cheaper than alternatives.\nBuilding the Leg Chains Each leg is represented as a set of joint indices from the skeleton. The last joint is the foot or end effector, which we will move to the target, then iteratively reposition the rest of the chain during backward and forward passes while preserving segment lengths.\nShow Leg Chain code for (size_t legIndex = 0; legIndex \u0026lt; myLegChains.size(); ++legIndex) { SpiderLegChain\u0026amp; legChain = myLegChains[legIndex]; IKChain originalChain = BuildIKChainFromLeg(legChain, referenceModelSpacePose); IKChain solvedChain = originalChain; const Tga::Vector3f footTargetWorld = BuildFootTarget(legChain, originalChain, aDeltaTime, static_cast\u0026lt;int\u0026gt;(legIndex)); //lift the middle joints before solving to bias FABRIK toward //a natural upward knee bend instead of collapsing underneath //for the scope of this project, it works in lieu of constraints! LiftChainForSolve(solvedChain, JOINT_Y_OFFSET, footTargetWorld); FABRIKSolver::Solve(solvedChain, footTargetWorld, MAX_ITERATIONS, TOLERANCE); ApplySolvedChainToPose(legChain, solvedChain, localPose); #ifdef _DEBUG SpiderLegDebugDraw debugDrawData; debugDrawData.originalChain = originalChain; debugDrawData.solvedChain = solvedChain; debugDrawData.footTargetWorld = footTargetWorld; myDebugLegDrawData.push_back(debugDrawData); #endif //update modelSpacePose after each leg so the next leg\u0026#39;s ApplySolvedChainToPose //reads parent rotations that already include the previous leg\u0026#39;s contribution mySkeleton-\u0026gt;ConvertPoseToModelSpace(localPose, modelSpacePose); } But before running FABRIK, you need to convert the current animated pose into model space and then into world space so you can get a chain of actual positions. Luckily I was working in our own engine, which already had ways to store our skeleton and poses in our AnimatedModelComponents, so all I really had to change before I could get to work was to save away the default pose when it was missing animations and save it into a new struct SpiderLegChain. After that, I had a nice default pose to start working with.\nShow Foot Target code Tga::Vector3f Spider::BuildFootTarget(SpiderLegChain\u0026amp; aLegChain, const IKChain\u0026amp; aChain) const { auto\u0026amp; navMesh = Singletons::GetLevelHandler().GetActiveLevel()-\u0026gt;GetNavmesh(); const Tga::Matrix4x4f\u0026amp; spiderWorldTransform = myAnimatedModel-\u0026gt;GetTransform(); if (!aLegChain.hasPlantedFootTarget) { const Tga::Vector3f footWorldPosition = aChain.jointPositionsWorld.back(); aLegChain.restOffsetLocalSpace = WorldToLocalPoint(spiderWorldTransform, footWorldPosition); aLegChain.plantedFootTargetWorld = footWorldPosition; aLegChain.plantedFootTargetWorld.y = navMesh.GetHeightOfPoint(aLegChain.plantedFootTargetWorld); aLegChain.hasPlantedFootTarget = true; return aLegChain.plantedFootTargetWorld; } const Tga::Vector3f naturalFootPositionWorld = LocalToWorldPoint(spiderWorldTransform, aLegChain.restOffsetLocalSpace); Tga::Vector3f desiredStepTargetWorld = naturalFootPositionWorld; desiredStepTargetWorld.y = navMesh.GetHeightOfPoint(desiredStepTargetWorld); aLegChain.plantedFootTargetWorld.y = navMesh.GetHeightOfPoint(aLegChain.plantedFootTargetWorld); const float distanceFromNaturalToPlanted = (aLegChain.plantedFootTargetWorld - desiredStepTargetWorld).Length(); const float maxDriftBeforeStep = 20.0f; if (distanceFromNaturalToPlanted \u0026gt; maxDriftBeforeStep) { aLegChain.plantedFootTargetWorld = desiredStepTargetWorld; } return aLegChain.plantedFootTargetWorld; } Solving with FABRIK At this point, things are still relative simple. Taking one leg at a time, we convert what we\u0026rsquo;ve stored from the spider into a generic IKChain which holds the joints position and lengths, and total length of the chain. Extremely short and sweet. And after population the new IKChain, we have everything to sovle it!\nThe first thing the solver does is handle the simplest edge case, which is when the target is too far away to ever be reached. If the distance from the root joint to the target is greater than the total length of the chain, then there is no fancy solution to find. The only thing the leg can do is stretch itself out as far as possible in the direction of the target.\nThe solve works in two passes that repeat over and over until the foot is close enough to the target. The end effector is snapped directly onto the target, because that is ultimately where we want the chain to end. Then the solver walks backward through the leg, moving each earlier joint so that it stays the correct distance away from its child.\nThe backward pass gets the foot where it should go, but it can pull the root away from where it belongs. Since the root joint is supposed to stay anchored to the spider’s body, the solver now snaps the root back to its original position and walks forward through the chain again. This time, each child joint is repositioned so that every segment length is preserved from the root outward.\nShow FABRIK solve code bool FABRIKSolver::Solve(IKChain\u0026amp; aChain, const Tga::Vector3f\u0026amp; aTargetWorld, int aMaxIterations, float aTolerance) { const int jointCount = static_cast\u0026lt;int\u0026gt;(aChain.jointPositionsWorld.size()); if (jointCount \u0026lt; 2) { return false; } const Tga::Vector3f originalRootPosition = aChain.jointPositionsWorld.front(); const float distanceFromRootToTarget = Length(aTargetWorld - originalRootPosition); if (distanceFromRootToTarget \u0026gt; aChain.totalLength) { for (int jointIndex = 0; jointIndex \u0026lt; jointCount - 1; ++jointIndex) { const Tga::Vector3f\u0026amp; currentJointPosition = aChain.jointPositionsWorld[jointIndex]; const float segmentLength = aChain.segmentLengths[jointIndex]; const Tga::Vector3f directionToTarget = NormalizeSafe(aTargetWorld - currentJointPosition); aChain.jointPositionsWorld[jointIndex + 1] = currentJointPosition + directionToTarget * segmentLength; } return true; } for (int iteration = 0; iteration \u0026lt; aMaxIterations; ++iteration) { aChain.jointPositionsWorld.back() = aTargetWorld; for (int jointIndex = jointCount - 2; jointIndex \u0026gt;= 0; --jointIndex) { const float segmentLength = aChain.segmentLengths[jointIndex]; aChain.jointPositionsWorld[jointIndex] = MoveToDistance(aChain.jointPositionsWorld[jointIndex + 1], aChain.jointPositionsWorld[jointIndex], segmentLength); } aChain.jointPositionsWorld.front() = originalRootPosition; for (int jointIndex = 0; jointIndex \u0026lt; jointCount - 1; ++jointIndex) { const float segmentLength = aChain.segmentLengths[jointIndex]; aChain.jointPositionsWorld[jointIndex + 1] = MoveToDistance(aChain.jointPositionsWorld[jointIndex], aChain.jointPositionsWorld[jointIndex + 1], segmentLength); } const float remainingDistance = Length(aTargetWorld - aChain.jointPositionsWorld.back()); if (remainingDistance \u0026lt;= aTolerance) { return true; } } return true; } At the end of each iteration, the solver checks how far the end effector still is from the target. If that remaining distance is smaller than the tolerance, then the solve stops early because the result is already good enough.\nWith this data we can now visualize the spiders bones and joints by drawing them out. To help me along the way I wanted two things: A way to visualize the pre-solved joints joints, and the solved one, and a way to pause and step through each iteration of the Spider moving. With these two simple but invaluable debug it became much easier to digest the problems that began to appear.\n\u0026hellip;Well, that doesn\u0026rsquo;t look right!\nRebuilding the Mesh The most important realization of the project came after the green debug lines, representing the solved FABRIK chain, finally started looking correct. I had assumed that once FABRIK produced a clean solution, the mesh would just follow automatically. That assumption was completely wrong, and honestly pretty naive.\nThe green line is only a chain of solved positions in space. The mesh, however, is driven by bone transforms, which means it needs correct rotations as well. That sounds like a tiny distinction at first, but it completely changes the problem. It also forced me to confront the fact that my understanding of animation had mostly been at the surface level up until that point. From there on, the challenge was no longer getting FABRIK to solve a nice chain, but figuring out how to make the actual rig follow that solution without tearing itself apart.\nShow Pose code void Spider::ApplySolvedChainToPose(const SpiderLegChain\u0026amp; aLegChain, const IKChain\u0026amp; aSolvedChain, Tga::LocalSpacePose\u0026amp; aLocalPose) const { Tga::ModelSpacePose currentModelSpacePose{}; mySkeleton-\u0026gt;ConvertPoseToModelSpace(aLocalPose, currentModelSpacePose); const Tga::Matrix4x4f spiderWorldTransform = myAnimatedModel-\u0026gt;GetTransform(); for (size_t chainJointIndex = 0; chainJointIndex + 1 \u0026lt; aLegChain.jointIndices.size(); ++chainJointIndex) { const int currentSkeletonJointIndex = aLegChain.jointIndices[chainJointIndex]; const int childSkeletonJointIndex = aLegChain.jointIndices[chainJointIndex + 1]; const int parentSkeletonJointIndex = mySkeleton-\u0026gt;Joints[currentSkeletonJointIndex].Parent; Tga::Vector3f currentDirectionWorld = GetJointWorldPosition(currentModelSpacePose, childSkeletonJointIndex) - GetJointWorldPosition(currentModelSpacePose, currentSkeletonJointIndex); Tga::Vector3f solvedDirectionWorld = aSolvedChain.jointPositionsWorld[chainJointIndex + 1] - aSolvedChain.jointPositionsWorld[chainJointIndex]; if (currentDirectionWorld.LengthSqr() \u0026lt;= 0.0001f || solvedDirectionWorld.LengthSqr() \u0026lt;= 0.0001f) { continue; } currentDirectionWorld.Normalize(); solvedDirectionWorld.Normalize(); //figure out how far the joint needs to rotate to match the solved direction const Tga::Quaternionf worldDeltaRotation = Tga::Quaternionf::CreateFromTo(currentDirectionWorld, solvedDirectionWorld); //model space accumulates the full parent chain, so the model space rotation //of the parent IS its world rotation when the spider itself has no rotation. //for the root joint there is no skeleton parent, so the spider world transform is the parent const Tga::Quaternionf parentWorldRotation = (parentSkeletonJointIndex \u0026gt;= 0) ? currentModelSpacePose.JointTransforms[parentSkeletonJointIndex].GetRotationAsQuaternion() : spiderWorldTransform.GetRotationAsQuaternion(); const Tga::Quaternionf parentWorldRotationInverse = parentWorldRotation.GetConjugate().GetNormalized(); //row-vector sandwich to convert the world delta into local space: //q_parent * worldDelta * inv(q_parent) const Tga::Quaternionf localDeltaRotation = parentWorldRotation * worldDeltaRotation * parentWorldRotationInverse; Tga::ScaleRotationTranslationf\u0026amp; jointLocalTransform = aLocalPose.JointTransforms[currentSkeletonJointIndex]; //row-vector convention applies delta on the right: old * localDelta jointLocalTransform.SetRotation((jointLocalTransform.GetRotation() * localDeltaRotation).GetNormalized()); //reconvert after each joint so the next joint reads correct parent transforms mySkeleton-\u0026gt;ConvertPoseToModelSpace(aLocalPose, currentModelSpacePose); } } I was applying each new IK solution on top of the previous frame\u0026rsquo;s already modified pose, which meant the values kept accumulating and mutating further and further away from the original skeleton. That was why the spider would eventually bend into absurd shapes, why the joint rotations spiraled out of control, and why parts of the leg sometimes looked like they were completely disconnecting from one another.\nThe fix was to cache the original clean pose and restart from that every frame before applying IK. As soon as I did that, the result became dramatically more stable. That did not solve every remaining issue with reconstruction or rotation, but it removed the feedback loop that had been corrupting the entire leg system over time. Once that was under control, I could finally move on to the more interesting part, which was improving how the spider actually stepped and moved across the terrain.\nIt was mostly cleaning up magic numbers and tweaking tolerances, speeds, and other values I had already established, and adding leg groups so the spider would walk \u0026ldquo;realistically\u0026rdquo;, meaning one front and one back leg at a time. Which lead the result you saw above.\nYour browser does not support the video tag. This satisfied my curiosity of how inverse kinematics were applied in games. FABRIK seems to be the most popular implementation overall, with Unreal and Unity both having some form of it, most likely to its low computotional cost. Even though the course is finished and I have to turn in this assignment, I\u0026rsquo;ll be continuing to work on this implementation and hopefully see it in our final project. Now that I\u0026rsquo;ve gotten a taste of it I want to see where we can start using it, and I have no shortage of ideas. I\u0026rsquo;ll most likely be updating this with any improvements or additions once I get to that point.\n","date":"April 6, 2026","permalink":"/AlexanderMelander/posts/specialisering/","summary":"Building a four-legged robotic spider that can walk on uneven surfaces using inverse kinematics in C++ by using F.A.B.R.I.K, an industry standard method implemented in engines such as Unreal and Unity.","title":"Inverse Kinematics using FABRIK","type":"posts"},{"content":" Overview A Call Of Duty Zombies-inspired Robot shooter, where I was primarily responsible for the Enemy Wave System and Player Progression, as well as animation-handling and gunplay, as well as a Sub-Level system allowing our leveldesigners to stitch together multiple scenes.\nEnemy Wave System Our enemy manager consists of four major parts, the EnemyController which runs on a behaviour tree, an EnemySpawner which sole responsibility is to instaniate and initialize enemies with the correct stats of a wave, which is the third component. A wave holds data which can be tuned from a visual scripting implementation we have, to allow level designers to change health, speed, and spawn frequency as well as number per wave, and lastly the manager itself, which handles all these components to string them together into a cohesive system.\nAs I\u0026rsquo;ve mentioned in other posts, I love states. I think they\u0026rsquo;re extremely clean and you never have to wonder \u0026ldquo;where\u0026rdquo; you are in the code when debugging. Knowing which state you are in, or even which state you\u0026rsquo;re supposed to be in, makes things easier.\nIt\u0026rsquo;s a singleton, so it lives across all instances of our game. We only have one level, so it made sense to make it into a singleton. You can access it easily to get important information such as current waves, or remaining enemies. The manager itself handles mostly everything else, otherwise. The first thing we do is to select a wave.\nvoid EnemyManager::AddWave(const WaveConfiguration\u0026amp; aWaveConfiguration) { myWaves.push_back(std::make_unique\u0026lt;EnemyWave\u0026gt;(aWaveConfiguration)); } And that\u0026rsquo;s it! This function is called in Nodes from our visual scripting implementation that I only worked partly on, but all it really does is read all the data that a WaveConfiguration holds and construct a new EnemyWave from it, before storing it into the manager. This all happens when we first start the game, but we can add waves wheneves we want just as easily. The LD\u0026rsquo;s use their scripting to decide how many waves they want, and then at which wave we should change the configuration. So you can easily select that you want 12 waves, and use the same configuration for every one. All the configuration really does is \u0026ldquo;Update\u0026rdquo; the waves rules.\nShow Enemy Wave struct WaveConfiguration { int waveNumber = 1; int enemyCount = 1; float spawnFrequencySeconds = 1.0f; float enemyHealthMultiplier = 1.0f; float enemySpeedMultiplier = 1.0f; bool isInfiniteWave = false; }; class EnemyWave { public: EnemyWave(); EnemyWave(const WaveConfiguration\u0026amp; configuration); void SetWaveConfiguration(WaveConfiguration aWaveConfig); const WaveConfiguration GetWaveConfig() const { return myConfiguration; } void IncrementSpawnedCount(int count = 1); void DecrementRemainingCount(int count = 1); void DecrementSpawnedCount(int count = 1); //getters for waveConfig int GetWaveNumber() const { return myConfiguration.waveNumber; } int GetEnemyCount() const { return myConfiguration.enemyCount; } bool GetIsInfiniteWave() const { return myConfiguration.isInfiniteWave; } int GetRemainingEnemyCount() const { return myRemainingEnemyCount; } int GetSpawnedEnemyCount() const { return mySpawnedEnemyCount; } float GetTimeSinceLastSpawn() const { return myTimeSinceLastSpawn; } bool GetIsComplete() const { return myRemainingEnemyCount \u0026lt;= 0; } void ResetSpawnTimer() { myTimeSinceLastSpawn = 0.0f; } void UpdateSpawnTimer(float deltaTime) { myTimeSinceLastSpawn += deltaTime; } bool ShouldSpawn() const; private: WaveConfiguration myConfiguration; int myTotalEnemyCount = 0; int myRemainingEnemyCount = 0; int mySpawnedEnemyCount = 0; float myTimeSinceLastSpawn = 0.0f; }; Now that we have our waves constructed, we can start using them in the manager. We start simple with guard statements and intermission time since it\u0026rsquo;s important to leave enough time for the player to explore the level between waves, or use any keycards they might have acquired to go get an upgrade. After that, we simply check wether or not we are eligable for a spawn. It\u0026rsquo;s decided by the waves internal timer, and should an enemy be spawned, we start sorting through our active spawnpoints.\nShow Enemy Manager Update const void EnemyManager::Update(float deltaTime) { if (myWavesState == WaveState::Paused || myCurrentWaveIndex \u0026lt; 0 || myCurrentWaveIndex == static_cast\u0026lt;int\u0026gt;(myWaves.size())) { return; } if (myWavesState == WaveState::Intermission) { myCurrentIntermissionTime -= deltaTime; if (myCurrentIntermissionTime \u0026lt;= 0) { myCurrentIntermissionTime = myIntermissionTime; StartNextWave(); myWavesState = WaveState::Active; } else { return; } } if (myCurrentWaveIndex == static_cast\u0026lt;int\u0026gt;(myWaves.size())) return; EnemyWave* currentWave = myWaves[myCurrentWaveIndex].get(); currentWave-\u0026gt;UpdateSpawnTimer(deltaTime); if (currentWave-\u0026gt;ShouldSpawn() \u0026amp;\u0026amp; !myActiveSpawnPoints.empty()) { SpawnEnemyAtRandomPoint(); currentWave-\u0026gt;IncrementSpawnedCount(); currentWave-\u0026gt;ResetSpawnTimer(); } if (!myEnemiesPendingRemoval.empty()) { ProcessPendingRemovals(); } } Through triggerboxes placed in the level, the Enemy Spawners that our LD\u0026rsquo;s have placed will be set as active. The Manager will then pick between these spawners at random to instaniate an enemy, based on the current waves ruleset. Out of all the active points, it will simply spawn an enemy on a random one, so you can never really anticipate where the enemies will come from since multiple points will always be active by design.\nNow the manager deals with slapping on all the extra stats and rulesets to the enemies base-states that the LD\u0026rsquo;s have set on the game object, and then we have an enemy in our game. We\u0026rsquo;ve already incriment the amount of enemies, and we check that we can\u0026rsquo;t spawn too many. By the time that the last enemy dies, we enter the intermission state and wait for the next wave to begin.\nSub-Level Loading and NavMesh Sitching Since we only have one level in this game, and three level designers, we needed a way for them to work on one coherent map while still not fighting in the same file. In engines like Unity you\u0026rsquo;d just have them all working in prefabs in a single scene, but we don\u0026rsquo;t have that type of system in our game. We have our Scene files, and they link to folders containing all our GameObjects. I created anchor points that they could place in a Hub level, which would then find the corresponding file and load in that folder along with the offset of where anchorpoint was closed. Of course there was a stitching point in the other scene as well, so you could place them both just about anywhere and the objects would seamlessly load in at start.\nShow Level Stitching void LevelHandler::LoadAndStitchSubLevel(Level\u0026amp; aLevel, const std::string\u0026amp; aSubLevelName, GameObject* aAnchorSnapPoint) { Level subLevel; BuildLevel(aSubLevelName, subLevel); GameObject* referenceSnapPoint = FindSnapPointInLevel(subLevel, aSubLevelName, true); if (!referenceSnapPoint) { std::cout \u0026lt;\u0026lt; \u0026#34;Failed to find reference snappoint in sublevel: \u0026#34; \u0026lt;\u0026lt; aSubLevelName \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return; } Tga::Vector3f offset = aAnchorSnapPoint-\u0026gt;AccessTransform().GetPosition() - referenceSnapPoint-\u0026gt;AccessTransform().GetPosition(); unsigned int nextObjectId = static_cast\u0026lt;unsigned int\u0026gt;(aLevel.GetObjects().size()) + 1; // Apply transform offset and update ID\u0026#39;s for (auto\u0026amp; subLevelGameObject : subLevel.GetObjects()) { if (subLevelGameObject-\u0026gt;AccessFirstComponentOfType\u0026lt;PlayerController\u0026gt;() || subLevelGameObject-\u0026gt;AccessFirstComponentOfType\u0026lt;CameraComponent\u0026gt;()) continue; Tga::Matrix4x4f transformedTransform = subLevelGameObject-\u0026gt;AccessTransform(); transformedTransform.SetPosition(transformedTransform.GetPosition() + offset); subLevelGameObject-\u0026gt;SetTransform(transformedTransform); for (auto model : subLevelGameObject-\u0026gt;AccessAnimatedModelComponents()) { model-\u0026gt;AccessModel().SetTransform(transformedTransform); } subLevelGameObject-\u0026gt;SetObjectID(nextObjectId++); aLevel.GetObjects().push_back(subLevelGameObject); } for (auto\u0026amp; pointLight : subLevel.myPointLightComponents) { aLevel.myPointLightComponents.push_back(pointLight); } for (auto\u0026amp; spotLight : subLevel.mySpotLightComponents) { aLevel.mySpotLightComponents.push_back(spotLight); } std::filesystem::path path = Tga::Settings::GameAssetRoot(); path /= \u0026#34;levels\\\\navmeshes\u0026#34;; std::filesystem::path localPath = aSubLevelName; localPath.replace_extension(\u0026#34;\u0026#34;); path /= localPath.filename(); path += \u0026#34;_navmesh.obj\u0026#34;; myActiveLevel-\u0026gt;GetNavmesh().LoadAdditionalMesh(path.string().c_str(), offset, referenceSnapPoint-\u0026gt;AccessTransform().GetPosition()); std::cout \u0026lt;\u0026lt; \u0026#34;Successfully loaded and stitched sublevel: \u0026#34; \u0026lt;\u0026lt; aSubLevelName \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } I\u0026rsquo;ll be honest, I\u0026rsquo;m not super proud of the way this one was implemented, mostly because the way our light components are treated are somewhat different from the other game objects and components. The nice thing is that it\u0026rsquo;s mostly handled exactly as loading in any other level, and this function could potentially be multi-threaded to eventually allow for seamless loading in and loading out of segments of a single level, much how Unreal\u0026rsquo;s sublevel system works. I\u0026rsquo;m hoping for our final project I\u0026rsquo;ll get to continue working on this to make that a reality. How cool wouldn\u0026rsquo;t it be to have a game without loading screens? These projects are small enough that we could totally be able to do it with a little bit of clever designing of the levels.\nOne problem still remained, however: all these levels have different NavMeshes, and we didn\u0026rsquo;t want to generate one big navmesh everytime we started the level, that would be too expensive. So instead I created a function to stich together the closest NavMesh points to create a coherent one, without gaps so the AI could seamlessly traverse it.\nShow Navmesh Welding void rhe::Navmesh::CalculateCrossMeshConnections() { if (myMeshRegions.size() \u0026lt; 2) return; constexpr float maxMidpointDistance = 500.0f; constexpr float maxMidpointDistanceSq = maxMidpointDistance * maxMidpointDistance; constexpr float maxVerticalDifference = 100.0f; for (size_t regionAIndex = 0; regionAIndex \u0026lt; myMeshRegions.size(); ++regionAIndex) { for (size_t regionBIndex = regionAIndex + 1; regionBIndex \u0026lt; myMeshRegions.size(); ++regionBIndex) { const MeshRegion\u0026amp; regionA = myMeshRegions[regionAIndex]; const MeshRegion\u0026amp; regionB = myMeshRegions[regionBIndex]; if (regionA.sourceFile == regionB.sourceFile) continue; std::vector\u0026lt;BoundaryEdge\u0026gt; edgesA = CollectBoundaryEdges(regionA); std::vector\u0026lt;BoundaryEdge\u0026gt; edgesB = CollectBoundaryEdges(regionB); if (edgesA.empty() || edgesB.empty()) continue; float bestDistance = maxMidpointDistanceSq; int bestAIndex = -1; int bestBIndex = -1; for (size_t aIndex = 0; aIndex \u0026lt; edgesA.size(); ++aIndex) { for (size_t bIndex = 0; bIndex \u0026lt; edgesB.size(); ++bIndex) { const BoundaryEdge\u0026amp; edgeA = edgesA[aIndex]; const BoundaryEdge\u0026amp; edgeB = edgesB[bIndex]; const float dy = std::abs(edgeA.mid.y - edgeB.mid.y); if (dy \u0026gt; maxVerticalDifference) continue; const float dx = edgeA.mid.x - edgeB.mid.x; const float dz = edgeA.mid.z - edgeB.mid.z; const float distSq = dx*dx + dz*dz; if (distSq \u0026lt; bestDistance) { bestDistance = distSq; bestAIndex = static_cast\u0026lt;int\u0026gt;(aIndex); bestBIndex = static_cast\u0026lt;int\u0026gt;(bIndex); } } } if (bestAIndex != -1 \u0026amp;\u0026amp; bestBIndex != -1) { AppendBridgeBetweenEdges(edgesA[bestAIndex], edgesB[bestBIndex]); } } } CreateNodes(); CalculateConnections(); } This one has a couple of magic numbers which isn\u0026rsquo;t supert neat, but once the closest boundary edges between two navmesh regions were identified, I generated a small bridge of geometry between them. This effectively “welds” the two previously separate meshes into a single continuous surface. After modifying the mesh, I rebuilt the navigation data by generating nodes from the updated geometry and recalculating adjacency between them. This ensures that the AI can traverse seamlessly across the stitched regions, as the pathfinding system now sees them as a single connected graph rather than isolated meshes.\nThe biggest thing I\u0026rsquo;m missing from this is implementing the navmesh regions that I have generated into the AI\u0026rsquo;s behaviour. They\u0026rsquo;re already there, but right now our enemies are sampling the entire Navmesh, and while it\u0026rsquo;s not super large, it would be impossible to scale this without seeing significant reduction of performance. This is something that I want to solve along with the Sub-Level loading extension I\u0026rsquo;m planning.\n","date":"March 17, 2026","permalink":"/AlexanderMelander/posts/botops/","summary":"A Call Of Duty Zombies-inspired Robot shooter, where I was primarily responsible for the Enemy Wave System and Player Progression, as well as animation-handling and gunplay, as well as a Sub-Level system allowing our leveldesigners to stitch together multiple scenes.","title":"BotOps","type":"posts"},{"content":" Overview Intense 2D Hack/Slash Sidescroller where my main responsibilites included VFX, and Scripted Events tied to the player progression.\nStart This was the first project in our education where we began working in their propertiety engine \u0026ldquo;The Game Engine\u0026rdquo;. This meant we would have to start implementing features almost completely from scratch, and we had a strong picture of what we wanted to do within the first weeks. We wanted every pipeline worked out and visible in-game. Of course, there was no \u0026ldquo;game\u0026rdquo; the first two weeks, but we quickly nailed down asset loading, animations, VFX, and even Audio in the first sprint. There was even a rudamentary moving enemy moving across the screen.\nVFX I was working closely with the Procedual Artists in the beginning of the project. This meant I was working on proving the VFX pipeline. Since the game was 2D, a simple sprite flipbook was perfect for our game. I called it a \u0026ldquo;VFXSystem\u0026rdquo;, but it was really just a Singleton that took in a type of Flipbook, size, spawnpoint, rotational offset, and wether or not it would follow an object or not. This worked perfectly for the scope of our game. We could have blood splatter from our enemies, dust when we ran or landed, or sword swipes and dash-lines following the player.\nOf course, it was very rigid. It was the first time I had worked in C++ on such a large scale, and the functions became overly long and eventually complex as we wanted more functionality. With the knowledge I have now, I would\u0026rsquo;ve instead made more overloads, or given the chance to create a struct that holds data so you can access the things you want to change, instead of specifying everything explicity in the function.\nScripted Events For our scripted event we wanted to have an enemy fall down from the mountain and drop a key for the player, so they could enter a dungeon. There\u0026rsquo;s an inside joke about this \u0026ldquo;Falling Man\u0026rdquo; event within our group, it was a while until I was able to let the task go, partly because I was confident in how fast I would be able to implement it. Of course, I greatly underestimated how much work goes into making one when there\u0026rsquo;s so little to work with. First and foremost I had to create states for the player: ones where they were free to move, and a \u0026ldquo;non-interactive\u0026rdquo; state that would null all movement input and momentum. Then, I had to create a camera controller that would follow and lerp to points of interests so we could lead the player. I also needed a text-system that would describe certain key points, such as \u0026ldquo;The Guard dropped a key!\u0026rdquo; in the case of our scripted event, since we deemed it to be easier than modelling and creatign an object for the key, that, and text is much more re-usable (which we eventually did use for our tutorials). Lastly came the falling man, of course. He was instaniating at the top of the mountain, played some audio when he fell, and then was animated when he landed the ground (with a fancy puff of dirt and dust from my VFX system).\nPerfect, right?\nOf course there were issues. Not only would this repeat every time we exited and re-entered the area, but the falling man followed you into different levels. Every single \u0026ldquo;scene\u0026rdquo; we had now had its own falling man. You would constantly get interrupted by the camera panning over with a scream, until he just landed somewhere in your level. This was of course because we were in the middle of working on our persistence flags in the game, and even our asset loading/unloading, which I got to dip my toes into to solve this issue. To this day it\u0026rsquo;s among the funniest bugs I\u0026rsquo;ve encountered.\nLastly of course, was the chery on top, of getting to unlock the iron gate which allowed you to continue into the dungeon, and eventually climb the mountain to fight the boss in our game.\n","date":"April 1, 2023","permalink":"/AlexanderMelander/posts/bladeofwill/","summary":"Intense 2D Hack/Slash Sidescroller where my main responsibilites included VFX, and Scripted Events tied to the player progression","title":"Blade of Will","type":"posts"},{"content":" Overview Published Game on Steam , I was responsible for the majority of gameplay, and utilizing the Unity 3D Dashboard API for seamless tweaking of values and gameplay enemies without a need of patches, and telemetry work to track individual players progress.\nCloud API Our implementation on CloudAPI allowed us to tweak values allowed us to save the settings of players, enemies, and other states. We could tweak the stats of our enemies to make them more agressive, have bigger vision cones, make them faster, more idle, etc\u0026hellip; It was extremely exciting to watch someone play the game, and then tweak the experience before their eyes.\nTogether with the DataPersistanceManager I made we could modify things very easily, and keep track of each indivudual players stats, progress, how much cheese or evidence they collected, and which levels they had unlocked. It was one of the more exciting parts during my proffesional work, and there was so much more I wanted to do. With the end of the project it sadly became a lot of \u0026ldquo;TODOs\u0026rdquo; in the project files however, but in that short period I learned incredibly much about developing games. There is so much more than just making gameplay, or even editors and tools. Watching every unique profile put the scope of these projects into perspective for me.\nEnemies My favourite part of the project was by far working with the enemies. This is where I first fell in love with statemachines and their potential simplicity and endless potential. It\u0026rsquo;s so neat working in clear-cut states, and the ease of debugging make iterating on them much simpler. As this was my first professional job and I had very little knowledge in programming prior, there is much I want to change with my previous implementation. It was simple and clean, but I did not properly divide the classes, meaning my EnemyController ended up getting pretty bloated despite the fact that StateMachiens were properly implemented.\nDespite this, it gave the perfect behaviour for this top-down stealth game. The enemies behaved in predictable patterns follow patrolpoints, investigated sounds, and attacked the player when they got caught. Perfect for a game where the player needs to be able to read and plan ahead for how the enemies react.\nDiegetic UI - Player Office Another favourite of mine is the office area. We wannted a diegetic UI for our main menum. The players \u0026ldquo;office\u0026rdquo; as they are an detective was meant to be highly interactive. To point a few things out, when collecting enough evidence in the first city for example, the player would go to their board and \u0026ldquo;pin\u0026rdquo; a picture to unlock the new level, with a red yarn being drawn to it. You\u0026rsquo;d unlock this \u0026ldquo;grid\u0026rdquo; of levels that you\u0026rsquo;d pick from.\nIf you wanted to delete your progress, you\u0026rsquo;d go to the trashcan to \u0026ldquo;reset\u0026rdquo;, and to exit the game, you\u0026rsquo;d go to the door to leave the office. Change clothes? There was a wardrove that opened and showed the clothes and hats you unlocked. Of course, you press \u0026ldquo;Esc\u0026rdquo; at any time to open your journal and select any of the options at any time too, so you didn\u0026rsquo;t need to interact with the office if you really didn\u0026rsquo;t want to, but it was such a neat and \u0026ldquo;fun\u0026rdquo; way to make everything feel so much more alive.\n","date":"April 1, 2023","permalink":"/AlexanderMelander/posts/bpm/","summary":"Published Game on Steam , I was responsible for the majority of gameplay, and utilizing the Unity 3D Dashboard API for seamless tweaking of values and gameplay enemies without a need of patches, and telemetry work to track individual players progress.","title":"Brie Parmesan Mysteries","type":"posts"},{"content":" Download CV ","permalink":"/AlexanderMelander/cv/","summary":" Download CV ","title":"","type":"page"}]