Circuit Artist DEV Blog

Introducing Custom Levels

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

Discover more from Circuit Artist DEV Blog

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

Continue reading