
Hello, Circuit Artist now supports custom levels as first-class citizens.
How it Works
Players can now load Lua scripts directly from local files and run them as levels. This way they can use their favorite code editor to edit/organize their scripts and load them directly in the game. Scripts can be reloaded by pressing F5.
Script Structure
Scripts are written in Lua 5.2 and should define 4 functions:
- _Setup() – Called when the script is loaded; should define the ports/pins
- _Start() – Called when the simulation starts; should initiate simulation state and can define some simulation parameters
- _Update() – Main logic for the level. Can read and write to pins/wires, and is called whenever the circuit stops or every N ticks (configurable)
- _Draw() – Called every rendering frame; can read ports/wires and can draw on top of the circuit screen
Example 1: Simple Reset + Clock
--[[
A level similar to original sandbox level, but:
- Clock updates at a fixed rate of 30 ticks instead of waiting the circuit stops.
- It pauses the game after 10 clock cycles.
- It runs the simulation at a slighter slower pace (base Ticks Per Second is
set to 100, while default is 240)
]]
function _Setup()
-- Port Out = Output of the script = entering the image.
-- The call to AddPortOut returns the ID of the port, used later to write or
-- read, depending on the type of port.
RST = AddPortOut(1, 'rst')
CLK = AddPortOut(1, 'clk')
end
function _Start()
cycle = 0
-- If SetUpdateInterval is set to 0, it will only call update when circuit
-- stops updating. If a number >0 is passed here, it is called every N ticks.
SetUpdateInterval(30)
-- Sets ticks per second slight slower just to show it's possible to change
-- speed.
-- In practice one might want to try higher TPS for very fast circuits.
SetBaseTPS(100)
end
local function UpdateReset(cycle)
if cycle == 0 then
-- Writes to the RST port
WritePort(RST, 1)
end
-- It stays up for 2 clock rising edges
if cycle == 4 then
WritePort(RST, 0)
end
end
local function UpdateClock(cycle)
-- rising edges happen on 2C+1 -> 2C+2
WritePort(CLK, cycle % 2)
end
function _Update()
UpdateReset(cycle)
UpdateClock(cycle)
if cycle == 20 then
-- Pauses the simulation
Pause()
end
cycle = cycle + 1
end
function _Draw()
-- Doesn't draw anything, but the function must be defined.
end
Example 2 : A = B + 1
--[[
Example where it reads port B and writes its value +1 in port A.
This example shows how to read from a port and how to draw stuff.
Color is represented by {red,green,blue,alpha} list, with each component
ranging from 0 to 255.
]]
function _Setup()
PORT_A = AddPortOut(7, 'a')
PORT_B = AddPortIn(7, 'b')
end
function _Start()
-- print's will go to terminal's output.
-- On windows, you can run the game with "console" launch option
print('started')
end
function _Update()
local n = ReadPort(PORT_B)
WritePort(PORT_A, (n + 1) % 128)
end
-- Draws a simple square on screen with scaling, with the text "hello"
function _Draw()
local white = {255, 255, 255, 255};
rlPushMatrix()
rlScalef(3,3,1)
DrawRectangle(0,0,200,500,{255,255,255,150})
DrawText("Hello!", 0, 0, {0,0,0,255})
rlPopMatrix()
end
What you can do with it
The idea is to allow players to extend their creations beyond images: they can now create external memory/components, inject “instructions” for CPU projects, create displays, debug/display internal state, test components, etc.
This feature opens up many possibilities:
- Create external memory or custom components
- Inject instructions for CPU projects
- Build custom displays or visualizations
- Debug and display internal circuit state
- Automate testing for your components
- Develop bigger projects part by part (test individual components before integrating them)
Conclusion
Now players can plan / create much bigger projects. The API is still very simple, the idea is to keep adding more things from raylib for the display and more ways to interact with the circuit as needed. For very very big projects where lua call might become bottleneck, there’s also a possibility to create a level in C using a similar api (but it requires compiling the game).
Differently from campaign levels, local custom levels are not sandboxed, so scripts can read/write to other files and use regular lua libraries.
For simplicity, the time travel is deactivated for custom levels for now. We need to stabilise dedicated data structures in order to make the time travel work properly (campaign levels are using it but they’re still very basic and inefficient).
We’re excited to see what you build! Share your creations and feedback on Discord.

Leave a Reply