Modularizing a script for extra customization

Hello everyone, I’m working on a lil’ survival sorta game. I had a question on how I should go about improving my script. I think this is good as is, but what if I say wanted a character to have a random weapon in a specified group (firearm - handgun / melee - axe)?

Script down below, feel free to advise things.

-- // SERVICES
local CollectionService = game:GetService("CollectionService")
local ServerStorage = game:GetService("ServerStorage")

-- // DEPENDENCIES
local assets = ServerStorage.Assets
local tools = assets.Tools
local spawns = workspace.Spawns:GetChildren()


local CharacterManager = {}

-- // Documentation or whatever
--[[
MaxHealth - The maximum amount of health the character can have.
MaxSanity - The maximum amount of sanity the character can have.
WalkSpeed - The character's walking speed.
SprintSpeed - The character's speed while sprinting.
DamageResistance - Damage multiplier.
HungerDecay - How fast the player goes hungry (per interval)
ThirstDecay - How fast the player goes thirsty (per interval)
HungerInterval - How often the player's hunger ticks down.
ThirstInterval - How often the player's thirst ticks down.

Tags - A list of tags to apply to the character.
StartingTools - A list of tool names to give the player when they spawn.
]]

CharacterManager.Characters = {
	["Test"] = {
		MaxHealth = 100,
		MaxSanity = 100,
		WalkSpeed = 16,
		SprintSpeed = 21,
		HungerDecay = 0.75,
		ThirstDecay = 1,
		HungerInterval = 7,
		ThirstInterval = 5,
		DamageResistance = 1,
		Tags = {"test_tag_hiii"},
		StartingTools = {"LinkedSword"}
	}
}

local function TPPlayer(player: Player, character: Model)
	local chosenSpawn = player:GetAttribute("Spawn")
	local target
	
	if #spawns == 0 then
		warn("No spawns found!")
		return
	end
	
	if chosenSpawn then
		target = spawns[chosenSpawn] :: Part
		character:MoveTo(target.Position)
	else
		target = spawns[math.random(1, #spawns)] :: Part
		character:MoveTo(target.Position)
	end
end

local function InitializeHumanoid(humanoid: Humanoid, characterData)
	if humanoid then
		humanoid.WalkSpeed = characterData.WalkSpeed
		humanoid.MaxHealth = characterData.MaxHealth
		humanoid.Health = humanoid.MaxHealth
		for dataVar, value in pairs(characterData) do
			if typeof(value) ~= "table" then
				humanoid:SetAttribute(dataVar, value)
			end
		end
	end
end

local function ApplyTags(character, characterData)
	local dataTags = characterData.Tags
	
	local existingTags = CollectionService:GetTags(character)
	for _, tag in existingTags do
		CollectionService:RemoveTag(character, tag)
	end
	
	if dataTags then
		for _, tag in dataTags do
			CollectionService:AddTag(character, tag)
		end
	end
end

local function GrantTools(player: Player, characterData)
	local dataTools = characterData.StartingTools
	local backpack = player:WaitForChild("Backpack")
	
	if dataTools then
		for _, toolName in dataTools do
			if tools:FindFirstChild(toolName) then
				local tool = tools[toolName]:Clone()
				tool.Parent = backpack
			end
		end
	end
end

function CharacterManager:LoadCharacter(player: Player)
	local chosenChar = player:GetAttribute("Character")
	local character = player.Character or player.CharacterAdded:Wait()
	
	if chosenChar then
		local characterData = self.Characters[chosenChar]
		local humanoid = character:FindFirstChild("Humanoid") :: Humanoid
		
		GrantTools(player, characterData)
		InitializeHumanoid(humanoid, characterData)
		ApplyTags(character, characterData)
		TPPlayer(player, character)
	else -- Random character
		local keys = {}
		for name, _ in pairs(self.Characters) do
			table.insert(keys, name)
		end
		
		local randomKey = keys[math.random(1, #keys)]
		local characterData = self.Characters[randomKey]

		local humanoid = character:FindFirstChild("Humanoid") :: Humanoid
		
		GrantTools(player, characterData)
		InitializeHumanoid(humanoid, characterData)
		ApplyTags(character, characterData)
		TPPlayer(player, character)
		player:SetAttribute("Character", randomKey)
	end
end

return CharacterManager

From a practical standpoint, if you think it’s perfectly fine, there’s not much reason to change it. Unless you have/plan to have other people working on your game and you think this’d be too hard for them to get used to, you should keep this format; just make sure to keep your other systems’ formats consistent with it.

From a personal standpoint, I feel like this is quite an awkward implementation of OOP and have no idea how you’ve managed to get the CharacterManager:LoadCharacter method to work without metatables. I also don’t see a constructor method. However, so long you’re using it like a regular OOP object, I wouldn’t have any qualms outside of including that constructor method. If you included details in how this (at least, what I assume is) module script works, I might have more complaints.

Please don’t use ipairs/pairs How we make Luau fast | Luau

I suggest you to use early returns more often becouse they don’t change bytecode (which is good) but add readability by a lot.

Also don’t create methods when you never get to use self.

im sorry oop? this isnt meant to be oop? isnt that how you write functions in modules for use in scripts???

early returns where exactly?
and what do you mean by “don’t create methods when you never get to use self.” what if i want to use the function in a different script? (which is exactly what i want to do) (disregard, dealt with the returns)

When I don’t use OOP, I just write functions like properties (ie. “function module.foo()”), just as I had learned when I first started (the Roblox guide on the subject now says you should be using “module.foo = function()”, though). I only follow the semantics you use while I’m writing an OOP class to make use of the hidden module argument that using a colon version provides, thus allowing me to write in the style I used when I was in a C++ course. This guy’s post explained it pretty well for me, so take a look if you’re interested in what the self keyword actually does.

Since you’re not using OOP, I’ll reiterate that this system is fine if you’re the only one scripting and you have a plan for future expansion; just that I personally would’ve preferred using OOP. And to implement Yarik_superpro’s suggestions, even if they aren’t really related to making it easier to organize a script.

I see, thank you for clarifying

You created a method.
You dont use self and yet you for some reason you added it.

i don’t see the problem with that?

NAMECALL is slower than CALL; There is everything wrong with that.

function CharacterManager.LoadCharacter(self, player: Player)
	local character = player.Character or player.CharacterAdded:Wait()
	local humanoid = character:FindFirstChild("Humanoid") :: Humanoid
	local chosenChar = player:GetAttribute("Character")
	
	local charKey = chosenChar and string.len(chosenChar) < 0 or GetRandomCharacterKey()
	local targetChar = self.Characters[charKey]

	GrantTools(player, targetChar)
	InitializeHumanoid(humanoid, targetChar)
	ApplyTags(character, targetChar)
	TPPlayer(player, character)
	player:SetAttribute("Character", charKey)
	
	if targetChar.OnSpawn then
		targetChar.OnSpawn(player, character)
	end
end

like this?

1 Like

Yes also please don’t use string.len.
Its outdated, has no FASTCALL and # has an actual bytecode OPCODE LENGTH

1 Like

oh, so use # instead? okay! thank you

1 Like

Here is a proof if you don’t belive me:
image

1 Like

i’d also like to ask - does it really matter that much if i NAMECALL or just CALL? is there some kind of massive performance tank of the sorts or something?

Its a 2 different OPCODES;
So yes there is a differance.
CALL is faster and is more dirrect.

1 Like

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