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
local RunService = game:GetService("RunService")
local x, speed = 0, 150
RunService.RenderStepped:Connect(function(dt)
x += speed * dt
end)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
local part = Instance.new("Part")
part.Position = Vector3.new(0, 5, 0)
part.Parent = workspaceClear 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
local UIS = game:GetService("UserInputService")
RunService.RenderStepped:Connect(function(dt)
if UIS:IsKeyDown(Enum.KeyCode.W) then y -= speed * dt end
end)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:
mouse.Button1Down:Connect(function()
local p = Instance.new("Part")
p.Position = mouse.Hit.Position
p.Parent = workspace
end)#Coming from Luau syntax
local x = 0becomeslet x: f64 = 0. Annotate decimals asf64; plain integer literals arei32.function f(a: number): numberbecomesfunction f(a: f64): f64.- Tables become typed
Array<T>,Map<K, V>or aclass. if … then … endbecomesif (…) { … };~=is!=;..(concatenation) is+.- Arrays are 0-indexed.
local x = 0becomesdouble x = 0;, orvarwhen the type is obvious.- Tables become
List<T>,Dictionary<K, V>, tuples or aclass/record. if … then … endbecomesif (…) { … };~=is!=;..is+or$"interpolation {x}".- Collections are 0-indexed.
- Indentation replaces
end;localisn't needed, but useglobalto assign module-level variables inside functions. - Tables become
listordict;~=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.