How do you architect a Single-Script Architecture?!

So obviously all the pros write code in a “Single-Script Architecture” where you have one script and it requires the ModuleScripts which require other ModuleScripts.

Where do you put the code that should run at Runtime? (like an onPlayerAdded function)

This is what I have now because it’s what I assume you would do.

image

In the Runtime server script I would loop through the “Runtime” folder and require all the Modules to get them started

Then NotRuntime is just a folder with ModuleScripts that can be required whenever.

Also let’s say one of these Runtime Modules doesn’t need to return anything, why even make it a ModuleScript? Just to follow the Single part of Single-Script Archtiecture?

Should there be any method to the requiring of Modules? Or just do as I did and require the ones you need at runtime at runtime and lazy load the rest?

I know Knit starts all the Services at runtime, but something might not be a service (like a little onPlayerAdded function)

Thanks.

2 Likes

Probably with an init or main function. Example:

local Players = game:GetService("Players")

local PointsManager = {}

function PointsManager.onPlayerAdded(player)
    -- Business logic goes here!
    player:SetAttribute("Points", 0)
end

function PointsManager.init()
    Players.PlayerAdded(PointsManager.onPlayerAdded)
    for _, player in ipairs(Players:GetPlayers()) do
        task.spawn(PointsManager.onPlayerAdded, player)
    end
end

return PointsManager

(Your main script would then require PointsManager and call .init() when things are ready, particulary things that onPlayerAdded might need)

On that note, a primary benefit of single-script architecture is that you can boil down dependencies and create a mental model of how your code executes (particularly, in what order). When you :GetChildren() and then require() a bunch of modules, you’re accepting the fact that whatever order the children come in is the order in which things get required.

A good rule of thumb is that if you have a require in your code - it should be toward the top of your script and nowhere else, and explicitly defined and not from iterating over the children of some modules folder.

4 Likes

Do they use a single main script? I’m clearly not a pro, but it feels hazardous. My combat “module” is self sustaining, so I run it separately. If it is compromised (broken/hacked/whatever), the rest of the code is untouched. The only immediate benefit I could see is if you could force shut down anything that isn’t in the main code. If you can do that, you could also have a separate script do the same thing.

:man_shrugging:

Okay but is every single script in my game going to be a “manager” or “service”

What if I want to a script completely unrelated to any other system to simply put a nametag above the players head when a character spawns. It doesn’t need to return anything

Do I have to have a NameTagHandlerService

Aparently so Roblox Development Script Architecture | by James Onnen (Quenty) | Roblox Development | Medium

The concept is essentially the less scripts the more predictable your system is. Therefore, a system with only one script is the most predictable, and thus tends to be easier to debug.

When you have two plain-old scripts in ServerScriptService - which one runs first? The answer is not defined - the engine provides no guaranteed answer to this question. Obviously you could use some sort of mechanism in one to guarantee it runs after the other, but that’s sorta pointless if I can have two module scripts and require them both in a defined sequence in one main script.

3 Likes

Not necessarily, it honestly depends on the business logic of your game. If it helps you start out by modelling individual features as individual services, then by all means start there. If you start noticing “wow, I’m adorning a lot of things to player characters” then improve your organizational structure by grouping things together logically, making sub-modules, etc.

1 Like

So basically any game mechanic, even if it’s a small little thing should be considered a service and have an init method. From there it can require libraries and other systems if it needs.

Also everytime I make a new Script (service) I don’t want to have to require it from main. It’ll be easier if I could just loop through a folder called “Services” (or Runtime in my example)

If I want two unrelated things to happen at Runtime I don’t necessarily care which one gets required first, I just want them to run an Runtime.

I see the temptation to just loop through a module and do it that way, but avoid it. Every require call implies the script has a dependency. Every dependency should be explicit and cause errors if it is missing. GetChildren doesn’t know if something’s missing.

So I read up on the article, and I’m not sure I agree single script is the only solution.

My 2 main objections to the article:

  1. Proper data management requires no global variables. I use no global variables. I’m also very specific what code can see which data.
  2. Modules don’t improve on events and functions, and are less safe in my opinion, because they aren’t particular whether they run on client or server. I have very few module scripts left, and I intend to remove them all. I want to know every communication channel between client and server.

Yeah I guess, so you’re saying every dependency should be called explicitly to ensure it exists and works

Another thing is what about something like a little weld script or killbrick script. I don’t think those would be considered Services.

From my understanding (from Knit) the “pro” equivalent of parenting scripts to Instances is to use Components

Would I manually add Component modules to be required by main.

Also Knit just requires Services and Components from Folder:GetChildren() is that because it’s a public resource so he doesn’t want to make it complicated.

The main question: What question can I ask my self to figure out “X goes where”. What type of script should go where.

I think you misunderstand what Single-Script Architecture is.

  1. It does not require that you have global variables.

  2. You seemed to be misinformed about ModuleScripts, you can control what environment a ModuleScript runs in. If you require it on the server it’ll be running on the Server. If you put it in ReplicatedStorage you can require it from the client and server.
    ModuleScripts are no “less secure” than Scripts and are much more advanced for making an efficient codebase (hence why I am asking about single-script arch)

