Runtime Carving

Poseidon was originally created to be an editor-time tool for designers to quickly create levels and modify them dynamically. Due to significant requests, we rewrote our core algorithm years ago to support carving at runtime. As of 3.0, runtime carving has grown up quite a bit.

One of our founding principles has always been: “Using Poseidon in your game should not make your performance worse.” So everything on this page is strictly opt-in. Poseidon does nothing at runtime, not a single tick, unless you explicitly turn it on.

There are now two ways to carve at runtime:

  1. The Runtime Service (new in 3.0, recommended): you tell Poseidon what changed, and it handles the queueing, neighbor tracking, and recarving for you. There’s even a drop-on component (PoseidonRuntimeWatcher) that detects changes automatically.
  2. Manual Operations (the original API): you build a PoseidonOperation yourself and pump it every frame. Maximum control, maximum responsibility.

Either way, carving is a frame-divided operation. We’re crunching tons of numbers and comparing numerous triangles, but we only do a certain amount of work per frame until we’ve hit a time budget that you decide. (Moving this onto the Unity Job System is still on our roadmap.)

The Runtime Service

Step 0: Enabling the Runtime Loop

The service needs a tick to run on, and remember: we refuse to cost you anything unless you opt in. So the Poseidon runtime loop must be installed into Unity’s Player Loop, in one of three ways:

  1. The config asset: Poseidon ships with a PoseidonRuntimeConfig asset in its Resources folder, with the runtime loop turned off. Flip Enable Runtime Loop on and you’re done.
  2. From code: call PoseidonRuntimeLoop.Install() from your own initialization code, if you prefer explicit control.
  3. The lazy way: add POSEIDON_RUNTIME_AUTO_INSTALL to your Scripting Define Symbols and skip all the config stuff.

The loop uninstalls itself automatically when play mode exits, and it’s fully robust to “Enter Play Mode without Domain Reload”.

If you queue work while the loop isn’t installed, Poseidon will warn you in the console rather than silently doing nothing.

The Easy Way: PoseidonRuntimeWatcher

Add the Runtime Watcher component (Poseidon → Runtime Watcher) next to any Poseidon, and it watches for changes and queues recarves for you. Three independent trigger sources, each toggleable:

  • Transform watching (on by default): samples the object’s transform and triggers a recarve when it moves, rotates, or scales. You can tune:
    • Sample Rate: only check every N frames (with a randomized phase so a hundred watchers don’t all fire on the same frame).
    • Matrix Tolerance: how much change counts as “moved”, so floating-point drift doesn’t cause recarves.
  • Recarve On Enable (off by default): recarve when the object is enabled. By default the very first enable (scene load / spawn) is skipped; disable Skip Initial Enable if you want it.
  • Recarve On Disable (off by default): when the object is disabled or destroyed, tell its former neighbors to recarve so the hole it was carving heals shut. It’s opt-in because games often toggle huge chunks of a level for occlusion/streaming reasons, and you really don’t want that recarving the world.

The watcher also lets you pick between a blocking recarve (everything finishes on one frame) or a budgeted one (Frame Time milliseconds per frame), and exposes an On Carve Completed UnityEvent that fires when a carve that touched this Poseidon finishes, including when it was recarved because a neighbor changed.

For most games, watchers on your movable Poseidons plus the config asset is the entire integration. Check out Example Scene 8 (Runtime Auto Recarving) to see it in action.

The Service From Code

Under the watcher sits PoseidonRuntimeService, a static API you can drive directly. The core idea: multiple Poseidons might change on the same frame, so reporting a change and asking for a carve are separate calls.

// Something moved. Tell the service about it (cheap, call as often as you like):
PoseidonRuntimeService.MarkDirty(poseidon, UpdateReason.TransformChanged);

// ...and whenever you're ready, queue the carve:
var handle = PoseidonRuntimeService.QueueAsyncRecarve(new CarveParameters
{
    FrameTime = 10, // ms of carve work per frame
});

