Hello, Natiny
Your first Natiny app needs no project generator and no assets. In a few lines it opens a window, runs a game loop, and draws a moving square.
You can paste this program into the browser sandbox and run it
immediately, or save it as data/main.lua in the Lua release bundle.
local natiny = require("natiny")
assert(natiny.backend.init())
local window = natiny.window.create("Hello, Natiny", 800, 600)
assert(window ~= 0, "failed to create the window")
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
local x = -80
natiny.backend.loop(function(dt)
x = x + 180 * dt
if x > natiny.window.get_width(window) then
x = -80
end
natiny.window.bind(window, function()
natiny.render.clear(0.07, 0.09, 0.13)
natiny.render.set_color(0.66, 0.76, 0.22, 1.0)
natiny.render.rectangle(x, 260, 80, 80)
end)
end)
Run it and a green square will travel across a dark window. That square is a useful first “hello”: it exercises the engine, the window, the frame loop, and 2D drawing without hiding any of them behind setup code.
Build the program in four steps
1. Start the engine
local natiny = require("natiny")
assert(natiny.backend.init())
require("natiny") returns the engine module. Natiny does not add hidden
globals to Lua.
A graphics backend is the platform-specific renderer Natiny uses to talk
to the GPU. backend.init chooses the preferred
one: Direct3D 12 on Windows, Vulkan on Linux, Metal on macOS, and WebGPU in the
browser. assert stops with an error if initialization fails.
2. Create a window with sharp display scaling
local window = natiny.window.create("Hello, Natiny", 800, 600)
assert(window ~= 0, "failed to create the window")
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
window.create opens a window whose requested
size is 800 by 600. In a web build, the same call uses the page's canvas. The
returned number is a handle that identifies this window in later calls.
A high-density display can provide more pixels than the requested window size.
window.set_scale_mode keeps the 800 by
600 drawing space while using those extra pixels for sharper shapes. The
window scaling guide explains all three modes
and when to choose each one.
3. Update once per frame
local x = -80
natiny.backend.loop(function(dt)
x = x + 180 * dt
if x > natiny.window.get_width(window) then
x = -80
end
-- draw the frame here
end)
backend.loop calls the function once per frame.
dt is the elapsed time in seconds since the previous frame.
The square moves at 180 drawing units per second, not 180 units per frame.
Multiplying movement by dt keeps its speed stable on a 60 Hz display, a
144 Hz display, or a slower machine. Once the square leaves the width returned
by window.get_width, it starts again just outside the left edge.
4. Draw the frame into the window
natiny.window.bind(window, function()
natiny.render.clear(0.07, 0.09, 0.13)
natiny.render.set_color(0.66, 0.76, 0.22, 1.0)
natiny.render.rectangle(x, 260, 80, 80)
end)
A render target is the image that receives drawing commands.
window.bind makes this window the current render
target. Every draw call inside the callback goes into it, and the completed
frame is presented when the callback returns.
The drawing calls use a small amount of current state:
clearfills the background;set_colorselects the colour for following draw calls;rectangledraws at(x, 260)with a size of 80 by 80 drawing units.
Window coordinates begin at the top-left. X grows to the right and Y grows downward.
Run it
Download a Lua bundle and unzip it. The executable looks for
data/main.lua beside itself, so a complete project can be this small:
my_game/
├── data/
│ └── main.lua
└── natiny.exe
Each platform has a page of its own with the commands for it: Windows, Linux, macOS, Android and Web.
You can also paste this program into the browser sandbox and run it without downloading anything.
Change the first program
Try one small change at a time:
- change the four values passed to
render.set_color; - replace
rectanglewithcircle(x, 300, 40); - change
180to control the speed; - add a
yvalue and move in two dimensions; - draw a second shape after the first one.
You now have the essential game loop: update state, bind a target, draw a frame. Input, textures, models, physics, sound, and custom NSL shaders all build on that same loop.