Three weeks ago I mentioned a three-part article dissecting the Python port of Square Shooter. That kind of thing is very interesting to me for several reasons:
- Seeing how another programmer would approach the same problem is immensely instructive, even if you disagree with them. Especially if that shows where your code should have been better commented.
- It tickles my ego that someone found my tiny game useful in any way.
I won’t go through the entire game now and try to rewrite it myself. But I do feel the need to explain a couple of high-level issues with the overall architecture.
We all know the saying: premature optimization is the root of all evil. Too bad modern programmers take that as an excuse to not optimize at all. If you’re wondering why computers grow slower as hardware grows faster, that’s one big reason. Besides, optimization is not premature if you measure first, and guess what, PyGame has a FPS counter. Use it.
This may not be obvious from the source code, but the first thing I did when porting Square Shooter to Python was to get the background rendering in a loop. While redrawing it every frame, the game ran at 120FPS on my machine doing nothing else. When I switched to blitting the pre-drawn background, the frame rate jumped to 200FPS — a 66% increase!
You’ll say, who cares. 120FPS is already enormous. Sure, but remember:
- That was with the game running in 640×480 doing nothing else.
The finished game has much more to do than render the background. And most games are significantly more complex and graphics-rich than mine.
- That’s on a relatively modern computer with a 2GHz CPU. I know people who would be grateful to have a machine two thirds as powerful.
If you waste 40% of your potential performance before you even start, what are you going to do further down the road when CPU cycles grow scarce?
There’s a more subtle example as well. In the world model’s update() method, only the first explosion in the list is age-checked every frame, and the same is true of power-ups. That’s not a bug. Because each new explosion and power-up is added at the end, the first one is guaranteed to always be the oldest. If it’s not old enough to die yet, the following ones certainly won’t be! And if it is, the next frame comes in just a few milliseconds. No-one’s going to notice a game object lingering on for an extra frame. It’s a trivial optimization with zero impact on code complexity OR correctness, so why not do it?
But wait, there’s an even more subtle example. In the init_level() method of the same class, I empty the bubble, explosion and powerup lists instead of re-creating them (e.g. with a list comprehension). I haven’t measured, and it may not be that important, but object creation is expensive as a general rule. And since
del self.bubbles[:] is no more long or complex than
self.bubbles = , why not do it?
In OOP 101, they teach you to make a Shape class with Circle, Triangle and Rectangle subclasses, each with a virtual draw() method. That’s good for a teaching example, I guess, but it ignores three obvious problems:
- If you change the graphics platform, you’ll need to chase down all the classes in your application that might have a graphics-related method and change them. And a large project can easily have hundreds or thousands of classes.
- It mixes concerns. Most of the triangle code is likely to deal with geometry calculations that have nothing to do with showing anything on a screen, yet that one draw() method drags all the graphics framework around with it. Reusability? Portability? What’s that?
- The obvious solution to problems 1 and 2, namely creating an OpenglTriangle derived from AbstractTriangle and so on for each shape leads to… extra classes, piled on top of other classes, among even more classes. Good luck keeping track of them all.
If that sounds theoretical, consider this horror story about a game developed with XNA just as Microsoft announced abandoning the platform…
Yes, Square Shooter having all the graphics code in a single class seems un-objectual. But when I ported the game to C++ and OpenGL, most of the JS code only needed type declarations added. That was a good feeling, not to mention how much easier it made my job.
It may seem more questionable that all the game objects are embodied by a single other class — whimsically called Bubble2D — with the various kinds of bubble created by a factory function (not a method!) and distingushed by which container they sit in. Admittedly, personal coding style has much to do with these choices. But remember that OOP is not the answer to everything! If you find yourself struggling to find a good name for a new class, or a good place for a particular method, it may be a sign that you need to rethink the architecture.
Obviously, all of the above should have been explained (briefly!) in code comments. That’s one of the things I need to fix. And it makes sense, when forking or taking over a project, to make the code more to your liking — it’s part of the understanding process. But that’s the key word: you’re supposed to understand the code, not alter it blindly because it doesn’t fit some checkpoints on a list in a manual.
Use profiling. Use assertions. But most importantly, use common sense.
Refactoring, optimization and personal coding style by Felix Pleșoianu is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.