Starter templates
Three small complete games in templates/ (GunArena, WizardDuel, Obby) that show framework kits configured and wired together with server-side rules.
templates/ holds three small complete games built on framework APIs only: a team gun arena, a wizard duel and a
checkpoint obby. Each is one server module and one client module, short enough to read in one sitting (71 to 110
lines on the server, 12 to 84 on the client). They are reference code. They are not part of the Rojo project and never
sync into a place by themselves, but CI formats and lints them with the same StyLua and Selene configuration as the
framework, and the directive linter applies the game-code rules to them, so they stay on current APIs.
Every client action in the templates binds a keyboard key, a gamepad button and a touch button, so each one plays on PC, console and mobile. Keep that when you add actions.
| Template | What it plays like | Kits and systems it uses |
|---|---|---|
| GunArena | Team deathmatch with hitscan weapons | WeaponKit, Rewind lag compensation, TeamKit, RoundKit, Leaderstats, Leaderboards |
| WizardDuel | Spell, staff and parry duels with double jumps | SpellKit, MeleeKit, MoveKit, Effects |
| Obby | Checkpoint tower with air steps and a speedrun board | CheckpointKit, MoveKit, TagWatch kill bricks backed by a Zones sweep, Leaderboards |
Installing one
Section titled “Installing one”Each template file returns a function(kernel) in the Bootstrap shape: the server half registers kit services and
attaches systems before start(), and the client half binds input. templates/README.md says to copy the bodies into
the two Bootstrap.luau files or require them from there. Since v0.9.0 the Bootstraps also load the Game layer, so
keep that line and call the template after it:
--!strict-- Server bootstrap: runs between kernel boot and start.
return function(kernel: any) require(script.Parent.Game)(kernel) -- templates/GunArena/Server.luau copied to src/Server/GunArena.luau require(script.Parent.GunArena)(kernel)endThe client side is the same with templates/GunArena/Client.luau and src/Client/Bootstrap.luau. Map requirements
sit at the top of each server file; build them in Studio before pressing Play. The kits declare their own channels
(WK_Fire, SK_Cast, ML_Attack and so on), so a template adds nothing to Game.Channels.
GunArena
Section titled “GunArena”Rounds cycle, shots validate against rewound hitboxes, and kills count per server and on a global board.
Rewind.attach(kernel)records hitbox history, andWeaponKit.servicegets it asRewind. Two weapons: a Rifle (25 damage, 6 shots per second, 250 studs) and a Sidearm (12 damage, 10 per second, 120 studs). WeaponKit errors at boot unless everyMaxRangeis in (0, 15000], the Raycast length limit.TeamKit.attachdefines Red and Blue with 8 slots each andSyncRoblox = true. It registers friendly-fire vetoes onWeapon.CanDamageitself.RoundKit.servicerunsLobby(waits for 2 players),Round(180 s) andResults(8 s).- A
Weapon.Hitsubscriber checks whether the victim’s Humanoid is dead and credits the shooter insession.Data.Kills, whichLeaderstatsshows andLeaderboardssubmits to theKillsboard. - A small
ArenaBoardsservice callsBoards:destroy()instop(), so queued board writes flush when the server closes.
The client fires WK_Fire with the weapon slot, the camera look direction and workspace:GetServerTimeNow() on F,
ButtonR2 or the touch button. The timestamp is the claim time Rewind checks against; the server raycasts, and the
client never reports a hit.
Map requirements: parts in Workspace tagged TeamSpawn with a Team attribute of "Red" or "Blue". A
SpawnLocation among them becomes the player’s RespawnLocation when it is Neutral or has its team’s TeamColor; any
other tagged part is a pivot target.
WizardDuel
Section titled “WizardDuel”Firebolt costs mana, curses tick, wards block curses, staff swings parry, and double jumps are metered on the server. There are no map requirements.
Effects.attachdefinesHex(a 6 s Curse that deals 3 damage each second) andWard(20 s, immune to the Curse category).SpellKit.servicewithMaxMana = 100defines three spells. Firebolt costs 20 mana, has a 2 s cooldown and a 0.4 s channel, and deals 30 damage. HexBolt costs 35, has a 6 s cooldown and a 1.2 s channel, and applies Hex. WardSelf costs 40, has a 15 s cooldown and casts instantly. Offensive spells target the nearest living opponent within 60 studs; since v0.9.0 they skip dead players.MeleeKit.attachdefines a combo: StaffJab (10 damage, 8 studs, 0.15 s windup, 0.25 s recovery) chains into StaffSlam (22 damage, 0.35 s windup, 0.5 s recovery, breaks guards).MoveKit.servermetersDoubleJumpat 1 charge with a 0.2 s cooldown.
| Action | Keyboard | Gamepad | Sends |
|---|---|---|---|
| Firebolt | Q | ButtonX | SK_Cast(1) |
| HexBolt | E | ButtonY | SK_Cast(2) |
| Ward | R | ButtonB | SK_Cast(3) |
| Staff jab | Z | ButtonR1 | ML_Attack("StaffJab") |
| Parry | X | ButtonL1 | ML_Parry() |
| Block (hold) | C | ButtonL2 | ML_Block(true), then ML_Block(false) |
The block handler releases on UserInputState.End or UserInputState.Cancel. Cancel arrives instead of End when a
rebind or another binding takes the input; before v0.9.0 the block stuck on in that case. The double jump comes from
MoveKit.recipes.doubleJump({ Power = 1.1 }) on the client.
Stages advance through validated checkpoints, kill bricks respawn, air steps are metered on the server, and completion times race a global speedrun board.
CheckpointKit.service({ MinimumLegitSeconds = 2 })validates each pad for sequence, minimum time and the character root’s reach, and publishesObby.Checkpoint(player, stage). Stage progress persists through the profile only when the game attaches a DataDriver. Without one it lasts the session.- Kill bricks use two layers.
TagWatch.watch("KillBrick", ...)follows tagged parts in Workspace only, connectsTouchedfor the instant kill, and adds each brick as a Zones volume withKeepQueryable = trueso the brick stays solid and touchable as authored. The zone sweep exists because a client can delete aTouchInterestand stop its own touches from registering; the server sweep also catches bricks moved by CFrame (Touched fires only for physical movement) and bricks authored with CanTouch off, which get a warning. MoveKit.servermetersAirStepat 2 charges with a 0.15 s cooldown; the client defines it withMoveKit.recipes.airStep({ Steps = 2 }).- The run timer starts at pad 1 and submits at pad 25 to the ascending
BestTimeboard as whole milliseconds, because OrderedDataStores store integers and the default Leaderboards backend rejects fractional values. AnObbyBoardsservice flushes the board instop().
Map requirements: a workspace.Checkpoints folder with numbered pads, and parts tagged KillBrick in Workspace.
Why they matter
Section titled “Why they matter”The templates show what correct usage looks like at small scale:
- Every gameplay decision happens on the server. The client code in each template is input binding and nothing else; the kits validate every intent through their fail-closed hook chains.
- Kits are configured through config tables and extended through Bus topics (
Weapon.Hit,Obby.Checkpoint), not by editing framework modules. - Work that must survive a server close runs in a service
stop(), which the kernel calls in reverse boot order during the close phase.
They predate the Game layer, so a full game should follow the Workspace structure instead:
channels in Game.Channels, one service per concern in Services/, kit configs in Config/. Read a template for the
kit wiring, then build the game in the workspace shape.