Creating Games
This guide covers everything you need to build a game in Rawframe. For the complete function reference, see the API Playground.
What is a Place?
A Rawframe game lives in a Place — a self-contained project of Luau scripts, content, and configuration that defines your entire game. A Place can:
- Spawn and control entities (cubes, players, projectiles, etc.)
- Send and receive network messages between server and client
- Play 2D and 3D positional audio
- Register per-frame tick callbacks for game logic
- Perform physics raycasts for hit detection
- Manage teams, spawns, weapons, and game states
The engine itself ships with no built-in gameplay. Your Place defines everything — the gamemode, the rules, the world. There is no persistent character or avatar system: each game defines its own characters.
Your game's script packages — and every Asset Store package you install — each run in their own sandboxed Luau VM with memory limits (16 MB default, configurable) and instruction limits (1M per execute). If a script errors, the engine logs it and keeps running. A faulty asset can never crash your game or the engine.
Project Structure
A Place is a directory with a manifest at its root:
my_game/
place.json -- Manifest (required)
scripts/
shared/ -- Scripts loaded on BOTH server and client
config.lua
server/ -- Server-only scripts
init.lua
weapons.lua
client/ -- Client-only scripts
init.lua
hud.lua
input.lua
sounds/ -- Audio files (OGG Vorbis)
assets/ -- Installed Asset Store packages
Key points:
place.jsonis the only required file. Without it, the engine ignores the directory.scripts/shared/scripts are loaded first on both sides. Use them for configuration tables, constants, and utility functions.scripts/server/scripts run only on the dedicated server. They have access tonet.Send()andnet.Broadcast().scripts/client/scripts run only on the client. They have access tonet.SendToServer(), audio playback, mouse input, and aim direction.assets/holds packages installed from the Asset Store — models, scripts, complete systems. Each script package keeps its own manifest and runs in its own VM.
place.json Format
Every Place must have a place.json file in its root directory:
{
"name": "my_game",
"version": "1.0.0",
"description": "My first game",
"author": "YourName",
"assets": [],
"scripts": {
"server": ["scripts/server/init.lua"],
"client": ["scripts/client/init.lua"],
"shared": ["scripts/shared/config.lua"]
}
}
Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Unique project identifier. Use snake_case. |
version |
string | Yes | Semantic version (e.g. "1.0.0"). |
description |
string | Yes | Human-readable description. |
author |
string | Yes | Author name or team name. |
assets |
string[] | Yes | Asset package IDs this game uses. Empty [] if none. |
optional_assets |
string[] | No | Packages loaded before the game if present, skipped if missing. |
engine_version |
string | No | Engine version constraint (e.g. ">=0.18.0"). |
scripts.server |
string[] | Yes | Server-side script paths. Supports glob patterns. |
scripts.client |
string[] | Yes | Client-side script paths. Supports glob patterns. |
scripts.shared |
string[] | Yes | Shared script paths loaded on both sides. Supports glob patterns. |
Script Glob Patterns
Script paths support glob patterns for automatic file discovery:
{
"scripts": {
"shared": ["scripts/shared/config.lua", "scripts/shared/utils/*.lua"],
"server": ["scripts/server/**/*.lua"],
"client": ["scripts/client/**/*.lua"]
}
}
scripts/*.lua— all.luafiles inscripts/(single level)scripts/**/*.lua— all.luafiles recursively- Expanded files are sorted alphabetically for deterministic load order
Script Loading Order
The engine loads script packages in a deterministic order:
- Dependency resolution — Packages are topologically sorted. If your game depends on asset package A, all of A's scripts run before any of your game's scripts.
- Per-package loading order: Shared scripts load first, then side-specific scripts (server or client).
- Per-package VM isolation — Each package runs in its own Luau VM. Globals set by one package are not visible to others.
Per-Place VM Isolation
Every script package — your game and each installed asset — gets its own Lua VM with independent memory and instruction limits:
- Crash isolation — A buggy asset cannot corrupt your game's state.
- Clean hot-reload — Reloading a package destroys and recreates only its VM.
- Memory accountability — Each package's memory usage is tracked separately.
Cross-VM hook dispatch: When a script calls hook.Run("MyEvent", ...), the engine dispatches the hook to all package VMs. Arguments are serialized via LuaValue (supports nil, bool, number, string, Vec3, and nested tables).
Cross-VM function calls: Use place.call("other_package", "fn_name", ...) to call exported functions in another package's VM:
-- In an asset package: export a function
place.export("get_config", function()
return { max_health = 100, respawn_time = 3.0 }
end)
-- In your game: call it
local config = place.call("pvp_core", "get_config")
local max_hp = config.max_health -- 100
Note: Functions, userdata, coroutines, and metatables cannot cross VM boundaries. Pass data as primitives, strings, Vec3, or tables of these types.
Entity Lifecycle
Entities are the fundamental building blocks of a Rawframe world. Every object (player, cube, projectile, NPC) is an entity with components.
-- Spawn a named entity
local ent = entity.create("MyCube")
-- Configure it
entity.set_position(ent, 10, 5, -3)
entity.set_rotation(ent, 0, 45, 0)
entity.set_scale(ent, 2, 2, 2)
entity.set_mesh(ent, "cube")
-- Read properties (multiple return values)
local x, y, z = entity.get_position(ent)
-- Destroy when done
entity.destroy(ent)
After destruction, any further operations on the entity ID will raise a Luau error.
Networking
Rawframe provides a built-in net library for network messaging between your game's server and client scripts.
Architecture
- Server can
net.Send(peer_id)to one client ornet.Broadcast()to all. - Client can
net.SendToServer()to send messages to the server. - Messages are identified by string names and use a binary stream (write in order, read in same order).
Sending Messages
-- Server: send to specific client
net.Start("PlayerDamaged")
net.WriteFloat(new_health)
net.Send(target_peer_id)
-- Server: broadcast to all
net.Start("ScoreboardUpdate")
net.WriteFloat(player_count)
net.Broadcast()
-- Client: send to server
net.Start("PlayerShoot")
net.WriteVec3(aim_x, aim_y, aim_z)
net.SendToServer()
Receiving Messages
-- Server-side: receive from clients
net.Receive("PlayerShoot", function(len, sender)
local x, y, z = net.ReadVec3()
rawframe.log("Player " .. tostring(sender) .. " fired!")
end)
-- Client-side: receive from server
net.Receive("PlayerDamaged", function(len)
local health = net.ReadFloat()
rawframe.log("New health: " .. tostring(health))
end)
Write/Read Functions
| Write | Read | Type |
|---|---|---|
net.WriteString(s) |
net.ReadString() |
string |
net.WriteInt(n) |
net.ReadInt() |
32-bit integer |
net.WriteFloat(n) |
net.ReadFloat() |
32-bit float |
net.WriteVec3(x,y,z) |
net.ReadVec3() |
3D vector |
net.WriteBool(b) |
net.ReadBool() |
boolean |
net.WriteTable(t) |
net.ReadTable() |
table |
Important: Always call
net.Start()before any Write function. Read data in the same order it was written.
Audio
The audio system provides cross-platform 3D positional sound.
-- 2D sound (same volume everywhere)
local handle = audio.play("sounds/click.ogg")
-- 3D positional sound (attenuates with distance)
local handle = audio.play_3d("sounds/explosion.ogg", 10, 0, -5)
-- Control playback
audio.stop(handle)
audio.set_volume(handle, 0.5)
audio.set_master_volume(0.8)
Supported formats: OGG Vorbis (recommended), WAV, MP3, FLAC.
On the server, all audio functions are no-ops — shared scripts can safely call audio functions without side-checking.
Hook System
The hook system provides priority-based event handling across all script packages:
-- Register a hook
hook.Add("PlayerSpawn", "my_game_spawn", function(peer_id)
rawframe.log("Player " .. tostring(peer_id) .. " spawned!")
end)
-- Fire a hook (dispatched to all packages)
hook.Run("MyCustomEvent", "arg1", 42)
-- Remove a hook
hook.Remove("PlayerSpawn", "my_game_spawn")
Hooks fire in priority order. Returning false from any hook cancels propagation.
Built-in Hooks
| Hook | Arguments | Description |
|---|---|---|
Think |
— | Fires every tick (replaces rawframe.on_tick) |
PlayerSpawn |
peer_id | Player spawned |
PlayerDeath |
victim_id, attacker_id | Player died |
PlayerChat |
peer_id, message | Chat message received |
PlayerConnect |
peer_id | Player connected |
PlayerDisconnect |
peer_id | Player disconnected |
Game State Machine
Manage game flow with states, teams, and spawn points:
-- Set the game state
game.set_state("playing")
-- Teams
team.create(1, "Red", {255, 0, 0})
team.create(2, "Blue", {0, 0, 255})
player.set_team(peer_id, 1)
-- Spawn points
spawn.add("red_spawn", 10, 0, 5, 0)
spawn.add("blue_spawn", -10, 0, 5, 180)
Place & Package API
Query and control script packages at runtime:
-- Check package state
if place.is_loaded("pvp_core") then
local info = place.get_info("pvp_core")
rawframe.log("Version: " .. info.version)
end
-- Export functions for other packages
place.export("get_score", function(peer_id)
return scores[peer_id] or 0
end)
-- Call functions in other packages
local score = place.call("pvp_core", "get_score", peer_id)
-- Lifecycle callbacks
place.on_start(function()
rawframe.log("Game started!")
end)
place.on_stop(function()
rawframe.log("Cleaning up...")
end)
Debugging
Logging
rawframe.log("Debug: player position = " .. tostring(x))
Log output appears in:
- Server terminal
- Client console
- Rawframe Studio console panel
Rawframe Studio Debugger
Rawframe Studio includes a full Luau debugger with:
- Breakpoints (F9)
- Step over (F10), step into (F11), step out (Shift+F11)
- Call stack and variable inspection
- Script profiler for performance analysis
Hot-Reload
During development, you can hot-reload script packages without restarting the server:
- Files are monitored for changes
- Modified scripts trigger automatic VM reload
- In Studio, press Ctrl+Shift+R to force reload
Example: Complete PvP Game
Here's a minimal PvP gamemode using multiple engine systems:
-- scripts/server/init.lua
local CONFIG = { max_health = 100, respawn_time = 3.0 }
-- Setup teams
team.create(1, "Red", {255, 0, 0})
team.create(2, "Blue", {0, 0, 255})
-- Handle player spawning
hook.Add("PlayerSpawn", "pvp_spawn", function(peer_id)
local ent = player.get_entity(peer_id)
char.set_max_health(ent, CONFIG.max_health)
char.set_health(ent, CONFIG.max_health)
-- Assign to team with fewer players
local red = team.get_players(1)
local blue = team.get_players(2)
local t = #red <= #blue and 1 or 2
player.set_team(peer_id, t)
end)
-- Handle kills
hook.Add("PlayerDeath", "pvp_death", function(victim_id, attacker_id)
if attacker_id then
player.broadcast_chat(
player.get_name(attacker_id) .. " killed " .. player.get_name(victim_id)
)
end
-- Respawn after delay
timer.delay(CONFIG.respawn_time, function()
char.respawn(victim_id, 0, 5, 0)
end)
end)
Publishing Your Game
Once your game works, you have two paths:
- Publish on Rawframe — register a server, host your game on your own hardware (you own the server, FiveM-style), and it appears in the server browser for everyone to join.
- Export standalone — package your game as an independent title and publish it outside the platform. Commercial export licensing details are announced before the export feature ships.
Next Steps
- Browse the API Playground for all 583+ functions.
- Read the Server Guide for hosting.
- Get building blocks from the Asset Store.
- Explore Rawframe Studio for visual editing.