Script Optimizations

I’ve been working on an asymmetrical horror game with some of my friends and my game so far consists of THOUSANDS upon THOUSANDS upon THOUSANDS of lines. I’ve wanted to delay this step, but I believe it is time I ask the crucial question.

I’ve tried doing some organization stuffs, though pretty basic with the folders and models and ModuleScripts, I still feel the game is very unoptimized.

HOW DO I PROPERLY OPTIMIZE A GAME??
HOW DO I ORGANIZE MODULES AND SCRIPTS EFFECTIVELY??

And lastly,
HOW DO I MAKE SCRIPTS MORE OPTIMIZED IN CODE??

Most of these are extremely hard to accomplish, because the code starts to become very hard to read overtime.

Any suggestions would help!!

4 Likes

Could we see an example piece of code that you think can be improved on in terms of organization and optimization? This will help identify potential areas of improvement and avoid redundant suggestions for practices that you may already be doing.

1 Like

Would you like a snippet of the main client script or the round script?

1 Like

It doesn’t matter - just whatever you think best displays how unorganized your code is.

1 Like

This is basically my snippet I made for handling 5 ability buttons along side a Mouse1 action.

			local function RunAbilityButton(button)
				
				local abilityFound = nil
				for _, ability in pairs(Configuration.Abilities:GetChildren()) do

					if ability.Order.Value == tonumber(string.match(button.Name, "%d+")) then

						abilityFound = ability
						break

					end

				end

				if abilityFound then

					button.Visible = true

					button.AbilityName.Text = abilityFound.Name
					button.Text = Player.Settings["Ability" .. abilityFound.Order.Value].Value
					if Player.Settings["Ability" .. abilityFound.Order.Value].Value == "MouseLeftButton" then

						button.Text = "M1"

					elseif Player.Settings["Ability" .. abilityFound.Order.Value].Value == "MouseRightButton" then

						button.Text = "M2"

					end
					if StatsFolder.AbilityCooldowns:FindFirstChild(abilityFound.Name) then

						button.Text = math.floor(StatsFolder.AbilityCooldowns[abilityFound.Name].Value)
						button.UIStroke.Transparency = 0.75

					else

						if CharacterInfo[Player.Data[Character.Role.Value .. "Used"].Value].AbilityInfo[abilityFound.Name].Decal.Texture ~= nil then

							button.Icon.Visible = true
							button.Icon.Image = CharacterInfo[Player.Data[Character.Role.Value .. "Used"].Value].AbilityInfo[abilityFound.Name].Decal.Texture

						else

							button.Icon.Visible = false

						end
						
						button.UIStroke.Transparency = 0

					end
					if abilityFound:FindFirstChild("Charge") then

						button.Charge.Visible = true

					else

						button.Charge.Visible = false

					end
					if StatsFolder.Extra:FindFirstChild(abilityFound.Name) then

						if tonumber(StatsFolder.Extra[abilityFound.Name].Value) then

							button.Counter.Visible = true
							button.Counter.Text = tostring(StatsFolder.Extra[abilityFound.Name].Value)

						else

							button.Counter.Visible = false

						end

					else

						button.Counter.Visible = false

					end

				else

					button.Visible = false

				end
				
			end
			for _, button in pairs(GameGui:GetChildren()) do
				
				if string.find(button.Name, "Ability") then
					
					RunAbilityButton(button)
					
				elseif button.Name == "M1" then
					
					if UserInputs.TouchEnabled then
						
						local abilityFound = nil
						for _, ability in pairs(Configuration.Abilities:GetChildren()) do

							if ability.Order.Value == 0 then

								abilityFound = ability
								break

							end

						end

						if abilityFound then

							button.Visible = true
							button.AbilityName.Text = abilityFound.Name
							if StatsFolder.AbilityCooldowns:FindFirstChild(abilityFound.Name) then
								
								button.Text = math.floor(StatsFolder.AbilityCooldowns[abilityFound.Name].Value)
								
							else
								
								button.Text = Player.Settings["Ability" .. abilityFound.Order.Value].Value
								if Player.Settings["Ability" .. abilityFound.Order.Value].Value == "MouseLeftButton" then

									button.Text = "M1"

								elseif Player.Settings["Ability" .. abilityFound.Order.Value].Value == "MouseRightButton" then

									button.Text = "M2"

								end
								
								if CharacterInfo[Player.Data[Character.Role.Value .. "Used"].Value].AbilityInfo[abilityFound.Name].Decal.Texture ~= nil then

									button.Icon.Visible = true
									button.Icon.Image = CharacterInfo[Player.Data[Character.Role.Value .. "Used"].Value].AbilityInfo[abilityFound.Name].Decal.Texture

								else

									button.Icon.Visible = false

								end
								
							end

						else

							button.Visible = false

						end
						
					end
					
				elseif button.Name == "Crawl" then
					
					if UserInputs.TouchEnabled then

						if Character.Role.Value == "Survivor" then

							button.Visible = true
							button.Text = "Crawl"

						else

							button.Visible = false

						end

					end
					
				end
				
			end
