Patterns
Good module architecture is mostly about answering two questions:
- which feature owns this state or callback?
- when does the owned resource stop existing?
The compiler already handles bundling and name scoping. You do not need a
framework, dependency container or one giant main.ts.
One file for one cohesive feature
Section titled “One file for one cohesive feature”A small always-on feature can register itself at the top level:
import { logger } from "@killscript/sdk/client";
const log = logger("DamageHint");
Agents.OnLocalPlayerReceivedDamage((event) => { NotificationController.ShowHint( `Received ${event.Damage} damage`, 1.5, ); log.debug(`Damage tick: ${event.DamageTick}`);});client/main.ts only imports it. State used solely by this feature stays in
the same file and is not exported.
Give a toggleable feature a scope
Section titled “Give a toggleable feature a scope”Anything created on enable must be released on disable. A scope makes the ownership visible:
import { Keyboard, type ClientScope, defineControls, scheduler, scope, visuals,} from "@killscript/sdk/client";
const controls = defineControls("positionMarker", { toggle: Keyboard.F6,});
let active: ClientScope | undefined;
controls.toggle.onPressed(() => { if (active !== undefined) { active.dispose(); active = undefined; return; }
const local = Agents.GetLocalAgent(); if (local === undefined) return;
const next = scope(); const marker = next.world(visuals.overlay({ position: local.Movement.Position, size: new Vector3(0.6, 0.02, 0.6), color: new Color(0, 1, 1, 0.5), })); if (marker === undefined) { next.dispose(); return; }
next.using(scheduler.every(0.25, () => { const current = Agents.GetLocalAgent(); if (current !== undefined) { marker.SetPosition(current.Movement.Position); } })); active = next;});The variable answers whether the feature is active. The scope answers what must be cleaned when it stops. See Feature lifecycle for disposal order and server scopes.
Share contracts, not runtime ownership
Section titled “Share contracts, not runtime ownership”Good shared files contain data that means the same thing in both runtimes:
export interface MarkerState { readonly enabled: boolean; readonly x: number; readonly y: number; readonly z: number;}Client UI state remains client-owned. Server decision state remains
server-owned. Serialize plain values through a
typed network channel instead of trying to
share an Agent, callback or mutable module variable.
Prefer direct data flow
Section titled “Prefer direct data flow”For most features, these forms are enough:
control → feature state → client presentationcontrol → network intent → Reflex state → network result → presentationgame event → feature state → presentation/logAvoid a global event bus when a direct function call or one typed network message shows the relationship more clearly.
Practical rules
Section titled “Practical rules”- keep entry files as lists of enabled feature imports;
- register top-level settings and controls near their owner;
- keep a variable private until another file genuinely needs it;
- use a scope for subscriptions, timers and visuals created dynamically;
- make client/Reflex ownership obvious from the directory and SDK import;
- use raw typed game globals when no SDK helper improves the call;
- remove a feature by deleting its one entry import and its owned files.
When a file starts handling several unrelated controls, several independent states or multiple cleanup lifecycles, split it by behavior rather than by arbitrary categories such as “all callbacks” and “all variables.”