How to write great code and speed up everything

Hello! This guide is massive, and is relevant whether you are a beginner, intermediate, or advanced programmer. No matter how much experience you have, I have compiled a LOT of things into this post. I’ve tried to keep it straightforward for everyone. Even if you have no idea what I’m talking about at times, I really encourage you to keep reading, because there’s a lot of great stuff in here. :smile:

Intro

In this post, I go over code structure, Git, Rojo, wally, various libraries and tools, etc. There is a lot more than this out there, and I am going to keep adding more relevant information to this post in the future.

My goal is to get a lot of great software engineering principles into the wild on Roblox, since it’s something that there are not a lot of great resources on yet.

In general, the cleaner and more straightforward your code is, the easier it will be to work on, and the more natural it will be to write things in ways that are fast and easy to keep track of. Cleanly structured code and performance aren’t directly correlated, but clean code is usually faster, and it is easier to optimize where it matters the most even if it is not the fastest it possibly could be. Too fast is a problem if it means the code is hard to edit and understand.

When you’ve got a game that will eventually have hundreds and hundreds of modules in it, the last thing you want is to accidentally encourage the sorts of things that will cause the code to get too tangled up. Those tangles can cement your code in a state where little changes could take a lot of work, and it makes it hard to follow and understand what’s going on.

There are a lot of great tools and libraries which address various things. They are each relevant in specific cases, and it is my goal in this post to give a basic, overall view of many of these things, how they are used, etc. All of these concepts are things that took me many years to fully grasp, and they are all things that are often misunderstood or not very well known about on Roblox, and I would love to accelerate the process for others by providing as much accurate and useful information as I can.


Git & Rojo

...

GitHub/GitLab and Rojo are great for having good code structure. Even if you do not fully utilize git the best that you could, it’s a great way to have a history of things, so that you can easily go back to any version of your code.

Together, these allow you to utilize wally, and they allow you to utilize VSCode. The Luau LSP extension will even give you the exact same Luau typechecking as in Roblox Studio. You get things like the F2 keybind for renaming variables. You get Ctrl+P for being able to type in the name of a module and just open it up right away. You get things like Wally, which allows you to access an enormous amount of useful libraries and packages, and even create your own. And you get lots of other fun stuff too.

Git

Every “commit” in git is nothing more than a little change to the code. It’s really common to have a lot of commits. I use GitHub desktop to make it easy to select which lines I want to go into a commit if I’ve made some changes in a few places. This makes it easy to separate different changes and then you can compose different versions of your code that have different features and all kinds of cool stuff. Removing a commit removes the changes, adding a commit adds them. All the commits stacked together make the latest version of your code.

Every “branch” in git is nothing more than a collection of changes. This means that you can work on multiple features at the same time, with each feature as a separate branch, and you can add or remove features at will. It feels like magic when you learn how to use it, because it lets you fluidly add and remove entire features, it lets you have access to every change that you have ever made, and it means that if something is taking a while to get working, it won’t stop you from continuing to work on and develop your game.

In GitHub desktop whenever you wanna merge in some changes you will be offered two particularly interesting options.

When you update your main branch with new code, you can do something called “rebasing” and rebase main into your other feature branches. Rebasing is a strategy for combining branches. What rebasing basically is is like taking your main branch, stacking your feature’s changes on top of it, and boom, that’s your new feature branch. This is the best way to keep your feature branches up to date, and it will avoid merge conflicts because ultimately when you eventually wanna merge your features into your main branch, when those features are done, and stable, for git it is as simple as slapping all your new changes on top.

Merging is like the opposite of rebasing. If you were to merge main into your feature instead, you would see that all new changes that you added to main actually go on top of your feature’s changes. But you don’t want that, because now all your features are all tangled together in the git history.

So the general rule of thumb is merge features into main, rebase main into features. It’s also really good to make sure that your feature is up to date with your main version of the code before you merge it into main, because that means you can really easily test with all the latest code. The more frequently you rebase main into your feature code, the less likely you will have merge conflicts, which is when two commits change the same lines of code.

When you have a merge conflict, you basically just have to figure out what combining the two changes should really look like, which ofc can mean a lot of different things. By rebasing main into your feature code, you make less merge conflicts by always being up to date, and you solve any merge conflicts ahead of time, that way you don’t accidentally make your main branch a bit messy or broken.

Rojo

As for Rojo, Rojo is what allows you to utilize git. Rojo unlocks a LOT of cool tools like Wally.

