About methods, I just meant functions. They may or may not return anything
Oh man, yeah. I try to stay under 1 thousand line, that’s where scripts start becoming hard to manage
Breaking stuff down into smaller scripts is not something that (I think) can be taught like um, scripting. It’s kind of something you have to experiment with and develop over time? Well, I wouldn’t be able to tell you how to do it
Modularity is (from my perspective) really important. Instead of having 1 giant chunk of code, you break it down into many many modules, and instead of having 1 big system, you have multiple smaller systems that allow you to abstract away some of the code to keep the main code more concise. It’s a very similar idea to object oriented programming, but without objects per say
This approach has helped me create big projects, while avoiding bugs and keeping the codebase mostly organized. I will also add that it doesn’t really impact performance, unless you do some dubious design decisions
Drop down to avoid having a very much too lengthy reply
Module for handling player stuff:
local RunService = game:GetService("RunService")
local DebrisService = game:GetService("Debris")
local ReplicatedStorage = game.ReplicatedStorage
local StarterPlayer = game.StarterPlayer
local Players = game.Players
local Characters = ReplicatedStorage.Characters
local Customization = ReplicatedStorage.Customization
local PlayerDatastore = require(ReplicatedStorage.Modules.DatastoreModule.PlayerDatastore)
local ModelTypes = {}
local RemoteCooldown = 2
local RESPAWN_DELAY = 5
local function LoadCharacter(Player : Player, Model : Model, Outfit : {any})
-- // Unsure if this is needed, but this would prevent :LoadCharacter from using the wrong model if two players respawn at the same time
local ExisitingCharacter = StarterPlayer:FindFirstChild("StarterCharacter")
while ExisitingCharacter do
ExisitingCharacter:GetPropertyChangedSignal("Parent"):Wait()
ExisitingCharacter = StarterPlayer:FindFirstChild("StarterCharacter")
end
local Character = Model:Clone()
Character.Name = "StarterCharacter"
local CharacterModule = require(Character.CharacterModule)
CharacterModule:LoadOutfit(Outfit)
ModelTypes[Player] = Model
Character.Parent = StarterPlayer
Player:LoadCharacter()
Character.Parent = nil -- Will get gc. Destroying it causes an error
end
Players.PlayerAdded:Connect(function(Player)
Player.CharacterAdded:Connect(function(PlayerModel)
local Humanoid : Humanoid = PlayerModel:WaitForChild("Humanoid")
Humanoid.Died:Connect(function()
task.wait(RESPAWN_DELAY)
if not Player.Parent then return end
local Outfit = PlayerDatastore:GetOutfit(Player.UserId, ModelTypes[Player].Name)
LoadCharacter(Player, ModelTypes[Player], Outfit)
end)
end)
end)
Players.PlayerRemoving:Connect(function(Player)
ModelTypes[Player] = nil
end)
local PlayerModule = {}
function PlayerModule:LoadCharacter(Player : Player, CharacterType : string, Outfit : {any})
if ModelTypes[Player] then error("Player "..Player.UserId.." is already loaded") end -- TODO -- Exploit logging -- TODO -- UnloadCharacter?
local Character = Characters:FindFirstChild(CharacterType)
if not Character then error("Player "..Player.UserId.." tried to load with invalid Character type of "..CharacterType) end -- TODO -- Exploit logging
local CharacterModule = require(Character:FindFirstChild("CharacterModule") or error("Character "..CharacterType.." does not have a CharacterModule"))
local Outfit = CharacterModule:ValidateOutfit(Outfit)
PlayerModule:SetReplicationFocus(Player, nil)
LoadCharacter(Player, Character, Outfit)
Player.ReplicationFocus = Player.Character
return
end
local ValidTeams = {
game.Teams.Danuwae,
game.Teams.Versteckt,
game.Teams["Black Creed"],
game.Teams["Azure Dynasty"],
}
type TeamsType =
|"Danuwae"
|"Versteckt"
|"Black Creed"
|"Azure Dynasty"
function PlayerModule:SelectTeam(Player : Player, TeamName : TeamsType)
if ModelTypes[Player] then error("Player "..Player.UserId.." tried to change teams while loaded in") end -- TODO -- Exploit logging -- TODO -- UnloadCharacter?
local Team = game.Teams:FindFirstChild(TeamName)
if not Team then error("Player "..Player.UserId.." tried to load with unknown Team of "..TeamName) end -- TODO -- Exploit logging
if table.find(ValidTeams, Team) then error("Player "..Player.UserId.." tried to load with invalid Team of "..TeamName) end -- TODO -- Exploit logging
Player.Team = Team
end
local Parts = {}
function PlayerModule:SetReplicationFocus(Player : Player, Position : Vector3?)
if ModelTypes[Player] and Position ~= nil then error("Player "..Player.UserId.." tried to change replication focus while loaded in") end -- TODO -- Exploit logging -- TODO -- UnloadCharacter?
if Position ~= nil and typeof(Position) ~= "Vector3" then error("Player "..Player.UserId.." sent a position for replication focus of type "..typeof(Position)) end -- TODO -- Exploit logging
if Parts[Player.UserId] then Parts[Player.UserId]:Destroy() end
if not Position then Player.ReplicationFocus = nil return end
local Part = Instance.new("Part")
Part.Size = Vector3.one
Part.CFrame = CFrame.new(Position)
Part.Anchored = true
Part.Transparency = 1
Part.CanCollide = false
Part.CanTouch = false
Part.CanQuery = true
Part.Name = Player.UserId.." ReplicationFocus Part"
Part.Parent = workspace
Parts[Player.UserId] = Part
Player.ReplicationFocus = Part
end
function PlayerModule:Stun(Player : Player)
local Character = Player.Character
if not Character then warn("Cannot stun player "..Player.Name..". Player doesn't have a character") return end
if Character:FindFirstChild("StunnedEffect") then
Character:FindFirstChild("StunnedEffect"):Destroy()
end
local StunEffect = script.StunnedEffect:Clone()
StunEffect.Parent = Character
DebrisService:AddItem(StunEffect, 2)
end
Players.PlayerRemoving:Connect(function(Player : Player)
if not Parts[Player.UserId] then return end
Parts[Player.UserId]:Destroy()
Parts[Player.UserId] = nil
end)
--
local Bridge = script.Bridge -- RemoteFunction
local Remote = script.Remote -- RemoteEvent
local RemoteFunctions = { -- For functions that do not return anything
"SelectTeam",
"SetReplicationFocus",
-- "Stun",
}
local BridgeFunctions = { -- For functions that return stuff & yield
"LoadCharacter",
}
if RunService:IsServer() then
Remote.OnServerEvent:Connect(function(Player : Player, Index, ...)
if not table.find(RemoteFunctions, Index) and not table.find(BridgeFunctions, Index) then error("Player "..Player.UserId.." is trying to access restricted methods "..Index) end -- TODO -- Report user as exploiter
PlayerModule[Index](PlayerModule, Player, ...)
end)
Bridge.OnServerInvoke = function(Player : Player, Index, ...)
if not table.find(RemoteFunctions, Index) and not table.find(BridgeFunctions, Index) then error("Player "..Player.UserId.." is trying to access restricted methods "..Index) end -- TODO -- Report user as exploiter
return PlayerModule[Index](PlayerModule, Player, ...)
end
end
if RunService:IsClient() then
for i, v in pairs(PlayerModule) do
if table.find(RemoteFunctions, i) then
PlayerModule[i] = function(self, Player, ...)
Remote:FireServer(i, ...)
end
end
if table.find(BridgeFunctions, i) then
PlayerModule[i] = function(self, Player, ...)
return Bridge:InvokeServer(i, ...)
end
end
-- else, do not change
end
end
return PlayerModule
I’ve done some unconventional stuff here at the end to make it so when some methods are called from the client, it actually sends a remote to the server and runs stuff on the server. The goal was to reduce the friction caused by remote events/functions, and I’d say, so far I like this design
Simple chaching module, for accessing the same property of an object multiple times within a frame:
-- // This module can be useful to improve performance when accessing the same property of an object multiple times within a frame
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local Cache = {}
local function GetProperty(Object, Property)
local IsEmpty = true
for i, v in pairs(Cache) do IsEmpty = false break end
Cache[Property] = Object[Property]
if not IsEmpty then
task.spawn(function()
RunService.Heartbeat:Wait()
table.clear(Cache)
end)
end
return Cache[Property]
end
local Methods = {}
function Methods:GetGuiSize(Object : GuiBase) : Vector2
return GetProperty(Object, "AbsoluteSize")
end
function Methods:GetGuiPosition(Object : GuiBase) : Vector2
return GetProperty(Object, "AbsolutePosition") + Vector2.new(0,GuiService.TopbarInset.Height)
end
function Methods:GetProperty(Object : Instance, Property) : Vector2
return GetProperty(Object, Property)
end
return Methods
Module scripts don’t have to be complicated or complex
BindToClose wrapper for some quality of life:
local RunService = game:GetService("RunService")
local RunningTasks = {}
local Functions = {}
local BindToClose = {}
local IsClosing = false
function BindToClose:IsClosing()
return IsClosing
end
function BindToClose:BindFunction(Function)
table.insert(Functions,Function)
end
function BindToClose:BindTask(TaskName : string)
if table.find(RunningTasks,TaskName) then error("Cannot bind a task more than once") end
table.insert(RunningTasks,TaskName)
end
function BindToClose:UnbindTask(TaskName : string)
local Index = table.find(RunningTasks,TaskName)
if not Index then error("Cannot unbind a task that isn't bound") end
table.remove(RunningTasks,Index)
end
if RunService:IsServer() then
game:BindToClose(function()
IsClosing = true
local BoundTasks = 0
for i, v in ipairs(Functions) do
BoundTasks += 1
task.spawn(function()
v()
BoundTasks -= 1
end)
end
while BoundTasks > 0 do task.wait(1) end
while #RunningTasks > 0 do task.wait(1) end
return
end)
end
return BindToClose
Storing assets:
local AssetIds = {}
AssetIds.LeftArrow = 15705347309
AssetIds.RightArrow = 15705324962
AssetIds.UpArrow = 18883779034
AssetIds.DownArrow = 18883779221
return AssetIds
This is an idea I’m trying out, centralizing where the assets are into a module script. I think I like it, but I haven’t had the chance to really put it to the test
a GameState module, for managing the states of a round based game
local HttpService = game:GetService("HttpService")
local Utils = require(game.ReplicatedStorage.Modules.Utils)
local GAME_SETTINGS = require(game.ReplicatedStorage.GAME_SETTINGS)
local SignalModule = require(game.ReplicatedStorage.Modules.SignalModule)
local GameState = {}
local ActivePlayersFolder = game.ReplicatedStorage.ActivePlayers
local CurrentGamemode : string? = nil
local RoundId = nil
local ActivePlayers : {Player} = {}
local StartTime = 0
local Connections : {RBXScriptConnection} = {}
-- This is called before GameState:StartRound(), but it is garanteed that GameState:StartRound() will be called after
function GameState:LoadPlayer(Player)
if not Player or not Player.Parent then return false, nil end
Player:LoadCharacter()
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)
local PlayingTag = Instance.new("ObjectValue", ActivePlayersFolder)
PlayingTag.Value = Player
PlayingTag.Name = Player.Name
local CharacterTable = {
Character = Character,
HumanoidRootPart = HumanoidRootPart,
Humanoid = Humanoid,
}
GameState.PlayerLoadedSignal:Fire(Player, CharacterTable)
return true, CharacterTable
end
function GameState:StartRound(Gamemode : string, Players : {Player})
StartTime = os.clock()
RoundId = HttpService:GenerateGUID(false)
CurrentGamemode = Gamemode
table.move(Players, 1, #Players, #ActivePlayers + 1, ActivePlayers)
for _, Player in ipairs(Players) do
table.insert(Connections, Player.CharacterRemoving:Connect(function()
local Index = table.find(ActivePlayers, Player)
if not Index then return end
table.remove(ActivePlayers, Index)
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 Cause = Player:FindFirstChild(Player.Creator.Value)
game.ServerScriptService.Dialogue.Choose:Fire("Betrayal", Player, Cause)
else
game.ServerScriptService.Dialogue.Choose:Fire("Defeat", Player)
end
Player:SetAttribute("Playing", false)
Player.Team = game.Teams.Lobby
GameState.PlayerDiedSignal:Fire(Player)
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))
end
GameState.GameStartedSignal:Fire()
end
function GameState:EndRound()
GameState.GameEndingSignal:Fire()
StartTime = 0
RoundId = nil
CurrentGamemode = nil
for _, Player in ipairs(ActivePlayers) do
Player:SetAttribute("Playing", false)
Player.Team = game.Teams.Lobby
Player:LoadCharacter()
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:IsRoundActive()
return RoundId ~= nil
end
function GameState:GetRoundId()
return RoundId
end
function GameState:GetActivePlayers()
return ActivePlayers
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
GameState.PlayerLoadedSignal = SignalModule.new()
GameState.PlayerDiedSignal = SignalModule.new()
GameState.GameStartedSignal = SignalModule.new()
GameState.GameEndingSignal = SignalModule.new()
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
Hope this helps