Optimizing a game client script structure

This is quite a long thread and a pretty open topic, hopefully my explanation below can be understood, hopefully at the end I’ll get some response on this long boring topic… lol

So lately I’ve been wanting to optimize my client main code a bit more for better overall performance.
In my game, I put everything inside a big local script. Along with few module scripts with functions that are frequently called. However, other than that, everything else is being STORED in the big client script, such as input data, character states etc. with around 6K lines in total. These things can be pretty wide, such as custom player list, custom backpack, vote-kicking player handler etc.

There are 2 reasons why I wanted to do some optimization on my client script:

Firstly, I have noticed few performance problem.
For an example, I have a custom footstep framework for my game, it checks every players’ character state and character velocity every step (I know there’s better approach). Recently, I’ve moved the footstep framework FUNCTIONS from my big client script to a module script, but it’s still being run inside the big script, before, everything was ran inside the big script with no slight problem.

For instance:


The client behavior was found to behave more poorly after this change, one of the most obvious behavior downsight was my gun engine was also handled on that big script, and the gun was lagging so much after this was implemented. After I’ve reverted this behavior, the guns seems to be normal again. I’m not 100% sure that this was the factor that was causing it, but it seems to be at this point.

Secondly, I’m really close towards the 200 local variable limits (actually reached it for few times)
I’ve look up on similar threads on DevForum regarding this problem, later finding out that reaching the limit should not be happening usually. Because of how code should be optimized, the limit should not be reached. Suggestions on resolving it is utilizing module script which is something that I’m already doing. I’ve also tried to categorize my variables but later finding out that this is not a long term solution. Indeed, there are quick solutions such as wrapping threads in do end or using function() instead of local function() but I was advised that local function() should be used instead of function(). And again, in a long term scope, I think I should start optimizing them now for future game development.

So what am I doing now?
Obviously, trying to optimizing them with MORE module scripts.
I’ve mentioned that I only use module scripts for functions, but I think I should store data and configurations on module scripts, and different module scripts interact with them to get the data. However, I have few concerns on module scripts:

  • Module scripts can be easily required by exploiters to change certain config data (it will be even easier for them to change when they are a module script rather than a client script)

  • Module scripts contains really redundant variables, such as services, instances etc. (E.g: Each module scripts have to define ReplicatedStorage or a certain instance such as PlayerGui if they want to reach them) I already have like over 15 module scripts already, will that be a problem?

  • Should I use local scripts or module script for code that is dependent from
    others? (No other moudle script will require it) I’ve seen Ruddev’s open source battle royale and apparently they utilize local scripts for different aspect of their game. One down sight of local script is it can be disabled by exploiters.

  • Different module scripts can initialize at different time, a certain data might not be fetched or avaliable when they firstly initialize, resulting into an error. In a big local script structure, I can order code and decide which runs first such that data integrity is mostly guaranteed.

  • Module scripts can not require each other, I afraid that I’ll create a clash between multiple module scripts when they are requiring each other.

To conclude, I don’t really need a solid answer for my problem, but I just want to hear suggestions from developers out there on how they optimize their game, and most importantly, I want to make sure what I’m doing right now (optimizing with module script) is correct and I’m going to a right direction.

3 Likes

Adding Formulas as variables can help a lot, people like boatbomber and other scripters as well as me use this strategy; This makes it way simpler to use and more organized

Here’s a example on how to use it

local sin,arc = math.sin,math.arc

2 Likes

Hello, i recommend you to use a framework like Knit

It’s great that you want to modularize your code, but module scripts are a way to structure your code for readability (no one should needlessly look at a couple hundred lines of code that performs a very specific task), debugging (errors are localized to that module script if it occurred within it), and preferences (I prefer them over bindables). They are not a way to optimize your code, but rather a way to organize your code. If you’re having problems with performance, then something else is the cause.

Hitting 200 local variables is quite a lot; I think the most I’ve hit is around 10 upvalues (I wasn’t as module-happy as I am today XD). I don’t want to set you down the wrong path, but I’m wondering if you’re missing out on the power of tables. 1 table = 1 tally mark towards your limit, regardless of how much is stored inside your table. In addition to this, localize your variables as much as possible for anything that does not need to retain its data. Localized variables inside a function are also garbage collected when that function finalizes; it’s a nice way of immediately reducing memory usage without any manual work.

Anything ran on the client is susceptible to exploitation, whether or not it’s a module or local script. You are handing your client code to the client because they need it to run the game; there’s nothing more you can do once you hand the client code over. Focus more on server verification since these cannot be exploited. I’m not saying that client checks are bad; in fact, I think they are a great filter before the action even gets to the server sanity check.

Pass whatever data the module script needs as arguments to save on the redundancy. Some even use a module script as a repository for their commonly used variables. There has also been tale of those on the devforum that have reached hundreds of module scripts, you’ll be fine. As mentioned, it’s just a way to organize your code.

Both can be disabled by exploiters. A module script placed inside replicated storage which is then required by both a local script and a server script has two completely separate copies, one for the client and one for the server. The client copy is definitely exploitable, but it will never affect the server’s copy. That being said, it’s really just preference whether or not you wish to use local scripts with bindables or module scripts. If the code inside is completely stand-alone and does not need access to any other script, then you might as well make it a local script.