Libraries, components, objects, and more, the super-guide

...

Signals

Signals themselves are probably the most notable example there because they exist in every codebase. They are just events, and events are amazing because they are the one true cure to circular dependencies.

What if you want to give a player a reward when they unlock an area? Instead of inserting some code into the zones code to check for rewards to give and giving the rewards and all that stuff, provide a :GetZoneUnlockedSignal method that creates a signal for when a particular zone is unlocked. Don’t worry about if your implementation is good or bad, because at the end of the day, you can make lots of optimizations and improvements to your :GetZoneUnlockedSignal method without ever changing the way that it works for any code that needs it to work.

Now you don’t have to have any stuff even remotely related to the idea of rewards inside of your zones code, and now you can add a new module that’ll listen for these signals and dish out those rewards. You can even automate it all with a simple table of rewards keyed by zone.

Troves

Troves (an alternative to Maids which are an almost identical concept) make cleanup easy in object oriented code. Inside the code which creates an object, make yourself a trove, and you can connect to events or signals, create instances, etc. Inside the code for when your object is destroyed, simply call the trove’s :Clean() method. If you’re using a trove you can guarantee that cleanup will happen when you want it to, which means no memory leaks.

Observers

Observers (aka “the observer pattern”) exist somewhere between a Trove and a Signal. The idea of an observer is that it will “observe” the current state of whatever you want it to immediately, for example, all the players in your game. It will also observe any future players that join your game. In multiple cases, for example with players, children, etc the observer can also handle when that child or player is removed.

Here’s a great example using version 0.1.4 of @4812571’s Observe library, which I linked earlier:

-- Without observers
for _, player in Players:GetPlayers() do
	doSomethingWithPlayer(player)
end
Players.PlayerAdded:Connect(doSomethingWithPlayer)
Players.PlayerRemoving:Connect(function(player)
	cleanUpPlayerStuff(player)
end)

-- With observers
Observe.ObservePlayers(function(player)
	-- Do something with the player

	return function()
		-- Clean up player stuff
	end
end)

An observer will return a function you’d call to remove the observer. For example, if you observe a property, you can disconnect the observer by calling the function it returned. The callback of an observer can also return a function which will be called with that value is cleaned up. For example, when a player leaves the game, the function returned by the observer callback will be called so that you can handle cleaning up all the stuff for that player.

Observers can also pair really nicely with troves:

self._trove = Trove.new()

self._trove:Add(Observe.ObserveAttribute(instance, "ExplosionPower", function(explosionPower)
	self.ExplosionPower = explosionPower
end))

Components

Components (aka “the binder pattern”) are an OOP concept that will make your OOP code OOP (over-over-powered). The idea is simple. What if everything had multiple “objects”? Instead of something being a single object, you can attach components to that object by giving it multiple CollectionService tags for example, and those tags will automatically construct and assign components to objects.

Components are a step up from regular OOP, because you can stop thinking about objects as objects and start thinking about objects as objects with behaviours. You can make generic components that add special behaviours to any object, like proximity prompts, lights, etc. You can also use components to describe features in your games like doors, which can open and close, and might tween.

You could create an Explosive component, which has an :Explode() method. Then you could create a Health component for things which have health, like the player, or a prop in the world. You could have an explosive barrel with a Health component, an Explosive component, and an ExplosiveBarrel component to describe the unique behaviour of the explosive barrel, which is that it explodes when destroyed. Now when the explosive barrel has “died” (which you can use a Signal on the Health component for), you can call the Explosive component’s :Explode() method. The explosive component can also look at attributes to figure out how big the explosion should be, how much damage, etc.

There are a few main pieces to a component:

  • Creating the component for an instance
  • Adding behaviours for the component and the instances it is a part of
  • Cleaning up the behaviours that the component adds
  • Destroying the component with the instance

There are also a few things you wanna be able to do when there are multiple components involved:

  • You want to be able to look for specific components for instances, if they exist
  • You might want to be able to wait for components to potentially exist (Promises would be very relevant here. You can even add the promise to a trove with Trove’s :AddPromise so that it will be cancelled and you’re not waiting forever, with is a really clean way to indicate that you’re done waiting for stuff)

With signals and methods on your components you can have your components work just like any other modules, with optional getting or waiting for components acting exactly like module requires.

Components can be added or removed to an object, and they should be completely cleaned up when the object is deleted, or when the component is removed from the object. Utilizing observers, troves, promises, etc makes it really easy to write components without any memory leaks, that are easy to read and understand, and have really complicated behaviours described in really simple and straightforward ways. Components are exactly like regular modules, and exactly like regular objects in OOP.

