Creating a Component
A Component is defined once with Component.new(), then watched from a Service's OwlStart. From that point on, Owl handles creating and destroying instances entirely on its own you only ever write the lifecycle and the custom methods.
Defining a Component
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Owl = require(ReplicatedStorage.OwlKnit.Owl)
local Component = require(Owl.OwlComponent)
-- 1. Define the Component type
local EnemyComponent = Component.new({
Tag = "Enemy", -- > // CollectionService tag
Type = "Server", -- > // "Server" | "Client" | "Shared"
Ancestors = { workspace }, -- > // ancestor whitelist (empty = accept everything)
})
-- 2. Define the lifecycle
function EnemyComponent:Construct()
-- > // Called when the tagged instance is created
-- > // Read Attributes, set up variables
self.Health = self.Instance:GetAttribute("MaxHealth") or 100
self.MaxHealth = self.Health
self.IsDead = false
end
function EnemyComponent:Start()
-- > // Called right after Construct, connect events here
-- > // self._trove is available and cleaned up automatically on destruction
self._trove:Add(self.Instance.Destroying:Connect(function()
print(self.Instance.Name, "destroyed.")
end))
end
function EnemyComponent:Destroy()
-- > // Custom cleanup (the _trove is destroyed automatically right after this runs)
print(self.Instance.Name, "component cleaned up.")
end
-- 3. Custom methods
function EnemyComponent:TakeDamage(amount: number)
if self.IsDead then return end
self.Health = math.max(0, self.Health - amount)
if self.Health <= 0 then
self.IsDead = true
self.Instance:Destroy()
end
end
function EnemyComponent:GetHealthPercent(): number
return self.Health / self.MaxHealth
end
-- 4. Start watching, from the Service
local EnemyService = Owl.CreateService({ Name = "EnemyService" })
function EnemyService:OwlStart()
-- > // Watch() starts the CollectionService monitoring.
-- > // Owl.Start() also calls it automatically if Type matches the current context.
EnemyComponent:Watch()
end
return EnemyServiceConstruct runs once, immediately when the tag is added this is where you read Attributes off the instance and initialize any state the rest of the Component needs. Start runs right after and is where you connect events; anything added to self._trove here is disconnected automatically the moment the Component is destroyed, so you rarely need to track connections manually. Destroy runs for any extraa cleanup beyond what the Trove already handles logging or side effects outside the instance itself.
Accessing a Component from elsewhere
-- > // From DamageService
local EnemyComponent = require(path.to.EnemyComponent)
-- > // Direct access to one instance's Component
local comp = EnemyComponent:Get(someInstance)
if comp then
comp:TakeDamage(25)
end
-- > // Every currently active Component of this type
for _, enemy in ipairs(EnemyComponent:GetAll()) do
enemy:TakeDamage(10)
end
-- > // Wait for a Component to be ready (Promise)
EnemyComponent:WaitFor(someInstance, 5)
:andThen(function(comp)
comp:TakeDamage(50)
end)
:catch(warn):Get(instance) returns nil if the instance isn't tagged (or its Component hasn't finished Construct yet) reach for :WaitFor(instance, timeout) instead when you know the tag was just added and there's a real chance you're asking before Construct has had a chance to run, rather than manually retrying :Get in a loop.
Where to go next
- Extensions - sharing
Construct/Start/Destroybehavior across multiple Component types. - API Reference - the full signature for
Component.new,Added,Removedand every method shown above.