Runtime Carving

Poseidon was originally created to be an editor-time tool for designers to be able to quickly create levels and be able to modify them dynamically. Due to significant requests, we have rewritten our core algorithm to support runtime support but are working on improving how this works for the future.

In the Editor, we do a lot of detection to know if any carver needs to be recomputed (the object was moved, the underlying mesh was changed, some settings on the object changed, etc.), but at runtime we do not yet want to be in control of managing when objects should change. At this point in the process, since we have exposed that logic to you, you are responsible for deciding when Poseidons should carve – we just handle the carve operation.

We are planning on eventually switching our system to the Unity Job System, but currently Poseidon is a Frame Divided operation. We are crunching tons of numbers and comparing numerous triangles, but we only do a certain number of operations per frame until we’ve hit our time budget. So in V1 of our Runtime Support, we’re running on the Main Unity Thread, but are running on a budget that you decide.

Creating Runtime Carves

The key to working in the runtime is being able to understand the PoseidonOperation object. It represents the current state of a long-running operation that may take a while. You are responsible for telling it when it will run, and it will tell you when it is done carving.

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: allPoseidon 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 each 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.

Strategy (PoseidonMode)

DEFAULT: PoseidonMode.Simple

This PoseidonMode/Strategy allows you to specific whether or not you are using some of the more advanced features. Currently, the options available are:

  • PoseidonMode.Simple -> Just a normal, everyday carve.
  • PoseidonMode.Layered -> Use the Layers feature we added to allow you to keep multi-layered Poseidon islands.

In the future, this may support a Hierarchical mode, where the way that objects are configured in the Unity hierarchy makes a difference to how they carve one another, but that isn’t built yet.

If you want to use the Layered mode features, this is where you enable it for Runtime.

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) => affectedPoseidons.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.

The filter property is something we’re hoping to one day get rid of, by providing a Runtime “Change Watcher” that looks for major changes at Runtime, similar to how we’re currently doing it in the Editor. At the moment, however, you need to just understand that this is a predicate that runs for each Poseidon in allPoseidons, and you decide if it requires a full recarve or not. By default, everything recarves, which is likely wasteful for your specific use-case.

Running the Operation

At the moment, in this current version of Runtime Carving, you are required to manually “run” the operation. It is asynchrous, 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 three things you can access:

  • bool Finished { get; }
    • Allows you to see if an operation is done.
  • 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 false. The return value here can be ignored if you’re also checking Finished.

There’s no definitely “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
  ...
}

// Or if you like, a simpler coroutine that does basically the same thing.
private IEnumerator RunCarveOperationSimple(PoseidonOperation operation)
{
  // RunFrame returns true if we're done. If it returns false, keep running.
  while(!operation.RunFrame())
    yield return null;

  // Post Operation logic here.
  yield break;
}

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.PostiveInfinity,
  LogResults = true,
});

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?

It has been a stated goal of ours since the beginning that “Using Poseidon in your game should not make your performance worse”, so we have intentionally shied away from doing ANYTHING while the game is running. The current implementation of things you see on this page are a result of that way of thinking, where we do nothing unless you EXPLICITLY tell us what to carve.

At runtime, every game has incredibly different assumptions about how it works and why it works, so we have been very cautious to avoid making any assumptions, and this has led to a somewhat toned down version of Poseidon for Runtime.

Part of what we believe makes Poseidon so magical at Editor Time is the ease with which things happen automatically. You move a gameobject and everything recarves around it. This simple experience is missing current from Runtime, since we ask you, the user to currently “keep track of what was carving what, and when things need to be recarved.”

We are thinking about a better system where you can opt-into using a Poseidon Runtime Registry, that keeps track of all Poseidons in the scene and sends messages to you when things require recarving, to let you fire off operations “asynchronously” without you needing to tell us exactly what changed and which Poseidons need to be recarved. This is something we’re currently planning and is on our roadmap, so once that becomes implemented it will still need to be an “opt-in” system so that we don’t slow down any user performance at runtime, unless an explicit action is triggered.

After that, we’re working on Job System, which will greatly speed up the rate that these operations are carving at.