Lua Class System
Create custom Lua classes with properties, events, inheritance, and network replication.
Introduction
StarForge provides a powerful class definition system that lets you create custom Lua classes with full engine integration. These classes can inherit from C++ engine types, define properties and events with automatic network replication, and integrate seamlessly with the engine's reflection system.
Basic Class Definition
Use class.define() to create a new class:
local MyClass = class.define("MyClass")
:extends("Instance")
:finalize()
return MyClassThis creates a basic class that inherits from Instance. The :finalize() call completes the class definition and returns the class table.
Adding Properties
Properties are declared using :addProperty(type, name, defaultValue):
local Player = class.define("Player")
:extends("Instance")
:addProperty("number", "Health", 100)
:addProperty("number", "MaxHealth", 100)
:addProperty("string", "DisplayName", "Player")
:addProperty("boolean", "IsAlive", true)
:finalize()Supported Property Types
| Type | Description | Example Default |
|---|---|---|
number | Floating-point number | 100, 3.14 |
string | Text string | "Hello" |
boolean | True/false value | true, false |
Instance | Reference to any Instance | nil |
Vector2 | 2D vector | nil |
Vector3 | 3D vector | nil |
Color | Color value | nil |
Adding Events
Events allow instances to notify listeners when something happens:
local Enemy = class.define("Enemy")
:extends("Instance")
:addProperty("number", "Health", 100)
:addEvent("OnDamaged")
:addEvent("OnDeath")
:finalize()
function Enemy:TakeDamage(amount)
self.Health = self.Health - amount
self.OnDamaged:Fire(amount)
if self.Health <= 0 then
self.OnDeath:Fire()
end
end
-- Usage:
local enemy = Enemy.new()
enemy.OnDamaged:Connect(function(amount)
print("Enemy took " .. amount .. " damage!")
end)
enemy.OnDeath:Connect(function()
print("Enemy defeated!")
end)Network Replication
Mark properties and events for automatic network synchronization:
local NetworkedPlayer = class.define("NetworkedPlayer")
:extends("Instance")
:addProperty("number", "Score", 0):replicates(true)
:addProperty("string", "TeamName", ""):replicates(true)
:addProperty("Vector3", "Position", nil):replicates(true)
:addEvent("OnScoreChanged"):replicates(true)
:finalize()When a replicated property changes on the server, it automatically syncs to all connected clients. Replicated events fired on the server broadcast to all clients.
Read-Only Properties
Prevent external modification with :readOnly():
local GameState = class.define("GameState")
:extends("Instance")
:addProperty("number", "CreatedTimestamp", 0):readOnly(true)
:addProperty("string", "GameId", ""):readOnly(true)
:finalize()The init() Method
Define an init() method for instance initialization:
local Inventory = class.define("Inventory")
:extends("Instance")
:addProperty("number", "MaxSlots", 20)
:finalize()
function Inventory:init()
-- Initialize private data (underscore prefix)
self._items = {}
self._slotCount = 0
-- Set up initial state
self.Name = "Inventory"
end
function Inventory:AddItem(item)
if self._slotCount >= self.MaxSlots then
return false
end
table.insert(self._items, item)
self._slotCount = self._slotCount + 1
return true
endThe init() method is called automatically after the instance is created.
Private Instance Data
Keys starting with underscore (_) are treated as private instance data:
function MyClass:init()
self._internalState = {} -- Private, not visible to other scripts
self._cachedValue = nil -- Private
self.PublicValue = 0 -- Public property (if defined)
endPrivate data:
- Is not accessible from outside the class
- Is not replicated over the network
- Is not serialized
- Provides a clean separation between internal state and public API
Inheritance
Inheriting from C++ Classes
Inherit from any reflected C++ class:
-- Inherit from UIFrame (C++ class)
local CustomButton = class.define("CustomButton")
:extends("UIFrame")
:addProperty("string", "ButtonText", "Click Me")
:addEvent("OnClicked")
:finalize()
function CustomButton:init()
-- Access inherited properties from UIFrame
self.Size = UDim2.new(0, 200, 0, 50)
self.BackgroundColor = Color.new(0.2, 0.4, 0.8)
-- Set up click handling
self.InputBegan:Connect(function(input)
if input.InputType == "MouseButton1" then
self.OnClicked:Fire()
end
end)
endInheriting from Lua Classes
Create class hierarchies entirely in Lua:
-- Base class
local Entity = class.define("Entity")
:extends("Instance")
:addProperty("number", "Health", 100)
:addProperty("boolean", "IsAlive", true)
:addEvent("OnDeath")
:finalize()
function Entity:TakeDamage(amount)
self.Health = self.Health - amount
if self.Health <= 0 and self.IsAlive then
self.IsAlive = false
self.OnDeath:Fire()
end
end
-- Derived class
local Character = class.define("Character")
:extends("Entity") -- Inherits from Entity (Lua class)
:addProperty("number", "Speed", 10)
:addProperty("number", "JumpHeight", 5)
:finalize()
function Character:init()
self._velocity = Vector3.new(0, 0, 0)
end
function Character:Move(direction)
self._velocity = direction * self.Speed
endCreating Instances
Use .new() to create instances:
local player = Player.new()
player.DisplayName = "Alice"
player.Health = 100
-- Or use the global new() function
local enemy = new("Enemy")
enemy.Health = 50Complete Example
Here's a complete example of a game entity system:
-- Entity.module.lua
local Entity = class.define("Entity")
:extends("Part")
:addProperty("number", "Health", 100):replicates(true)
:addProperty("number", "MaxHealth", 100)
:addProperty("string", "EntityType", ""):replicates(true)
:addProperty("Instance", "Target", nil)
:addEvent("OnDamaged"):replicates(true)
:addEvent("OnHealed")
:addEvent("OnDeath"):replicates(true)
:finalize()
function Entity:init()
self._isDead = false
self._damageMultiplier = 1.0
self._healMultiplier = 1.0
self.Name = "Entity"
end
function Entity:TakeDamage(amount, source)
if self._isDead then return end
local actualDamage = amount * self._damageMultiplier
self.Health = math.max(0, self.Health - actualDamage)
self.OnDamaged:Fire(actualDamage, source)
if self.Health <= 0 then
self._isDead = true
self.OnDeath:Fire(source)
end
end
function Entity:Heal(amount)
if self._isDead then return end
local actualHeal = amount * self._healMultiplier
local oldHealth = self.Health
self.Health = math.min(self.MaxHealth, self.Health + actualHeal)
if self.Health > oldHealth then
self.OnHealed:Fire(self.Health - oldHealth)
end
end
function Entity:IsDead()
return self._isDead
end
return EntityUsing the entity:
local Entity = require("Game/Entity")
-- Create an enemy
local enemy = Entity.new()
enemy.EntityType = "Goblin"
enemy.MaxHealth = 50
enemy.Health = 50
-- Listen for events
enemy.OnDamaged:Connect(function(amount, source)
print(enemy.EntityType .. " took " .. amount .. " damage!")
end)
enemy.OnDeath:Connect(function(source)
print(enemy.EntityType .. " was defeated!")
enemy:Destroy()
end)
-- Deal damage
enemy:TakeDamage(25, player)
enemy:TakeDamage(30, player) -- This kills the enemyMethod Chaining Reference
| Method | Description |
|---|---|
:extends(className) | Set the base class (C++ or Lua) |
:addProperty(type, name, default) | Add a property to the class |
:addEvent(name) | Add an event to the class |
:replicates(bool) | Mark last property/event as replicated |
:readOnly(bool) | Mark last property as read-only |
:finalize() | Complete definition, returns class table |
Best Practices
- Always call
:finalize()- The class isn't usable until finalized - Use private data for internal state - Prefix with underscore
- Define
init()for setup - Don't rely on property defaults for complex initialization - Return the class from module scripts - Makes it easy to require
- Use events for notifications - Don't poll for changes
- Only replicate what's needed - Network bandwidth is precious
- Inherit from the most specific base - If you need UI, inherit from a UI class
Type Checking with typeof()
Use typeof() to check instance types:
local entity = Entity.new()
print(typeof(entity)) -- "Entity"
print(entity:IsA("Entity")) -- true
print(entity:IsA("Part")) -- true (Entity extends Part)
print(entity:IsA("Instance")) -- true (Part extends Instance)Module Script Pattern
The recommended way to organize Lua classes:
Content/
Scripts/
Game/
Entity.module.lua
Character.module.lua
Inventory.module.lua
UI/
HealthBar.module.lua
Minimap.module.luaEach module script exports a single class:
-- Character.module.lua
local Entity = require("Game/Entity")
local Character = class.define("Character")
:extends("Entity")
:addProperty("string", "CharacterName", "")
:finalize()
return CharacterThen require it elsewhere:
local Character = require("Game/Character")
local player = Character.new()This keeps your codebase organized and makes dependencies clear.