1 Like

Alright, one thing I noticed is that you don’t seem to like variables very much. I’ll explain

Variable underusage

First, you use this exact value 3-4 times, yet you decide to use the full path rather than making a variable for this. Not only will this improve organization, but optimization too.

Again, you might want to make variables because this is a VERY long path. You don’t have to make a variable for this specific path, just for instances that you may be reusing often. Perhaps a variable for CharacterInfo[Player.Data[Character.Role.Value .. "Used"].Value].AbilityInfo.

Same instance as the 2nd code I mentioned


Another thing is that you seem to be copying and pasting code - it might benefit you to use functions more.

Repetition

Is the same (or similar) to


Lastly, this is just a preference, but I think you are misusing line spacing. Yes, this is important, but I think you are overdoing it. I mainly use it for separating variable definitions, actual code logic, functions, and different “types”/“purposes” of code or variables.

Thanks for the suggestions. I will try to see how I can further optimize my code.

For this, it is just a preference for me. Of course, I have glasses and I literally can’t see nothing so I just space things out with spaces so I can read code easier.

For example:

game.Players.PlayerAdded:Connect(function(plr)
    print("player joined print")
    plr.CharacterAdded:Connect(function(char)
        print("character respawned")
    end)
end)

This is really hard to read for me. I can still read it, but when it comes to thousands of lines of this, it gets hard to understand imo.

This is how I would organize this (for reference):

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player: Player)
    print("player joined print")

    player.CharacterAdded:Connect(function(character: Model)
        print("character respawned")
    end)
end)

Yes, the connections already return the type, but it’s just a good habit to type hint (autocomplete is so useful).

1 Like

Make sure to use folders to organize scripts like a desktop. For example: A folder for match scripts, a folder for player (survivor/killer) scripts and etc. Try making systems with more implementation, for example (dont know if you have done this) like a system that allows you to just add the killers/survivors stats to a module, and it will automatically show on the shop and inventory uis, and allow you to spawn in with them (obviously youd still have to script the abilities, but just the basic stats as modules help alot).

1 Like

Can you elaborate on what you mean? I feel like this implementation seems a little interesting to add in before I release my game.

I’ll check this documentation when I wake up tomorrow.

1 Like

For example, something like a module script that contains all the characters stats, where you can just type in the name, ability names, ability cooldowns, run speed, health and other stats, create a folder with their name, throw their rig in there, and they just appear, like the buttons are already there, they have all of their stats, and you just have to script the individual abilities as modules, with a function like module.UseAbility() or something, and throw it in the same folder as the character, probably under another folder for abilities, and they just work. Reduces alot of the scripting needed, which would help you pump out updates faster, and more reliably, which helps gain more players.

Im not the best at explanations, so i think it would be better if i just showed an example.

Server Code:

function rusher.New(number, generatedRooms, entityName, bypass)
	
	local avLocker = rusher.LockerCheck(number, generatedRooms)
	if not bypass then
		if avLocker == false then spawnError:Fire() return end
	end

	local dataCurrentEntity = entityData[entityName]


	local entityData = require(script.Parent.EntityData)
	local entityAbility = nil

	if dataCurrentEntity.SpawnAbility or dataCurrentEntity.NearAbility then
		entityAbility = require(entityAssets.RusherEntityAbilties[entityName])
	end

	if not entityAssets.Entities:FindFirstChild(entityName) then return warn(entityName, "is not a valid entity.") end

	local currentEntity = entityAssets.Entities[entityName]:Clone()

	local spawnRoomN = number-dataCurrentEntity.SpawnRoom
	local endRoomN = number+dataCurrentEntity.EndRoom

	local ignoreList = {}

	if not generatedRooms[spawnRoomN] then spawnRoomN = 1 end
	if not generatedRooms[endRoomN] then endRoomN = #generatedRooms end


	local spawnRoom = generatedRooms[spawnRoomN]
	local endRoom = generatedRooms[endRoomN]

	local totalRebounds = dataCurrentEntity.Rebounds
	if not totalRebounds then totalRebounds = 1 end

	local entityDelay = dataCurrentEntity.Delay
	if not entityDelay then entityData = 0 end

	if dataCurrentEntity.Backwards then

		currentEntity:PivotTo(endRoom.Exit.CFrame)

	else

		currentEntity:PivotTo(spawnRoom.Entrance.CFrame)

	end

	currentEntity.Parent = workspace.CurrentEntities

	if dataCurrentEntity.SpawnAbility then
		entityAbility.SpawnAbility(currentEntity)
	end

	local navigatedEnd = Instance.new("BindableEvent")
	rusher.Nav(currentEntity, spawnRoomN, endRoomN, generatedRooms, dataCurrentEntity.Speed, dataCurrentEntity.Damage, dataCurrentEntity.Raycast, dataCurrentEntity.Backwards, dataCurrentEntity.AudioSlowdown, dataCurrentEntity.MinRebounds, dataCurrentEntity.MaxRebounds, entityDelay, navigatedEnd, ignoreList)


