Skip to content

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

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:

src/Server/Bootstrap.luau
--!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)
end

The 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.

Rounds cycle, shots validate against rewound hitboxes, and kills count per server and on a global board.

  • Rewind.attach(kernel) records hitbox history, and WeaponKit.service gets it as Rewind. 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 every MaxRange is in (0, 15000], the Raycast length limit.
  • TeamKit.attach defines Red and Blue with 8 slots each and SyncRoblox = true. It registers friendly-fire vetoes on Weapon.CanDamage itself.
  • RoundKit.service runs Lobby (waits for 2 players), Round (180 s) and Results (8 s).
  • A Weapon.Hit subscriber checks whether the victim’s Humanoid is dead and credits the shooter in session.Data.Kills, which Leaderstats shows and Leaderboards submits to the Kills board.
  • A small ArenaBoards service calls Boards:destroy() in stop(), 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.

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.attach defines Hex (a 6 s Curse that deals 3 damage each second) and Ward (20 s, immune to the Curse category).
  • SpellKit.service with MaxMana = 100 defines 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.attach defines 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.server meters DoubleJump at 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 publishes Obby.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, connects Touched for the instant kill, and adds each brick as a Zones volume with KeepQueryable = true so the brick stays solid and touchable as authored. The zone sweep exists because a client can delete a TouchInterest and 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.server meters AirStep at 2 charges with a 0.15 s cooldown; the client defines it with MoveKit.recipes.airStep({ Steps = 2 }).
  • The run timer starts at pad 1 and submits at pad 25 to the ascending BestTime board as whole milliseconds, because OrderedDataStores store integers and the default Leaderboards backend rejects fractional values. An ObbyBoards service flushes the board in stop().

Map requirements: a workspace.Checkpoints folder with numbered pads, and parts tagged KillBrick in Workspace.

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.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.