Some nice properties of this system:

  • Dirty objects bring their neighbors. A dirtied Poseidon is recarved along with both its former and current neighbors, so when an object moves away, the hole it left behind heals, and the place it arrived at gets carved. You no longer have to track “who was touching whom”.
  • Requests merge. Only one operation runs at a time. If you queue five recarves before the next one starts, they collapse into a single operation (a blocking request wins; otherwise the smallest frame budget is kept).
  • QueueBlockingRecarve() completes the whole carve in a single frame, which is handy for loading screens or “the level just got generated” moments.

Carve Handles

Every queue call returns a CarveHandle, a tiny, allocation-free struct that answers “has the operation that consumed my request run yet?”

var handle = PoseidonRuntimeService.QueueAsyncRecarve(parameters);

// Poll it:
if (handle.IsDone) { /* the world reflects your changes */ }

// Or promise-style (fires immediately if already done, so there's no race):
handle.WhenCompleted(h => Debug.Log("Carved!"));

// Or in a coroutine:
yield return handle.Wait();

handle.Progress gives you 0 while pending, the operation’s weighted progress while running, and 1 when done.

Service Events and Status

PoseidonRuntimeService.CarveStarted   += () => ShowSpinner();
PoseidonRuntimeService.CarveCompleted += result =>
{
    // result.Succeeded, result.Duration, result.FrameCount, result.ItemCount
    HideSpinner();
};

There’s also PoseidonRuntimeService.IsCarving and PoseidonRuntimeService.Progress (0..1) if you’d rather poll for a progress bar. Both events are cleared automatically at the start of each play session, so you can subscribe from anywhere without leaking across sessions.

Telling “why” apart

MarkDirty takes an UpdateReason, and that reason is delivered to the Poseidon’s OnCarveFinished callback when the carve completes. Poseidons that were swept in as neighbors carry only UpdateReason.DirtyNeighbor, so your callback can tell “I carved because I was asked to” apart from “I was collateral of a neighbor’s change.”

Manual Operations

The original v1 runtime API is still fully supported, and it’s the right tool when you want total control. For example: carving a procedurally generated level exactly once, on your schedule, with your own progress UI. The key type is the PoseidonOperation: it represents a long-running carve, you decide when it runs, and it tells you when it’s done.

The basic gist of it is:

  1. You find which Poseidons you would like to carve.
  2. You call PoseidonRuntime.GetCarveOperation to get an operation.
  3. You then run the operation every frame until it is finished.
  4. You stop running the operation when it tells you it is done.

Getting a Runtime Carve Operation

PoseidonRuntime.GetCarveOperation(IEnumerable<PoseidonBase> allPoseidons,
	CarveParameters parameters) => PoseidonOperation

GetCarveOperation is the main function you need to call in order to generate a PoseidonOperation which will contain all the data for the carve. You’re then responsible for calling PoseidonOperation.RunFrame(), which will run one frame tick of the carve operation for however long was set up in the CarveParameters.

Our goal with creating this function in this way is to not force a specific way of working with the tool. The first parameter is important: allPoseidons means literally ALL Poseidons that you want included. For instance, if you’re adding a new Poseidon to the scene at runtime, you’ll still need to pass in the full list of every poseidon you care about… otherwise, those other Poseidons may not be included in the carve operation.

What if I am only trying to carve a subset of my poseidons? Should I pass in only a subset of my poseidons to allPoseidons? Let’s say that you have a scene with 100 Poseidon objects and you’re trying to only carve 3 of them together and you don’t care about 100 other carvers in your scene. The correct approach here would be to use the Filter parameter below, and to still pass all 100 objects into allPoseidons. TECHNICALLY…, however, allPoseidons can contain only the 3 poseidons you care about… which will mean that your carve will be much faster… but be forewarned, that in those cases, your Poseidons are basically isolated in their own world and will only intersect among themselves. Using the Filter parameter below is a much better way of reducing the size of your carves.

    void SomeMethodToKickOffTheCarve() 
    {
      var operation = PoseidonRuntime.GetCarveOperation(poseidons, new CarveParameters
			{
				FrameTime = 30, // We give each frame 30ms to compute itself
				LogResults = true, // Lets log at the end to see how long the operation took
			});

      // Let's save this variable off somewhere and then use it somewhere else.
    }
		
    // Now I have an operation I can use to run stuff on each frame (let's say I save it off):       
    void Update() 
    {
      if(operation != null && !operation.Finished)
      {
        operation.RunFrame();
      }  
    }

