Skip to content

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.json is 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 to net.Send() and net.Broadcast().
  • scripts/client/ scripts run only on the client. They have access to net.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 .lua files in scripts/ (single level)
  • scripts/**/*.lua — all .lua files recursively
  • Expanded files are sorted alphabetically for deterministic load order

Script Loading Order

The engine loads script packages in a deterministic order:

  1. 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.
  2. Per-package loading order: Shared scripts load first, then side-specific scripts (server or client).
  3. 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 or net.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