Code Complexity Buildup When Creating Games

I’ve been running into a constant problem when scripting for games. The more I work on the game, the more complex the code behind everything becomes. More code means more things that can go wrong. For every feature comes ten new bugs. Some days I’m just fixing bugs and trying to get the game to breath again after adding something new.

Bugs are usually easy to fix but there so numerous in number. Like hundreds of bee’s stinging you.

They have made my game development come to a crawl. It feels like I cant get anything done. I want to know if anyone has suggestions on workflow and if there is a method of writing more clean and simpler code. Without getting bogged down by thousands of micro problems that take a whole day’s worth away of progress.

Congrats ur a game dev now!

Yeah comment it and split it up, don’t dump it one long class our you’re asking for trouble.

Extract and organise as you go - keep the update functions/methods small and have them hand off the processing to other custom classes.

Ideally, you should be able to look at a method/function and be able to immediately tell what it’s supposed to be doing - if you have to track variables in your head or have to re-read the code a few times to figure it out, then you probably need to split it up into multiple methods.

Also - always write unit tests. Even if it seems like a hassle at the start, you’ll be glad you did as your project grows larger, and it’ll give you a lot more confidence in your code.

One thing I learned early on is not to write any hacks…always take the time do it “right” even though you may be tempted to say, “eh, one little shortcut won’t hurt”. Well, it will, eventually. One little shortcut soon becomes two, since you inevitably need to make a workaround for the shortcut somewhere, then you need another, and then it just multiplies from there and sooner or later you get to the point where you can’t add any features without breaking stuff everywhere.

–Eric

It all starts with thinking about a good design for your code. Some things that can really help you in this are design patterns.
Wiki link
There are also a lot of books on this subject.
Buy book link
Design patterns are not holy imo, and they don’t fix every problem you have. They are a good toolset for solving common programming problems.

Also something to always think about, like Eric5h5 said, is to not just hack in some feature. Implement it in a way that makes sense, in a way that does not interfere with existing code when it does not need to. When it does need to interfere with existing code, make it so that any changes to the code don’t affect the feature. Again, design patterns hold some answers as to how to do this.

One thing i’ve learned in my years as a software developer is that programming is not so much about making things work, but about making code that is easy to understand and maintain.

Not sure I agree with this …

Doing things the “right way first” implies you already know the right way before you’ve started building things, and often times, especially for more complex things, you just don’t know. These “hacks” you put in are ugly, but they helped move you forward and they helped give you a more accurate picture of what you’re trying to build. At some point, you can refactor out those hacks into a more elegant solution.

While I think it’s good you pointed the OP to design patterns, I think maybe backing up and telling the OP why those patterns are a good idea in the first place may help him significantly more…especially for situations not encompassed by those patterns.

These patterns are all spiritually based by the philosophy, ‘SOLID’, which refers to five principles for writing maintainable code.

Single-Responsibility Principle - A block of code should do one thing, do it only, and do it well.
Let’s say you’ve written a component for keeping track of a player’s stats in the context of a very simple JRPG. I would argue, this violates the Single Responsiblity Principle because you have a unit of code (the component) that is keeping track of health, experience, level, attack, and defense…for a simple RPG. This one component knows about, and controls a lot of stuff, with complex interdependencies.

A better way to write this system is break out the necessary components across various smaller components, called by some higher level component on the same game object - you would have a Health System, Attack System, Defense System, and Level System all called by the Character System. The Level System would be able to track XP gains, and level up events. On level up events, it would notify the other systems that they need to naturally improve, which each one would know how to do.

The benefit of doing this is a particular unit of code contains less code for you to keep track of at any time. The relationships are simplified. You can have a firm knowledge of what a particular unit of code is doing at any given time. This is maintainable code.

Open-Close Principle - Well-built code requires minimal changes in the face of new use cases.
While put as a ‘principle’, the way a former co-worker explained this to me is that this is much more of a trait that well-engineered code exhibits.

One of the tests I’ve encountered as a junior programmer, is to write a function that tests if for some string the parentheses are balanced. A string like, ‘()’ should be balanced, while ‘)(’ should not. Now, the way you can solve this is simply to loop through each character of the string and see if a ( has been opened (if so, increment a counter), see if a ) comes after a ) (if so, decrement the counter), and test to see if a ) is dangling.

But…that’s very specific. And, there’s many braces you can use in a programming language like square brackets, curly braces, even the less than and greater than signs!

If you have to write another method that follows the same algorithm, just with different data, you can logically assume it does not exhibit the open-closed principle - it’s pretty much a copy/paste. The better thing to do would be to assign some default parameters to your original function that let it deal with a different set of conditions that follow the same rules ([ ] is balanced; ][ is not. Ditto for {} and }{.)

That’s what is meant by open/closed - it’s open to changes, while being closed to modification. You don’t have to change the unit of code for it to adapt to some new behaviors.

Liskov Substitution Principle - Academically, the wording is awful, but pretty much, if you have two classes that have a common
base class, both classes should be able to be handled as if they are an object of said base class.

This is one of three related ideas in SOLID that design patterns exploit the heck out of. More below.

Interface Separation - Instead of having a single, all-encompassing interface, it’s much easier for there to be multiple client-specific interfaces that enforce a contract on some functionality, while still inheriting from a more basic interface that provides other base functionality.

So, in programming, an interface has less to do with a GUI used to collect user input, and display some output, but more of a contract between a class that implements the contract, and the clients that consume this class.

Dependency Inversion Principle - Following the last two principles, the contracts provided by interfaces should be relied on to do work, instead of implementation details of a ‘concrete’ class.

So, these three blend together as they’re all talking about a common thing - interfaces. In many modern languages you can say, ‘this class has these capabilities’ by stating what they are in an interface. If you’ve used C# at all, you know that funny IEnumerable class we see everywhere? That’s an interface for objects that can be operated on as if they have an enumerator, and functions for using that enumerator.

There’s also a List class. Fun fact - most of the time that you could use a List for something, you can use an IEnumerable. The reason is, List implements IEnumerable. That means, Lists follow the same rules as an Enumerable collection. The difference is, Lists provide very specific functionality for a list of objects; enumerable collections just say, this is a collection of stuff that you can do sequential operations on.

This is a big deal, because you’re pretty much telling parts of your code how to all play by a generally similar ruleset. Sure individual implementations will vary, but you have the advantage of knowing when and where those are.

I know that’s a lot to take in, so I suggest doing some research - Wikipedia is a great place to start. I hope this helps you to figure out what parts of your code need improvement to be better maintainable.

I have two rules I go by that help keep things under control.

1.) Don’t get attached to code, no matter how long you worked on it, or how awesome it seemed when you wrote it. If it looks kind of crappy but works, it will most likely look really crappy and not work when you start connecting other things to it. If you can’t let it go, comment it out and stick it at the bottom of the file before you rewrite.

2.) Fix bugs now. If you can’t fix it, your code isn’t clear, and you need to clean it up or rewrite it. If you can fix it, it will likely reveal design flaws, which only get harder to fix as the project grows. If you have more than a few things on your bug list, you MUST stop and fix them.

No, trust me, I’ve been through this. What I’m referring to is that you do know there’s a “right” way (maybe not the right way, but some way that makes more sense), but you decide that implementing it that way would take more time, when there’s a “simpler”/lazy way that’s easy to do but involves a special case. It’s tempting because for this one circumstance, it actually does make things easier, and it’s not hard to keep track of because it’s just one thing. But inevitably it ends up being more than one thing, and the hacks beget more hacks. And then it’s just a mess. You can refactor later, yes, but it’s a lot easier when you have some kind of system to begin with. When you end up with a tangle of hacks, “refactoring” really means “dumping everything and starting over from scratch”.

–Eric

But how do you know that you’ll need the more general case? Many times, you don’t.

If it passes your tests, why go for the more generic solution? Your statements completely violate the ideas of test driven design.

In some cases, it makes more sense to violate the ideas of certain patterns/methodologies than stick with them, though I’m not sure that’s what Eric’s meaning here - and I think you’re almost arguing the same thing.

I definitely agree and think you should go with the “right” way when coding - I’ve come across situations where I’ve implemented something that just felt “hacky”- but it worked and saved a couple of days of work - and it has come back to bite me in the future - sometimes years later with a lot of surrounded/supporting code now requiring refactoring. So while you may not know the “right”/“best” way, you’ll often know the wrong way - which is something you should avoid.

