Better Isn't Always Best: Choosing the Right Tool Under a Sprint Deadline
Publisher: Anthony Halstead
Date: 10/17/25
Better Isn’t Always Best: Choosing the Right Tool Under a Sprint Deadline
For most of this sprint I chased what felt like the “better” solution: Unreal’s Smart Objects + EQS + Behavior Trees + GAS tags, all working together to drive my harvester behavior. On paper it was elegant—prebuilt systems, lots of editor-driven authoring, future-proof. In practice, I spent several days wiring, debugging, and re-wiring and still couldn’t get the harvester to actually use the objects. I could see Smart Objects lighting up in the visual debugger, but the last mile—claiming/using and moving based on slot locations—kept slipping.
With the sprint deadline looming, I had a choice: keep pushing for the “better” system, or pivot to the “best for right now.” I pivoted. I replaced the Smart Object dependency with a focused, code-driven approach built on Behavior Trees and the Navigation System. Velocity jumped immediately, and I got back on pace for the sprint—at the cost of a few features I’ll revisit later.
This post is the story of that decision, what I built, and what I learned.
The Original Plan
Goal: Make a Harvester unit that searches for a resource node and gathers from it, all orchestrated by a Behavior Tree.
Chosen stack (initially):
-
Smart Objects for slots/availability/entrances
-
EQS to find/score candidate objects
-
GAS tags to filter activity types
-
Behavior Trees to glue it together
Status midweek:
-
Smart Objects were placed correctly and visible in the SO debugger.
-
EQS context and tests were configured; I could watch the queries run.
-
But the chain Find -> Claim ->MoveTo/Use never stabilized. Blackboard keys would flip invalid, slot selection had edge cases, and some of the helper APIs were stricter than expected (e.g., tag query expressions not taking the arguments I passed, slot handles vs. claim handles, etc.). I was burning time on plumbing instead of shipping behavior.
The Pivot
I swapped the Smart Object pipeline for a targeted, code-first approach:
-
A simple component on each crystal: UAC_HarvestNode
-
Tracks capacity, max users, current users, and availability.
-
Provides TryReserve, Release, Consume, and a helper to suggest a good standing point.
-
-
A Behavior Tree task to find nodes: UBTTask_FindHarvestNode
-
Scans all UAC_HarvestNode s, filters by availability and type, and
-
Writes TargetActor (the node’s owner) and TargetLocation (a reachable navmesh point near it).
-
-
A Behavior Tree task to harvest: UBTTask_HarvestTick
-
When the pawn is within range, it reserves the node, ticks a repeating timer (every N seconds), syphons resources via Consume , and adds to the agent’s carried amount.
-
Stops when agent is full or node is depleted, then releases the reservation cleanly.
-
-
A tiny bit of state on the agent: AA_BaseAgent
-
CarryCapacity and CarriedCrystals , with helpers GetFreeCarry(), AddCrystals, and DropAllCrystals .
-
Result: Behavior came online immediately. The harvester can find a crystal node, move to a reachable point near it, reserve a slot, and tick up resources until full or empty—no Smart Object or EQS dependencies required.
What I Built (Under the Hood)
-
UAC_HarvestNode(component on each crystal)
-
Editor fields: Capacity, MaxUsers, HarvestRadius, NodeType, bEnabled
-
Runtime: TryReserve(AActor*), Release(AActor*),Consume(int32 Amount), plus a small static registry so tasks can discover all nodes.
-
Keeps “who’s using me?” logic local to the node.
-
-
UBTTask_FindHarvestNode(BT task)
-
Finds the nearest available node to an origin (HQ if provided, otherwise the pawn).
-
Projects a reachable navmesh point near the node and sets TargetLocation directly on the blackboard.
-
Includes built-in debug draw (lines, spheres, reasons for failure) to see why a candidate was rejected.
-
-
UBTTask_HarvestTick(BT task)
-
Validates range (so the tree can re-run MoveTo if the unit drifted).
-
Reserves the node, starts ticking every X seconds, and on each pulse:
-
Takes min(HarvestPerTick, NodeCapacity,AgentFreeCarry)
Node.Consume(Take) and Agent.AddCrystals(Take)
-
-
Stops on full, depleted, or timeout, and releases the reservation on finish/abort.
-
-
AA_BaseAgent (AgentState)
-
Added simple, non-GAS harvesting state: CarryCapacity and CarriedCrystals.
-
Kept GAS in place for future growth, but it’s not required for this core loop.
-
Why This Worked (and the Other Path Didn’t—Yet)
-
Determinism & visibility. With a small amount of code I controlled every edge case: nav projection, path reachability, exact reservation semantics, and explicit debug visuals. I could see why a node was rejected or why a location was invalid.
-
Editor friction disappeared. No more wrestling with SO Behavior Definitions, slot entrance querying, or EQS contexts not quite matching runtime needs.
-
Fewer moving parts. The BT + Navigation + two small tasks were simply easier to reason about for an incremental MVP.
This isn’t a knock on Smart Objects. They’re powerful and I may circle back when I have the time to wrap the whole “find/claim/use/release” lifecycle the way UE expects. But this sprint needed a working harvester, not an architectural victory lap.
What I Lost (and Can Add Back Later)
-
Slot entrances & complex usage conditions. I replaced them with a ring-sample and best-reachable point near the node. Good enough for harvesters; less ideal for intricate station interactions.
-
Authoring scalability. Smart Objects shine when gameplay designers need to author lots of interactions in the editor. My code-first path is lighter but less “data-driven” out of the box.
-
Built-in reusability. I’ll encapsulate more of this logic into clean components so it’s easy to reapply to other interactables.
Lessons in Time Management (aka How Not to Miss a Sprint)
Timebox experiments. If a system isn’t working in a day or two, set a hard pivot point. I waited too long.
Define an MVP behavior first. “Harvester gathers and stops when full/empty” is the core loop. Everything else can be layered on later.
Prefer the most debuggable path under pressure. More moving parts mean more places for things to fail silently. Simpler code made bottlenecks obvious.
Keep Blackboard lean. Use it to coordinate (what and where), not to store long-lived game state.
Build tool-free tests. A quick custom FindLocation and debug draw often tell you more than an hour in a visual debugger.
Document pivot criteria. Explicitly list what success looks like and the deadline to switch strategies if you can’t get there.
What’s Next
-
Node lifecycle: Visual feedback on depletion (FX/swap mesh), optional respawn.
-
Concurrency polish: Improve multi-agent fairness (e.g., reservation queuing or per-node priority).
Pathfinding: Improve path finding for agents and prevent intersecting agent pathing. (Causing collisions)
Closing Thought
“Better” systems aren’t always best right now. The right tool is the one that helps you ship the behavior you need within the constraints you have. This week, the right tool was a focused BT + Nav + component approach. It wasn’t as fancy, but it was clear, fast to iterate, and it delivered.
If you’re stuck: timebox, define the smallest proof of behavior, and don’t be afraid to pivot. Your sprint (and your sleep) will thank you.
Get Iron Reclamation
Iron Reclamation
Status | Prototype |
Author | LGHTS |
Tags | First-Person, Real time strategy, Singleplayer |
More posts
- Bone to Bone: Animation Trouble1 day ago
- When a Simple Menu Isn’t So Simple: Building an Options Menu in Unreal1 day ago
- From Continent to Contained: Learning to Build a World That Works1 day ago
- Perfection Isn't Perfect2 days ago
- Problems with Unreal Engine8 days ago
- From a Grid to a Continent: A Coder's Bumpy Journey into Level Design8 days ago
- Engineering Hybrid Intelligence: Lessons from Building Iron Reclamation’s AI8 days ago
- The Issue Of Not Following Existing Knowledge8 days ago
- Perforce issues8 days ago
Leave a comment
Log in with itch.io to leave a comment.