KKMIN

Event driven approach with Lua and Roblox (2)

In the previous post, we looked at how to make a simple lock puzzle in Roblox and Lua. I highlighted some pitfalls where the code was not scalable; in this post, we will look at how to refactor the system to make it more flexible and extensible.

Context

In the context of the lock system, scaling the system up mainly involves adding more unlocker pads to it. There are also issues of duplicate code due to each unlocker pad requiring its own script. Let us look at the relevant parts of the code which causes these issues.

UnlockerModule.lua

local unlocker1Hit = require(script.Parent.Unlocker1.ModuleScript)
local unlocker2Hit = require(script.Parent.Unlocker2.ModuleScript)
local unlocker3Hit = require(script.Parent.Unlocker3.ModuleScript)
local unlocker4Hit = require(script.Parent.Unlocker4.ModuleScript)
unlocker1Hit.Hit:Connect(function()
userInput = userInput.."1"
authenticate()
end)
unlocker2Hit.Hit:Connect(function()
userInput = userInput.."2"
authenticate()
end)
unlocker3Hit.Hit:Connect(function()
userInput = userInput.."3"
authenticate()
end)
unlocker4Hit.Hit:Connect(function()
userInput = userInput.."4"
authenticate()
end)

In the first chunk, we have to manually find all 4 unlocker pads' scripts to access the event that they will fire. Then we have to manually set the event handlers in response. How can we avoid this?

Eliminating Code Duplication

The first step is to eliminate the code duplication, which in a way is also the source of the scalability problem. This means that instead of every unlocker having a script to fire off the event, we only want to use one script. First, I organized all the unlocker pads into a folder:

Structure of the lock system v2

Structure of the lock system v2.

Notice how the Unlockers no longer have any script inside them, and they are all handled by a single script UnlockerPadModule. Inside UnlockerPadModule, we will iterate through the folder, and then create events for each pad in the folder UnlockPads. Since we are not identifying any of the unlockers by their name, how will event handler script get these events? To do so, we will keep a list of the events we created for the pads which can get acquired through a getter function.

UnlockerPadModule.lua

local module = {}
local folder = script.Parent
-- Get all parts in parent folder, excluding this script
local unlockerPads = folder:GetChildren()
local index
for i, v in ipairs(unlockerPads) do
if v == script then
index = i
end
end
table.remove(unlockerPads, index)
--List/Array to store our created events
local events = {}
local function setUpPad(part, padID)
--Create new event for when pad is touched, send padID
local bindableEvent = Instance.new("BindableEvent")
part:SetAttribute("debounce", true)
part.Touched:Connect(function(touchedPart)
local partParent = touchedPart.parent
local humanoid = partParent:FindFirstChild("Humanoid")
if humanoid then
if(part:GetAttribute("debounce")) then
part.Color = Color3.fromRGB(10, 40, 210)
bindableEvent:Fire(padID)
part:SetAttribute("debounce", false)
end
end
end)
--Insert event into events table
table.insert(events, bindableEvent.Event)
end
function module.lockPads()
for _, pad in ipairs(unlockerPads) do
pad:SetAttribute("debounce", false)
end
end
function module.unlockPads()
for _, pad in ipairs(unlockerPads) do
pad:SetAttribute("debounce", true)
end
end
function module.getEvents()
return events
end
function module.resetPads()
for _, pad in ipairs(unlockerPads) do
pad.Color = Color3.fromRGB(237, 234, 234)
end
end
function module.animatePart(unlockPart)
if(unlockPart) then
unlockPart.Color = Color3.fromRGB(10, 40, 210)
wait(0.5)
unlockPart.Color = Color3.fromRGB(237, 234, 234)
wait(0.5)
end
end
function module.animateSecret(number)
for i=1, #number do
local c = tonumber(number:sub(i,i))
local part = unlockerPads[c]
module.animatePart(part)
end
end
function module.getNoOfPads()
local size = 0
for _, pad in unlockerPads do
size = size + 1
end
return size
end
for i, pad in ipairs(unlockerPads) do
setUpPad(pad, i)
end
return module