I don’t think that’s a good idea (nice way of saying that’s not a good idea) from my aforementioned points.

A pattern you might enjoy is the Binder pattern. It binds a CollectionService tag to the instantiation of a object. Like an idiomatic Object.__index = Object metatable-y type of class. When it comes to single-script architecture, it is perfect for stuff you’d normally copy and paste a bunch of scripts for similar objects. Here is my implementation:

Binder.lua (3.0 KB)

For reference, the binder pattern is featured in this video from RDC 2020, which this is built off:

Stuff I’ve implemented using the binder pattern include:

  • Simple kill-bricks
  • Melee weapons
  • Hit-scan projectile weapons
  • TextBox that only accepts Numbers
  • Humanoids that emulate the appearance of a Roblox user
  • ProximityPrompts that prompt the purchase of something
  • Any object which lets the user claim a badge (BasePart, ClickDetector, ProximityPrompt)
3 Likes

I just read the article, and the article did a very poor job of convincing me I’m doing it wrong. I don’t need to reduce the lines in my code. I don’t use globals. And as was suggested in this thread (not in the article) there is not question of order of operations. There is no “need” so there must be more than one solution (my opinion, of course, I am not a pro).

As I understand it, anything in ReplicatedStorage is fair game for the client. It’s one of the perks of a ModuleScript. It’s also a weakness. My data will stay on the server, so the data can be manipulated on the server. NPCs and players are subject to the same rules, so that stays on the server too. When it comes time to put a number on the screen, I call a RemoteFunction. The client is a UI and not much else.

Everything is still coded once. /shrug

Yeah that’s what I was talking about when I said Components (Knit calls it components)


Again you are misinformed about how ModuleScripts work, if you don’t want a client to run a ModuleScript - put it in ServerScriptService. The client cannot access ServerScriptService.

Also even a Module in ReplicatedStorage will be running two separate instances they do not share memory

By all means single-script is not a be-all end-all game architecture. A mix can be just as effective conceptually for those who are just starting out with it.

I’m not sure what you mean by (1) in relation to SSA, but there’s definitely a time and place for globals/constants. I use them for attribute names like this:

local ATTR_RELOAD_TIME = "ReloadTime"

-- Later, in your gun code or maybe some UI:
wait(tool:GetAttribute(ATTR_RELOAD_TIME))

(Edit: apparently I wrote script:GetAttribute instead of tool:GetAttribute - whoops)

Primary benefit above is not needing to repeat “ReloadTime” (heaven forbid you mispell it RelaodTime and not get an error), and also ATTR_ autocompletes in Studio nicely.

SSA doesn’t exactly replace events/functions, so again not sure what you mean here. Modules which should only run on certain network peers can ensure this by doing assert(RunService:IsServer()), either on-require or for specific functions within them.

This is my idea:

Whenever I am making a new Script:

  • If it’s code that should run for multiple Instances make it a Binder.
  • If it’s a game mechanic that should start at Runtime, then make it a Service
  • If it’s a library, lazy load it and use it like any ModuleScript

I guess this is why Knit is set up

  • Components
  • Modules
  • Services

Okay, thank you for your help!

I’m not confused. I’m not giving the client any reason to need memory. The client shouldn’t have that, and it won’t get that. Let’s forget about ReplicatedStorage. There will be client scripts and there will be server scripts. There is no overlap.

So what is the functionality of a ModuleScript in ServerScriptService? I am currently only aware of one use server side: to create code “objects” that can be called during a dialog event, processed the same way you would process any child, and then forgotten. That type of script can hang out in ServerStorage.

That is the entirety of my argument. But does it solve any problem?

Globals used to be the only way of doing things. Now there are better ways. I’ll use your example. “ReloadTime” is needed for combat. Specifically, ranged weapons. But most importantly, you should at least prepare for the added functionality that ReloadTime is is different for every weapon, every player, and every NPC, and changes throughout the game.

It’s no longer a global variable. It’s a table. Even without the added functionality, there’s no significant reason to store this value outside the ranged combat code as a local variable. Allowing a variable to be seen everywhere sets it up to be used everywhere.

It’s the strongest reason I have to compartmentalize the whole game (whatever the opposite of SSA is). Nothing should have access to everything. Nothing should be accessible by everything. Just opinions.

I am not seeking any coding advice today. I am simply critiquing your article. You will discover the word “opinion” 4 times in this thread if you search closely. I will leave you to it!

Thanks! Good luck to you in your efforts!

If you are not seeking coding advice, don’t come to me and tell me a bunch of wrong information and then say it’s just an “opinion”

I’m well aware you have “opinions”, but you can’t have an “opinion” on a fact.

As for me, I am always learning new things so I naturally assumed you would want to learn the correct information, but to each their own amirite.

Same to you!

I’d just like to point out the fact your mostly just saying random computer terms. Whilst battery is breaking it down and trying to explain the fact that modules are just basically another script which can be accessed by other scripts to run functions etc, if you don’t want to take such information at face value it doesn’t make sense for you to interact with this thread in the first place.

1 Like