Moud

Components#

A component is behaviour you attach to every instance that wears a tag. You write it once, and it starts on each tagged instance when the instance appears and cleans up when the instance loses the tag or is destroyed. Tag twenty parts spinner in the editor and one component spins all of them, including the ones a script clones in later.

An obby's spinning bars are a good fit. Each bar is tagged spinner in the editor, with a speed attribute in degrees a second, and hurts whoever it hits:

luau
-- server
local component = require("@moud/component")

local Spinner = component.new({
    tag = "spinner",
    damage = 20,

    start = function(self)
        self.home = self.instance.cframe
        self.angle = 0
        self.speed = math.rad(self.instance:getAttribute("speed") or 90)
        self:track(self.instance.touched:connect(function(other)
            local body = other:firstAncestorOfClass("Character")
            if body then
                body.humanoid:takeDamage(self.damage)
            end
        end))
    end,

    step = function(self, dt)
        self.angle += self.speed * dt
        self.instance.cframe = self.home * cframe.angles(0, self.angle, 0)
    end,

    stop = function(self)
        if self.instance.parent then
            self.instance.cframe = self.home
        end
    end,
})
  • start reads the bar's own attribute, so a fast bar and a slow one share the component.
  • The touched connection is tracked, so it is disconnected when the bar stops. Without track, a bar that lost its tag would keep hurting players.
  • stop puts a bar that lost its tag back the way it started. A destroyed bar has no parent and is left alone.

The definition#

Field
tag the tag the component follows
start(self) runs when an instance with the tag appears, or gains the tag, in its own thread so it can wait
stop(self) runs when the instance loses the tag, is destroyed or moves into storage
step(self, dt) runs on every game.stepped for each running instance
renderStep(self, dt) runs every frame on a client, for each running instance
where(instance) return false to leave an instance out
inStorage true to also run on instances inside ServerStorage, ReplicatedStorage, StarterPack or StarterCharacterScripts

Any other field, like damage above, is shared by every object of the component, and functions you add become methods: self:explode().

The object#

Each tagged instance gets its own object, self in every function:

  • self.instance is the instance.
  • self.running is true from the start until it stops.
  • self:track(thing) keeps thing and cleans it up when the object stops, newest first. A connection is disconnected, an instance destroyed, a function called, a task handle cancelled, and anything with cancel, disconnect or destroy has that called. It hands thing back, so it wraps the line that makes it.
  • Fields you set on self are that instance's own state.

If start is still waiting when the instance goes away, its thread is stopped.

The component#

What component.new hands back follows every object it runs:

  • get(instance) is the object for that instance, or nil.
  • all() lists every running object.
  • started and stopped fire with the object.
  • destroy() stops every object and stops following the tag.

Fields on an object can be changed from outside. A round that ends with every bar stopped:

luau
-- server
local function freezeSpinners()
    for _, spinner in Spinner:all() do
        spinner.speed = 0
    end
end

Where it runs#

A component runs on the side that made it. Tags replicate, so a server script can tag a part and a client component can react to it: the server decides which doors are locked, and the client component on locked draws the padlock and plays the sound.

Instances inside a storage container are kept, not in the world, so a template coin in ServerStorage does not start a coin component. A copy cloned out of storage starts one, and an instance moved out of storage starts one too. Set inStorage = true when you want the templates as well.