# Learning Lua by Building a Roblox Guardian
Last weekend, my girlfriend and I were immersed in a Roblox gaming session when she presented an intriguing challenge: "Wouldn't it be cool if we could create something that follows us around in the game?" This question sparked my curiosity and presented the perfect opportunity to dive into Lua programming and Roblox scripting. I'm also currently watching [Mythic Quest](https://tv.apple.com/us/show/mythic-quest/umc.cmc.1nfdfd5zlk05fo1bwwetzldy3) new season which is a sitcom about a group of people who work at a gaming company. Also you may be familiar that vibecoding games with Three.js is very popular these days on [X](https://x.com/search?q=Three.js%20game&src=typed_query&f=top).
In this tutorial, I'll walk you through how we created a companion cube that loyally follows your character throughout any Roblox world and serves as a digital guardian.
## The History and Purpose of Lua
Before we dive into the code, let's understand what makes Lua special. Lua was created in 1993 at PUC-Rio (Pontifical Catholic University of Rio de Janeiro) in Brazil by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes. It was developed as a solution for a partnership between the university and Petrobras, the Brazilian oil company, which needed portable and customizable tools.
### A Scripting Language by Design
Lua was never intended to replace system programming languages like C or C++. Instead, it was designed as a scripting language to extend applications. Think of it this way: the core of your system (the "kernel") is written in a compiled language for speed and efficiency, while Lua provides scripting capabilities on top of that system.
This approach offers several advantages:
- Non-programmers can customize applications without touching the core code
- The core system remains protected from scripting errors
- Complex, performance-intensive tasks are handled by the system language, while high-level logic is managed through Lua
### Why Lua Became Popular
Lua gained international recognition around 1994-1996 through publications in Dr. Dobb's Journal. Its popularity exploded in 1997 when LucasArts adopted it for the game Grim Fandango, replacing their internal scripting system. The gaming industry quickly embraced Lua for its simplicity, portability, and small footprint - at just around 9,000 lines of C code for the entire language.
Today, Lua powers scripting in numerous popular games including Angry Birds, World of Warcraft, and Roblox. It's also used in applications like Adobe Lightroom, security tools like Wireshark and Snort, and even embedded systems and IoT devices.
### Key Features of Lua
- **Portability**: Written in standard ANSI C, Lua runs on virtually any device with a C compiler
- **Small size**: The entire implementation is compact enough for resource-constrained environments
- **Simple syntax**: Easy to learn, even for non-programmers
- **Extensibility**: Designed to be embedded in other applications
In the context of Roblox, Lua serves as the scripting layer on top of the Roblox engine. This allows developers and players to create game logic without having to understand or modify the underlying engine code.
### Lua's Unique Approach to Programming
What truly sets Lua apart from other scripting languages is its elegant simplicity combined with powerful features:
- **Tables as the universal data structure**: Unlike most languages that have arrays, dictionaries, objects, etc., Lua uses a single construct - tables - for everything. A table can act as an array, a dictionary, an object, or a combination of these.
```lua
-- Table as an array
local fruits = {"apple", "banana", "orange"}
print(fruits[2]) -- Outputs: banana (Lua arrays are 1-indexed)
-- Table as a dictionary
local person = {name = "Alex", age = 25, occupation = "Developer"}
print(person.name) -- Outputs: Alex
-- Table as an object with methods
local calculator = {
value = 0,
add = function(self, num) self.value = self.value + num end,
subtract = function(self, num) self.value = self.value - num end
}
calculator:add(5) -- The colon syntax automatically passes 'self'
print(calculator.value) -- Outputs: 5
```
- **First-class functions**: Functions in Lua are values that can be stored in variables, passed as arguments, and returned from other functions.
- **Coroutines**: Lua supports cooperative multitasking through coroutines, allowing you to pause and resume execution at specific points.
## The Problem: Creating a Following Companion
While playing Roblox, our requirements are to create a companion object:
- Always follow her character
- Maintain a consistent distance
- Move smoothly and naturally
- Look visually appealing
- Serve as a digital guardian that could look after her character when I'm not around
After some research and experimentation, I realized this could be accomplished with Roblox's physics engine using BodyPosition and BodyGyro—powerful tools that let you control how objects move and orient themselves in the Roblox world.
### Understanding Roblox's Physics Constraints
Before diving into the code, it's important to understand the physics tools we'll be using:
- **BodyPosition**: A physics constraint that applies a force to move a part toward a target position
- `MaxForce`: Determines how much force can be applied (higher values = faster movement)
- `Position`: The target position the part will try to reach
- `P`: Proportional gain - affects how aggressively the part moves toward the target (higher = more "springy")
- `D`: Damping - affects how quickly oscillations are reduced (higher = less overshooting)
- **BodyGyro**: Controls the orientation of a part
- `MaxTorque`: How much rotational force can be applied
- `CFrame`: The target orientation
- `P` and `D`: Similar to BodyPosition, affect the rotation behavior
> ⚠️ Physics Parameters Can Be Tricky
When adjusting physics parameters like MaxForce and P values, small changes can lead to dramatically different behavior. I once increased the MaxForce value too much and watched in horror as my companion cube launched my character into the stratosphere! Start with conservative values and increase gradually.
These physics constraints work based on PID controllers (Proportional-Integral-Derivative), which is why they have P, I, and D properties. For our companion follower, we'll mainly adjust the P value to fine-tune the movement.
## Solution Approach
Here's how we'll create our companion follower:
1. Create a companion cube part
2. Add physical properties to make it follow properly
3. Create a script to control its movement
4. Apply the proper physics forces
5. Test and refine
## Step 1: Setting Up Your Companion Object
> **Note:** This tutorial uses Lua 5.1, which is the version that Roblox Studio implements. Some features may not work in other Lua versions.
First, we need to create the object that will follow our character:
```lua
-- Create a simple cube part
local companion_cube = Instance.new("Part") -- Creates a new Part instance
companion_cube.Name = "CompanionCube" -- Gives the part a descriptive name
companion_cube.Parent = workspace -- Places the part in the workspace (game world)
companion_cube.Size = Vector3.new(2, 2, 2) -- Sets the size to 2x2x2 studs
companion_cube.Position = Vector3.new(0, 10, 0) -- Starting position above the terrain
companion_cube.Anchored = false -- Allows physics to affect the part
companion_cube.CanCollide = false -- Prevents collision with other objects
companion_cube.Massless = true -- Makes it ignore physics mass calculations
```
For our project, I decided to recreate the iconic companion cube from Portal. To give it that distinctive look, you can add textures using decals:
```lua
-- Add textures to each face (simplified example)
local faces = {"Front", "Back", "Top", "Bottom", "Left", "Right"} -- Define all cube faces
for _, face in pairs(faces) do -- Loop through each face
local decal = Instance.new("Decal") -- Create a new decal for the current face
decal.Face = Enum.NormalId[face] -- Set which face the decal will cover
decal.Texture = "rbxassetid://1234567" -- Replace with actual texture ID from Roblox catalog
decal.Parent = companion_cube -- Attach the decal to our companion cube
end
```
> ⚠️ Naming Conventions Matter
>
> When working with complex scripts that might grow over time, establishing clear naming conventions from the start will save you hours of debugging later. I use prefixes like "companion" for all objects related to our follower to make them easily identifiable in the Explorer panel. Also will stick to snake_case convention for variable names.
### Creating a More Complex Shape
While a simple cube works well, you might want to create a more complex shape. You can use Roblox's built-in parts or create a model from multiple parts. For our guardian companion, adding visual elements that suggest protection can enhance its purpose:
```lua
-- Creating a compound shape (example: robot companion)
local robot = Instance.new("Model")
robot.Name = "GuardianCompanion"
robot.Parent = workspace
-- Create the body
local body = Instance.new("Part")
body.Name = "Body"
body.Size = Vector3.new(2, 3, 1.5)
body.Position = Vector3.new(0, 10, 0)
body.Parent = robot
-- Create the head
local head = Instance.new("Part")
head.Name = "Head"
head.Shape = Enum.PartType.Ball
head.Size = Vector3.new(1.5, 1.5, 1.5)
head.Position = Vector3.new(0, 12, 0)
head.Parent = robot
-- Create a weld to connect the head to the body
local weld = Instance.new("WeldConstraint")
weld.Part0 = body
weld.Part1 = head
weld.Parent = body
-- Set the primary part (important for models)
robot.PrimaryPart = body
```
## Step 2: Creating the Follow Script
Now for the fun part! We need to create a script that makes our companion cube follow the character. Let's add a script inside the companion cube:
> **Performance Note:** While Lua in Roblox is optimized for game development, infinite loops can significantly impact performance if not implemented correctly. The script below uses a `while true do` pattern with a `wait()` call to prevent the script from consuming excessive CPU resources. Without the `wait()`, this would create a "tight loop" that could freeze or crash the game.
```lua
-- Place this script inside your companion cube
local follower_part = script.Parent -- References the part this script is inside
local master_name = "YourPlayerName" -- Change this to your player's username
-- Create physics objects needed for movement
local follower_position = Instance.new("BodyPosition") -- Creates a force that moves the part to a target position
follower_position.Parent = follower_part -- Attaches the BodyPosition to our companion cube
follower_position.MaxForce = Vector3.new(30000, 30000, 30000) -- How much force it can use to reach target (higher = faster)
follower_position.P = 3000 -- Proportional term - affects how aggressively it moves (higher = more "springy")
local follower_body_gyro = Instance.new("BodyGyro") -- Creates a force that rotates the part to a target orientation
follower_body_gyro.Parent = follower_part -- Attaches the BodyGyro to our cube
follower_body_gyro.MaxTorque = Vector3.new(900000, 900000, 900000) -- How much rotational force to apply
follower_body_gyro.P = 3000 -- Affects rotation responsiveness
-- Define the offset position for the follower
local follow_position = Vector3.new(0, 5, 0) -- This will make it float 5 studs above the player
```
### Fine-Tuning the Physics Parameters
The default values work well, but here's how different physics parameters affect your companion's behavior:
| Parameter | Low Value Effect | High Value Effect | Recommended Range |
|-----------|------------------|-------------------|-------------------|
| MaxForce | Slow movement, struggles against gravity | Fast, sometimes jerky movement | 10000-50000 |
| P Value | Sluggish, slow to respond | Springy, may overshoot | 1000-5000 |
| D Value | Bouncy, oscillates | Rigid, dampens quickly | 500-1000 |
You can experiment with these values to get different movement styles:
```lua
-- For a slower, heavier companion
follower_position.MaxForce = Vector3.new(15000, 15000, 15000) -- Lower force = slower movement
follower_position.P = 1500 -- Lower P value = less responsive, smoother following
follower_position.D = 800 -- Higher D value = more dampening, less overshooting
-- For a quick, agile companion
follower_position.MaxForce = Vector3.new(45000, 45000, 45000) -- Higher force = faster movement
follower_position.P = 4500 -- Higher P value = more responsive, quicker to target
follower_position.D = 600 -- Lower D value = less dampening, more bouncy
```
## Step 3: Adding the Main Logic Loop
Here's where we add the logic that continuously updates our companion's position:
```lua
-- Main movement loop
while true do -- Creates an infinite loop that runs until the script is stopped
-- Wait a frame to prevent script overload
wait() -- Yields the thread to prevent excessive CPU usage (crucial for performance)
-- Find the master player
local master = workspace:WaitForChild(master_name) -- Waits until the named player appears in workspace
local player = game.Players:FindFirstChild(master_name) -- Gets the player object if it exists
if master and master:FindFirstChild("HumanoidRootPart") then -- Check if player character is fully loaded
-- Get the master's position
local master_position = master.HumanoidRootPart.Position -- The character's current position
-- Define where the follower should stop
local stop_at_position = (follower_part.Position - master_position).magnitude -- Calculate distance to player
-- Update the follower's target position
follower_position.Position = master_position + follow_position -- Set target to offset from player
-- Update the follower's orientation
follower_body_gyro.CFrame = master.HumanoidRootPart.CFrame -- Match player's facing direction
-- Check master's health for guardian behavior
local humanoid = master:FindFirstChild("Humanoid") -- Get the character's Humanoid component
if humanoid and humanoid.Health < humanoid.MaxHealth * 0.5 then -- Check if health is below 50%
-- Health is below 50%, position the guardian in front to protect
local look_vector = master.HumanoidRootPart.CFrame.LookVector -- Get the direction player is facing
follower_position.Position = master_position + (look_vector * 3) + Vector3.new(0, 3, 0) -- Position in front
end
end
end
```
### Adding Distance-Based Behavior
Let's enhance the script to make our companion behave differently based on how far it is from the player and act as a guardian when needed:
```lua
-- Main movement loop with distance-based behavior and guardian features
while true do
wait() -- Yield to prevent excessive CPU usage
local master = workspace:WaitForChild(master_name) -- Find the player in the world
if master and master:FindFirstChild("HumanoidRootPart") then -- Make sure character is fully loaded
local master_position = master.HumanoidRootPart.Position -- Get current player position
local distance = (follower_part.Position - master_position).magnitude -- Calculate distance to player
local humanoid = master:FindFirstChild("Humanoid") -- Get the player's Humanoid component
-- Check for nearby threats (potentially expensive operation - only done once per frame)
local nearby_threats = false
for _, part in pairs(workspace:GetChildren()) do -- Loop through all objects in workspace
if part:IsA("Model") and part:FindFirstChild("Humanoid") and part ~= master then -- Find characters that aren't the player
local threat_distance = (part:GetPrimaryPartCFrame().Position - master_position).magnitude
if threat_distance < 10 then -- Check if they're within 10 studs
nearby_threats = true
break -- Exit loop early once we find any threat (optimization)
end
end
end
-- Adjust follow behavior based on distance and threats
if nearby_threats and humanoid then
-- Guardian mode - position between player and threat
local threats = {}
for _, part in pairs(workspace:GetChildren()) do -- This loop could be optimized further
if part:IsA("Model") and part:FindFirstChild("Humanoid") and part ~= master then
local threat_distance = (part:GetPrimaryPartCFrame().Position - master_position).magnitude
if threat_distance < 10 then
table.insert(threats, part:GetPrimaryPartCFrame().Position)
end
end
end
if #threats > 0 then
-- Position between player and closest threat
local threat_pos = threats[1]
local guardian_pos = master_position + ((threat_pos - master_position).unit * -3) + Vector3.new(0, 3, 0)
follower_position.Position = guardian_pos
-- Face toward the threat for protective stance
local look_direction = (threat_pos - follower_part.Position).unit
local look_c_frame = CFrame.new(follower_part.Position, follower_part.Position + look_direction)
follower_body_gyro.CFrame = look_c_frame
end
elseif distance > 30 then
-- Too far away - teleport closer (for when player teleports or respawns)
follower_part.CFrame = CFrame.new(master_position + follow_position)
wait(0.5) -- Give time to stabilize after teleport
elseif distance > 15 then
-- Far but catching up - move faster
follower_position.MaxForce = Vector3.new(50000, 50000, 50000) -- Temporarily increase force
follower_position.Position = master_position + follow_position
else
-- Close enough - normal follow behavior
follower_position.MaxForce = Vector3.new(30000, 30000, 30000) -- Reset to normal force
follower_position.Position = master_position + follow_position
-- Regular orientation - look where the player is looking
follower_body_gyro.CFrame = master.HumanoidRootPart.CFrame
end
-- Guardian behavior when player's health is low
if humanoid and humanoid.Health < humanoid.MaxHealth * 0.5 then
-- Flash a warning light when health is low
if not warning_light then
warning_light = Instance.new("PointLight")
warning_light.Color = Color3.fromRGB(255, 0, 0)
warning_light.Range = 10
warning_light.Parent = follower_part
end
-- Pulse the light (using tick() for timing)
if tick() % 1 < 0.5 then
warning_light.Brightness = 1
else
warning_light.Brightness = 0.3
end
elseif warning_light then
-- Remove warning light when health recovers
warning_light:Destroy()
warning_light = nil
end
end
end
```
## Step 4: Making it More Flexible
Instead of hardcoding the master's name, we can add a StringValue object to make our script more flexible:
```lua
-- In your companion cube, add a StringValue
local master_name_value = Instance.new("StringValue")
master_name_value.Name = "MasterName"
master_name_value.Value = "YourPlayerName" -- Change this to your player's name
master_name_value.Parent = follower_part
-- Then in your script, replace the hardcoded name with:
local master_name = follower_part:WaitForChild("MasterName").Value
```
This way, you can easily change who the cube follows by modifying the StringValue without editing the script.
### Creating a Companion Configuration System
Let's go a step further and create a complete configuration system that lets players customize their companions:
```lua
-- Create a configuration module
local CompanionConfig = {}
-- Set up the companion with default or custom settings
function CompanionConfig.setup(companion, settings)
settings = settings or {}
-- Create configuration values with defaults
local config = {
follow_height = settings.follow_height or 5,
follow_distance = settings.follow_distance or 3,
follow_speed = settings.follow_speed or 30000,
rotation_speed = settings.rotation_speed or 3000,
enable_particles = settings.enable_particles ~= false, -- Default to true
enable_interaction = settings.enable_interaction ~= false, -- Default to true
max_distance = settings.max_distance or 30,
teleport_threshold = settings.teleport_threshold or 50
}
-- Store configuration on the companion
for name, value in pairs(config) do
local config_value
-- Choose the appropriate value type
if type(value) == "number" then
config_value = Instance.new("NumberValue")
elseif type(value) == "boolean" then
config_value = Instance.new("BoolValue")
else
config_value = Instance.new("StringValue")
end
config_value.Name = name
config_value.Value = value
config_value.Parent = companion
end
return config
end
return CompanionConfig
```
Then in your main script:
```lua
-- Using the configuration system
local CompanionConfig = require(game.ReplicatedStorage.CompanionConfig)
-- Setup companion with custom settings
local config = CompanionConfig.setup(follower_part, {
follow_height = 4,
follow_distance = 5,
follow_speed = 40000,
enable_particles = true
})
-- Use the configuration in your script
follower_position.MaxForce = Vector3.new(config.follow_speed, config.follow_speed, config.follow_speed)
follow_position = Vector3.new(0, config.follow_height, -config.follow_distance)
```
## Debugging Your Follower Script
When we first tested our companion cube, it behaved erratically—sometimes flying across the map or getting stuck. Here's how we debugged it:
1. Add breakpoints to your script to see what's happening line-by-line
2. Inspect variable values by hovering over them in debug mode
3. Step through the script execution using F10 (Step Over) to see when issues occur
4. Check for errors in the output window
You can set breakpoints by clicking next to the line number in Roblox Studio's script editor:
```lua
-- Add this code to help debug
print("Master position: " .. tostring(master_position))
print("Follower target: " .. tostring(follower_position.Position))
```
### Advanced Debugging Techniques
For more complex debugging, create a visual debugging system:
```lua
-- Visual debugging system
local debug_enabled = true
-- Create debug visualization
local function create_debug_part(position, color)
if not debug_enabled then return end
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.Size = Vector3.new(0.5, 0.5, 0.5)
part.Position = position
part.Color = color or Color3.fromRGB(255, 0, 0)
part.Transparency = 0.5
part.Parent = workspace.DebugFolder
-- Auto-cleanup after 2 seconds
game:GetService("Debris"):AddItem(part, 2)
return part
end
-- Usage in your main loop
while true do
wait()
-- Show the target position with a red marker
local target_pos = master_position + follow_position
create_debug_part(target_pos, Color3.fromRGB(255, 0, 0))
-- Show a line representing the path
local dist_part = create_debug_part(follower_part.Position, Color3.fromRGB(0, 0, 255))
dist_part.Size = Vector3.new(0.2, 0.2, (follower_part.Position - target_pos).magnitude)
dist_part.CFrame = CFrame.lookAt(
follower_part.Position,
target_pos
) * CFrame.new(0, 0, -dist_part.Size.Z/2)
end
```
## Enhancing Your Companion Cube
After the basic functionality was working, I made some improvements:
### Adding Visual Effects
```lua
-- Add a particle emitter for a cool trail effect
local particle_emitter = Instance.new("ParticleEmitter")
particle_emitter.Parent = follower_part
particle_emitter.Texture = "rbxassetid://7851784603" -- Sparkle texture
particle_emitter.Rate = 10
particle_emitter.Speed = NumberRange.new(1, 3)
particle_emitter.Lifetime = NumberRange.new(1, 2)
```
### Creating Advanced Particle Effects
For more impressive visual effects, combine multiple particle emitters:
```lua
-- Function to create advanced particle effects
local function create_advanced_particles(part)
-- Main trail effect
local trail_emitter = Instance.new("ParticleEmitter")
trail_emitter.Parent = part
trail_emitter.Texture = "rbxassetid://7851784603"
trail_emitter.Rate = 15
trail_emitter.Speed = NumberRange.new(0.5, 1.5)
trail_emitter.Lifetime = NumberRange.new(1, 1.5)
trail_emitter.Transparency = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.2),
NumberSequenceKeypoint.new(0.8, 0.5),
NumberSequenceKeypoint.new(1, 1)
})
trail_emitter.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.3),
NumberSequenceKeypoint.new(1, 0)
})
trail_emitter.Color = ColorSequence.new(Color3.fromRGB(255, 255, 255), Color3.fromRGB(170, 170, 255))
-- Occasional sparkle effect
local sparkle_emitter = Instance.new("ParticleEmitter")
sparkle_emitter.Parent = part
sparkle_emitter.Texture = "rbxassetid://7851784603"
sparkle_emitter.Rate = 2
sparkle_emitter.Speed = NumberRange.new(3, 6)
sparkle_emitter.Lifetime = NumberRange.new(0.5, 1)
sparkle_emitter.Size = NumberSequence.new({
NumberSequenceKeypoint.new(0, 0.1),
NumberSequenceKeypoint.new(0.5, 0.5),
NumberSequenceKeypoint.new(1, 0)
})
sparkle_emitter.Color = ColorSequence.new(Color3.fromRGB(255, 255, 150))
-- Add a point light for a glow effect
local light = Instance.new("PointLight")
light.Parent = part
light.Range = 8
light.Brightness = 0.5
light.Color = Color3.fromRGB(200, 200, 255)
return {trail_emitter, sparkle_emitter, light}
end
-- Use the function to add effects to your companion
local effects = create_advanced_particles(follower_part)
```
### Making it Interact with the Environment
```lua
-- Add a touched event for interaction
follower_part.Touched:Connect(function(touched_part)
-- Ignore character parts to prevent constant triggering
if touched_part.Parent:FindFirstChild("Humanoid") then
return
end
-- Make a small hop when touching objects
follower_position.Position = follower_position.Position + Vector3.new(0, 2, 0)
wait(0.5)
end)
```
### Creating Context-Aware Interactions with Guardian Behavior
Let's add more intelligent interactions based on what the companion touches, emphasizing its role as a guardian:
```lua
local interaction_cooldown = false
local guardian_mode = false
follower_part.Touched:Connect(function(touched_part)
-- Prevent multiple interactions in quick succession
if interaction_cooldown then return end
-- Check what type of object was touched
local object_type = touched_part.Name:lower()
-- Different interactions based on object type
if touched_part.Parent:FindFirstChild("Humanoid") then
-- It's a player or NPC
if touched_part.Parent.Name ~= master_name then
-- It's not the master - enter guardian mode
local start_pos = follower_part.Position
guardian_mode = true
-- Create a protection shield effect
local shield = Instance.new("Part")
shield.Shape = Enum.PartType.Ball
shield.Size = Vector3.new(6, 6, 6)
shield.CFrame = follower_part.CFrame
shield.Transparency = 0.7
shield.CanCollide = false
shield.Material = Enum.Material.ForceField
shield.Color = Color3.fromRGB(0, 200, 255)
shield.Parent = workspace
-- Create weld
local weld = Instance.new("WeldConstraint")
weld.Part0 = follower_part
weld.Part1 = shield
weld.Parent = follower_part
-- Flash shield briefly
for i = 1, 5 do
shield.Transparency = 0.3
wait(0.1)
shield.Transparency = 0.7
wait(0.1)
end
-- Remove shield after a few seconds
wait(3)
shield:Destroy()
guardian_mode = false
end
elseif object_type:find("water") or object_type:find("liquid") then
-- It's water - float higher to keep player accessible
follower_position.Position = follower_position.Position + Vector3.new(0, 4, 0)
wait(2)
elseif touched_part.Material == Enum.Material.Fire or object_type:find("fire") then
-- It's fire - move away quickly and warn the player
local away_direction = (follower_part.Position - touched_part.Position).unit
follower_position.Position = follower_part.Position + away_direction * 10
-- Create warning text above companion
local warning = Instance.new("BillboardGui")
warning.Size = UDim2.new(0, 100, 0, 40)
warning.StudsOffset = Vector3.new(0, 3, 0)
warning.Parent = follower_part
local text = Instance.new("TextLabel")
text.Size = UDim2.new(1, 0, 1, 0)
text.BackgroundTransparency = 1
text.TextColor3 = Color3.fromRGB(255, 0, 0)
text.TextScaled = true
text.Font = Enum.Font.GothamBold
text.Parent = warning
-- Remove after a few seconds
wait(3)
warning:Destroy()
else
-- Default interaction for other objects
follower_position.Position = follower_position.Position + Vector3.new(0, 2, 0)
end
-- Set cooldown
interaction_cooldown = true
wait(1.5)
interaction_cooldown = false
end)
```
## Automation and Workflow Improvements
You can automate the creation of companion cubes with this script:
```lua
-- CompanionCubeCreator.lua
local function create_companion(player)
local cube = Instance.new("Part")
cube.Name = "CompanionCube_" .. player.Name
cube.Size = Vector3.new(2, 2, 2)
cube.Parent = workspace
-- Add master name value
local master_name = Instance.new("StringValue")
master_name.Name = "MasterName"
master_name.Value = player.Name
master_name.Parent = cube
-- Clone the follower script into the new cube
local script_folder = game:GetService("ServerStorage"):WaitForChild("Scripts")
local follower_script = script_folder:WaitForChild("FollowMaster")
local new_script = follower_script:Clone()
new_script.Parent = cube
return cube
end
-- Give every player a companion when they join
game.Players.PlayerAdded:Connect(function(player)
wait(2) -- Wait for character to load
create_companion(player)
end)
```
### Creating a Companion Selection System
Let's add a system that lets players choose from different companion types:
```lua
-- Create a module with companion templates
local CompanionTemplates = {
cube = {
shape = "Block",
size = Vector3.new(2, 2, 2),
color = Color3.fromRGB(220, 170, 255),
properties = {
follow_height = 5,
follow_distance = 3,
particle_color = Color3.fromRGB(230, 200, 255)
}
},
sphere = {
shape = "Ball",
size = Vector3.new(2, 2, 2),
color = Color3.fromRGB(170, 220, 255),
properties = {
follow_height = 6,
follow_distance = 4,
particle_color = Color3.fromRGB(200, 230, 255)
}
},
robot = {
is_model = true,
model_creator = function()
-- The robot model creation code from earlier
local robot = Instance.new("Model")
robot.Name = "RobotCompanion"
-- Body
local body = Instance.new("Part")
body.Size = Vector3.new(2, 3, 1.5)
body.Color = Color3.fromRGB(100, 100, 100)
body.Parent = robot
-- Head
local head = Instance.new("Part")
head.Shape = Enum.PartType.Ball
head.Size = Vector3.new(1.5, 1.5, 1.5)
head.Color = Color3.fromRGB(150, 150, 150)
head.Parent = robot
-- Position the head above the body
head.Position = body.Position + Vector3.new(0, 2, 0)
-- Weld them together
local weld = Instance.new("WeldConstraint")
weld.Part0 = body
weld.Part1 = head
weld.Parent = robot
robot.PrimaryPart = body
return robot
end,
properties = {
follow_height = 3,
follow_distance = 5,
particle_color = Color3.fromRGB(200, 200, 200)
}
}
}
-- Function to create a companion from a template
local function create_companion_from_template(player, template_name)
local template = CompanionTemplates[template_name] or CompanionTemplates.cube
local companion
if template.is_model then
companion = template.model_creator()
companion.Parent = workspace
else
companion = Instance.new("Part")
companion.Shape = Enum.PartType[template.shape]
companion.Size = template.size
companion.Color = template.color
companion.Parent = workspace
end
companion.Name = template_name .. "Companion_" .. player.Name
-- Add configuration based on the template
local ConfigModule = require(game.ReplicatedStorage.CompanionConfig)
ConfigModule.setup(companion, template.properties)
-- Add master name
local master_name = Instance.new("StringValue")
master_name.Name = "MasterName"
master_name.Value = player.Name
master_name.Parent = companion
-- Add the script
local script_folder = game:GetService("ServerStorage"):WaitForChild("Scripts")
local follower_script = script_folder:WaitForChild("FollowMaster")
local new_script = follower_script:Clone()
new_script.Parent = companion
return companion
end
-- Example of a companion selection GUI
local function create_companion_selection_gui(player)
local screen_gui = Instance.new("ScreenGui")
screen_gui.Name = "CompanionSelector"
screen_gui.Parent = player.PlayerGui
local frame = Instance.new("Frame")
frame.Size = UDim2.new(0.4, 0, 0.6, 0)
frame.Position = UDim2.new(0.3, 0, 0.2, 0)
frame.BackgroundColor3 = Color3.fromRGB(40, 40, 40)
frame.BackgroundTransparency = 0.2
frame.BorderSizePixel = 0
frame.Parent = screen_gui
local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, 0, 0.1, 0)
title.Text = "Choose Your Companion"
title.TextColor3 = Color3.fromRGB(255, 255, 255)
title.BackgroundTransparency = 1
title.Font = Enum.Font.GothamBold
title.TextSize = 24
title.Parent = frame
local buttons_layout = Instance.new("UIListLayout")
buttons_layout.Padding = UDim.new(0.02, 0)
buttons_layout.FillDirection = Enum.FillDirection.Vertical
buttons_layout.HorizontalAlignment = Enum.HorizontalAlignment.Center
buttons_layout.VerticalAlignment = Enum.VerticalAlignment.Center
local buttons_container = Instance.new("Frame")
buttons_container.Size = UDim2.new(0.8, 0, 0.8, 0)
buttons_container.Position = UDim2.new(0.1, 0, 0.15, 0)
buttons_container.BackgroundTransparency = 1
buttons_container.Parent = frame
buttons_layout.Parent = buttons_container
-- Create buttons for each companion type
for name, _ in pairs(CompanionTemplates) do
local button = Instance.new("TextButton")
button.Size = UDim2.new(0.8, 0, 0.2, 0)
button.Text = name:sub(1,1):upper() .. name:sub(2) .. " Companion"
button.BackgroundColor3 = Color3.fromRGB(60, 60, 60)
button.TextColor3 = Color3.fromRGB(255, 255, 255)
button.Font = Enum.Font.GothamSemibold
button.TextSize = 18
button.Parent = buttons_container
-- Button click handler
button.MouseButton1Click:Connect(function()
-- Remove any existing companions
for _, obj in pairs(workspace:GetChildren()) do
if obj.Name:find("Companion_" .. player.Name) then
obj:Destroy()
end
end
-- Create the new companion
create_companion_from_template(player, name)
-- Hide the GUI
screen_gui.Enabled = false
end)
end
return screen_gui
end
-- Show the selection GUI when a player joins
game.Players.PlayerAdded:Connect(function(player)
wait(3) -- Wait for character to load and player to be ready
create_companion_selection_gui(player)
end)
```
## Performance Optimization
When your game has many players, each with their own companion, performance can become an issue. Here are some optimization techniques:
> **Important Performance Consideration:** In Lua 5.1 (which Roblox uses), infinite `while true do` loops with `wait()` calls have certain inefficiencies. The Roblox engine provides RunService events that offer better performance, more consistent timing, and lower overall CPU usage.
```lua
-- Performance optimization for companion scripts
local RunService = game:GetService("RunService") -- Roblox service that provides timing events
-- Store the last calculated distance to avoid unnecessary updates
local last_distance = nil -- Will track the previous distance to player
-- Use heartbeat instead of while-wait loops
RunService.Heartbeat:Connect(function(delta_time) -- Runs once per frame, right before rendering
-- Skip if no player is found (avoids errors and wasted computation)
if not workspace:FindFirstChild(master_name) then return end
local master = workspace[master_name]
if not master:FindFirstChild("HumanoidRootPart") then return end
-- Get the target position
local master_pos = master.HumanoidRootPart.Position
-- Calculate distance to player
local distance = (follower_part.Position - master_pos).magnitude
-- Only update if the distance has changed significantly
-- This optimization reduces physics calculations when positions are stable
if not last_distance or math.abs(distance - last_distance) > 0.5 then
follower_position.Position = master_pos + follow_position
follower_body_gyro.CFrame = master.HumanoidRootPart.CFrame
last_distance = distance
end
end)
```
### Additional Performance Tips for Lua in Roblox
When working with Lua in Roblox, especially for companions that need to follow many players, consider these additional optimizations:
1. **Throttle expensive operations:**
```lua
-- Check for threats less frequently (once per second instead of every frame)
local last_threat_check = 0
RunService.Heartbeat:Connect(function(delta_time)
local current_time = tick()
-- Only check for threats once per second
if current_time - last_threat_check >= 1 then
-- Perform threat detection here
last_threat_check = current_time
end
-- Update position every frame (more responsive)
follower_position.Position = master_pos + follow_position
end)
```
2. **Cache frequently accessed values:**
```lua
-- Cache player's humanoid to avoid repeated FindFirstChild calls
local player_humanoid = nil
if master and not player_humanoid then
player_humanoid = master:FindFirstChild("Humanoid")
end
-- Now use player_humanoid directly
if player_humanoid and player_humanoid.Health < player_humanoid.MaxHealth * 0.5 then
-- Guardian behavior
end
```
3. **Use spatial partitioning for threat detection:**
```lua
-- Only check objects that are already reasonably close
for _, part in pairs(workspace:GetPartBoundsInRadius(master_position, 20)) do
-- This is more efficient than checking every object in workspace
local model = part:FindFirstAncestorOfClass("Model")
if model and model:FindFirstChild("Humanoid") and model ~= master then
-- It's a character that's not the player
local threat_distance = (model:GetPrimaryPartCFrame().Position - master_position).magnitude
if threat_distance < 10 then
nearby_threats = true
break
end
end
end
```
## Conclusion
Throughout this tutorial, we've explored the process of creating a companion cube follower in Roblox using Lua. What started as a simple idea—"wouldn't it be cool if something followed us around in the game?"—evolved into a deep dive into Roblox's physics system, Lua programming, and creative problem-solving to create a digital guardian.
The guardian companion doesn't just follow its master around; it actively protects them when threats are near, alerts when health is low, and even creates protective barriers when needed. These behaviors transform a simple follower into a truly useful companion that provides both practical assistance and peace of mind when you can't be there to help your friend directly.
There are several advanced topics that could enhance your guardian companion further. Implementing pathfinding would allow for more intelligent navigation around obstacles in the game world. Adding animations would give your creations more personality and visual appeal through fluid movement and expressive behaviors. Custom UI development enables deeper customization options for players to configure their companions. Expanding the AI behavior allows your guardian to recognize and intelligently respond to different types of threats. Finally, implementing persistence would allow the guardian to remain active and protective even when its creator is offline.