In the first part of the script, we get a list of all unlocker pads in our folder, then define a function called setupPad(part, padID) which takes in a pad as well as its assigned ID, and creates an event which will fire when that specific part (pad) is touched. In the last part of the script, we call setupPad() for every unlocker pad in our list of pads. Using a loop to set up the pads' events means that we can now create as many unlocker pads as we want in the folder, and we no longer need to rewrite or duplicate the script.

Sending Information via Events

You may have noticed that we passed in the padID into our setUpPad() function, and when we fire the event, we put it in as an argument. This is to facilitate the event handler's job; unlike the previous version, we are now no longer manually defining events and writing hard-coded event handlers for them.

If that is the case, how do we know what is the number assigned to the pad? The padID ensures that the event handler knows which is the number assigned to the particular pad that has fired off the event.

UnlockerModule.lua

local unlockerModule = {}
local Players = game:GetService("Players")
Players.PlayerAdded:Wait()
local unlockerPadModule = require(script.Parent.UnlockPads.UnlockerPadModule)
local bindableEvent = Instance.new("BindableEvent")
unlockerModule.authenticate = bindableEvent.Event
local userInput = ""
local size = unlockerPadModule.getNoOfPads()
local function generateSecret()
local secret = ""
local secretTable = {}
for i=1, size do
table.insert(secretTable, i)
end
for i=1, size do
local randomIndex = math.random(#secretTable)
local randomElement = secretTable[randomIndex]
secret = secret .. randomElement
table.remove(secretTable, randomIndex)
end
return secret
end
local generatedSecret = generateSecret()
local function authenticate()
if(string.len(userInput) == size) then
if(userInput == generatedSecret) then
bindableEvent:Fire(true)
else
bindableEvent:Fire(false)
end
end
end
unlockerPadModule.animateSecret(generatedSecret)
function unlockerModule.reset()
userInput = ""
unlockerPadModule.lockPads()
unlockerPadModule.resetPads()
generatedSecret = generateSecret()
wait(1)
unlockerPadModule.animateSecret(generatedSecret)
unlockerPadModule.unlockPads()
end
--Get events generated by UnlockerPadModule
local events = unlockerPadModule.getEvents()
for _, event in events do
event:Connect(function(padID)
userInput = userInput..padID
authenticate()
end)
end
return unlockerModule

In the last for loop of the UnlockerModule script, we set up event handlers for all the events that we generated in UnlockerPadModule. Notice how padID can be received as a parameter into the event handler function, which we can use to concatenate onto the user input.

Secret Sequence

In our original implementation, our secret sequence length was fixed to 4. However, this is no longer the case if we are intending to scale the system up to encompass more than 4 pads.

Thus, this size value must be retrieved from a getter function, getNoOfPads() in UnlockerPadModule. This results in some minor changes to our generateSecret() and authenticate() functions.

Debounce

Typically, debounce is used within a script as a programming pattern when we are trying to write code for firing a single event. However, notice how our events are now written in a loop for each unlocker pad, and there is no local debounce variable for each of them.

In our particular case, we also want to enforce the debounce such that it does not allow our players to fire the event more than once. How can we do this for each unlocker pad when we only have 1 for loop?

The perfect solution here was to use Instance Attributes given by the Roblox API. It allows us to set any kind of local variable inside a Roblox part, thus I stored the debounce variable inside each of the pads during the creation of the event. In a sense, we have transformed a programming pattern which was originally used in a script tied to a specific part into an instance attribute/property. This is necessary since we no longer have any script that is tied to any pads.

Closing Thoughts

And just like that, we have successfully refactored our lock puzzle system into one that is more scalable. Here's an example with 8 pads:

Wow, it's getting difficult to remember the sequence!

Now, all we need to do is to simply add more pads into the UnlockPads folder, and the system should still function as we expect. We can see how refactoring the code to be scalable makes our lives a lot easier by making the system more flexible, so we can change it according to our needs.

There are some other fun ideas that we could use to extend this little puzzle; for example, what if we have more than 1 player? Each pad could light up in a different colour and the player assigned to it has to press that pad in order to authenticate. But that's for another time!

← Back to home

Comments