Guides /Coming from Roblox

Guides

Coming from Roblox

For Roblox and Luau developers. How RunService, UserInputService and Instances translate to Codetoy's draw-every-frame model.

Roblox and Codetoy both give you a frame loop, input events and a scripting-first workflow. The big difference is rendering: Roblox renders the Instances you create, and Codetoy has no Instances at all. You paint the screen yourself, every frame.

Think of Codetoy as a single LocalScript that owns the whole game and draws onto a 2D canvas. It's a sketchbook for mechanics, math and ideas, with nothing to install and no Studio to open.

Which language?

If you write typed Luau, you'll find AssemblyScript comfortable: it's TypeScript-style syntax with real types, and it's the language that can publish your game as a Discord Activity. Every example here also works in C# and Python. Pick in the sidebar.

#The mental model

Roblox Codetoy
Workspace, Part, Model Nothing persists. You draw shapes each frame.
Physics engine Movement and collisions are your code
Studs, 3D camera Pixels, 2D, origin top-left, Y down
Many Scripts and LocalScripts One file
IntValue, attributes Plain variables
Services (RunService, UserInputService) export function update()Screen.OnUpdate()def update(), input.isKeyDown()Input.IsKeyDown()isKeyDown(), export function onKeyDown()Input.OnKeyDown()def keyDown() …

#The game loop

Roblox (Luau)
local RunService = game:GetService("RunService")
local x, speed = 0, 150

RunService.RenderStepped:Connect(function(dt)
    x += speed * dt
end)
Codetoy
let x: f64 = 0
const speed: f64 = 150

export function update(): void {
  x += speed * deltaTime()
  circle(x, screen.centerY(), 20)
}
double x = 0;
double speed = 150;

Screen.OnUpdate(() => {
    x += speed * Screen.DeltaTime;
    Canvas.Circle(x, Screen.CenterY, 20);
});
x = 0.0
speed = 150.0

def update():
    global x
    x += speed * deltaTime()
    circle(x, centerY(), 20)

export function update()Screen.OnUpdate()def update() is your RenderStepped connection and deltaTime()Screen.DeltaTimedeltaTime() is dt. Logic and drawing happen in the same place.

#Parts vs. drawing

Roblox (Luau)
local part = Instance.new("Part")
part.Position = Vector3.new(0, 5, 0)
part.Parent = workspace
Codetoy
export function update(): void {
  fill(30, 30, 30)
  rect(0, 0, screen.width(), screen.height())   // clear the last frame
  fill(160, 160, 170)
  rect(100, 100, 50, 50)                         // "the part", drawn this frame
}
Screen.OnUpdate(() => {
    Canvas.Fill(30, 30, 30);
    Canvas.Rect(0, 0, Screen.Width, Screen.Height);   // clear the last frame
    Canvas.Fill(160, 160, 170);
    Canvas.Rect(100, 100, 50, 50);                    // "the part", drawn this frame
});
def update():
    fill(30, 30, 30)
    rect(0, 0, width(), height())   # clear the last frame
    fill(160, 160, 170)
    rect(100, 100, 50, 50)          # "the part", drawn this frame

Clear the screen yourself

Roblox redraws the world every frame for you; Codetoy doesn't. If you don't paint a background (or call reset()Canvas.Reset()reset()), each frame draws on top of the last. That's great for trails, and confusing the first time.

#Cheat sheet

Roblox Codetoy
RunService.RenderStepped / Heartbeat export function update()Screen.OnUpdate()def update()
dt deltaTime()Screen.DeltaTimedeltaTime()
os.clock() / time() elapsedTime()not in C#elapsedTime()
UserInputService:IsKeyDown(Enum.KeyCode.W) input.isKeyDown()Input.IsKeyDown()isKeyDown() with "w"
InputBegan (keyboard) export function onKeyDown()Input.OnKeyDown()def keyDown()
UserInputService:GetMouseLocation() input.mouseX()Input.MouseXmouseX(), input.mouseY()Input.MouseYmouseY()
MouseButton1Down export function onMouseDown()Input.OnMouseDown()def mouseDown() with button 0
TouchStarted / TouchEnded export function onTouchStart()Input.OnTouchStart()def touchStart() / export function onTouchEnd()Input.OnTouchEnd()def touchEnd()
print / warn console.log()Console.Log()print() / console.warn()Console.Warn()not in Python
math.sin, math.random Math.sin()Math.Sin()math.sin(), Math.random()Random.Shared.NextDouble()random.random()
workspace.CurrentCamera.ViewportSize screen.width()Screen.Widthwidth(), screen.height()Screen.Heightheight()

