Skip to content

Scopes and feature lifecycle

A module is loaded once, but an individual feature may be enabled and disabled many times. Creating a new timer or event subscription on every enable without cancelling the old one causes duplicate callbacks. World visuals can remain on screen for the same reason.

A scope is a small container for everything owned by one active feature. Create it when the feature starts, register its resources, and call dispose() when the feature stops.

import { scheduler, scope } from "@killscript/sdk/client";
const active = scope();
active.using(scheduler.every(0.25, () => {
// Update the HUD from current game state.
}));
active.defer(() => print("HUD feature stopped"));
// Later, when the feature is disabled:
active.dispose();

using() accepts an EventSubscription, such as the result of a scheduler or event watcher. defer() stores an ordinary cleanup callback for state that the SDK cannot reset automatically.

MethodWhat it owns or does
using()A subscription or task with Cancel()
defer()A cleanup callback supplied by your code
world()A client object created through WorldVisuals
dispose()Releases every resource stored by the scope
isDisposed()Reports whether this scope has already been disposed

Keep the current scope next to the feature state. Each activation receives a fresh scope; each deactivation disposes exactly that instance:

import {
type ClientScope,
scheduler,
scope,
visuals,
} from "@killscript/sdk/client";
let active: ClientScope | undefined;
export function enableMarker(getPosition: () => Vector3 | undefined): void {
if (active !== undefined) return;
const position = getPosition();
if (position === undefined) return;
const next = scope();
const marker = next.world(visuals.overlay({
position: position,
size: new Vector3(0.8, 0.02, 0.8),
color: new Color(0, 1, 1, 0.55),
}));
if (marker === undefined) {
next.dispose();
return;
}
next.using(scheduler.every(0.25, () => {
const current = getPosition();
if (current !== undefined) marker.SetPosition(current);
}));
active = next;
}
export function disableMarker(): void {
active?.dispose();
active = undefined;
}

Client scopes add world(). It returns the same object, but also remembers to remove it through WorldVisuals during disposal. This is useful for visuals that should live exactly as long as a feature.

When dispose() is called, the scope performs these steps once:

  1. cancels tracked subscriptions in reverse order;
  2. removes tracked client world objects in reverse order;
  3. runs deferred callbacks in reverse order.

Calling dispose() again is safe and does nothing. Adding a subscription, world object or callback to an already disposed scope cleans it immediately, which prevents a late asynchronous result from leaking a resource.

Reflex exposes the same scope() and the same using()/defer() behavior, but not world() because WorldVisuals is client-only.

See Scheduling for cancellable jobs and World visuals for objects a client scope can own.