Nox

Nox Docs

Making games with Nox Studio and the Nox language.
Getting started Your first obby Coming from Roblox How scripts run Services Permissions Multiplayer Classes Datatypes Examples

Getting started

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).

Opening Studio

  1. Download Nox Studio (Windows), unzip it, and open noxstudio.exe in the NoxStudio folder.
  2. Under New game, type a name, pick 3D or 2D, and create it. Your games are listed under Your games, so you can open them again later.
  3. A new game starts with a baseplate and a spawn point.

The Studio window

Saving, sharing, publishing

Handy keys

1 / 2 / 3 / 4Select, Move, Scale, Rotate
Ctrl+D, Del, Ctrl+GDuplicate, delete, group into a Model
Ctrl+Z / Ctrl+YUndo / redo
F5 / Shift+F5Play / stop
Right-drag, WASD, Q / E, wheelLook around, fly, down / up, zoom

Your first obby

About 10 minutes. You'll make some platforms, a lava floor that kills you, and a moving platform.

1. Platforms

  1. On the Home tab, click Part. A grey block appears.
  2. Use Move (2) and Scale (3) to make it a platform, and drag it into place.
  3. In Properties, tick Anchored, or it will fall when you play.
  4. Ctrl+D to copy it, then move the copy to make a path of jumps.

2. Lava

  1. Add another Part, make it big and flat under your platforms, and anchor it.
  2. In Properties, rename it Lava and set its Color to orange (click the color swatch).
  3. Select Lava, then click Script on the Home tab. A script is added inside Lava and opens in the editor.
  4. Type this:
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.

3. A moving platform

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.

Tip: the 3D Model view (Model tab, 3D Model) lets you make your own meshes, like guns, props or characters, without leaving Studio.

Android phones

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:

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.

Coming from Roblox

Most of what you know carries over: Instance.new, :Connect, FindFirstChild, Vector3, CFrame, tweens, GUIs. The main differences:

Different service names

game:GetService only takes the Nox names. The Roblox names don't work.

NoxRobloxNoxRoblox
World (global world)Workspace (workspace)LoopRunService
EnvironmentLightingTweensTweenService
SharedReplicatedStorageInputUserInputService
ServerScriptsServerScriptServiceActionsContextActionService
ServerOnlyServerStorageCleanupDebris
ScreensStarterGuiTagsCollectionService
StartingToolsStarterPackWebHttpService
PlayerSetupStarterPlayerPromptsProximityPromptService
AudioSoundServicePathfindingPathfindingService
ChatTextChatServicePlayers, Teams(same)
SavesDataStoreServiceLinks, Clipboard(new in Nox)

Language differences

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:

LuauWrite this in Nox
local x: number = 5local 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 dofor k, v in pairs(t) do
table.find, table.clear, table.clonewrite a small loop
math.clamp, math.round, math.signmath.max(lo, math.min(hi, x)), math.floor(x + 0.5)
string.splitstring.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.

Other differences

How scripts run

TypeRuns when it's in
ScriptWorld or ServerScripts (at game start)
LocalScriptPlayerSetup.PlayerScripts, PlayerSetup.CharacterScripts (copied into each character), a GUI in Screens, or a Tool
ModuleScriptonly 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.

Globals

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.

Events

local conn = part.Touched:Connect(function(hit) ... end)
conn:Disconnect()
part.Touched:Once(function(hit) ... end)
local hit = part.Touched:Wait()

Services

Get them with game:GetService("Name").

World

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).

Players

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.

Loop

Events Heartbeat(dt), RenderStepped(dt), Stepped(time, dt); :BindToRenderStep(name, priority, fn), :UnbindFromRenderStep(name).

Tweens

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 and Actions

Input: events InputBegan(input, busy), InputEnded, InputChanged, JumpRequest; :IsKeyDown(Enum.KeyCode.X), :GetMouseLocation(), MouseBehavior.
Actions: :BindAction(name, fn, false, Enum.KeyCode.E), :UnbindAction(name).

Environment

ClockTime, TimeOfDay, Brightness, Ambient, OutdoorAmbient, FogColor, FogStart, FogEnd, GlobalShadows, RealisticGraphics, UltraGraphics. Put Sky, Atmosphere and post effects (Bloom, Blur, ColorCorrection, SunRays, DepthOfField) inside it.

Others

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=...})
ChatTextChannels.RBXGeneral:DisplaySystemMessage(text); TextChatCommand for /commands
Teams:GetTeams(); players are auto-assigned to AutoAssignable teams
Promptsevent PromptTriggered(prompt, player)
PlayerSetupcontrols, built-in UI switches, window and export settings (also in File > Settings)

Permissions

A game can't reach anything outside itself unless you turn it on in File > Settings > Permissions. Scripts can't change these settings.

PermissionWhat it allowsPlayers asked?
Save dataSaves: up to 1 MB per game, kept on the player's device. On for new games.No
InternetWeb: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 textLinks:Open and Clipboard:Copy. Each web page is also shown to the player before it opens.Yes
Play online with other playersMultiplayer:Host and Join. Players you play with can see your IP address.Yes
Direct connectionsThe 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.

WebSockets

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.

Multiplayer

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:

CallWhere 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.

Your own multiplayer: TCP and UDP

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)

Dedicated servers

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.

Hosting and joining from your game

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, .Codewhere 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)

Classes

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).

Parts

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).

Balls and cylinders collide as boxes for now. Touched fires for characters and NPCs walking into a part, and for a held Tool's parts.

Models and values

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).

Characters and NPCs

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.

If a script raises WalkSpeed or JumpPower, also raise 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, light and effects

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.

Interaction and physics

ClickDetector (MouseClick(player)), ProximityPrompt (Triggered(player)), Dialog. Attachment, Weld, WeldConstraint, HingeConstraint, RopeConstraint, SpringConstraint, RodConstraint, AlignPosition, AlignOrientation, LinearVelocity, AngularVelocity.

GUI

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).

Datatypes

Vector3Vector3.new(x, y, z); X Y Z, Magnitude, Unit; :Dot, :Cross, :Lerp; + - * /
CFrameCFrame.new(pos), CFrame.new(pos, lookAt), CFrame.Angles(rx, ry, rz), CFrame.lookAt; Position, LookVector, RightVector, UpVector; :Inverse, :Lerp, :ToWorldSpace, :ToObjectSpace
Color3Color3.new(r, g, b) (0-1), Color3.fromRGB(255, 0, 0), Color3.fromHSV, Color3.fromHex("#ff8800")
BrickColorBrickColor.new("Bright red"), BrickColor.random()
UDim2UDim2.new(xScale, xOffset, yScale, yOffset), UDim2.fromScale, UDim2.fromOffset
Vector2, UDimas in Roblox
TweenInfoTweenInfo.new(time, style, direction, repeats, reverses, delay)
OthersRay, RaycastParams, Random, NumberRange, NumberSequence, ColorSequence
EnumEnum.Material.Neon, Enum.KeyCode.E, Enum.EasingStyle.Quad, ...

Examples

A coin counter (leaderstats)

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)

Saving coins between plays

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)

A button on screen (LocalScript in a ScreenGui)

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)

Press a key

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)

An NPC walking a route

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 Nox language on its own

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.