#WASD movement

Roblox (Luau)
local UIS = game:GetService("UserInputService")
RunService.RenderStepped:Connect(function(dt)
    if UIS:IsKeyDown(Enum.KeyCode.W) then y -= speed * dt end
end)
Codetoy
let x: f64 = 200, y: f64 = 200
const speed: f64 = 300

export function update(): void {
  const step = speed * deltaTime()
  if (input.isKeyDown("w")) y -= step
  if (input.isKeyDown("s")) y += step
  if (input.isKeyDown("a")) x -= step
  if (input.isKeyDown("d")) x += step

  fill(20, 20, 30)
  rect(0, 0, screen.width(), screen.height())
  fill(0, 170, 255)
  circle(x, y, 20)
}
double x = 200, y = 200, speed = 300;

Screen.OnUpdate(() => {
    double step = speed * Screen.DeltaTime;
    if (Input.IsKeyDown("w")) y -= step;
    if (Input.IsKeyDown("s")) y += step;
    if (Input.IsKeyDown("a")) x -= step;
    if (Input.IsKeyDown("d")) x += step;

    Canvas.Fill(20, 20, 30);
    Canvas.Rect(0, 0, Screen.Width, Screen.Height);
    Canvas.Fill(0, 170, 255);
    Canvas.Circle(x, y, 20);
});
x, y, speed = 200.0, 200.0, 300.0

def update():
    global x, y
    step = speed * deltaTime()
    if isKeyDown("w"): y -= step
    if isKeyDown("s"): y += step
    if isKeyDown("a"): x -= step
    if isKeyDown("d"): x += step

    fill(20, 20, 30)
    rect(0, 0, width(), height())
    fill(0, 170, 255)
    circle(x, y, 20)

Note that up is minus Y on a canvas.

#Spawning things

Where you'd Instance.new and parent to workspace, keep a list and draw everything in it:

Roblox (Luau)
mouse.Button1Down:Connect(function()
    local p = Instance.new("Part")
    p.Position = mouse.Hit.Position
    p.Parent = workspace
end)
Codetoy
const xs = new Array<f64>()
const ys = new Array<f64>()

export function onMouseDown(button: i32): void {
  xs.push(input.mouseX())
  ys.push(input.mouseY())
}

export function update(): void {
  fill(20, 20, 30)
  rect(0, 0, screen.width(), screen.height())
  fill(255, 170, 60)
  for (let i = 0; i < xs.length; i++) circle(xs[i], ys[i], 15)
}
var points = new List<(double X, double Y)>();

Input.OnMouseDown((int button) => points.Add((Input.MouseX, Input.MouseY)));

Screen.OnUpdate(() => {
    Canvas.Fill(20, 20, 30);
    Canvas.Rect(0, 0, Screen.Width, Screen.Height);
    Canvas.Fill(255, 170, 60);
    foreach (var p in points) Canvas.Circle(p.X, p.Y, 15);
});
points = []

def mouseDown(button):
    points.append((mouseX(), mouseY()))

def update():
    fill(20, 20, 30)
    rect(0, 0, width(), height())
    fill(255, 170, 60)
    for px, py in points:
        circle(px, py, 15)

#Coming from Luau syntax

  • local x = 0 becomes let x: f64 = 0. Annotate decimals as f64; plain integer literals are i32.
  • function f(a: number): number becomes function f(a: f64): f64.
  • Tables become typed Array<T>, Map<K, V> or a class.
  • if … then … end becomes if (…) { … }; ~= is !=; .. (concatenation) is +.
  • Arrays are 0-indexed.
  • local x = 0 becomes double x = 0;, or var when the type is obvious.
  • Tables become List<T>, Dictionary<K, V>, tuples or a class/record.
  • if … then … end becomes if (…) { … }; ~= is !=; .. is + or $"interpolation {x}".
  • Collections are 0-indexed.
  • Indentation replaces end; local isn't needed, but use global to assign module-level variables inside functions.
  • Tables become list or dict; ~= is !=; .. is + or an f-string.
  • Lists are 0-indexed.

#Codetoy or Roblox?

Use Codetoy to: prototype a mechanic without Studio, learn graphics and math, test logic fast, or share a runnable demo with a link. AssemblyScript games can even run in Discord.

Use Roblox when you need: 3D worlds, built-in physics and multiplayer, avatars, assets and the Roblox audience.

Esc