Hello, I just got done making an object-oriented event-based confirmation system for my game. It’s going to be used as a confirmation UI for when the player wants to delete vehicles and do other things that can’t be undone.
Module code:
local repStor = game:GetService("ReplicatedStorage")
local players = game:GetService("Players")
local uiFolder = repStor.UIs
local player = players.LocalPlayer
local playerGui = player.PlayerGui
local confScreen = playerGui:WaitForChild("Confirmation")
local confirmationMessage = {}
confirmationMessage.ConfFrame = uiFolder.Confirmation
confirmationMessage.__index = confirmationMessage
function confirmationMessage:CreateNewMessage(message)
local newMessage = {}
setmetatable(newMessage, confirmationMessage)
local newFrame = self.ConfFrame:Clone()
local event = Instance.new("BindableEvent")
newFrame.mid.action.Text = message
newFrame.Parent = confScreen
newMessage.Frame = newFrame
newMessage.Bindable = event
newMessage.ActionFinished = event.Event
newMessage.Connections = {}
newMessage:Connect()
return newMessage
end
function confirmationMessage:Connect()
local yesButton = self.Frame.yes
local noButton = self.Frame.no
local con1 = yesButton.MouseButton1Click:Connect(function()
self.Bindable:Fire(true)
self:End()
end)
local con2 = noButton.MouseButton1Click:Connect(function()
self:End()
end)
table.insert(self.Connections, con1)
table.insert(self.Connections, con2)
end
function confirmationMessage:End()
for i,v in pairs(self.Connections) do
v:Disconnect()
self.Connections[i] = nil
end
self.Bindable:Destroy()
self.Frame:Destroy()
end
return confirmationMessage
LocalScript controller code:
InteractFunctions.Delete = function(currentModel)
local deleteMessage = confMessage:CreateNewMessage("Are you sure you want to delete this vehicle?")
local con
con = deleteMessage.ActionFinished:Connect(function()
local remote = repStor.Remotes.DeleteVehicle
remote:InvokeServer(currentModel)
con:Disconnect()
end)
end
So I’m looking for ways to improve readability/reuseability, since I want this module to be used all over the place for confirmation. It’s pretty robust right now imo but I don’t want any bugs/weird edge cases. Tell me if you see something that can be improved!
I would say the main issue I had was the variable names frequently used abbreviations that only detracted from readability. In my snippet as well, I’ve modified names of game objects to be more consistent, but this can be adapted to your needs if you wish to use this in full:
Module code:
local Players = game:GetService("Players");
local ReplicatedStorage = game:GetService("ReplicatedStorage");
local player = Players.LocalPlayer;
local playerGui = player.PlayerGui;
local confirmationScreen = playerGui:WaitForChild("Confirmation");
local uiFolder = ReplicatedStorage:WaitForChild("UIs");
local ConfirmationMessage = {};
--- @class ConfirmationMessage
--- A class used in prompting the player the confirmation of an action.
--- @field frame Frame
--- @field bindableEvent BindableEvent
ConfirmationMessage.prototype = {};
--- Construct a new confirmation message with the given `message` that performs
--- `onConfirm` only when the player confirms the action.
--- @param message string
--- @param onConfirm fun(): nil
--- @return ConfirmationMessage, RBXScriptConnection
function ConfirmationMessage.new(message, onConfirm)
local self = {};
setmetatable(self, { __index = ConfirmationMessage.prototype });
local bindableEvent = Instance.new("BindableEvent");
bindableEvent.Event:Connect(onConfirm);
bindableEvent.Name = "ConfirmationMessageEvent";
local frame = uiFolder.ConfirmationFrame:Clone();
frame.Prompt.Action.Text = message;
frame.Parent = confirmationScreen;
self.bindableEvent = bindableEvent;
self.frame = frame;
self:initialize();
return self;
end
--- Initializes the confirmation message.
--- @param self ConfirmationMessage
--- @return nil
function ConfirmationMessage.prototype:initialize()
--- Event listener that fires the event when signaled, then deinitilizes
--- the confirmation message.
--- @return nil
local function onConfirm()
self.bindableEvent:Fire();
self:deinitialize();
end
--- Event listener that, when signaled, deinitializes the confirmation
--- message
--- @return nil
local function onCancel()
self:deinitialize();
end
self.frame.YesButton.MouseButton1Click:Connect(onConfirm);
self.frame.NoButton.MouseButton1Click:Connect(onCancel);
end
--- Deinitializes the confirmation message.
--- @param self ConfirmationMessage
--- @return nil
function ConfirmationMessage.prototype:deinitialize()
self.bindableEvent:Destroy();
self.frame:Destroy();
end
return ConfirmationMessage;
Controller code:
-- I assume there's other stuff here...
local DELETE_VEHICLE_PROMPT = "Are you sure you want to delete this vehicle?"
--- Prompt the deletion of the vehicle `currentModel` and, if confirmed, delete
--- said vehicle.
--- @param currentModel Model
--- @return nil
function Interactions.delete(currentModel)
--- An event listener to be used with a confirmation message.
--- @see ConfirmationMessage
--- @return nil
local function onConfirm()
local deleteVehicleRemote = ReplicatedStorage.Remotes.DeleteVehicle;
deleteVehicleRemote:InvokeServer(currentModel);
end
ConfirmationMessage.new(DELETE_VEHICLE_PROMPT, onConfirm);
end
As always, if you have any questions about how something functions or why I did something a certain way, feel free to ask!
Edit: Also, thanks for providing a video! It made this rather painless to refactor.
I really like how you did the controller there. Much more readable, I will definitely be implementing that and I might re-do everything else in my game to follow that same technique. I didn’t even realize I could do that.
I also like the module code refactor, but I follow the same naming conventions (like repStor for ReplicatedStorage) in all of my scripts so changing variable names doesn’t really improve readability for me. Note that I’m the only programmer working on this game, so I don’t really care about others being able to read it. However I do like how you re-did the constructor function, I even think it’s more readable. Thanks!
Went ahead and implemented your major changes and of course it works perfectly, and now it’s like 200% more readable in both the controller and in the module.
I just have one question, is there any benefit in doing this:
function confirmationMessage.New(message, callback)
local self = {}
setmetatable(self, {__index = confirmationMessage.prototype})
As opposed to this?:
function confirmationMessage:CreateNewMessage(message)
local newMessage = {}
setmetatable(newMessage, confirmationMessage)
I know to use : in functions other than the constructor because it automatically defines self. But both options work for the constructor. I do admit the first option looks better in the controller script but in the module both are just as readable to me.
I make the object’s name self to be idiomatic with both Lua’s self colon syntactic sugar as well as other language’s use of this and self.
I also use .new as the constructor of class objects to be idiomatic with other Lua libraries and packages, as the createX pattern is seen more with functional programming and factory design patterns as opposed to object-oriented constructors.
Finally, I use prototype and a custom metatable for instances to
Dissociate static properties and methods from instance methods.
Dissociate instance methods and metamethods.
Remove recursive metatables.
Less importantly, compatiblity with EmmyLua language server for use with VSCode to apply rich intellisense.
All of these decisions have a very negligible impact on code performance, and by no means is an “optimization”.