It’s really common to see attributes used by components as extra fields. In fact, using components & attributes is pretty similar to making games on Unity, because the architecture is equivalent.

Sleitnick has a Component library for basic use. It has some coupling issues like Knit, but I use it a lot myself.

Promises

Promises are a concept that’s really useful in game programming. They let you convert between things, schedule things, they are cancellable, and more. Promises will help to avoid race conditions which are way more common than you might think, because race conditions can have a very low chance of occurring or may only occur in specific scenarios making them seem as if they never come up.

Promises are great for networking, for example using promises in client code to make requests to the server which require a result. Promise are also really great for anything which takes time. And they’re really great for stuff that can fail, such as loading player data.

For example, maybe the player leaves the game while you’re getting their data still. Well, with promises you can just discard the promise and never resolve it, and now your promise’s callbacks will never run, so it’s safe. This also doesn’t result in any memory leaks, even when using :await() or :expect(). You could also reject, to let the promise’s user (typically yourself) know that the data has failed to load.

These are some really cool mechanics of promises that are not obvious at first:

  1. If a promise results in a promise, the result isn’t a brand new promise, instead, once it resolves or rejects or cancels you get a result. This means that you can return promises from promises to schedule things in a really clean, predictable, and easy to read and edit way.
  2. The callback of :andThen for example is not just a callback. :andThen actually makes a new promise, which will resolve to whatever the callback resolves to. This means you can convert things into eachother, and perform intermediary steps for things in code.
  3. :catch actually works the same way too. The catch method allows you to handle errors, then they will produce a new promise that actually resolves with whatever catch’s callback returned. You can use this to turn an error into something happy, it doesn’t just have to be for error handling.

Side note, but, I recently made a simple tween library on wally as a “promisified” replacement for TweenService. I am working on allowing you to compose tweens for multiple instances in a nice way too. It’s nothing complicated at all, purely for convenience.

Later I would additionally like to offer a Tween.value with an :Observe method, which will follow the observer pattern to let you tween values without creating intermediary instances.

It’s a good, simple demonstration of how promises can be useful. You can sequence stuff pretty easily by doing stuff like this:

local tweenA = Tween.new(instanceA, tweenInfoA, propertiesA)
local tweenB = Tween.new(instanceB, tweenInfoB, propertiesB)

-- Play tween A, and then play tween B 1 second after it is finished
local tweenABeforeB = tweenA:Play()
    :andThenCall(Promise.delay, 1)
    :andThen(function()
        tweenB:Play()
    end)

Tween.new accepts the same arguments as TweenService:Create.

Tween provides :Play(), :Cancel(), and :Pause() like Roblox tweens, the only difference being that :Play() returns a Promise which resolves when the tween completes. You can also call :cancel() on the promise, and it will even cancel the tween.

There is also Tween.all which takes in an array of tween objects and returns a new TweenGroup object, which does all the above things with the same APIs as above, but will act on all the tweens it contains, which is really useful for when you have something with a lot of moving or changing pieces. For example, maybe you have a door with two pieces. You can actually construct a close and an open TweenGroup using this, and you can even re-use the tween objects just like with TweenService.

At the moment, I will note that currently tween groups are a work in progress and aren’t fully ready, especially because TweenGroup:Pause() does not actually work as intended yet. It pauses the tweens but you are unable to resume it which is something I fixed the other day (and haven’t published), but I wanted to test everything thoroughly first so that TweenGroup works as expected.

Structure & architecture

...

The way I personally structure my code is really simple, and straightforward, and for good reason. I don’t use any frameworks, and I don’t have any complicated wrappers, just modules to modules. Fundamentally, my code is still structured identically to how I would structure it if I were using Knit.

There may be multiple modules related to a single feature, and for this reason, they can be grouped up into folders.

Circularity

Circular dependencies can come up a lot, and it isn’t often clear at first why they are to be avoided. The reason is really as simple as code structure. Two things can rely heavily on eachother, but you always want a clear directionality so that what you need to change to make stuff happen is clearer, and so that your code will only affect the things that depend on it. When you have circular requires, you end up with a muddied structure where it is unclear which thing depends on what, and so not only can it be hard to figure out if the code needs to change in one place or another to achieve something, it can result in bugs that are harder to understand and diagnose too.

