Circuit Artist DEV Blog

Rewind Support in Custom Levels

Custom levels now support rewind! It requires a slightly different structure for the level code.

To enable rewind support:

  • The script must call the new EnableRewind() function inside _Setup(). Once called, the engine will run the script with rewind support enabled.
  • The _Update() function should now return a patch object rather than modifying script variables directly.
  • You’ll need to implement two additional functions: _Forward(patch) and _Backward(patch), which are responsible for modifying the script variables.

The idea is that, instead of mutating level variables directly inside _Update(), you return a “patch” object from it. This object is then passed to _Forward() during normal playback, or to _Backward() during rewind, to apply the actual changes.


Example 1: The Basics

For example, let’s say you have a simple cycle counter in Lua. Without rewind, you’d implement it like this:

function _Setup()
CYCLE_PORT = AddPortOut(5, 'cycle')
end
function _Start()
cycle = 0
end
function _Update()
WritePort(CYCLE_PORT, cycle)
cycle = cycle + 1
end
function _Draw() end

With rewind enabled, the code would look something like this:

function _Setup()
CYCLE_PORT = AddPortOut(5, 'cycle')
EnableRewind()
end
function _Start()
cycle = 0
end
function _Update()
WritePort(CYCLE_PORT, cycle)
-- Needs to return something, otherwise the
-- _Forward() won't be called.
return {}
end
function _Forward(patch)
cycle = cycle + 1
end
function _Backward(patch)
cycle = cycle - 1
end
function _Draw() end

Example 2: Variable Counter

Now let’s say you have a second counter that increments a value based on an output from the circuit. For this, you’ll need to pass information from _Update() to _Forward(), which you can do via the patch object:

function _Setup()
CYCLE_PORT = AddPortOut(5, 'cycle')
COUNTER_PORT = AddPortOut(10, 'counter')
INC_PORT = AddPortIn(5, 'inc')
EnableRewind()
end
function _Start()
cycle = 0
counter = 0
end
function _Update()
WritePort(CYCLE_PORT, cycle)
WritePort(COUNTER_PORT, counter)
local inc = ReadPort(INC_PORT)
return {inc=inc}
end
function _Forward(patch)
cycle = cycle + 1
counter = counter + patch.inc
end
function _Backward(patch)
cycle = cycle - 1
counter = counter - patch.inc
end
function _Draw() end

Counter example running


How it Works

Under the hood, the engine stores the patch objects and calls _Forward() when time moves forward, and _Backward() when rewinding. Patch objects are not stored in Lua — they are serialized by value using msgpack. This means patch objects should be kept small, otherwise memory issues may arise. It also means Lua references are not supported: any value placed in a patch object will be copied by value, not by reference.

For things like memory emulation, you’ll want to pass only the values that actually change, rather than dumping the entire memory state into the patch — for example, a triple (address, valueBefore, valueAfter).

Conclusion

Players can now write levels with rewind support, which was not available in the previous release. A simple commented example has also been added to the examples/ folder of the game.

It requires a slightly different code structure, but this can be made easier with some dedicated helper data structures.

Leave a Reply

Discover more from Circuit Artist DEV Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading