How to code in a more scalable and maintainable way?

Hello everyone. I’m a hobbyist game programmer and from time to time I get to work on some stuff on Roblox, most related to programming. However, apart from all the weird engine limitations, I’m having some problems by myself related on how to code in a scalable way, considering the scopes of a big project.

Let’s say you have a Mission System, now speaking more conceptually, you get to start a Mission and, for example, some activities that you are able to perform outside of a mission get disable, such as Shops, Car Workshops, other Missions, etc. These Shops, for example, are shown in a Minimap also, and when you start a Mission, its icon gets hidden so it indicates you can’t buy stuff there anymore, atleast during the mission.

Now consider programming this in OOP style, you code some classes such as: Mission, Shop, Minimap, Blip, etc. What would be the most efficient way to link all these features together in a way that you tweak them easily, expand upon and maintain it properly?

Everytime I try to design some reusable framework, I fail because I can’t seem to find a good way to do it. Explicitly calling code of one object from another seems too coupled, and it’s not one object’s responsability to make other object do something. I thought of a event-driven system, but the code where the object listen to events would be just spaghetti code, one event listening after the other. Maybe have a system where each object can have ModuleScripts acting as code holders, and these Scripts have the name of the event, so the code inside them gets called upon event firing. I don’t really know, only thing I know is that it is an abstract idea, but I would like to know some ideas of maybe how y’all handle this kind of interaction in your games. Thanks.

3 Likes

Use events to reduce the need for loops where possible and where you need to notify scripts that can’t access the events, for example, server-to-client communication.

An object’s responsibility to call another object is not bad design. It probably is, if both call each other though in modules.

There is inheritance and composition in OOP. I believe in your case, you do not need inheritance but just in case you are misled into thinking that it is necessary in OOP, you can use composition. Composition means to make each object unique rather than inheriting from a superclass or another object.

There may be times that it won’t get any simpler than what you have made, so be aware of that. I look at public modules and they don’t always look easy to read but they use good coding etiquette and they use abstraction which you should definitely use to reduce confusion. They can be a valuable resource in learning to make reusable code. A beginner mistake would be to try to over-optimize or over-simplify things.

When designing a reusable framework, make sure to keep in mind how modifiable it is in the process. If you think adding another feature to it creates a headache, then you know you’re straying off.

I found for many people who struggled in code design, they simply just drew it out first before coding. This could help you. An example is making a flowchart, or writing it out on paper, or however you would like.

You may need to provide more details on your struggles with creating reusable framework for a more tailored answer.

Remember that you can only think about things you have a word for. Probably these are the ones you need:

An object (instance of a class) only exists to group state (data that can change). In the old days we only had “structs” which specify that some items are always found next to each other in memory. You should always be thinking about what states are valid for your object and write code that makes it impossible to reach an invalid state. e.g. a car with four doors cant have five doors open.

Closely related to this is the concept of “lifetime”, this is just the range of time in your code where an object is known to be valid. This is not specific to languages with manual control over memory, certain things just naturally have a lifetime, such as your quest markers. At some point in time a quest marker is created and some time later it is done being used. Where you choose to put data is dependant on its lifetime. Classically, only data that lasts the entire length of the program is placed as a “global” variable. Since your quest markers don’t live longer than the quest they belong to, it would make good sense to make them a part of a quest object. Then, if you throw away the quest, the markers get thrown away with it.

Interface segregation means that it should be possible to use an object by only knowing its “on paper” design. In the languages for which this term was designed, that means you know the names and arguments of the functions it has, and nothing else. This one is the one people have the most trouble with because nothing in the design of languages forces you to do it.

A “smell” is a pattern of code that indicates you should try and do something another way, these used to be called anti-patterns as well, while a pattern is a common useful code structure. With lua, the most common offender is having several ways of doing the same thing to an object. When this is the case, changes to the object are not guaranteed to have the same effect to all users of that object.

If you use OOP in lua, I strongly recommend you try OOP in other languages as well. There are many things left unspoken about Lua OOP, because the language wasn’t designed for it. You will learn what these are and why they’re important in C#, C++, Java. The history of the various features of OOP is very important to understanding it, and its almost always left unspoken. The entire point is to allow an object to be used with less and less knowledge about its inner workings.

When you add layers of complexity to code, always try and push down instead of up. That means move features into a new object whose job is more specific, don’t try and make a “Manager” object above what you currently have. This is just a rule I’ve made up, but it might align with some existing pattern or anti-pattern.

I guess the example I gave is good enough, I didn’t want to make a very specific example which makes it harder to understand what I mean. When I say an event-driven system, I mean when something happens in one code, gets propagated to other via events, not necessarily meaning the code where the event fires knows about its listeners, it is an event that can be listened by any code.

The problem with this desing in my opinion is handling the event listening part, imagine the code in the Minimap object, for example, it can be affected by multiple parts of the game, such as the very Mission object, the Player object, PauseMenu object (pausing the game would most likely hide the minimap), and lot more depending on the game.

I’m not really sure how I would deal with all these code, I thought I could separate it in different files so each event would be another file with its own code upon firing.

I thought of using ECS in a moment, but it not appealing for me, I like the idea of each part of the game being an object that can be modified, atleast that’s easier in my opinion, although the part containing object interaction, mainly the event listening part, it is kinda hard to deal with.

I wasn’t thinking in creating a whole Manager class which handles everything, although in some cases it makes sense, when there is a global rule where some instances should follow and these instances should be aware of each other, like a Traffic Interface, Enemy Interface, Weather Interface, etc.

Imagine you create an Enemy class, and this enemy class can be part of a group of other Enemies, and these should knoow about each other, would it be better to create a Enemy Interface containing all groups of enemies, or have a “group” array inside the enemy containing itself and other enemies of the group? The latter seems kinda bad, I can’t explain it properly, I’m not that good with code design, it is like, I can probably code something like that, but I know it will be hard to expand upon and change some behavior later on, I would like to know the better way of coding something so I can perform all these actions easily

It depends fully on how you plan to use Group.

If its like an RTS, where a group is a selection of enemies that take one move order and all the enemies in that group move to about the same place:
Only code that handles groups should need to know what a group is. By extension, the enemies themselves should not know about what a group is, and their code should not depend on knowing.
If later you add a feature that requires enemies to know what group they are in, they still should not be changed to know what a group is, only the information about their group. You could do this by having a function that takes an enemy and returns the list of enemies in that group. This way enemies still do not need any knowledge about what a group is, so you’ve prevented a dependency.
You’re correct that having an array inside each enemy of its group would be bad. Specifically it would be violating DRY by having multiple copies of the same information. If you change the repeated info in one place but not the other, your data is now in a nonsensical state. The abstraction of making this information available through a function prevents this.