DEV Community

Tack k
Tack k

Posted on

I Built a Playable Arcade Inside My FiveM Server — Tetris, Breakout, Space Invaders and More

The idea

Our QBCore RP server had an arcade cabinet prop sitting in a few locations around the map. It was purely decorative. Players would walk past it every session and nothing happened.

I wanted it to actually work — walk up, press E, and play a real game inside the game.

The result: exs-arcade — a fully playable in-game arcade system built with Svelte + TypeScript for the UI, Lua for FiveM integration, and a server-side ranking system backed by MySQL.


What's inside

Four playable games, each with its own engine and Svelte component:

  • Block Puzzle (Tetris) — classic falling block game
  • Alien Shooter (Space Invaders) — wave-based shooter
  • Brick Breaker (Breakout) — paddle and ball
  • Racing — time attack mode

Plus a global ranking board that tracks top scores per game and an overall leaderboard combining all scores.


How it works

The architecture is split into three layers.

Lua client detects nearby arcade cabinet props and shows an interaction prompt. When the player presses E, it freezes their character and opens the NUI (web-based UI overlay).

local obj = GetClosestObjectOfType(
    coords.x, coords.y, coords.z,
    Config.InteractDistance,
    Config.CabinetModel,
    false, false, false
)

if obj ~= 0 then
    DrawText3D(objCoords.x, objCoords.y, objCoords.z + 1.0, '[E] Play Arcade')

    if IsControlJustReleased(0, 38) then -- E key
        FreezeEntityPosition(PlayerPedId(), true)
        openArcade()
    end
end
Enter fullscreen mode Exit fullscreen mode

Svelte NUI handles the full game UI — main menu, individual game screens, score submission, and the ranking board. Each game is a self-contained Svelte component with its own TypeScript engine.

Node.js server handles score submission, validation, and ranking queries via oxmysql. It also deduplicates rankings so only each player's personal best counts toward the leaderboard.


The ranking system

Scores are stored in MySQL with a game_id field. The server deduplicates by player so one person can't flood the top 10 — only their best run counts.

For racing, scores are sorted ascending (lower time = better). For all other games, scores are sorted descending. The server handles this automatically based on game type.

var TIME_ATTACK_GAMES = ['racing'];
var SCORE_GAMES = ['tetris', 'invaders', 'breakout'];

var order = isTimeAttack(gameId) ? 'ASC' : 'DESC';
Enter fullscreen mode Exit fullscreen mode

There's also an overall leaderboard that sums each player's personal bests across all score-based games — giving a "best arcade player on the server" title.


Cabinet detection — two modes

The script supports two ways to trigger the arcade:

Prop-based (default): Automatically detects any prop_arcade_01 object within range. No coordinate setup needed — if the prop is in the world, it works.

Location-based: Define exact coordinates in config for tighter control over where the arcade is accessible.

Config.UseLocations = false  -- true = coordinate mode, false = prop detection
Enter fullscreen mode Exit fullscreen mode

Tech stack breakdown

Layer Tech
FiveM client Lua (QBCore)
UI framework Svelte 5 + TypeScript
Build tool Vite
Server logic Node.js (FiveM native)
Database MySQL via oxmysql

Building the UI in Svelte rather than plain HTML/JS made the game state management dramatically cleaner. Each game component owns its own engine instance and lifecycle — no global state leaks between games.


What's next

A rhythm game component is already stubbed out in the config (marked enabled: false). That's Phase 4. For now the four working games keep players busy.

Next up in this series: Vol.4 — Customizing ox_inventory for QBCore: adding item categories, weight display, and a custom search UI.


Built with Claude Opus as my coding partner. I designed the system architecture and UX — the AI handled implementation. That's how this whole series works.

Questions or want to see a specific game engine in detail? Drop a comment.

Top comments (0)