I do like the little script you gave. There will always be different ways to do things, yours is good, others could include tagging prompts with a collection service tag, and then creating a script for each tag (for example, the door tag), which you get every door prompt using CollectionService:GetTagged(), and run the door functionality when those prompts are activated. Is it better than what you have? Eh, don’t think so
A variation of your structure could also be to make every prop into it’s own module. This is mostly useful if you end up having a lot of prop with perhaps lengthy logic, it becomes more manageable to separate that logic into different files
I’ve done something similar with a place of fate type of game, where each even is it’s own module script
When the server starts up, EventsModule parses all the module scripts, and puts all the events into a singular table (which then leads to something equivalent to what you have in your code example)
As for using module scripts to build entire systems, well you don’t have to learn OOP and frameworks and stuff, you might end up doing stuff alike to it just because it’s natural to do so. For OOP, I usually do some really simple function that returns a table containing properties and functions
Module scripts shine when you have other systems in the game that have to interact with the system associated with this module script. Here I have an example of a module script that isn’t really a system, but rather the core of the plate of fate like game. But I chose this one as an example because it’s goal is literally communication between modules and the core of the game
local HttpService = game:GetService("HttpService")
local Utils = require(game.ReplicatedStorage.Modules.Utils)
local GAME_SETTINGS = require(game.ReplicatedStorage.GAME_SETTINGS)
local DialogueModule = require(game.ReplicatedStorage.Modules.DialogueModule)
local SignalModule = require(game.ReplicatedStorage.Modules.SignalModule)
local LogsModule = require(game.ReplicatedStorage.Modules.LogsModule)
local GameState = {}
local ActivePlayersFolder = game.ReplicatedStorage.ActivePlayers
local CurrentGamemode : string? = nil
local RoundId = nil
local PlayerTeams : {[Player] : Team?} = {}
local ActivePlayers : {Player} = {}
local StartTime = 0
local Connections : {RBXScriptConnection} = {}
-- // Signals
type CharacterTable = {
Character : Model,
HumanoidRootPart : Part,
Humanoid : Humanoid,
}
local GamemodeChangedSignal : SignalModule.Signal<string?> = SignalModule.new()
local PlayerLoadedSignal : SignalModule.Signal<Player, CharacterTable> = SignalModule.new()
local PlayerDiedSignal : SignalModule.Signal<Player> = SignalModule.new()
local GameStartedSignal : SignalModule.Signal<> = SignalModule.new()
local GameEndingSignal : SignalModule.Signal<> = SignalModule.new()
GameState.PlayerLoadedSignal = PlayerLoadedSignal.Event
GameState.PlayerDiedSignal = PlayerDiedSignal.Event
GameState.GameStartedSignal = GameStartedSignal.Event
GameState.GameEndingSignal = GameEndingSignal.Event
GameState.GamemodeChangedEvent = GamemodeChangedSignal.Event
-- // Methods
local function UnloadPlayer(Player)
local Index = table.find(ActivePlayers, Player)
if not Index then warn("Not found") return end
table.remove(ActivePlayers, Index)
Player:SetAttribute("Playing", false)
Player.Team = game.Teams.Lobby
PlayerTeams[Player] = nil
end
-- This is called before GameState:StartRound(), but it is garanteed that GameState:StartRound() will be called after
function GameState:LoadPlayer(Player : Player, Team : Team?)
if not Player or not Player.Parent then return false, nil end
local Success, _ = pcall(Player.LoadCharacter, Player)
if not Success then return false, nil end
local Character = Player.Character
if not Character then return false, nil end
local Humanoid = Character:FindFirstChild("Humanoid")
if not Humanoid then return false, nil end
local HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart")
if not HumanoidRootPart then return false, nil end
Player:SetAttribute("Playing", true)
if Team then
Player.Team = Team
PlayerTeams[Player] = Team
end
local PlayingTag = Instance.new("ObjectValue", ActivePlayersFolder)
PlayingTag.Value = Player
PlayingTag.Name = Player.Name
local CharacterTable = {
Character = Character,
HumanoidRootPart = HumanoidRootPart,
Humanoid = Humanoid,
}
PlayerLoadedSignal:Fire(Player, CharacterTable)
return true, CharacterTable
end
function GameState:StartRound(Gamemode : string, Players : {Player})
GamemodeChangedSignal:Fire(CurrentGamemode)
StartTime = os.clock()
RoundId = HttpService:GenerateGUID(false)
CurrentGamemode = Gamemode
table.move(Players, 1, #Players, #ActivePlayers + 1, ActivePlayers)
for _, Player in ipairs(Players) do
local Character = Player.Character
if not Character then UnloadPlayer(Player) end
local Humanoid : Humanoid? = Character:FindFirstChild("Humanoid")
if not Humanoid then UnloadPlayer(Player) end
local function PlayerDied()
if not GameState:IsPlayerActive(Player) then return end
PlayerDiedSignal:Fire(Player)
UnloadPlayer(Player)
if Player:FindFirstChild("Creator") and Player:FindFirstChild(Player.Creator.Value) and Player.Team == Player:FindFirstChild(Player.Creator.Value).Team and Player.Team.Name ~= "Lobby" and Player.Team.Name ~= "Playing" then
local Player2 = Player:FindFirstChild(Player.Creator.Value)
DialogueModule:Talk("King", DialogueModule.Dialogues.Betrayal:FormatRandomLine(Player, Player2))
else
DialogueModule:Talk("King", DialogueModule.Dialogues.Defeat:FormatRandomLine(Player))
end
for i, Tile in pairs(workspace.Tiles:GetChildren()) do
local PlayerNames = string.split(Tile.Owners.Value,".")
local Index = table.find(PlayerNames,Player.Name)
if not Index then continue end
table.remove(PlayerNames,Index)
local Owners = ""
for i, v in ipairs(PlayerNames) do
local Prefix = i == 1 and "" or "."
Owners = Owners..Prefix..v
end
if Owners == "" then
Tile:SetAttribute("Active", false)
end
Tile.Owners.Value = Owners
end
end
table.insert(Connections, Humanoid.Died:Connect(PlayerDied))
table.insert(Connections, Player.CharacterRemoving:Connect(PlayerDied))
end
GameStartedSignal:Fire()
end
function GameState:EndRound()
GameEndingSignal:Fire()
GamemodeChangedSignal:Fire(nil)
StartTime = 0
RoundId = nil
CurrentGamemode = nil
for _, Player in ipairs(ActivePlayers) do
Player:SetAttribute("Playing", false)
if not Player or not Player.Parent then continue end
Player.Team = game.Teams.Lobby
PlayerTeams[Player] = nil
pcall(Player.LoadCharacter, Player)
end
table.clear(ActivePlayers)
for _, v in ipairs(ActivePlayersFolder:GetChildren()) do
v:Destroy()
end
for _, c in ipairs(Connections) do
c:Disconnect()
end
end
function GameState:IsPlayerLoadedInGame(Player : Player) : boolean
return Player:GetAttribute("Playing") or false
end
function GameState:IsRoundActive()
return RoundId ~= nil
end
function GameState:GetRoundId()
return RoundId
end
function GameState:GetActivePlayers()
return ActivePlayers
end
function GameState:GetPlayerTeam(Player : Player) : Team?
return PlayerTeams[Player]
end
function GameState:GetActiveTeams() : {[Team] : {Player}}
local ActiveTeams = {}
for Player, Team in pairs(PlayerTeams) do
if not ActiveTeams[Team] then ActiveTeams[Team] = {} end
table.insert(ActiveTeams[Team], Player)
end
return ActiveTeams
end
function GameState:SanitizeActivePlayersTable(ActivePlayers : {Player})
local InvalidPlayers = {} -- Player no longer in game
for i, Player in Utils.r_ipairs(ActivePlayers) do
if Player and Player.Parent then continue end
table.insert(InvalidPlayers, Player)
table.remove(ActivePlayers, i)
end
return ActivePlayers, InvalidPlayers
end
function GameState:IsPlayerActive(Player : Player)
return table.find(ActivePlayers, Player) ~= nil
end
--
function GameState:GetCurrentGamemode() : string?
return CurrentGamemode
end
function GameState:GetRemainingTime()
return math.clamp(GAME_SETTINGS.GAME_TIME_LIMIT - (os.clock() - StartTime), 0, GAME_SETTINGS.GAME_TIME_LIMIT)
end
function GameState:GetGameProgressionAlpha()
return math.clamp(os.clock() - StartTime,0, GAME_SETTINGS.GAME_TIME_LIMIT)/GAME_SETTINGS.GAME_TIME_LIMIT
end
return GameState
It contains events (from a custom signal module), for other scripts/modules to perform actions when certain states are reached, and methods for other scripts/modules to get information about the state of the game, or to perform an action associated with the core of the game (loading players to start a round, etc)
You can also see how this module interacts with another module, the DialogueModule, which is responsible for displaying npcs that have predetermined phrases they say
DialogueModule:Talk("King", DialogueModule.Dialogues.Betrayal:FormatRandomLine(Player, Player2))
DialogueModule.Dialogues.Betrayal is a submodule of the
DialogueModule, that is responsible for containing the predetermined phrases, and formatting, and it used to send the formatted result to the
:Talk() method. It is not needed, as it would be possible to pass directly a string with an associated emote (the npc has different emotes depending on the phrase) to
:Talk(), although less practical. The formatting was done separated like this because different categories can have different formatting (some need the name of 2 players, while others need the names of 1 or 0 players)
In your case, your script as is has no reason to be a module script. But it would be useful to make it a module script if you want to have other scripts be able to “trigger a prompt”, for example, if you have npcs, you might want the npc to open the door. Then the npc script could call a method that your module exposes, to “artificially” trigger the prompt
So if you find the need to have systems communicated between them, that is when you can turn your normal script into a module script. And to ensure your module script still starts up at the start of the game, regardless of whether or not it is getting required from the outside, you can put a script as a direct child of the module, that has the sole purpose of requiring its parent module (if the module is in ReplicatedStorage, you can use the RunContext property to make the script execute even inside ReplicatedStorage)