Doing Modular Approach Correctly?

I don’t know if this is a problem for others but I have a serious problem when it comes to Roblox Studio script organisation.

Every time I’m working on a project, I question if my organisation method is efficient enough and when I find a few downsides to it I spend ages trying to see how I can adjust my method only for it to end up even worse than what it was before which has killed my motivation many times.

However, I’ve seen countless script hierarchy’s where it’s just a bunch of modules that each do a specific task or handle a single responsibility and they all seem really well structured, clean and they all promote reusability which is what I’m aiming for.

I tried to do something like this with a very quick NPC interaction / dialog system that works as I expect it to.

This is my hierarchy:

This is what it looks like in the NPC script:

I already know how modules work and have a good understanding of OOP.
Is this the right way of going about this? Anyway ways on how to improve? How would you go about this? I feel like compared to the others I’ve seen I’m not doing it quite right.

PS: (MovementToggler is for the local player’s character. Not the NPC. I know It shouldn’t be under the NPC folder but this is just a quick example.)

1 Like

I’d say this is pretty good (i think) I remember watching a few videos by Lodius (youtuber) that explained everything really well, for me at least. But he has stuff of module scripts and OOP. I’d say check out his videos. They might help you, but like I said earlier I think thos is pretty good. The only way I see this getting cleaner is using a module loader. (Lodius did a vid on that to)

That’s not a modularization; that’s a balkanization.
Modularization: easy to apply to anything.
Balkanization: splitting code for the sake of it.

I balkanise too much, or i just have code in weird places

Thankfully I found this post while starting a new project that I want to be Modular, so I have to ask the question: What exactly IS the modular approach? Is it just using module scripts for ALL your functions and calling them in on a script?

That’s purpose and also with this you can make table in modulescript and use it and so on it..

Some recommend avoiding the approach where your entire game runs off of one script. Arguing the most efficient practice on roblox is:

  • Each game system should be ran off its own script. Cross-System communication to be achieved by bindable events/functions.
  • Use ModuleScripts for shared logic and data.
  • Avoid one giant “god script” that controls everything.

IMO, games set up like this are easier to debug, especially with profiler, as this is how games were meant to be created.

Now,

It is still okay to have a strictly module based approach, where you have one main script act as the bootstrap to your many module based systems. But you should be cautious of drawbacks.

  • when youre initializing systems in the bootstrap, dont do heavy work on the main thread. You will delay starting up other critical game systems. The key is to still give each system its own thread(s), just all initialized from one place.
  • Memory management is a little more hands on. No more :Destroy() on scripts to just clean everything up when youre done. Leaks will come up more often prob.
  • Strict adherence can create overhead in small pieces of game, like a single fountain. That fountain could be implemented by a single script with 5 lines, independent from any other system.
  • More overhead in general. Everything becomes inherently connected.
  • Debugging becomes much more difficult, specifically with the profiler. debug.setmemcategory exists, but there are plenty of known bugs which will cause you issues down the line.
2 Likes

Thanks for the details, this is pretty interesting. But yea, I decided to not go through with the whole great modular approach, and instead I’m using a module script for seperate logic.

I’m trying to rush a horror game by the end of the month, and I just started on saturday, so attempting this approach is pretty experimental, but an example I can give is Players joining and leaving a lobby, and I have 2 seperate modules for it with one handling the client and it’s Ui, and the other handling server and lobby placement. When I did the first iteration of the system, I ran into this issue where it all became sorta difficult to debug, or find oversights in my code, which led to me rewriting it. Modules help out as I can make the local script really small, and go back to the same functions in the module script.

Overrall, the approach is one of those things that requires and understanding of module scripts and how they work, as well as practice. But once you get it, it becomes relatively easy.

And about OOP, I rarely see a use for it that can’t already be achieved by a basic script.

Just because modulescripts exist doesn’t necessarily mean you should use one script on the server and one on the client and require a bunch of modulescripts.

I’ll often put modulescripts as children of regular scripts, and regular scripts as children of modulescripts.

