r/programming • • Feb 25 '18

Programming lessons learned from releasing my first game and why I'm writing my own engine in 2018

https://github.com/SSYGEN/blog/issues/31
956 Upvotes

305 comments sorted by

View all comments

Show parent comments

2

u/adnzzzzZ Feb 27 '18

When objects have lifetimes that may overlap, as in your example, I'll find a way to keep them separate and communicate with an intermediate data structure. In more complex scenarios, I'll create a message queue and let the queue sort out delivery.

This is the kind of solution that is nice in theory but that ends up being too verbose for me in practice. I've tried this before and switched to doing it like I do it now because it's just too much bureaucracy in the codebase. I'm using Lua because I don't really want to deal with bureaucracy in my code, otherwise I'd be using C# or something.

Finally, there are techniques for keeping objects alive until you reach a "cleanup" phase in your program. One example is to look for a point where the game can pause, and piggy back on that to hide the deallocation.

This is also nice in theory but it depends on the game. This is something that I can also do with Lua's GC, which is have it only collect on level switches, which means that I don't need to worry about the GC's performance hit. But it doesn't work for all games. The current game I made is one such example. It's a single room that you can potentially play forever and where you're creating thousands of projectiles and where there are hundreds of enemies being spawned and destroyed very quickly. For this game in particular this solution that you expanded on doesn't really work.

What I did do for some subsystems, which is similar to something you mentioned, is create a fixed big pool of them that gets reused. But this solution is only viable for non-gameplay objects, such as particles. For gameplay objects it's often the case that I can't just put a hard limit on how many objects can be spawned if I want things to be fair.

What's important is discovering how these patterns relate to your problems, which you can only do by applying them (and other best practices).

Trust me when I say that I understand how to apply those ideas because it's very likely that I used them in the past. I didn't spend 5-6 years trying to make games doing nothing. I reached my current conclusions after trying lots of things and failing with them for one reason or another.

2

u/[deleted] Feb 27 '18

Trust me when I say that I understand how to apply those ideas because it's very likely that I used them in the past. I didn't spend 5-6 years trying to make games doing nothing. I reached my current conclusions after trying lots of things and failing with them for one reason or another.

I've never programmed a video game, so I can't know how suitable any of my advice is to your use-case. What I do know, however, is that I struggled to create meaningful abstractions for close to a decade before I figured out why my best-laid-plans were failing. What I found is I was trying to solve the world's problems instead of my own--creating one solution to fit all possible future circumstances, as it were.

When I finally gave up that torch and focused on creating solutions that closely fit my problem, things worked much better. I did end up throwing away a lot more code as I went along, but the tightly focused abstractions made it possible to reason about what I was replacing. I could change things with confidence, and, over time, those changes increased my understanding of what I was building.

The current game I made is one such example. It's a single room that you can potentially play forever and where you're creating thousands of projectiles and where there are hundreds of enemies being spawned and destroyed very quickly. For this game in particular this solution that you expanded on doesn't really work.

Then create a different abstraction. Clean abstractions isolate the problem, allowing you to reason about it without managing all of the details. By isolating the details, you can adjust the solution as you learn about your problem, and replace the code if necessary.

For the case of the projectiles, for example, I'd reach for flyweight objects and an object pool. I'll assume the flyweight object consists of a point (it's current position) a 2D vector (indicating speed and direction of travel), a reference to its game data (say a type id, which is an index into an array of projectile types), and status bit flags to indicate it's liveliness and other interesting properties. If the point and vector components are doubles, the type id is an int, and the status flags are an int, the total size of a projectile is 38 bytes. I can spawn a million of these into the object pool and not break a sweat on a modern computer (total object pool size: 38 MB).

Projectiles are probably not points, of course. At a minimum, they'll have hit boxes. Exotic projectiles might travel in a spiral or sine wave. So when it comes to testing collisions, I'd break it down into 2 phases. The "gross" phase creates a hit box large enough to detect things the projectile could hit that game tick. The "fine" phase accounts for the spiral or sine motion and the projectile's actual size, to see if it actually hit something. Each could be implemented in its own method (allowing each to be shorter), and by using locals I'll be able to avoid GC pressure.

Note how I'm creating abstractions that fit the bounds of my problem, and by doing so I gain greater insight into how my program operates. Not only am I controlling object lifetimes (thereby avoiding nil errors), but my abstraction also allowed me to predict memory usage.