Help making frameworks stuff like that

this whole paragraph might be a big clusterduck but my brain is fried rightnow

this year decided to stop being lazy and push myself to learn, and ive been trying to learn to make a system/framework for stuff like guns, interactable props, etc. Im really struggling to learn how to use stuff like object-oriented programming or frameworks with module scripts. I know how module scripts work but dont really understand how to make entire systems with them

for eample this is a simple server script I made to serve as a prop system, i basically want to learn how to rework this entire system this to be more modular and optimized, sorry i cant find a better way to word this right now :tired_face:

local PromptService = game:GetService("ProximityPromptService")

local PropFunctions = {
	["Door"] = function()
		--open door code stuff
	end,
	["Light"] = function()
		--togle light do things
	end,
	["Register"] = function()
		--loot cash reguster
	end
}

PromptService.PromptTriggerEnded:Connect(function(Prompt,Player)
	local PromptType = Prompt:GetAttribute("PromptType")
	
	local func = PropFunctions[PromptType]
	
	if func then
		func()
	end
end)
1 Like

The thing you are trying to do looks like a State Machine.
Research that up.

1 Like

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)

1 Like

Hello! I see your request and I’m assuming that you want to make your code use more ModuleScripts instead of the traditional scripts. I’ll be glad to help!

To begin, we will first notice that when we start out our module script, we get the simple

local module = {}

return module

This is basically the same as (when in your script):

local PropFunctions = { ["Door"] = function() end }

The way that this is the same is by both are utilizing a key (which in this case is the function() and the value (which would be what the script is using).

Meaning that in order to convert our script to fit in the more modular way that you had previously anticipated, we would simply need to add a “Door” key (which was previously stated to be the function) to the module script.

local module = {}

module.Door = function()

end)

return module

Now when we do:

require(module).Door

We can get the value that we are expected by what we wrote within the door function. You can either use the same module script for all of the props (and call the module script PropFunctions) or you can make a different module script for each prompt, it’s truly up to you!

In order to have communication with the main server, remember that you must use the require() syntax on the module (as shown in the example above).

Combining everything we have learnt here, we can do the following

For the PropFunctions Modulescript:

local PropFunctions = {}

PropFunctions.Door = function()

end)

PropFunctions.Lights = function()

end)

PropFunctions.Register = function()

end)

return PropFunctions

In order to then connect it to make it a Client-Server system. You then can either make another module script (or use a normal script to your liking) to connect it to the ProximityPromptService as so (Using the example of another ModuleScript)

local module = {}
local PropFunctionModule = require(RootToPropModule)
local PromptService = game:GetService("ProximityPromptService")

module.Init = function()
PromptService.PromptTriggeredEnded:Connect(function(Prompt, Player)
local PromptType = Prompt:GetAttribute("PromptType")

if PropFunctionModule[PromptType] then 
PropFunctionModule[PromptType]() 
end 
end)
end)

return module

Remember that ultimately you do need to call these methods and the way you do that is by a singular regular script which sole purpose is to Initialize (Which is why we use Module.Init()) all other module scripts starting a chain reaction. Which would be written out like this in a normal script.

require(PathToModuleScript).Init()

At first I wanted to make this long and complex like the other post, but then I realized that my audience might want something more entry level, if you have any questions about the way ModuleScripts work or what is the most optimal way to do something please ask, but besides that this is the most basic way to break down how most modern day ModuleScript systems work in it’s absolute most basic form. Thank you!

thanks a whole bunch gonna look into that more depth tomorrow when I am less tired, if I am understanding you right, my props will also be triggered by explosions, raycasts, etc so that is a valid reason to use module scipts?

Yep! It’s like the npc example I gave, you can make the scripts responsible for the raycast then call your module to “artificially” trigger the prompt. The main alternative to using a module script would be a normal script, but with bindable events for other scripts to communicate with it, which isn’t ideal

1 Like

awesome and thanks so much this has been such a struggle, i feel stupid but this is so hard for me :sweat_smile:

1 Like

No worries, this is a skill that has to be built up with time and experience

1 Like

hopefully quick question, how do i connect module scripts to a main module? i thought this would be easier uh what i mean is like if i have a module script for each prop under a main module, i could call their functions from the main script like:

mainmodule.Door:Open(Model)

This is the example of one of the events from the event system I mentionned

The main part is Event.Function. The weights are to determine the likelihood of the event being chosen, with default applying to every gamemode, other than the Altitiles gamemode where it is disabled (the Altitiles setting overwrites default)

Then, the main module gets a list of every module script, and moves them into a table

image

Although my code is really more complicated than it needs to be

I would recommend you do something like this

local Props = {}

for i, v in ipairs(mainmodule:GetChildren()) do
	 -- Could add pcalls around the require, so a broken module doesn't break the whole system
	-- Although if you are the only one working on the game, that isn't really necessary
	Props[v.Name] = require(v)
end


PromptService.PromptTriggerEnded:Connect(function(Prompt,Player)
	local PromptType = Prompt:GetAttribute("PromptType")
	
	local prop = Props[PromptType]
	if not prop then return end
	
	prop.Function()
end)

If you have a module named “Door” under your main module, then Props["Door"] will be set to what the module returned. And then it basically acts very similarly to what you had previously (with the difference that the module returns a table instead of a function directly, where prop.Function() is the function call. But it is possible to make the module script return a function directly, and then you’d do prop())

1 Like

thanks alot again dude i had pretty much the same code in my head :sweat_smile: but I was confusingly looking into stuff like metatables and OOP

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.