At the same time, don’t make your code generic just because you can - or just because it may be useful in the future. This will lead to unnecessary bloat and complexity (which I think is your argument turkeypotpie?) Focus on the task/desired result at hand.

But you forgot about the cases where it did NOT come back to bite you.

Let’s say there is a 50% chance it will bite you, and a 50% chance it won’t. Why not put it off until later? On average, you’ll end up saving more time.

For a more detailed explanation, read: yagni

Thanks for the replies. I will look into software patterns to see if they can help me. Some of the terms are over my head though.

I generally like to have the least components on objects as I can. In order to decrease the complexity of the whole system. Then break the different operations into seperate functions in the script.

A funny habit I noticed doing is give a name for a script that correlates to the function it does. Such as ‘‘player health’’. But when time goes on, the script starts to build up and its no longer just health but many other attributes, yet its still named ‘‘player health’’. However, I’m scared to change the name because of all the references.

Perhaps I did forget some of the good cases, but I remember those bad cases and I remember how painful they were to sort out later - that’s something I try to avoid now. I can’t remember the specific details of my examples (it was a huge project so would take too long to explain it properly anyway), but I’m meaning things like using a hard-coded value when a dynamic one makes more sense, or persisting data around as a character-delimited string when a struct/class is more appropriate because you don’t want to write a custom XML serializer for your class… By the time you realise these problems have become an issue, they’re usually littered throughout your code.

Though I’m not suggesting trying to code for future possibilities (yagni), I definitely agree with you there.

The fact that threads like this seem to come about every other day or so, and the myriad of code design books, courses and general learning assets on the subject, IMO just indicates that only one thing is certain about this: Code will grow complex for everyone, and the perfect “right way” to code something doesn’t exist.

There is no single recipe to make sure your code will always be clean and maintainable. There are only ways you can try to improve your everyday coding so that complexity is kept at a manageable level, and the times when you have to do a complete overhaul are minimized.

But the main thing to take away from all this is that good code is a process, and no single solution will be the end-all-be-all, ‘do this and you’ll never have to restructure your program’ solution. Writing good maintainable code can definitely get a good head start from experience and following best practices, but the final solution to your “problem X” is going to be a result of iteration.

So, what I learned over time is that good code is this elusive, almost mythical thing, but as you learn more about coding and get better at it, your code will eventaully start getting better and more maintainable. If there is one key thing, one piece of advice I can give that I can be relatively certain will be helpful, it is this: Have no qualms about taking your old code apart and throwing it all out when necessary.

It might seem drastic and even counterproductive at first, but keep this in mind: That piece of code that took you 20 hours to write is not going to take nearly as long to rewrite a second time around, even if you delete the whole thing and start again from scratch. After it’s done, your second version of your code will be better organized, cleaner, and far easier to maintain as the project grows, and you’ve now levelled up yourself in writing better code.

Plus, once you get to it, it feels awesome to finally bulldoze through that chunk of wiry code that’s been a constant source of bugs.

That’s my 2 cents. :slight_smile:

Cheers

What I’ll normally do is create a “Player” script, then manage all the dependencies inside that object. So Player has a property PlayerHealth (property type is the PlayerHealth class).

Whoa, breaking functionality down into more components is good. And it’s not like this isn’t causing you issues:

Player heath should do about one thing, and that is manage the players health. Not entirely sure why you have ‘many other attributes’. Furthermore TBH, often there’s no need for a ‘player health’ component, but a more generic ‘health’ component that can be applied to npcs, enemies, objects etc.

Woah, I didn’t know there was some “right way” of coding. In my opinion, there is no such thing as “right way” or “wrong way” in programming.

Being a programmer is very much relatable to being an inventor. You don’t say that the people who built a car, or a boat, didn’t do it the “right way”, because that doesn’t even make sense. A car can be built a million different ways, all with their own unique advantages and disadvantages, no one is “right” or “wrong”. A boat functions how it’s supposed to, and that’s all we care about.

Yeah, the idea is to find a middle ground. If a hack exists in one place in your code, it generally shouldn’t be too difficult to fix. But if people copy/paste that same hack to 20 different places, then things definitely get worse. The idea is that if you see that hack in 2 or 3 places, that’s a good signal that you should refactor that code into something more elegant.