Yet I’ll still say that I love module scripts, and once could say I even overuse them.

The entire idea of OOP is that its a design principle. It is never something that is strictly necessary, however it can drastically simplify implementations of various systems, mostly by improving readability.

1 Like

The one thing that prevents me from utilizing a lot of OOP is the fact it remains in Memory when not in use. Do you have any ideas on how I can get around that? I’ve tried looking at garbage collection systems but I don’t generally find them anywhere. Any tips?

What do you mean exactly?

I think you may be misunderstanding OOP on roblox.

Mimicking “OOP” behavior is achieved through metatables, these metatables are subject to the same garbage collection rules as any other table in any other script.

1 Like

I’d say that means you’re doing it wrong. This is how I emulate OOP:

type Point = { x: number, y: number }

function Point(x: number, y: number): Point
    return { x = x, y = y }
end

function exampleMethod(self: Point): string
    return `{self.x}, {self.y}`
end

Doing it like this means my “objects” are just tables and the “methods” only exist once.

This of course only fits my needs, as I don’t really use inheritance all that much.

1 Like

Really? I had heard somewhere they are exceptions to those rules. If so, then I may experiment with that in my project. Thanks for letting me know

1 Like

If you happen to have that article id love to read. There are always edge cases, for example you need to be very careful to clean up connections when removing objects (because those will leak). But in general, there is not some big overhead associated with OOP.

1 Like

I personally created a physical NPC that ran with the whole

function Module.new

I would then spawn it in with a method, and use another method for movement and attacking. However, I didn’t want to fully use it as I believe I would have to use a seperate script for each individual NPC (I’m not that experienced in scripting to be fair), so I unfortunately never used it to what I believe it can do. I’ll have to do more research on OOP, the only issue is that I can’t seem to find OOP tips for what I want to do that isn’t just printing.

Each NPC does not strictly need its own script, however if you have many NPCs having multiple threads handling them can help to balance the load depending on what these NPCs are doing. This is a situation were OOP could definitely be used.

1 Like

How would I be able to do that? Using coroutines? Task.spawn? I honestly don’t really know.

This wouldn’t be the whole “It stays in memory” thing, but I’m pretty sure I heard somewhere that it stays in memory.

Having a GC module is very unnecessary in almost any context. In OOP, implement a :Destroy() method for each object. or :Cleanup().

In normal code, just handle your temporary connections as needed.

Specifically, with OOP:

Handler Script

require(NPC_Object)

local NPCs = {NPC_Object.new("Todd"),NPC_Object.new("Ben"),NPC_Object.new("Jerry")} --some table of NPCs

local function destroyNPC(i) --destroy NPCs[i]
     local toCleanup = table.remove(NPCs,i) --removes the object from NPCs, defines it to cleanup.  Now, the NPC we are looking to cleanup is only defined in the scope of this function, as opposed to globally under NPCs.
     toCleanup:Destroy() --calls :Destroy of the object, cleaning up all connections and deleting the character instance.
end --the function ends.  Because "Ben" was stored in toCleanup var (which is defined on the stack), no variable points to ben anymore.  The garbage collector recognizes this, and will automatically allow the memory Ben was stored in to be reallocated.

destroyNPC(2) --Destroy and clean up NPC named "Ben"

NPC Script

local NPC = {}
NPC.__index = NPC

function NPC.new(name)
     local self = setmetatable({},NPC}

     self._connections = {} --a place to store connections craeted by this object, which we will need to GC manually.
     self.name = name
     self.character = someModel:Clone()

     self:Initialize()

     return self
end

function NPC:Initialize()
     table.insert(self._connections,...) --define whatever connectinos you need here, appending each of them to _connections.
     self.character.Parent = workspace
end

function NPC:WalkTo(p) --some random function
   ...
end

function NPC:Destroy() --this will run when we want to remove the NPC
     for _,v in ipairs(self._connections) do
          v:Disconnect()
     end
     self.character:Destroy()
end

the above illustrates the basic idea of using :Destroy() in an object.

does this clear things up at all? wnat clarification anywhere?

1 Like