end

Module script:

local entityInfo = {
	["A-60"] = {
		["Type"] = "Rusher",
		["SpawnRoom"] = 6,
		["EndRoom"] = 9,
		["Speed"] = 250,
		["Damage"] = 150,
		["Raycast"] = true,
		["AudioSlowdown"] = true,
		["Delay"] = 7,
		["Weight"] = 1,
		["MinRoom"] = 15,
	},
	
	["A-95"] = {
		["Type"] = "Rusher",
		["SpawnRoom"] = 6,
		["EndRoom"] = 4,
		["Speed"] = 55,
		["Damage"] = 300,
		["Raycast"] = true,
		["Backwards"] = true,
		["Delay"] = 7,
		["Weight"] = 0.25,
		["MinRoom"] = 33,
		["SpawnAbility"] = true,
	},
	
	["A-120"] = {
		["Type"] = "Rusher",
		["SpawnRoom"] = 4,
		["EndRoom"] = 3,
		["Speed"] = 85,
		["Damage"] = 400,
		["Raycast"] = false,
		["Backwards"] = true,
		["MinRebounds"] = 2,
		["MaxRebounds"] = 3,
		["Delay"] = 5.5,
		["Weight"] = 0.45,
		["MinRoom"] = 28,
	},
	
	["A-140"] = {
		["Type"] = "Rusher",
		["SpawnRoom"] = 5,
		["EndRoom"] = 0,
		["Speed"] = 65,
		["Damage"] = 1200,
		["Raycast"] = false,
		["Backwards"] = false,
		["Delay"] =  8,
		["Weight"] = 0.15,
		["MinRoom"] = 35,
	},

This is a script used in my game, inspired by doors. This code handles all the “rusher” category monsters, which rush through every room. As you can see, i dont have to individually script all of them, instead, i simply throw their model in a folder, write all their stats, and it works. The ones with a “spawn ability” value, have special modules scripts in another folder, allowing me to easily script a new monster without tons of effort.

1 Like

Holy, @Henodel has been writing for like 30 minutes.

Edit: Didn’t mean to reply to you

1 Like

haha, ive been afk for a bit

aaaaaaaaaaaaaaaaaaa

2 Likes
Guard Clauses

There’s a lot of nesting here. Consider using guard clauses, nesting decreases readability and makes it difficult to track your logic .

So instead of:

if abilityFound then
    -- code
end

A guard clause would look like this:

if not abilityFound then
    return
end
-- code
Functions

This function’s way too long and doing way too many things. You should break this down into a few smaller functions, these smaller functions may be inlined by the compiler anyways so any performance difference is negligible.

Instead of running all your abilities in one function, you can try breaking it down into a function for each ability.

Lookup tables

Currently, your looping through Configuration.Abilties. You can try creating a lookup table for ability.Order.

-- This should only be run once
for _, ability in pairs(Configuration.Abilities:GetChildren()) do
      orderedAbilities[ability.Order.Value] = ability
end

local order = tonumber(string.match(button.Name, "%d+"))

abilityFound = orderedAbilities[order]
Variables

Exactly what ig1oo1 said, you should be using way more variables. It would really help with your code’s readability and your code’s efficiency.

I’d also avoid relying on button names for your logic. This can easily break whenever you edit your UI.

3 Likes

guard clauses and simplified conditions go a long way

i also avoid else/elseif to reduce indentation

1 Like

This is how I would organize it.

--!nonstrict
--!optimize 0
--!nolint
x = getfenv

Table = {data={}}
function Table:new()
	o = {}
	setmetatable(o, self)
	self.__index = self
	return o
end

function Table:set(k,v)
	rawset(self.data, k, v)
end

function Table:get(k)
	return self.data[k]
end

local Metatable = Table:new{}
function Metatable:assign(t)
	setmetatable(t.data, self.data)
end

__index = "__index"

services = Table:new{}
servicesMT = Metatable:new{}
servicesMT:set(__index, function(t, k)
	local Service = Game:service(k)
	return Service
end)
servicesMT:assign(services)

function OnPlayerChangedFunction(player, changed)
	if changed == "Character" then
		character = player.Character
		print"character respawned"
	end
end

function OnPlayersChildAddedFunction(object)
	print"player joined print"
	object.changed:connect(function(prop)
		OnPlayerChangedFunction(object, prop)
	end)
end

services:get"Players"
	.childAdded:connect(function(p)
		OnPlayersChildAddedFunction(p)
	end)

My opinion is the only correct one.

3 Likes

I’m sorry, but I don’t understand what half of these mean.

1 Like

Could you give me an example of the hierarchy so I could get a better understanding?

Sorry, what do you mean by example of the hierarchy? I don’t really know what part your referring to. Do you mean the hierarchy of the character system folder? (the part where you store the character and abilities and etc.

Prepare for unforeseen consequences with such an approach, Mr. Kaelen.
This code feels like it’s one __index away from opening a resonance cascade, Kaelen.

4 Likes