Components
A component is the basic unit in Pulse. One .ts file = one feature = one component.
The mental model is SolidJS: reactive signals for state, on.* hooks for effects, and a return value that declares the UI.
The structure
defineComponent('SpeedHack', () => {
// 1. Signals — reactive state
const enabled = signal(false)
const speed = signal(16)
// 2. Local variables — non-reactive internal state
let lastFrame = 0
// 3. Event hooks — like createEffect scoped to a Roblox event
on.heartbeat({ when: enabled }, () => {
const h = _PulseGetHumanoid()
if (h) h.WalkSpeed = speed()
})
on.respawn(() => {
const h = _PulseGetHumanoid()
if (h) h.WalkSpeed = enabled() ? speed() : 16
})
// 4. Return the UI widgets
return [
toggle('Speed Hack').bind(enabled),
slider('Walk Speed', { min: 16, max: 250 }).bind(speed),
]
})
Signals
const enabled = signal(false) // boolean
const speed = signal(16) // number
const mode = signal('Normal') // string
Read by calling enabled(), write with enabled.set(true). See Signals for the full API.
Event hooks
Frame events
on.heartbeat(opts?, fn) // RunService.Heartbeat — every frame
on.renderStepped(opts?, fn) // RunService.RenderStepped — before render
on.stepped(opts?, fn) // RunService.Stepped — physics step
Options:
{ when: PulseSignal<boolean> } // only run when signal is truthy
{ every: number | PulseSignal<number> } // throttle in seconds
Character events
on.characterAdded(fn) // LocalPlayer.CharacterAdded
on.characterRemoving(fn) // LocalPlayer.CharacterRemoving
on.respawn(fn) // shorthand for CharacterAdded
Input events
on.inputBegan((input, gpe) => {
if (gpe) return // Roblox already handled it (chat, menus)
if (input.KeyCode === Enum.KeyCode.E) {
// E pressed
}
})
on.inputEnded((input, gpe) => { ... })
Signal change
on.signal(enabled, (v) => {
if (v) startLoop()
else stopLoop()
})
One-shot delay
on.after(2.5, () => {
// runs once, 2.5 seconds after load
})
Character helpers
Inside any on.* handler, use these globals:
const char = _PulseGetChar() // character Model | undefined
const hrp = _PulseGetHRP() // HumanoidRootPart | undefined
const h = _PulseGetHumanoid() // Humanoid | undefined
const alive = _PulseGetAlive() // boolean
These read live from LocalPlayer.Character on every call — never stale, never need to be refreshed.
on.heartbeat({ when: enabled }, () => {
const h = _PulseGetHumanoid()
if (!h) return // dead or no character
h.WalkSpeed = speed()
})
UI widgets
Return an array of widget builders from defineComponent:
return [
toggle('Speed Hack').bind(enabled),
slider('Walk Speed', { min: 16, max: 250 }).bind(speed),
dropdown('Mode', { options: ['Normal', 'Rage'] }).bind(mode),
button('Reset', () => speed.set(16)),
keybind('Activate', { default: 'F' }).bind(enabled),
separator(),
label('Advanced'),
]
See Building UI for the full widget reference.
Lifecycle
- Load —
defineComponentsetup function runs, signals are created,on.*hooks are registered - Running — hooks fire in response to Roblox events and signal changes
- Destroy —
_PulseDestroy()is called: all connections disconnect, all watchers unsubscribe
There is no explicit unmount hook. Cleanup is handled automatically by the framework when _PulseDestroy runs.
Accessing another component
Every component is a global by its name:
// From inside another component:
SpeedHack.enabled.set(true) // write its signal
const v = SpeedHack.speed() // read its signal
SpeedHack.enabled.watch((v) => { // subscribe to changes
...
})
What you cannot do
No require() — the output is a flat Lua file. No module system. Put shared helpers in globals.lua.
No async in signal watchers — on.signal fires synchronously. Don't await inside it. Use task.spawn for async work:
on.signal(enabled, (v) => {
if (v) {
task.spawn(() => {
// async work here
})
}
})
No Roblox method calls with dot notation — Roblox Instance methods need colon syntax in Lua. TSTL with noImplicitSelf compiles obj.method() as dot notation. Use the Pulse character helpers (_PulseGetChar, _PulseGetHumanoid, etc.) instead of calling methods on character objects directly.