As a simple example, let’s say you want to purchase an area. But to purchase an area, you wanna take the correct kind of currency. But you only want that currency to be unlocked if the player has a zone that provides them that currency. Well, now you have a problem, because your zones code needs your currency code, and your currency code needs your zones code.

The solution is simple in hindsight. If the purchase logic is its own thing, the circularity is resolved simply because the zones purchasing can rely on the zones code and the currency code. Now the currencies code can happily rely on the zones code.

When you split up features is arbitrary, and entirely your choice. Too many splits and it’s excessive and the structure becomes muddy, too few and the structure becomes muddy. So it is a balance between both, but with an emphasis on the idea that splitting is at the very least not going to ruin a codebase.

The most natural point where you will split something is one where the behaviour is related. The behaviour of zones as a concept is its own unique thing, and the behaviour of purchasing zones is its own unique thing. This is a really natural way to think about your code too, thinking about it by its concepts and its behaviours and features. Purchasing zones could be thought of as a sub feature of zones, but it itself still makes up the whole feature of zones.

How many modules is too many?

You can consider modules to be nearly costless or even sometimes negative cost when they help the structure of your code. Because often if you need a feature, it’s only one require away. Splitting that feature into a new module will usually not result in a new require, and even then, at worst you’re just adding one. And even then, many requires is never much of an issue, because you only have to require something once.

Sometimes it’s not uncommon to see modules in well structured codebases which return a simple function.

It all comes down to the “vibe” of the code. It can be better to split things sometimes because it just makes sense, and sometimes it can be better to keep things together because it just makes sense. The one time you can have too many modules is really just when it makes the code make a lot less sense, and that’s usually when you can’t really come up with a good way that makes the idea of that code distinctly separate from something else.


But what about frameworks?

The idea behind a framework initially sounds very appealing, they are something that sounds like it will encourage better code writing. (A frame for your work, if you will :sunglasses:) For example, Knit is a super common and widely used framework, and it is one I personally would recommend if you would like to use a framework.

The problem, however, is that frameworks themselves as a concept are a bit flawed when you look at the software design principles that they use and encourage. The one major flaw with frameworks is that they provide you with the features, rather than you using the features that you need, the coupling between that framework’s features and your code is inverted.

On one hand, this allows frameworks like Knit to make minor encouragements around how you structure your code. On the other hand, it comes with extra cost. Knit doesn’t support type inferences & therefore autocompletes and that means Luau won’t be able to check your code, so you miss bugs. Knit also accidentally makes it easy to make typos in its network code, because you must use self.Server and Service.Client, when really you just want self.. These flaws can likely be attributed mostly to the fact that Knit was new when Luau and types were, and so it’s only natural.

In my experience, frameworks don’t save codebases from architectural issues, and they don’t make the code any prettier. They are not bad, and they are not going to drastically harm your code, but the benefits they provide are usually not worth the cost for any level of programmer. You should not be afraid to use frameworks, but you should be wary of the fact that frameworks as an idea will change the way you write code and change how code acts on the inside for better or for worse.

But don’t frameworks provide useful features? And what can you use to encourage good structure?

If you avoid the use of frameworks and instead resort to using your own structuring with simple module to module requires, the features that frameworks provide naturally themselves become simple modules too, in the form of libraries.

This is exactly what tools like wally give you in Rojo projects. Wally packages are simply modules, and libraries are simply modules. Wally itself is just a robust way to grab the libraries for your Rojo projects, and ensure that the right versions will always be available to you.

Libraries like Promise for easy & straightforward scheduling & monadic transformations, TableUtil for simple, readable table manipulation, Signal as a performant & lightweight version of BindableEvents, Observe for using the observer pattern, Trove for object oriented code, and Net for simple remote based networking all provide you with useful features and tools that won’t restrict how you structure your code while resulting in cleaner, easier to use and write code.

Some other stuff I wrote a while back

64 Likes

Man, Wish I had done some stuff like many professionals do, This tutorial is good! I will use it in the future

7 Likes

Hello @Hexcede,

I haven’t had the opportunity to delve deeply into the article; I’ve only skimmed through it quickly. One thing I noticed is that there was no mention of Automated Testing (specifically, I use TestEz).

I would like to offer some constructive feedback: Please don’t interpret this as a challenge to your expertise or methods; you’re a highly respected developer. However, I tend to seek sources of credibility and tangible reasons to have confidence in the information presented. Would it be possible for you to share some insights or references that could further substantiate your credibility and showcase your knowledge on these topics?