Module scripts can be structured in the same way. Treat them like a function and make use of their table formatting. For example, do you need upvalues inside the module immediately or can you require the module and fetch the data when it’s needed at a later time? When you require a module, it will only run its top scope; meaning functions are only declared, but not actually ran. The function below will be initialized for a later time, but will not run valueIn+= 1 when the module is required (you’ll need to call module.addValues in order to run the code within):

function module.addValues(valueIn)
	valueIn += 1
end

If you absolutely need a circular dependency, then you can get around it by creating an initializer function. This takes the same concept as mentioned above, but uses it to require module scripts which depend on one another:
Server Script

local moduleScript1 = require(game:GetService("ServerScriptService").Modules.ModuleScript1)
moduleScript1.Initialize()

Module Script 1

local moduleScript2 = require(game:GetService("ServerScriptService").Modules.ModuleScript2)
function moduleScript1.Initialize()
    moduleScript2.Initialize() -->moduleScript2 was already required, so no errors will be thrown
end

Module Script 2

local moduleScript1
function moduleScript2.Initialize()
	moduleScript1 = require(game:GetService("ServerScriptService").Modules.ModuleScript1)
end

[EDIT] Added the outside server script to fully show how it is done.

3 Likes

hello. i don’t really know the implementation of lua, but i have some things pretty clear and i learned quite a lot from the implementation of other languages that i think apply to lua.

Creating a large local script apparently has its pros, but it also has its cons:

  • a huge script is like an enormously complex machine with so many states that you simply can’t control it completely.

You probably didn’t factored a state correctly.

  • Every script and ModuleScript has a program memory. This memory has a maximum limit. A huge script simply exceeds that limit and what lua will do is break that huge code into pieces. This is equivalent to splitting your code into modules.

  • creating a huge script is actually making life easier for hackers. There are advanced techniques such as code parsing and pattern searching with which you can add, remove or replace any piece of code in a script. Doing all this in a single script is much easier. It’s true that using modules doesn’t save you from this either, but it’s certainly a bit more difficult for hackers. Plus, you can always move and rename your modules every so often to make it even harder.

Lua threads are not the same as system threads. Lua threads do not run in parallel, i.e. only one thread is running at any given time. This means that an error of this type will never occur (as long as it is not a logic error) because the first time a module is required it will be executed completely. And while the module is executed no other thread is executed at the same time (unless it is intentionally yielded, i.e., a logic error).

Cyclic dependencies can be solved by making requirements dynamically (requires within functions for example).

By the way I can’t say anything more about optimizations in general that hasn’t been said in the forum. I can only say that performance is closely linked to the implementation of a certain logic.

2 Likes

I have been trying to split my huge local script into different scripts lately (losing my mind) because I ran into a lot of undefined variables and functions, for an example if I wanted to move A into a separate script but it involves a variable or function B, I also have to move variable/ function B into a module script and B might actually involve C and so on. I don’t know is this something I should do right now as it’s kinda time consuming and it can get pretty frustrating, and after all these works are done, nothing much will change to the overall gameplay, I only hope for better performance for the game, but apparently that is not guaranteed. But anyways:

I’m trying to create a repository for instances, will this be a good practice, and how does this say in terms of performance?

Module script library:

local InstancesLibrary = {}

InstancesLibrary.GameMisc = ReplicatedStorage:WaitForChild("GameMisc")
InstancesLibrary.Remotes = ReplicatedStorage:WaitForChild("RemoteEvents")

Other scripts:

local InstancesLibrary = require(ReplicatedStorage:WaitForChild("Libraries"):WaitForChild("InstancesLibrary"))
local GameMisc = InstancesLibrary.GameMisc 

Does requiring the instance module on every script consumes quite an amount of memories as the instance library grows?

Or is this approach better?

image

Library main:

local LibraryMain = {}

local Library = require(script.Library)

function Library.Get(Name)
    return Library[Name]
end
return LibraryMain

Library:

local Library = {
	GameMisc = ReplicatedStorage:WaitForChild("GameMisc")
}

return Library

To define GameMisc in a script:

local GameMisc = Library.Get("GameMisc")

Which one should I use? I feel like I should use the first one, or is there any better approaches?

Each module has its own memory place and is set when the module is required for the first time. The rest of the requires only returns the reference to that memory place. Even in cases where modules are loaded dynamically, memory places that have lost reference are collected by the GC.

Modules are just a way to organize your code. Creating hundreds of modules, placed anywhere in the hierarchy and required hundreds or thousands of times will hardly affect performance. Almost all of this has to do with calls to references whose time cost is negligible (perhaps on the order of microseconds or less). Just create and organize your modules as best suits your project.

Where performance issues really exist is in the logic(s) of your game or in the algorithms you implement. In my experience this is usually artificial intelligence and graphical effects.An example is the Aquaman game, where they clearly abused the graphical effects. They broke the limits of the engine back then, and they had to be quite ingenious to solve it.

1 Like

Alright, thanks for your explanation.

I want to know more about this library system whether it’s appropriate to be applied on, Should I apply this approach in different scripts?

using instance handlers (or objects in general) is a pretty common practice in roblox, and certainly a good idea. you just have to be as specific as possible. I mean you do things like PlayerLibrary, PetsLibrary, WeaponsLibrary, etc. Handling all your logic internally.

As an example you could look at this roblox template in ServerStorage.ModuleScripts.

That’s a really smart idea, might start using that actually.