Over the years, I‚??ve seen plenty of different code bases ‚?? open source projects, internal game engines, my own experiments. Some of them were just bad and buggy, but in many situations I found something that could only be described as ‚??smartness overload‚??. An obviously skilled & experienced programmer just tried too hard. There‚??s a great quote attributed to Brian Kernighan: ‚??Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it‚?Ě. I find it very true, it‚??s even more dangerous when working in team, you cannot assume that everyone is as smart/experienced as you.
My favourite example of code that‚??s just too complicated/smart/advanced for its own good is Boost. It‚??s obviously written by great programmers, who know every corner of C++. It (mostly) works, but I wouldn‚??t want to be the one trying to debug/maintain/extend itd. It‚??s exactly like someone tried hard to write the smartest code possible and made sure it‚??s visible.v
One of the most common signs of smartness overload is overgeneralization (nice recent summary here). While it may make sense in some cases (when writing public library), in our world (gamedev), usually it‚??s just waste of time. Stop thinking about the future too much, focus on what you need now. In many situations overgeneralization is a handy excuse for laziness. Managing object lifetimes is one of my pet peeves. It‚??s common to use single, ‚??universal‚?Ě system for all kinds of situations instead of spending 5 minutes and think. In games it‚??s really rare that object can have more than 1 owner and it should be released from every place in the program. Yet, folks insist on using reference counted pointers or GC everywhere. What? Some of counters can be manipulated from multiple threads? Well, let‚??s make _all_ pointers thread-safe, instead of thinking for another 5 minutes and separating special cases. It may be tempting to have a solution that just works in every case, write it once and use everywhere. Sadly, in many cases it means unnecessary overhead. I like to divide ‚??ownership‚?? patterns in two main categories (there are more, but those are most popular in my experience):
- single owner, object valid throughout the whole game: the very basic, yet popular case, mostly for game subsystems. No, you really don‚??t need GC/refcounting here _at all_.
- single owner, multiple observers, object can be deleted while game is running: there should be only one function that‚??s allowed to remove/delete object. Observers use some kind of weak references (my personal favourite is ID).
First case is rather straighforward, second may be tricky. Question is ‚?? can the object we‚??re observing be deleted before us? Safe bet is to use weak references everywhere, but they come with overhead (we need to test if obejct exists). In many cases it‚??s guaranteed that object is alive, but again ‚?? you need to think about every situation a little bit. For example, it‚??s probably safe to assume that our owner/parent is valid (if it‚??s deleted, it removes us too) and we can get away with plain, old pointer. There are some other problems related to this system (mostly cascades of unnecessary ID->ptr conversions), but it‚??s still way simpler to grasp, debug and control than some generic centralized solution where objects can be deleted in any given moment and it‚??s not even visible at first sight (e.g. last reference going out of scope). If you‚??re using refcounted pointers in your codebase ‚?? make an experiment, try to see how often does your refcount go over 3.
Games are getting bigger & bigger, so do our codebases. They‚??re complicated enough as they are. Say no to ‚??one size fits all‚?Ě solutions. These days I often find more pride and satisfaction in removing code rather than writing it.