Introduction
Hi. The purpose of this module is to remove some of the boilerplate involved with using Instance Attributes in scripts and custom metatables. Instead of using :GetAttribute()
and :SetAttribute()
everywhere, this module allows you to get and set attributes more conventionally while also reflecting any outside changes to the Attribute into the class state. This module is also strictly-typed so you get all of the code-completion, hints, and other weird stuff.
Usage & Examples
Creating an Attribute
After being required, a new Attribute can be created with .new(Instance, Name, Value)
.
local Attribute = require(ReplicatedStorage.Attribute)
local Health = Attribute.new(workspace.Part, "Health", 20)
In this example, an Attribute is being created on workspace.Part
, with the name "Health"
and the initial value set to 20
. Passing an instance that already has an Attribute with a matching name and value type will also not set the initial value, so you can still use this with Attributes that have been created in Studio while preserving their set value.
Getting and setting Attributes
You can get and set the Attribute’s value by only using the .Value
property! Any assignments to this property implicitly set the actual value of the Instance’s Attribute and throw an error if the value assigned does not match the type of the Attribute.
local Health = Attribute.new(workspace.Part, "Health", 20)
print(Health.Value) -- Prints 20.
Health.Value = 100 -- Trying to assign a boolean to this value would error.
print(Health.Value) -- Now prints 100!
Detecting changes
This class has a :GetChangedSignal()
method that simply returns the Instance:GetAttributeChangedSignal(Name)
signal of the Instance so you can hook into changes. You could also use the .Changed
property.
Health.Changed:Connect(function()
local Humanoid = workspace.Part:FindFirstChildWhichIsA("Humanoid")
if not Humanoid then
return
end
Humanoid.MaxHealth += Health.Value
end)
Everything together
I’ve used this module personally for a while now, mostly in custom classes and Knit Components. Here’s sort of what that would look like.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Attribute = require(ReplicatedStorage.Shared.Attribute)
local Stats = {}
Stats.__index = Stats
function Stats.new(Entity)
local self = setmetatable({}, Stats)
self.Instance = Entity
self.Health = Attribute.new(self.Instance, "Health", 100)
self.Health:GetChangedSignal():Connect(function()
local Humanoid = self.Instance:FindFirstChildWhichIsA("Humanoid")
if not Humanoid then
return
end
Humanoid.MaxHealth += self.Health.Value
end)
return self
end
function Stats:ReduceHP(Amount)
self.Health.Value -= Amount
end
return Stats
Assets
The module is available on the Marketplace here. The code is just under 70 lines so I’ll also post it below.
Source
--!strict
local Attribute = {}
type self<T> = {
Instance: Instance,
Name: string,
Value: T,
Changed: RBXScriptSignal,
GetChangedSignal: (self: self<T>) -> RBXScriptSignal
} & {[any]: any}
export type Attribute<T> = typeof(
setmetatable({}::self<T>, Attribute)
)
function CompareTypes(Value1: unknown, Value2: unknown): boolean
return typeof(Value1) == typeof(Value2)
end
function Attribute.new<T>(Instance: Instance, Name: string, Default: T): Attribute<T>
local self = setmetatable({}::self<T>, Attribute)
self.Instance = Instance
self.Name = Name
-- https://devforum.roblox.com/t/attribute-module/2784809/8
self.Changed = self.Instance:GetAttributeChangedSignal(self.Name)
self.Inner = Default
self.GetChangedSignal = function(self: self<T>)
return self.Changed
end
local ExistingAttribute = Instance:GetAttribute(Name)
if not CompareTypes(Default, ExistingAttribute) then
Instance:SetAttribute(Name, Default)
end
return self
end
function Attribute:__newindex(Index: unknown, Value: any): ()
if Index == "Value" then
if not CompareTypes(self.Inner, Value) then
error(
`Attempt to set attribute of type '{typeof(self.Inner)}' to type '{typeof(Value)}'`,
2
)
return
end
self.Instance:SetAttribute(self.Name, Value)
return
end
rawset(self, Index, Value)
end
function Attribute.__index<T>(self: self<T>, Index: unknown): T
if Index == "Value" then
return self.Instance:GetAttribute(self.Name)
end
return rawget(self, Index)
end
return Attribute