PolyWorld Scripts are things in a PolyWorld game that preform tasks on the server.
LocalScripts run on each player's client. Create them in Studio (blue script icon). They use a smaller API focused on local visuals, input-side events, and talking to the server.
How scripts work
If the script is parented to a part, script.Parent (or script.parent) will be it's parent part. If not, you still have access to the entire workspace, not much changes except script.Parent
Useful Globals:
| What | Notes |
|---|---|
wait(seconds) | Pause this script (like Roblox task.wait / wait) |
print(...) | Print to the server log |
script.Parent / script.parent | The Part this Script is under (if any) |
workspace / Workspace | The world |
Shared | Table shared by all Scripts in this place (same as bare globals) |
Scripts in the same place share globals (Score = 1 in one script is readable in another). Touched / PlayerAdded / PlayerDied / CharacterAdded stay per-script. Don't worry, games can't touch your variables.
Tip: wait() only works in the main script body (loops, etc.). Inside Touched / PlayerAdded, set a flag and handle timing in a while true loop.
Some other things:
| What | Notes |
|---|---|
| Part touched | function Touched(player) on the Script under that Part |
| Player joined | function PlayerAdded(player) |
| Player died | function PlayerDied(player) |
| Player respawned | function CharacterAdded(player) |
| Disable reset button | Players.ResetEnabled = false |
| Player interaction | player.health, player.x/y/z, player.name, player.transparency, player.customNameColor, player:Kill(), player:SetPosition |
| Part properties | Position, Size, Anchored, CanCollide (or lowercase) |
| Color | RGBColor.new(r, g, b) with 0-255 for each field |
| Vector3 Creation | Vector3.new(x, y, z) |
| Spawn tools | Map respawn points, or player:SetSpawn(x, y, z) for checkpoints |
| Music | Sound.play, Sound.playMusic (Upload your own on the Edit page of your game) |
| Player UI | UI.notify, UI.hud, Chat.broadcast, player:Notify |
Events
Touched(player)
Fires when a player enters the script's parent part (OBB overlap with the full avatar; enter-only — not every frame while standing on it).
Example killbrick:
function Touched(player)
player:Kill()
end
PlayerAdded(player)
Fires when someone joins the place.
function PlayerAdded(player)
print("Hello there, " .. player.name)
Sound.playMusic("https://polyworld.games/assets/bgm/and-remix.ogg", 0.4)
end
PlayerDied(player)
Fires whenever health hits zero (via Kill(), reset character, health naturally hitting zero...)
function PlayerDied(player)
Chat.broadcast("Someone died!")
end
CharacterAdded(player)
Fires when a dead player respawns.
function CharacterAdded(player)
player:Notify("ELIMINATED")
end
OnRemoteEvent(name, player_id, data)
Fired when the client sends a remote event (e.g. tool use). data is a table.
If the payload is at least 24 bytes (6 floats), these fields are set:
| Field | Meaning |
|---|---|
dir_x, dir_y, dir_z | Aim direction |
origin_x, origin_y, origin_z | Aim origin |
data_len | Raw payload length in bytes |
function OnRemoteEvent(name, player_id, data)
if name == "tool_use" and data.dir_x then
print("Aim", data.dir_x, data.dir_y, data.dir_z)
end
end
Players
local players = workspace:GetPlayers() -- you can also do Players:GetPlayers()
for _, p in ipairs(players) do
print(p.id, p.health, p.x, p.y, p.z)
end
Place settings
| Field | Meaning |
|---|---|
Players.ResetEnabled | Default true. Set false to ignore pause-menu Respawn |
Players.ResetEnabled = false
Read
| Field | Meaning |
|---|---|
id | Player id |
username / name / Name | Display name |
health | 0–100 |
x, y, z | Position |
yaw | Facing |
animation | Anim state number |
equippedHead / Shirt / Pants | Catalog ids |
transparency / Transparency | 0–1 (avatar fade) |
customNameColor / CustomNameColor | {r,g,b} or nil if unset |
Write
| Field | Effect |
|---|---|
health | Clamped 0–100 |
x, y, z | Teleport (syncs to client) |
animation | Set anim |
transparency / Transparency | 0–1 |
customNameColor / CustomNameColor | {r,g,b} (0–1 or 0–255); nil clears |
player.transparency = 0.5
player.customNameColor = RGBColor.new(255, 80, 0)
player.customNameColor = nil -- back to default name color
Methods
| Call | Effect |
|---|---|
player:Kill() | Health → 0 (respawn) |
player:Damage(n) | Take damage |
player:TakeDamage(n) | Same as Damage |
player:SetPosition(x, y, z) | Teleport now |
player:SetSpawn(x, y, z) | Next respawn goes here (checkpoints!) |
player:ClearSpawn() | Back to normal SpawnLocations |
player:Message(text) | System chat to this player |
player:Notify(text, seconds?) | Center toast |
player:Hud(text, seconds?) | Top HUD (0 = until clear) |
player:ClearUI() | Clear toast + HUD |
player:PlayMusic(url, volume?) | BGM for this player only |
player:StopMusic() | Stop that player's BGM |
Parts & Workspace
You can use these functions to find parts:
local part = workspace:FindFirstChild("PartName")
local part = workspace:FindByName("PartName") -- search entire world
local kids = workspace:GetChildren()
local child = model:WaitForChild("Name", 5)
local brick = workspace:FindFirstChildOfClass("Part") -- also "BasePart", "Model"
You can also use dot fields:
local part = workspace.PartName
Part properties
| Property | Notes |
|---|---|
Name | string |
ClassName | "Part", "Model", or "Workspace" |
Parent | Instance or nil |
Color / color | {r,g,b} as 0-1, or use RGBColor.new (0–255) |
Position / position | {x,y,z} |
Rotation / rotation | Degrees |
Size / size | {x,y,z} |
Anchored / anchored | bool |
CanCollide / cancollide | bool — whether players collide with this part |
Methods
:Destroy(), :Clone(), :FindFirstChild(name), :FindFirstChildOfClass(class), :GetChildren(), :WaitForChild(name, timeout?)
Create parts
local p = Instance.new("Part") -- also Instance.new("Brick") or "Model"
p.Name = "MyBrick"
p.Parent = workspace
p.Position = Vector3.new(0, 5, 0)
p.Size = Vector3.new(4, 1, 4)
p.Color = RGBColor.new(255, 0, 0)
p.Anchored = true
p.CanCollide = true
Colors & vectors
RGBColor.new(255, 0, 0) -- 0-255 in.
RGBColor.random()
Vector3.new(0, 10, 0)
script.Parent.Color = RGBColor.random()
script.Parent.Position = Vector3.new(0, 10, 0)
script.Parent.Rotation = { x = 0, y = 45, z = 0 }
Sound
| Call | Notes |
|---|---|
Sound.play("tick") | Full volume SFX |
Sound.playAt("explosion", x, y, z) | Quieter with distance |
Sound.playMusic(url, volume?) | Looping BGM for everyone — use an .ogg URL |
Sound.stopMusic() | Stop world BGM |
Sound.playMusicFor(player, url, volume?) | BGM for one player only |
Sound.stopMusicFor(player) | Stop that player's BGM |
player:PlayMusic(url, volume?) | Same as playMusicFor |
player:StopMusic() | Same as stopMusicFor |
Built-in SFX names: jump, land, footstep / footsteps, explosion, chat, rocket, tick.
You can also pass numeric ids 0–6 (same order as the names above).
Hosting your own BGM: open My Places > Edit > Place music and upload OGG, MP3, WAV, FLAC, or M4A (converted to OGG on upload). Copy the URL into your script. Warning! Your music will be converted to an .ogg
function PlayerAdded(player)
player:PlayMusic("https://polyworld.games/assets/bgm/jumper-cminor.ogg", 0.4)
end
Chat / UI
| Call | Who sees it | Notes |
|---|---|---|
Chat.broadcast(text) | Everyone | Chat panel as Server |
player:Message(text) | One player | Chat panel as Server |
UI.notify(text, seconds?) | Everyone | Center toast (default 3s) |
player:Notify(text, seconds?) | One player | Center toast |
UI.hud(text, seconds?) | Everyone | Top HUD (default 8s; 0 = until clear) |
player:Hud(text, seconds?) | One player | Top HUD |
UI.clear() | Everyone | Clears toast + HUD |
player:ClearUI() | One player | Clears that player's toast + HUD |
function Touched(player)
player:Notify("Checkpoint saved!")
Chat.broadcast("Someone reached a checkpoint")
UI.hud("Find the exit", 30)
end
Explosions
Explosion.new(x, y, z, radius) -- radius optional, default 8
-- Not deadly! You'd have to kill the player yourself...
NPCs
local id = NPCs.create(
"Guide", -- name (shows with [NPC])
shirtId,
pantsId,
headId,
"#eaeaea", -- skin hex
0, 5, 0 -- position (optional; default 0, 4, 0)
)
NPCs.chat(id, "Welcome!")
NPCs.moveTo(id, x, y, z)
NPCs.tpTo(id, x, y, z)
NPCs.setYaw(id, 90)
NPCs.setAnim(id, NPCs.Animations.Walking)
print(NPCs.getX(id), NPCs.getY(id), NPCs.getZ(id), NPCs.getYaw(id))
NPCs.destroy(id)
| Call | Notes |
|---|---|
NPCs.create(...) | Returns slot id (max 32 NPCs) |
NPCs.destroy(id) | Remove NPC |
NPCs.chat(id, text) | Bubble / chat line |
NPCs.moveTo(id, x, y, z) | Walk toward point |
NPCs.tpTo(id, x, y, z) | Instant teleport |
NPCs.setYaw(id, deg) | Face direction |
NPCs.setAnim(id, anim) | See Animations table |
NPCs.getX(id) / getY / getZ | Current position |
NPCs.getYaw(id) | Current yaw (degrees) |
NPCs.Animations | Value |
|---|---|
Idle | 0 |
Walking | 1 |
Jumping | 2 |
Dancing | 3 |
Dancing2 | 4 |
Dancing3 | 5 |
Examples
Killbrick
Put this Script under the kill Part:
function Touched(player)
player:Kill()
end
Color-changing Part
while true do
script.Parent.Color = RGBColor.random()
wait(1)
end
Spinner + boom
function Touched(player)
local p = script.Parent.Position
Explosion.new(p.x, p.y, p.z, 10)
player:Kill()
end
local t = 0
while true do
t = t + 1
script.Parent.Rotation = { x = t * 2, y = t * 5, z = t * 10 }
wait(0.016)
end
Trap platform (rises, then returns)
Parent this to the trap Part. (wait can't run inside Touched, so we use a flag.)
local busy = false
function Touched(player)
if busy then return end
busy = true
end
local start = script.Parent.Position
local sx, sy, sz = start.x, start.y, start.z
while true do
if busy then
script.Parent.Position = Vector3.new(sx, sy + 11, sz)
wait(1)
script.Parent.Position = Vector3.new(sx, sy, sz)
busy = false
end
wait(0.05)
end
Checkpoints (obby)
1. Name every checkpoint Part exactly Checkpoint. 2. Add one Script in the place (doesn't need a Part parent) with this:
-- Checkpoint Manager: Create parts named "Checkpoint" to make checkpoints
local checkpoints = {}
local on_pad = {}
local function collect(node)
local ok, kids = pcall(function() return node:GetChildren() end)
if not ok or not kids then return end
for _, child in ipairs(kids) do
local class = child.ClassName
local name = child.Name
if class == "Part" and name == "Checkpoint" then
table.insert(checkpoints, child)
elseif class == "Model" then
collect(child)
end
end
end
local function overlaps(part, px, py, pz)
local pos = part.Position
local size = part.Size
local hx = size.x * 0.5 + 0.4
local hy = size.y * 0.5 + 0.4
local hz = size.z * 0.5 + 0.4
-- Touching?
return math.abs(px - pos.x) < 1.0 + hx
and math.abs(py - pos.y) < 2.3 + hy
and math.abs(pz - pos.z) < 0.5 + hz
end
local function spawn_above(part)
local pos = part.Position
local size = part.Size
-- Stand on top
return pos.x, pos.y + size.y * 0.5 + 3.0, pos.z
end
wait(0.2)
collect(workspace)
print("[Checkpoints] Found", #checkpoints, "Checkpoint parts")
while true do
local players = workspace:GetPlayers()
local seen = {}
for _, p in ipairs(players) do
local id = p.id
seen[id] = true
local hit = nil
for i, cp in ipairs(checkpoints) do
if overlaps(cp, p.x, p.y, p.z) then
hit = i
break
end
end
if hit then
if on_pad[id] ~= hit then
on_pad[id] = hit
local x, y, z = spawn_above(checkpoints[hit])
p:SetSpawn(x, y, z)
print("[Checkpoints] Player", id, "-> checkpoint", hit)
end
else
on_pad[id] = nil
end
end
for id, _ in pairs(on_pad) do
if not seen[id] then
on_pad[id] = nil
end
end
wait(0.05)
end
When a player steps on a Checkpoint, their respawn point updates. Die later → come back there instead of the start.
Welcome NPC
wait(1)
local npc = NPCs.create("Guide", 31, 32, 19, "#eaeaea", 0, 5, 0)
NPCs.setAnim(npc, NPCs.Animations.Idle)
NPCs.chat(npc, "Welcome!")
while true do
local players = workspace:GetPlayers()
if players[1] and players[1].health > 0 then
NPCs.moveTo(npc, players[1].x, players[1].y, players[1].z)
NPCs.setAnim(npc, NPCs.Animations.Walking)
end
wait(0.1)
end
LocalScripts (client)
LocalScripts run on each player's client (Studio: blue script icon). Use them for local VFX, UI toasts, and sending events to server Scripts.
| What | Notes |
|---|---|
wait(seconds) | Pause this LocalScript |
print(...) | Print to the client console |
script.Parent / script.parent | Parent Part (if any) |
player | Local player (name, health, x/y/z, position, dead) |
player:GetHealth() / :GetPosition() / :SetPosition(...) / :Notify(text, secs?) | |
FireServer(name [, data]) | Sends to server OnRemoteEvent(name, player_id, data) |
UI.notify(text, secs?) / UI.hud(text) | Local toast / HUD |
RGBColor / Vector3 / Explosion.new | Same as server (Explosion is local visual) |
function Touched(player) | Fires when you enter the parent Part |
Parent part properties (local visual only — not authoritative on the server): color/Color, position/Position, rotation/Rotation, size/Size, Transparency (0=opaque).
Example — client button feel + tell the server:
function Touched(player)
UI.notify("Checkpoint!", 2)
FireServer("CheckpointTouch")
end
Server Script:
function OnRemoteEvent(name, player_id, data)
if name == "CheckpointTouch" then
print("Player " .. player_id .. " hit a checkpoint")
end
end
Tips & gotchas
- Server Scripts are the source of truth for world/player state. LocalScripts can tweak local visuals and call
FireServer. Touchedwill only trigger on enter.- Prefer
RGBColorwhen assigning new colors to parts. - Player methods use PascalCase (
PlayMusic,Notify);Sound.*uses camelCase (playMusic). - You can have many Scripts per place as you need, duplicate scripts will be turned into a single file, while being loaded in the context of multple parts.
- Upload places as
.rbxlxfrom Create Place. Scripts inside of it will convert over! - You can use a pcall with Workspace:GetPlayers() to detect if you are running it in Roblox or PolyWorld.
Have fun making your game!
.png)