Skip to main content

Components, Signals & UI

A Pulse component is one TypeScript file = one feature. It owns its state (signals), its event subscriptions (on.*), and declares its UI widgets. rb build compiles every component into a single flat Lua file.


Component structure

// src/combat/SpeedHack.ts
defineComponent('SpeedHack', () => {
// 1. Signals — reactive state
const enabled = signal(false)
const speed = signal(16)

// 2. Internal state (non-reactive)
let cachedTarget: Model | null = null

// 3. Event hooks
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 UI widgets
return [
toggle('Speed Hack').bind(enabled),
slider('Walk Speed', { min: 16, max: 250 }).bind(speed),
]
})

Signals

Signals are reactive state variables. Anything subscribed to them updates automatically when they change.

const enabled = signal(false) // boolean
const speed = signal(16) // number
const mode = signal('Normal') // string

Read: call like a function — enabled(), speed()

Write: .set()enabled.set(true), speed.set(250)

Toggle: .toggle()enabled.toggle()

Transform: .update(fn)speed.update(v => v + 10)

Use a signal when the value drives a UI widget, another component reads it, or you want code to react when it changes. Use a plain let for everything else — loop handles, cached refs, counters.


on.* hooks

Each hook registers a Roblox event connection that Pulse tracks and disconnects automatically on re-run or destroy.

// Frame events
on.heartbeat(fn)
on.heartbeat({ when: enabled }, fn) // skip when enabled() === false
on.heartbeat({ when: enabled, every: 0.5 }, fn) // throttle to 0.5 s
on.renderStepped({ when: enabled }, fn) // before render
on.stepped(fn) // physics step

// Character events
on.respawn(fn) // CharacterAdded — most common
on.characterAdded(fn) // same as respawn
on.characterRemoving(fn)

// Input
on.inputBegan((input, gpe) => {
if (gpe) return // already handled by Roblox (chat, menus)
if (input.KeyCode === Enum.KeyCode.F) { ... }
})
on.inputEnded((input, gpe) => { ... })

// Signal change
on.signal(enabled, (v) => {
if (v) start() else stop()
})

// One-shot delay
on.after(2.5, () => { /* runs once, 2.5 s after load */ })

Character helpers

Always use these inside handlers — never cache character references between calls:

const h = _PulseGetHumanoid() // Humanoid | undefined
const hrp = _PulseGetHRP() // HumanoidRootPart | undefined
const char = _PulseGetChar() // Model | undefined
const alive = _PulseGetAlive() // boolean

// Always check before use
on.heartbeat({ when: enabled }, () => {
const h = _PulseGetHumanoid()
if (!h) return // dead, in vehicle, or not spawned
h.WalkSpeed = speed()
})

These read live from LocalPlayer.Character on every call — never stale after respawn.


UI widgets

Return an array of widget builders from defineComponent. Each binds bidirectionally to its signal:

return [
toggle('Speed Hack').bind(enabled),
toggle('Speed Hack', { default: true }).bind(enabled), // starts enabled

slider('Walk Speed', { min: 16, max: 250 }).bind(speed),
slider('FOV', { min: 1, max: 120, default: 90, suffix: '°' }).bind(fov),

dropdown('Mode', { options: ['Normal', 'Rage', 'Legit'] }).bind(mode),
multidropdown('Remove', { options: ['Trees', 'Rocks'] }).bind(selected),

button('Reset', () => speed.set(16)),
keybind('Activate', { default: 'F' }).bind(enabled),

separator(),
label('Advanced'),
]

{ default: true } on a toggle fires on.signal(enabled, ...) once at load — this is how features auto-start without user interaction.


Reacting to signal changes

on.signal(enabled, (v) => {
if (v) buildESP() else clearESP()
})

// Multiple signals — call for each
on.signal(enabled, rebuildESP)
on.signal(showNames, rebuildESP)

Handlers fire synchronously — keep them fast. Use task.spawn for any async work inside.


Re-apply on respawn

function apply() {
const h = _PulseGetHumanoid()
if (!h) return
h.WalkSpeed = enabled() ? speed() : 16
}

on.respawn(apply)
on.signal(enabled, apply)
on.signal(speed, apply)

When the player respawns on.respawn fires. It re-reads current signal values. No stale reference — _PulseGetHumanoid() always returns the fresh humanoid.


Start/stop loops

Standard pattern for polling features:

const loop = Pulse.Loop.new(1.0, () => {
// scan entities, update ESP, check conditions…
})

on.signal(enabled, (v) => {
if (v) loop.start() else loop.stop()
})

Cross-component access

Every component is a global by its name:

// From any other component:
SpeedHack.enabled.set(true)
const v = SpeedHack.speed()

SpeedHack.enabled.watch((v) => {
if (v) showIndicator()
})

Shared state — Pulse.Store

For state that genuinely belongs to multiple components:

// Define once (in the component that owns it)
Pulse.Store.define('LockedTarget', null)

// Write from anywhere
Pulse.Store.set('LockedTarget', target)

// React from anywhere
Pulse.Store.watch('LockedTarget', (t) => {
updateAimbot(t as Model)
})