If I just have C++ and say SDL or Raylib, how do I structure game code to keep it scalable (ie not a huge mess when I add more levels, items, mechanics, etc)? I have been able to make very simple stuff but the moment I try to add to it, it always gets out of hand and I can’t really refactor it without starting fresh.
As someone that has been a professional developer for 20-ish years, and has always dabbled in game development since I was a kid, I can share a couple bits of advice. I’ve written a couple different purpose built engines, and have used a few of the current popular ones as well.
- Plan your engine before you write a single line of code. It’s fine to do some experiments to work out the technical details of your chosen language/library, but don’t just start coding without planning.
- Draw a diagram (whether it’s UML or something less formal) of what your engine would need to look like to accommodate the type of game you’re making.
- Don’t attempt to make a general purpose engine that is fit for any type of game - make sure it works for your chosen game/genre because designing the kitchen sink of engines is futile.
- Don’t fall prey to the current design paradigm de jour. First it was Entity-Component Systems, then it was Behavior Trees, etc. I know that OO Design has fallen by the wayside, but attempting to learn some new game development/engine methodology in addition to making a game is going to be way more work than it’s worth. More often than not, the simple solution is the best one.
- Don’t be afraid to refactor. If you’ve programmed yourself into a corner where your engine is hampering your content additions, walk away, and come back with a fresh perspective to see if refactoring can save you time and headache.
I know none of this is specific to any particular language or tech stack, but that’s the point. Don’t get bogged down in the minutia of it, and instead focus on the big picture. I hope that at least helps a bit - and be sure to share your progress!
The game engine is the integration point for your content. If you’re running into architectural issues that require you to start afresh, it’s probably because you haven’t figured out the requirements for your game and the type of content it requires. For example, if you’re making a Super Mario clone, then it’s pretty easy to design an engine for it since you know exactly what the game will look like at the end. Like @boaratio suggested, don’t try making a generic off the shelf engine, which is something you’ll inadvertently do if you don’t know what game you’re making.
Then spend some time learning about game engine design. The book “Game Engine Architecture” by Json Gregory is a good introduction, and probably all you’ll need. It’s kind of short and doesn’t go super deep on anything, but it does give you good perspective on everything, and is a good jumping off point for everything else. This ebook also has good content: https://gameprogrammingpatterns.com/ (in particular, read the chapter on state machines)
if you arent using a game engine then you are creating a game engine. i found this out using monogame with c#. monogame has tons of classes to help you and even a game loop but there is still so much to build
You can create a simple 2D engine with sprites and tilemaps relatively easily. It only took me ages as it wasn’t my main project, and I’m very OCD on pixelart graphics and unaligelned pixels, also I did a lot of things in my own unique way (soon-to-be deprecated CPU remdering pipeline, software synths for audio, etc.).
Ten rules of thumb I have honed down over many years working on indie games:
- Start by aiming to cartoon the screen. Computers are devices known for returning wrong answers infinitely fast - they are only better than doing something by hand when automation can return a similar-or-better answer. Your job when making a game is not to return the highest quality answers for all purposes, but to return a specific quality of wrong answer for the task, because games are within the “magic circle” of social constructs and don’t have to exactly match reality. That means dropping down in your initial implementation from “computing things properly” to “use a lookup table” or “use a switch statement” or “return true”. There are a lot of ways to cheat, implement a feature in a way that definitely has a form of error, but it’s an error you can understand how to move forward from. The classic example of this is to start not by building an actual game loop, but just to demonstrate that you can put things on the screen, then to add movement, then add input and interaction, and then collision…
- Aim to write code that you can throw away easily. The point of the code is not to be an edifice, but to help you “arrive in the future” in some sense, by giving you useful output. If your design is changing all the time, which is often true of games, you don’t want to invest a lot in the architecture right at the beginning. Instead, you want to start with primitive code with obvious redundancies, keep it simple to hack away for a little while by just copy-pasting, and then when you start feeling the pain from having those redundancies and they turn into errors, go back and harvest what you’ve learned about how the code should work. That lets you avoid a lot of pain from trying to implement some “design pattern” that may or may not be right for this code.
- When you can’t use a lookup table, you might be able to define a state machine. This applies to a lot of gameplay flow and simple AI problems.
- When you can’t use a state machine, you might be able to define a constraint solver. This applies to physics issues like platformer collision, or more complex AI like pathfinding. In both cases there is a need to go from “many possible solutions” to a “best candidate solution”. Often you can define a really optimized form of solver for a task and there is a known algorithm that does it well, but if you’re just trying to start solving the problem, brute force comparison of the entire solution set against a heuristic is a good starting point.
- When the problem is about turning one kind of data into another, which tends to happen with loading assets and populating scenes, saving games, etc., knowledge of compilers is very helpful. The algorithmic tools used in compilers let you change the abstraction you’re talking to to address the problem, which is extremely helpful for creating higher quality automation.
- The most straightforward variable name to use throughout your code is “ans” (answer) because you’re always returning an answer from a function. (Pascal used “result” but that’s more characters.)
- Sometimes you should use a spreadsheet to automate. Anything with figuring out sizes, you can use it as a calculator and keep the sheet around to recompute it when your design changes. Anything where you’re defining a lookup table, you can export a .csv file and compile it down. It’s a very handy tool.
- Jackson Structured Programming is one of the best “programming methodologies” you can use, because it does not rely on being highly abstract. It simply tells you that you can go about organizing your loops differently, and doing so will make it easier to make changes, which helps with 2.
- Likewise, coding standards used in embedded programming(e.g. MISRA-C) are very applicable to game programming because one of your most pressing goals is to figure out how to synchronize things that are supposed to be happening concurrently - collisions, entities spawning and being destroyed, etc. - and these are things that need a clear understanding of sequencing and “when” things happen, which is also true when you’re working very close to hardware and there are various timers and buffers to handle.
- Sketch ideas using a small 8x5 gridded paper notebook and a color multi-pen. The notebook acts as a second screen, but one that doesn’t require any configuration or updating. The colors clarify your thinking by helping you add multiple “views” to a diagram or “comments” to a thought.
No one has mentioned this yet, but break it up into multiple files or at least separate out some of your logic. You might also consider using a simple database for things like levels, items, etc, so that adding a new item follows a pre-established item scheme. The easiest kind to implement is just a bunch of files with values separated by ‘,’ if you are absolutely determined not to use any libraries.
Ideally, your logic for potion item effects and the actual item potion should not be in the exact same place. Instead, your potion should do something like have an ‘effect’ set, so if you later wanted to add yet another potion, you could end up with a database table looking like this:
Itemname | Effect | Effectvalue | Spawn | Use
Potion, ChangeHP, 10, common, Drink
Better Potion, ChangeHP, 20, rare, Drink
Sword, ChangeHP, -5, common, Weapon
Staff, ScaleDMGwithStat, Wisdom, bossdrop, WeaponDo a loop creating a new member/extension of the Item class with the inputs from the table, attach a simple function called Use that looks up that data and decides what to do as appropriate to the effect.
This will help a lot in keeping things nice and clean. Simply adding a single new item or monster should not be forcing you to start fresh.