Nox Studio is where you build games. It works a lot like Roblox Studio: you place parts in a 3D world, change them in the Properties panel, and add scripts written in the Nox language (Lua-based, very close to Luau).
games folder (World.json plus your scripts and files).THIRD_PARTY_NOTICES.txt next to it; share that with your game.| 1 / 2 / 3 / 4 | Select, Move, Scale, Rotate |
| Ctrl+D, Del, Ctrl+G | Duplicate, delete, group into a Model |
| Ctrl+Z / Ctrl+Y | Undo / redo |
| F5 / Shift+F5 | Play / stop |
| Right-drag, WASD, Q / E, wheel | Look around, fly, down / up, zoom |
About 10 minutes. You'll make some platforms, a lava floor that kills you, and a moving platform.
local lava = script.Parent
lava.Touched:Connect(function(hit)
local humanoid = hit.Parent:FindFirstChild("Humanoid")
if humanoid then
humanoid.Health = 0
end
end)
Press F5 and jump in the lava. You'll respawn after a moment.
Name one of your platforms Mover. Click on empty space (so nothing is selected) and click Script: it goes into ServerScripts. Type:
local Loop = game:GetService("Loop")
local mover = world:WaitForChild("Mover")
local start = mover.Position
local t = 0
Loop.Heartbeat:Connect(function(dt)
t = t + dt
mover.Position = start + Vector3.new(0, 0, math.sin(t) * 8)
end)
Play again: the platform slides back and forth. Save with Ctrl+S, then try Publish and join from the Nox app.
File > Export game > Android app (.apk) makes your game a phone app (exports/YourGame.apk). It uses the name, icon and version above it, plus:
com.yourname.yourgame. Pick it once and never change it: phones see
an app with a new package name as a different app.The first time, Studio makes your signing key (nox_android_key.txt, in %APPDATA%\Nox on Windows)
and says so in the Output. Keep a copy of it somewhere safe and never share it: updates of your apps must be signed with the same
key, or phones won't install them over the old version. To try the app, copy the .apk to a phone and open it (the phone may ask
to allow installing apps from that place). For the Google Play Store, which wants a different format (.aab), that's coming later.
Most of what you know carries over: Instance.new, :Connect, FindFirstChild,
Vector3, CFrame, tweens, GUIs. The main differences:
game:GetService only takes the Nox names. The Roblox names don't work.
| Nox | Roblox | Nox | Roblox |
|---|---|---|---|
World (global world) | Workspace (workspace) | Loop | RunService |
| Environment | Lighting | Tweens | TweenService |
| Shared | ReplicatedStorage | Input | UserInputService |
| ServerScripts | ServerScriptService | Actions | ContextActionService |
| ServerOnly | ServerStorage | Cleanup | Debris |
| Screens | StarterGui | Tags | CollectionService |
| StartingTools | StarterPack | Web | HttpService |
| PlayerSetup | StarterPlayer | Prompts | ProximityPromptService |
| Audio | SoundService | Pathfinding | PathfindingService |
| Chat | TextChatService | Players, Teams | (same) |
| Saves | DataStoreService | Links, Clipboard | (new in Nox) |
Nox is Lua 5.5 with some Luau favorites added: x += 1 (also -= *= /= //= %= ^= ..=) and continue work like in Luau. These Luau extras are not supported:
| Luau | Write this in Nox |
|---|---|
local x: number = 5 | local x = 5 (no type annotations) |
`Hi {name}` | "Hi " .. name |
if a then b else c (expression) | a and b or c |
for k, v in t do | for k, v in pairs(t) do |
table.find, table.clear, table.clone | write a small loop |
math.clamp, math.round, math.sign | math.max(lo, math.min(hi, x)), math.floor(x + 0.5) |
string.split | string.gmatch |
Nox has some things Luau doesn't: // (whole-number division), bitwise & | ~ << >>, and goto.
Note that tick() gives whole seconds; use time() or os.clock() for timing.
sound.SoundId = "sounds/coin.wav".Saves:GetDataStore(name) works like DataStores, but the data
stays on the computer or phone that played (see Permissions).Humanoid and HumanoidRootPart. In Touched,
hit is the HumanoidRootPart, so hit.Parent is the character..nox files; a Script made with Instance.new doesn't run.BorderSizePixel defaults to 0.| Type | Runs when it's in |
|---|---|
| Script | World or ServerScripts (at game start) |
| LocalScript | PlayerSetup.PlayerScripts, PlayerSetup.CharacterScripts (copied into each character), a GUI in Screens, or a Tool |
| ModuleScript | only when you require() it |
Scripts in Shared and ServerOnly don't run; those are storage. A script that runs 10 seconds without waiting is stopped.
game, world, script, require, print, warn,
wait, task (spawn, defer, delay, wait, cancel),
spawn, delay, time, tick, typeof, _G/shared,
the datatypes below, and Lua's string, table, math, utf8, coroutine
and os (time, clock, date, difftime only).
Not available to game scripts, for safety: files (io), load, debug, package.
local conn = part.Touched:Connect(function(hit) ... end)
conn:Disconnect()
part.Touched:Once(function(hit) ... end)
local hit = part.Touched:Wait()
Get them with game:GetService("Name").
Gravity, CurrentCamera, FallenPartsDestroyHeight, Mode2D.
:Raycast(origin, direction, params?) returns Instance, Position, Normal, Distance.
:GetPartBoundsInRadius(pos, r, params?), :GetPartBoundsInBox(cf, size, params?), :GetPartsInPart(part).
If the game has terrain: world.Terrain:GetHeight(x, z).
LocalPlayer, :GetPlayers(), :GetPlayerFromCharacter(model); events PlayerAdded, PlayerRemoving.
A Player has Name, DisplayName, UserId, Team, Character,
PlayerGui, Backpack; :LoadCharacter(), :Kick(msg), :GetMouse();
events CharacterAdded, Chatted. A leaderstats Folder with value objects shows in the player list.
Events Heartbeat(dt), RenderStepped(dt), Stepped(time, dt);
:BindToRenderStep(name, priority, fn), :UnbindFromRenderStep(name).
local Tweens = game:GetService("Tweens")
local tween = Tweens:Create(part, TweenInfo.new(1, Enum.EasingStyle.Quad), { Position = Vector3.new(0, 10, 0) })
tween:Play()
tween.Completed:Wait()
Tweens numbers, Vector3, Color3, CFrame, UDim2, UDim, Vector2 and NumberRange.
Input: events InputBegan(input, busy), InputEnded, InputChanged, JumpRequest;
:IsKeyDown(Enum.KeyCode.X), :GetMouseLocation(), MouseBehavior.
Actions: :BindAction(name, fn, false, Enum.KeyCode.E), :UnbindAction(name).
ClockTime, TimeOfDay, Brightness, Ambient, OutdoorAmbient,
FogColor, FogStart, FogEnd, GlobalShadows, RealisticGraphics, UltraGraphics.
Put Sky, Atmosphere and post effects (Bloom, Blur, ColorCorrection, SunRays, DepthOfField) inside it.
| Cleanup | :AddItem(instance, seconds) |
| Tags | :AddTag, :RemoveTag, :HasTag, :GetTagged(tag), :GetInstanceAddedSignal(tag) |
| Web | :JSONEncode(t), :JSONDecode(s), :GenerateGUID(), :UrlEncode(s);
:GetAsync(url, nocache, headers), :PostAsync(url, text, contentType, compress, headers),
:RequestAsync({Url, Method, Body, Headers}), :ConnectWebSocket(url) with the Internet permission |
| Saves | :GetDataStore(name), then :GetAsync(key), :SetAsync(key, value),
:UpdateAsync(key, fn), :IncrementAsync(key, n), :RemoveAsync(key) |
| Links | :Open(url): asks the player, returns true if they opened it |
| Clipboard | :Copy(text) |
| Pathfinding | :CreatePath(), then path:ComputeAsync(a, b), path:GetWaypoints() |
| Screens | :SetCoreGuiEnabled(Enum.CoreGuiType.Chat, false), :SetCore("SendNotification", {Title=..., Text=...}) |
| Chat | TextChannels.RBXGeneral:DisplaySystemMessage(text); TextChatCommand for /commands |
| Teams | :GetTeams(); players are auto-assigned to AutoAssignable teams |
| Prompts | event PromptTriggered(prompt, player) |
| PlayerSetup | controls, built-in UI switches, window and export settings (also in File > Settings) |
A game can't reach anything outside itself unless you turn it on in File > Settings > Permissions. Scripts can't change these settings.
| Permission | What it allows | Players asked? |
|---|---|---|
| Save data | Saves: up to 1 MB per game, kept on the player's device. On for new games. | No |
| Internet | Web:GetAsync, PostAsync, RequestAsync, ConnectWebSocket. Websites only (never the player's own
computer or network), 60 requests a minute, answers up to 1 MB. | Yes |
| Open web pages and copy text | Links:Open and Clipboard:Copy. Each web page is also shown to the player
before it opens. | Yes |
| Play online with other players | Multiplayer:Host and Join. Players you play with can see your
IP address. | Yes |
| Direct connections | The Net service: TCP and UDP. Never the player's own computer or special addresses, never
ports for email, remote desktop or databases; on the player's home network only ports 1024 and up, and not web ports.
1 MB a second over TCP, 200 UDP packets a second, 16 connections. | Yes |
When an exported game starts, players see what it wants and can untick anything; the answer is kept for that game
(on Windows, delete nox_permissions.txt in %APPDATA%\Nox to be asked again). In the Nox app, players are asked each time
they join. When you test with Play, everything you ticked is allowed, and test saves are kept apart from players' saves.
If a player says no, the call errors with a message, so wrap it in pcall:
local Web = game:GetService("Web")
local ok, body = pcall(function()
return Web:GetAsync("https://example.com/news.json")
end)
if ok then
local news = Web:JSONDecode(body)
print(news.title)
else
print("No news: " .. body)
end
Most online services need a key in the headers:
local answer = Web:RequestAsync({
Url = "https://example.com/api/scores",
Method = "POST",
Headers = { Authorization = "Bearer YOUR_KEY" },
Body = Web:JSONEncode({ name = "Sam", score = 120 }),
})
print(answer.StatusCode, answer.Body)
Keys inside a game can be read by anyone who has the game, so only use keys that are meant to be public, or your own server in between.
A WebSocket stays connected to your own server (for example a small Node.js server on a free host), so it can send to the game any time. It works through any router, with nothing for players to set up.
local socket = Web:ConnectWebSocket("wss://my-game-server.example.com/play") -- waits until it's open
socket.MessageReceived:Connect(function(text)
local msg = Web:JSONDecode(text)
print("from the server:", msg.kind)
end)
socket.Closed:Connect(function() print("the server went away") end)
socket:Send(Web:JSONEncode({ kind = "hello", name = game:GetService("Players").LocalPlayer.Name }))
Messages are text, up to 1 MB each, and a game can have 4 WebSockets open. socket:Close() closes it;
socket.IsOpen says if it still is.
Every player runs the whole game on their own computer. Nox shares where players are, how they look, and chat. The rest is up to your scripts: tick RemoteEvents go between players in File > Settings > Multiplayer, and they work like Roblox, with the host as the server:
| Call | Where it goes |
|---|---|
remote:FireServer(...) | the host's OnServerEvent(player, ...) |
remote:FireClient(player, ...) | that player's OnClientEvent(...) (host only) |
remote:FireAllClients(...) | everyone's OnClientEvent(...), the host's too (host only) |
func:InvokeServer(...) | the host's OnServerInvoke(player, ...), and waits for what it returns |
Loop:IsServer() | true on the host (and when playing alone) |
You can send numbers, text, true/false, tables, Vector3, Vector2, Color3, CFrame, Enums, players and objects in the game (up to 32 KB at once). Players can send the host 200 messages a second at most. Make the RemoteEvents in a script that every player runs, or put them in Shared, so each computer has them with the same name.
local Shared = game:GetService("Shared")
local coinTaken = Instance.new("RemoteEvent")
coinTaken.Name = "CoinTaken"
coinTaken.Parent = Shared
if game:GetService("Loop"):IsServer() then
-- the host decides: is that coin still there?
coinTaken.OnServerEvent:Connect(function(player, coin)
if coin and coin.Parent then
coin:Destroy()
coinTaken:FireAllClients(coin.Name, player.Name)
end
end)
end
coinTaken.OnClientEvent:Connect(function(coinName, who)
local coin = world.Coins:FindFirstChild(coinName)
if coin then coin:Destroy() end
print(who .. " took " .. coinName)
end)
Never trust what a player sends: check it on the host, like above.
Testing it: Test > Play with 2 players (or 2 Players on the Test tab) opens two windows on your computer: Player1 hosts and Player2 joins. Studio's Output shows Player1's messages; press F9 in Player2's window for its own.
For games that make multiplayer their own way, the Net service (with the Direct connections permission) gives plain connections. Players behind a router may need to forward the port to host.
Net:ListenTCP(port) | a server: .ConnectionAccepted(conn), .Port, :Close() |
Net:ConnectTCP(host, port) | a connection (waits until it's made): :Send(text), .DataReceived(text),
.Closed, .Address, :Close() |
Net:OpenUDP(port) | packets: :SendTo(address, port, text), .PacketReceived(text, address, port), .Port |
Net:Resolve("example.com") | the address as numbers, for SendTo |
TCP is a stream: one Send can arrive in pieces, or two together. End each message with a newline (or send its length first) and put the pieces back together.
local Net = game:GetService("Net")
local server = Net:ListenTCP(30500)
server.ConnectionAccepted:Connect(function(conn)
print("connected:", conn.Address)
conn.DataReceived:Connect(function(text) conn:Send("you said " .. text) end)
end)
Nox Studio's download has a server folder: noxserver.exe for Windows and noxserver-linux
for Linux servers. It runs your exported game with no window and no player of its own, as the host, so the game can stay
online all the time:
noxserver.exe MyGame.exe --port 47800 --max 16
Players join with the code it prints, or the server's address and port from the internet (open that port in the server's
firewall). On the server, Loop:IsServer() is true and Players.LocalPlayer is nil; Scripts run but
LocalScripts and screens don't. Whatever the game turned on in Permissions is allowed there, and saves are kept on the server.
Games played in the Nox app are multiplayer already. An exported game (a Windows .exe, later Steam) can host and join by itself with the Multiplayer service and the Play online with other players permission:
Multiplayer:Host({ Online = true, MaxPlayers = 8 }) | starts hosting and returns the code others join with.
Online = true gives a 6-letter code that works over the internet; without it, only players on the same Wi-Fi can join. |
Multiplayer:Join(code) | joins that game (waits until in; errors if it can't). Both players need the same version of the game. |
Multiplayer:Leave() | back to playing alone |
Multiplayer.IsHost, .InGame, .IsGuest, .Code | where this player stands |
Players who have the Nox app on that computer play with their Nox username and look. Everyone else plays as a guest (Guest4821): guests can't use the chat, and can't join private games.
local MP = game:GetService("Multiplayer")
hostButton.MouseButton1Click:Connect(function()
codeLabel.Text = "Code: " .. MP:Host({ Online = true })
end)
joinButton.MouseButton1Click:Connect(function()
local ok, err = pcall(function() MP:Join(codeBox.Text) end)
if not ok then codeLabel.Text = err end
end)
Every object has Name, Parent, ClassName, :Destroy(), :Clone(),
:FindFirstChild(name), :WaitForChild(name), :GetChildren(), :GetDescendants(),
:IsA(class), :SetAttribute/:GetAttribute, :GetPropertyChangedSignal(prop),
and events ChildAdded, ChildRemoved, Changed, Destroying.
Make new ones with Instance.new("Part", parent).
Part, SpawnLocation, WedgePart, CornerWedgePart, TrussPart (climbable), MeshPart (MeshId = a .obj/.gltf/.glb file),
Seat, VehicleSeat, and Sprite (a flat picture, for 2D games).
Properties: Position, Size, Orientation, CFrame, Color, BrickColor,
Transparency, Reflectance, Material, Shape, Anchored, CanCollide,
CanTouch, CastShadow, AssemblyLinearVelocity.
Methods: :ApplyImpulse(v), :GetTouchingParts(), :PivotTo(cf). Event: Touched(hit).
Touched fires for characters and NPCs walking into a part, and for a held Tool's parts.Model: PrimaryPart, :GetPivot(), :PivotTo(cf), :MoveTo(pos), :GetBoundingBox(), :ScaleTo(n).
Folder, Configuration.
Values: StringValue, NumberValue, IntValue, BoolValue, Vector3Value, Color3Value, ObjectValue, CFrameValue (Value, Changed).
BindableEvent (:Fire, Event), BindableFunction, RemoteEvent, RemoteFunction (local only, see above).
Humanoid: Health, MaxHealth, WalkSpeed (16), JumpPower (50), DisplayName;
:TakeDamage(n), :MoveTo(pos), :EquipTool(tool), :LoadAnimation(anim);
events Died, HealthChanged, Running, Jumping, MoveToFinished.
An NPC is a Model with a Humanoid and a HumanoidRootPart.
PlayerSetup.MaxWalkSpeed / MaxJumpPower,
or multiplayer movement checks pull players back.Tool (put it in StartingTools): Activated, Equipped, Unequipped; needs a part named Handle.
Animation: AnimationId can be "nox://wave", "nox://dance", "nox://point",
"nox://cheer", "nox://sit", or a file. Also ForceField, Accessory, Shirt, Pants, BodyColors.
Sound: SoundId (a .wav/.ogg/.mp3 file), Volume, Looped, :Play(), :Stop(), Ended. Inside a part, it fades with distance.
Lights: PointLight, SpotLight, SurfaceLight. Effects: ParticleEmitter, Fire, Smoke, Sparkles, Explosion, Beam, Trail, Highlight. Decal, Texture.
ClickDetector (MouseClick(player)), ProximityPrompt (Triggered(player)), Dialog.
Attachment, Weld, WeldConstraint, HingeConstraint, RopeConstraint, SpringConstraint, RodConstraint, AlignPosition,
AlignOrientation, LinearVelocity, AngularVelocity.
ScreenGui (put it in Screens), BillboardGui, SurfaceGui, Frame, ScrollingFrame, TextLabel, TextButton, TextBox, ImageLabel,
ImageButton, ViewportFrame, and UIListLayout, UIGridLayout, UICorner, UIStroke, UIGradient, UIPadding, UIScale, UIAspectRatioConstraint.
Buttons: MouseButton1Click, Activated. TextBox: FocusLost(enterPressed).
| Vector3 | Vector3.new(x, y, z); X Y Z, Magnitude, Unit; :Dot, :Cross, :Lerp; + - * / |
| CFrame | CFrame.new(pos), CFrame.new(pos, lookAt), CFrame.Angles(rx, ry, rz), CFrame.lookAt; Position, LookVector, RightVector, UpVector; :Inverse, :Lerp, :ToWorldSpace, :ToObjectSpace |
| Color3 | Color3.new(r, g, b) (0-1), Color3.fromRGB(255, 0, 0), Color3.fromHSV, Color3.fromHex("#ff8800") |
| BrickColor | BrickColor.new("Bright red"), BrickColor.random() |
| UDim2 | UDim2.new(xScale, xOffset, yScale, yOffset), UDim2.fromScale, UDim2.fromOffset |
| Vector2, UDim | as in Roblox |
| TweenInfo | TweenInfo.new(time, style, direction, repeats, reverses, delay) |
| Others | Ray, RaycastParams, Random, NumberRange, NumberSequence, ColorSequence |
| Enum | Enum.Material.Neon, Enum.KeyCode.E, Enum.EasingStyle.Quad, ... |
local player = game:GetService("Players").LocalPlayer
local stats = Instance.new("Folder")
stats.Name = "leaderstats"
stats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Parent = stats
local coin = world:WaitForChild("Coin")
coin.Touched:Connect(function(hit)
if hit.Parent:FindFirstChild("Humanoid") and coin.Parent then
coins.Value = coins.Value + 1
coin:Destroy()
end
end)
local store = game:GetService("Saves"):GetDataStore("Main")
local player = game:GetService("Players").LocalPlayer
local coins = player:WaitForChild("leaderstats"):WaitForChild("Coins")
coins.Value = store:GetAsync("coins") or 0
coins.Changed:Connect(function(value)
store:SetAsync("coins", value)
end)
local gui = script.Parent
local button = gui:WaitForChild("Panel"):WaitForChild("ClickMe")
local count = 0
button.MouseButton1Click:Connect(function()
count = count + 1
button.Text = "Clicked " .. count .. " times"
end)
local Input = game:GetService("Input")
local Environment = game:GetService("Environment")
Input.InputBegan:Connect(function(input, busy)
if busy then return end
if input.KeyCode == Enum.KeyCode.T then
Environment.ClockTime = (Environment.ClockTime + 3) % 24
end
end)
local npc = world:WaitForChild("Bob")
local hum = npc:WaitForChild("Humanoid")
local points = { Vector3.new(10, 3, -10), Vector3.new(20, 3, -10), Vector3.new(20, 3, -20) }
local i = 1
while hum.Parent do
hum:MoveTo(points[i])
hum.MoveToFinished:Wait()
i = i % #points + 1
task.wait(0.5)
end
The language is open source, with sandboxing, JSON and web requests (those extras are for standalone Nox programs, not game scripts): Nox language on GitHub.