Skip to content

CSharp and Unity

nick_tsaizer edited this page Apr 25, 2026 · 6 revisions

Tensor Planner includes a Unity-first C# wrapper over the native C ABI.

Main source file:

dev.nick.tensor-planner/Runtime/TensorPlanner.cs

The .NET project links the same file:

csharp/TensorPlanner/TensorPlanner.csproj

Package identity

Unity package:

{
  "name": "dev.nick.tensor-planner",
  "displayName": "Tensor Planner",
  "version": "0.1.0",
  "unity": "2019.4"
}

Minimal C# planning task

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");
        }
    }
}

API shape

Main types:

  • Planner
  • Limits
  • Predicate
  • NumericFunction
  • PlannerAction
  • ActionBuilder
  • StateBuilder
  • SolveResult
  • PlanStep

Common methods:

Predicate at = planner.Predicate<Character, Location>("at");
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(energy.Create("who").GreaterOrEqual(1.0f))
    .Adds(at.Create("who", "to"))
    .Decreases(energy.Create("who"), 1.0f)
    .Commit();

StateBuilder state = planner.State()
    .Object(player)
    .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);
SolveResult asyncResult = await planner.SolveAsync(state, cancellationToken);

Predicate.Create(...) is the preferred factory. Predicate.Call(...) remains as a compatibility alias.

Use StateBuilder.Edge(predicate, a, b) for undirected binary relations. It adds both directed facts:

state.Edge(connected, home, forest);
// equivalent to:
state.Fact(connected.Create(home, forest));
state.Fact(connected.Create(forest, home));

Use Fact(...) directly for one-way relations.

Numeric functions use NumericFunction and NumericTerm objects:

NumericFunction energy = planner.Function<Character>("energy");

planner.Action("act")
    .Param<Character>("who")
    .Require(energy.Create("who").GreaterOrEqual(1.0f))
    .Decreases(energy.Create("who"), 1.0f)
    .Commit();

StateBuilder state = planner.State()
    .Object(player)
    .Value(energy.Create(player), 2.0f);

Supported comparisons are LessThan, LessOrEqual, EqualTo, GreaterOrEqual, and GreaterThan. Numeric effects use Sets, Increases, and Decreases.

Async solving

The wrapper exposes async solve methods for tools and Unity code that should not block the caller while the native solver runs:

SolveResult result = await planner.SolveAsync(state);
SolveResult resultWithCancellation = await planner.SolveAsync(state, cancellationToken);

SolveAsync queues the existing synchronous native solve work to the .NET ThreadPool. It does not change the native planner algorithm.

Cancellation is cooperative:

  • a pre-canceled token cancels before work starts,
  • cancellation is checked before entering the native solve and after it returns,
  • cancellation cannot interrupt an in-progress tp_solver_solve call because the C API currently has no native cancellation hook.

Only one solve operation may run on a Planner at a time. Starting another Solve or SolveAsync while one is active throws InvalidOperationException. Disposing a planner while a solve is active also throws InvalidOperationException.

Unity note: do not access UnityEngine APIs from work running on the background thread. Read the result after await on the Unity main thread before touching scene objects, components, transforms, or assets.

Unity usage

In Unity projects, gameplay code produces current facts, Tensor Planner finds a valid action sequence, and Unity code executes that sequence on the main thread.

Native library placement

The wrapper uses:

DllImport("tensor_planner")

For Unity, native plugins should be placed under:

dev.nick.tensor-planner/Runtime/Plugins/x86_64/

Linux:

Runtime/Plugins/x86_64/libtensor_planner.so

Windows:

Runtime/Plugins/x86_64/tensor_planner.dll

The build.sh and build.ps1 scripts stage these automatically for selected target OS values.

Build C# wrapper

dotnet build csharp/TensorPlanner/TensorPlanner.csproj -c Release

Run smoke test:

dotnet run --project csharp/TensorPlanner.Smoke/TensorPlanner.Smoke.csproj -c Release

Build Unity package staging:

sh ./build.sh -release -target unity -os linux -o ./dist
pwsh ./build.ps1 -release -target unity -os windows -o ./dist

Output:

dist/unity/dev.nick.tensor-planner/

Lifetime rules

  • Planner implements IDisposable; use using or call Dispose().
  • StateBuilder keeps managed references to registered objects.
  • Keep the state/result alive while reading object references from plan steps.
  • Do not unload or move the native plugin while planner objects exist.

Clone this wiki locally