# 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.