Regarding the content in the article, I have a few specific questions:


Can you explain more about Rojo and Wally?

I found the section about Observers interesting. I use this pattern in my code, but I didn’t know it had a specific name.

For the Components section, could you provide sample code for illustrative purposes? I’m keen to understand how to implement this in my own codebase.

I also follow a similar approach when it comes to structure and architecture. I avoid using frameworks and instead rely on a specific structure, and I use Signals to prevent cyclic requiring.

I share your sentiments about frameworks. I used to create custom frameworks, but I realized I was spending more time working on the framework itself rather than my game. Frameworks can be restrictive and filled with unnecessary code, especially when I prefer simplicity and avoiding extensive boilerplate code.


Thank you for sharing, I look forward to a future where programmers on Roblox adopts more professional workflow and methods

7 Likes

Knit is a terrible option for anyone in the long run. @lolmanurfunny uses it effectively as a game bootstrapper instead of the framework itself.

Knit was discussed by sleitnick to use bad design principals in this article.

6 Likes

Absolutely… I wanted to provide some more stuff regarding components specifically, but I also didn’t want to go heavily into specific library or API, because some play some kinda dangerous games haha… I was considering whether or not I could write a simple example of a custom binder implementation, but I decided that it was a bit more complicated when I am trying to write for a wide variety of people. I am still considering how to improve that section, and I definitely want to provide more information since the pattern is fairly similar to Unity. Maybe I can write a post dedicated specifically to binders and components to better serve that purpose.

For sure, the reason I did not is pretty similar to components, I didn’t want to crowd too much information together at once. I think this would be good to go into at least a bit more detail on, though I am unsure what level of detail I’d like to write.

This may be a good thing to add, I was definitely focused fairly heavily on code architecture/structural stuff. Git & Rojo were adjacent to that, and that is part of why I gave them some attention in my post but did not go into a whole lot of specifics. I think TestEZ would still be great to talk about, though I have tended to find that I do not often get to utilize it as much as I would like to in practice. I also recall seeing some good TestEZ resources that I can link in my post as well.

Part of my philosophy is that there will always be code with architectural or design flaws, but as long as that flawed code is decently maintainable, readable, and editable than that is a lot better than not because it means that as you learn or when you’re working with other programmers with more experience in areas where you are lacking it will result in more beneficial information all around and generally just more maintainable code. That is my primary motivation for having written this, everything here are things that I actively have observed being major flaws in codebases that other programmers have written, and in my own past code too.

As for adding resources and such, I would love to. Much of the information I have written about in my post is based both on my experience as well as the information provided by other developers that I’ve interacted with over time, but it would be great to provide some other resources which go into more detail about each of the topics I am discussing, and generally just provide some additional more concrete information.

I have made a little bit of effort to focus a lot more on detailing these concepts and giving examples of the ways that they can be utilized and what sorts of benefits I have personally experienced in using them rather than trying to establish the best usage. In other words, I’m not looking to provide a perfect set of best practices, I’m mostly looking to provide a lot of information that I find would be helpful for a majority of the projects that I have worked on. Especially because programming knowledge is very much non-linear, you can be a very inexperienced programmer and know a lot of concepts that most beginners don’t, or you can be a very experienced programmer and not know plenty of great concepts.

1 Like

It’s funny that you say that…
image
image

3 Likes

It’s true that it’s funny because the people who used it have “lolman” in the name

1 Like

Don’t worry! I was in the state too wondering how does one get to this level of scripting. Just keep being persistent, trying, and improving yourself!

I highly suggest creating a fully-Rojo managed experience to get the feel and basics of it down. It’s really simple once you know and familiarize yourself with it.

2 Likes

Someone can misinterpret this as a Git thing or something; You should edit this

3 Likes

I will make a new game which is fully Roho managed, not Ohio Hide and Seek will be fully Roho managed

ignore that I call it Roho, It’s a spanish word for “Red” and “J” in spanish makes the “H” like in “Home”

I’ve adopted a Rojo + Wally + Luau LSP setup recently, and it really is a game changer. It allows me to push code faster, and test easier.

Take a look at my post on this for some tips covering code formatting, logic, etc: Programming Guidelines || Make your code industry-standard

Feel free to derive from this.

1 Like

I just came from reading Observer · Design Patterns Revisited · Game Programming Patterns, which lead me to find this post but I still dont understand how to apply the observer pattern in lua.

Like what if I want to observe a value inside a table that im treating as an object, How do I make an observer for that?