Tensor Planner is a compact C++20 planning library for games, simulations, and tooling. You describe a world as typed objects, facts, actions, and goals; the library searches for a valid sequence of actions and returns a plan you can execute in your own runtime.
It is designed around a small C ABI, with friendly bindings layered on top:
- C++ fluent API for strongly typed native projects.
- C# / Unity API for managed gameplay code and Unity packages.
- Jai module with generated C bindings plus a type-first Jai wrapper.
- Unreal Engine source plugin with Unreal-native types and soft object references.
Full documentation lives in the GitHub Wiki:
Tensor Planner is useful when the desired outcome is clear, but the valid path depends on current world state.
Instead of writing a separate script for every possible sequence, you define:
- what objects exist,
- what facts are true now,
- which actions can change those facts,
- and what goal facts must become true.
The planner searches the possible action space and returns a valid ordered plan. That makes it useful for goal-directed agents, dependency-heavy systems, simulation tooling, reachability checks, and model-assisted action ranking.
- Typed domains: predicates, numeric functions, action parameters, facts, values, and goals all carry type information.
- STRIPS-like action schemas with preconditions, add effects, and delete effects.
- Numeric preconditions and numeric effects for resources, costs, counters, and other scalar state.
- Guided search with bounded candidate grounding and deterministic behavior for fixed inputs.
- Optional scorer callback that receives tensor exports and ranks grounded candidate actions.
- Tensor exports for schema, problem state, candidates, and action graphs.
- Explicit memory ownership for native callers.
- Unity package structure with runnable scripts.
- Distribution script for C++, C#, Unity, and Jai artifacts.
- Unreal Engine wrapper with
FString,TArray, andTSoftObjectPtr<>-based planning objects.
#include "tensor_planner.hpp"
#include <string>
struct Character { std::string name; };
struct Location { std::string name; };
int main() {
Character player{"player"};
Location home{"home"};
Location forest{"forest"};
tp::Planner planner;
auto at = planner.predicate<Character, Location>("at");
auto connected = planner.predicate<Location, Location>("connected");
auto energy = planner.function<Character>("energy");
auto move = planner.action("move")
.param<Character>("who")
.param<Location>("from")
.param<Location>("to")
.require(at("who", "from"))
.require(connected("from", "to"))
.require(energy("who") >= 1.0f)
.removes(at("who", "from"))
.adds(at("who", "to"))
.decreases(energy("who"), 1.0f)
.commit();
auto state = planner.state()
.object(player)
.object(home)
.object(forest)
.fact(at(player, home))
.edge(connected, home, forest)
.value(energy(player), 2.0f)
.goal(at(player, forest));
auto result = planner.solve(state);
for (const tp::PlanStep &step : result.steps()) {
if (step.is(move)) {
Character *who = step.arg<Character>("who");
Location *to = step.arg<Location>("to");
// Execute: move who to destination.
}
}
}The planner returns your real object pointers from plan steps. Objects passed to the state are borrowed and must outlive the solve result.
using TensorPlanner;
sealed class Character { public Character(string name) { Name = name; } public string Name { get; } }
sealed class Location { public Location(string name) { Name = name; } public string Name { get; } }
Character player = new Character("player");
Location home = new Location("home");
Location forest = new Location("forest");
using (Planner planner = new Planner()) {
Predicate at = planner.Predicate<Character, Location>("at");
Predicate connected = planner.Predicate<Location, Location>("connected");
NumericFunction energy = planner.Function<Character>("energy");
PlannerAction move = planner.Action("move")
.Param<Character>("who")
.Param<Location>("from")
.Param<Location>("to")
.Require(at.Create("who", "from"))
.Require(connected.Create("from", "to"))
.Require(energy.Create("who").GreaterOrEqual(1.0f))
.Removes(at.Create("who", "from"))
.Adds(at.Create("who", "to"))
.Decreases(energy.Create("who"), 1.0f)
.Commit();
StateBuilder state = planner.State()
.Object(player)
.Object(home)
.Object(forest)
.Fact(at.Create(player, home))
.Edge(connected, home, forest)
.Value(energy.Create(player), 2.0f)
.Goal(at.Create(player, forest));
SolveResult result = planner.Solve(state);
foreach (PlanStep step in result.Steps) {
if (step.Is(move)) {
Character who = step.Arg<Character>("who");
Location to = step.Arg<Location>("to");
}
}
}For background solves in .NET or Unity tools, use the async wrapper:
SolveResult result = await planner.SolveAsync(state, cancellationToken);Cancellation is cooperative around the native solve call; it can cancel before
the solve starts or after it returns, but it cannot interrupt the current native
tp_solver_solve call mid-execution.
Requirements:
- CMake 3.20+
- C++20 compiler
- .NET SDK for C# wrapper validation
- Jai compiler for the Jai module and runnable snippets
x86_64-w64-mingw32-g++only when cross-building Windows artifacts on Linux- Unreal Engine 5.6+ to consume the packaged Unreal source plugin
Build distribution artifacts:
sh ./build.sh -release -target unity cpp sharp jai unreal -o ./distOn Windows, use the PowerShell equivalent:
pwsh ./build.ps1 -release -os windows -target unity cpp sharp jai unreal -o ./distUseful variants:
sh ./build.sh -target unity cpp -os linux windows -o ./dist
sh ./build.sh -debug -target cpp sharp -o ./dist -no-clean
sh ./build.sh -target unreal cpp -os linux -o ./distpwsh ./build.ps1 -target unity cpp -os windows -o ./dist
pwsh ./build.ps1 -debug -target cpp sharp -o ./dist -no-clean
pwsh ./build.ps1 -target unreal cpp -os windows -o ./distBuild native library and tests manually:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failureRun the C# smoke test:
dotnet run --project csharp/TensorPlanner.Smoke/TensorPlanner.Smoke.csproj -c Releaseinclude/ C API and C++ fluent wrapper headers
src/ Native planner implementation
tests/ Native C++ smoke tests
csharp/TensorPlanner/ .NET project linking the Unity-first C# wrapper
csharp/TensorPlanner.Smoke/ C# smoke test
dev.nick.tensor-planner/ Unity package source and runnable scripts
modules/Tensor_Planner/ Jai generated binding workflow and wrapper
unreal/TensorPlanner/ Unreal Engine source plugin template and wrapper API
build.sh POSIX distribution build script
build.ps1 PowerShell distribution build script for Windows
| Binding | Best for | Entry point |
|---|---|---|
| C API | Engines, tools, other language bindings | include/tensor_planner.h |
| C++ fluent API | Typed C++ gameplay/simulation code | include/tensor_planner.hpp |
| C# / Unity | Unity gameplay code and .NET tools | dev.nick.tensor-planner/Runtime/TensorPlanner.cs |
| Jai | Jai modules and compile-time typed domains | modules/Tensor_Planner/module.jai |
| Unreal Engine | UE 5 source plugin with soft object references | unreal/TensorPlanner/Source/TensorPlanner/Public/TensorPlannerUnreal.h |
- Define a domain: object types, predicates/functions, and action schemas.
- Build a state: real objects, true facts, numeric values, and goals.
- The planner grounds candidate actions from available objects and action schemas, filters impossible actions, then searches for a sequence that makes all goals true.
- Optional scorer callbacks can rank candidates using exported tensors.
- The result contains ordered plan steps and action arguments.
Read the full explanation in How the Planner Works.
Native API callers must dispose output structs and destroy handles explicitly:
tp_schema_tensors_disposetp_problem_tensors_disposetp_action_graph_disposetp_candidate_action_list_disposetp_solve_result_disposetp_solver_destroytp_state_destroytp_domain_destroy
The C++ and C# wrappers manage native handles, but object references in plan steps still point back to application objects. Keep those objects and the state alive while reading results.
- Search is deterministic for a fixed domain, state, limits, and scorer output.
- Candidate grounding is bounded by
TP_Limits::max_candidates. - Action arity and predicate arity are bounded by
TP_MAX_PARAMSandTP_MAX_ARITYin the C API. - The public package intentionally exposes the guided planner path only.