In the rather crude example above you can clearly see how the operation is being created and then being invoked on an Update loop in Unity. It is imperative that you fine-tune the CarveParameters to your particular carve to get the effect you’re looking for.

Carve Parameters

Instead of creating an API that changes constantly, we’ve decided to boil it down to a parameter class that contains most of the commonly used properties in Poseidon carves. The parameters are as follows:

FrameTime (float)

DEFAULT: float.PositiveInfinity

How much time (in milliseconds) should be devoted to the carve operation? Since these can take a decently long amount of time, we split the operation to only do a particular amount per frame. If you want to run the entire carve operation at once, feel free to leave this blank or pass in float.PositiveInfinity.

We recommend something like 15ms-20ms per frame, but if you’re running this on a loading screen or somewhere where frame rate doesn’t matter, you can pass a bigger number to the operation. You can even get the runtime system to compute the entire operation in one frame if you pass float.PositiveInfinity as the frame budget.

Basically, if you pass in a small number, it will take slightly longer to compute, but it will not eat up any of your game’s performance during the operation. If you pass in a bigger number, the game will run a bit slower while it is carving, but it will take less overall time to compute.

LogResults (bool)

DEFAULT: false Whether or not to log information about how many Poseidons were carved and how long the entire operation took.

AssembleTiming (AssemblePhaseTiming)

DEFAULT AssemblePhaseTiming.ImmediatelyAfterEachCarve

Let’s say you’re carving 50 objects together for the first time. If you’re using AssemblePhaseTiming.AllTogetherAtEndOfProcess, it will calculate the final output of all 50 objects, then, at the end, it will assemble all of the Poseidons and turn them from our internal representations of meshes into the final products all at once. You’ll basically “blink” and suddenly everything will be done after crunching through the numbers.

If you’re using AssemblePhaseTiming.ImmediatelyAfterEachCarve (default), you’ll basically see the items “pop” into completion one-by-one. As soon as we’re done carving an item, we’ll “assemble” it and turn it into a proper Unity mesh, then move on to the next item.

EnableCleanupStages (bool)

DEFAULT: false

Whether the post-carve cleanup passes (Mesh Cleanup) are allowed to run as part of this operation. At runtime you almost always want a faster carve more than a prettier wireframe, so cleanup is skipped by default, even for Poseidons that have Mesh Cleanup enabled on them. Turn this on only if you’re doing something like exporting meshes, where clean geometry genuinely matters, and budget for the extra time.

Filter (Func<PoseidonBase, bool>)

DEFAULT: (p) => true

This is arguably one of the trickier, but yet more important parameters to pass in.

By default, it says: “for each Poseidon, return true”, which basically means: go through every single Poseidon in the allPoseidons list, and yes, I want to carve it. If you are trying to recarve your entire set of Poseidons in your scene, then you can leave this untouched.

Use Case:

Imagine a world where you are keeping track of all of the Poseidons in your scene. The user executes some command to move one object over, which affects these two carvers. (Let’s say, the user moves a Window to the other side of the wall, the two affected carvers would be the Window and the Room The Window Was On).


// Let's pretend we have these two carvers saved off as variables

List<PoseidonBase> affectedCarvers = new List<PoseidonBase> { window, roomTheWindowWasOn };

var operation = PoseidonRuntime.GetCarveOperation(allPoseidonsInTheScene, new CarveParameters
{
  Filter = (p) => affectedCarvers.Any(affected => affected == p);
});

Now, even though there may be other Poseidons in the scene, the algorithm will only recompute the meshes for window and roomTheWindowWasOn, but will also recarve anyone who was touching them in the scene. Since only those objects technically “changed”, we can tell Poseidon “only these two things changed, so please, recarve them and anyone they are now affecting”.

This means that, if the room was previously also touching a hallway, that carve will still occur; we run our Poseidon operation as an isolated operation and don’t presume to know anything other than what you pass into us - this is why it is important to pass in ALL poseidons for the allPoseidons parameter, and then filter it down using the Filter.

(If all of this “keep track of what changed yourself” business sounds tedious, that’s exactly why the Runtime Service exists now. MarkDirty + QueueAsyncRecarve does all of this for you.)

Running the Operation

With manual operations, you are required to “run” the operation yourself. It is asynchronous, but not truly async, because it requires access to certain Unity functions that must be accessed on the Main thread - for this reason, you need to “run” this every frame, but we’ve encapsulated the operation in a simple wrapper, this PoseidonOperation.

It contains a few things you can access:

  • bool Finished { get; }
    • Allows you to see if an operation is done (whether it succeeded or failed).
  • OperationStatus Status { get; }
    • None, Running, Succeeded, or Failed. If an exception occurs mid-carve, the operation logs it, stops at the first error, and reports Failed rather than throwing at you every frame.
  • float Progress { get; }
    • Returns a 0.0f - 1.0f estimated progress for the operation.
  • bool RunFrame()
    • Runs the next bit of the operation if there’s any left, and returns whether or not the operation is finished. If the operation is already done, it will no-op and return true. The return value here can be ignored if you’re also checking Finished.

There’s no definitively “correct” way for how you should run your Carve Operation - but we personally like two methods that we’ve used for our own development, running it on Update or as part of a Unity Coroutine.

The Update Approach

private PoseidonOperation operation; // This is set by some other method
public Image progressMeter;

void Update() 
{
  // If we have a running operation, and it's not done,
  // let's run a bit of it every frame.
  if (operation != null && !operation.Finished)
  {
    operation.RunFrame();
    // Do something with the progress, if you'd like, such as showing a progress bar: 
    progressMeter.fillAmount = operation.Progress;
    return;
  }

  // Do other stuff in your update when the operation is 
  // done or not running or has never run. 
  ...
}

The Coroutine Approach

If you want something that is a bit more “async” you can try to use Unity’s coroutines.


private void StartCarving() 
{
  var operation = PoseidonRuntime.GetCarveOperation(poseidons, new CarveParameters
  { 
    FrameTime = 20  
  });

  StartCoroutine(RunCarveOperation(operation));
}


private IEnumerator RunCarveOperation(PoseidonOperation operation)
{
  while(!operation.Finished)
  {
    operation.RunFrame();
    progressMeterImage.fillAmount = operation.Progress;
    yield return null;
  }

  // When the operation is done, let's do some post operation logic here
  ...
}

An Easier Way: CarveEverything

If all this seems too complicated and you just wanna carve everything all at once you can just run:

PoseidonRuntime.CarveEverything(allPoseidons);

This… immediately carves all passed in Poseidons against one another, all on the same runtime frame. This WILL block the main thread. It’s the equivalent of doing:

var operation = PoseidonRuntime.GetCarveOperation(allPoseidons, new CarveParameters 
{
  FrameTime = float.PositiveInfinity,
});

operation.RunFrame();

We don’t necessarily recommend this approach, but if you’re just prototyping something and need a quick way of doing it, this might come in handy for quickly telling everything to carve and blocking until it finishes carving everything.

What’s Next?

For years, this page ended with us promising a “Runtime Change Watcher” that would keep track of what needs recarving so you didn’t have to. That’s shipped: it’s the Runtime Service and PoseidonRuntimeWatcher above, and it’s opt-in exactly the way we promised it would be.

The next big item on the runtime roadmap is moving the carving math onto the Unity Job System, which should greatly speed up how fast operations carve. Same APIs, faster crunching.