|By Shamus||Sep 6, 2013||57 comments|
Sometimes people ask me what the difference is between a programmer and a software engineer, and my usual flippant answer is that software engineers are what programmers call themselves when they want to sound important. But that’s not entirely fair and often the distinction is worth making.
Metaphorically, software engineering is designing a bridge that won’t fall over, and programming is the act of rolling up your sleeves and laying bricks or nailing boards together to make the bridge a reality. It’s easy to see the difference between the two disciplines when we talk about it in terms of construction, because you’ve usually got two very different kinds of people doing those jobs. The designer does her thing, then the builder does his, and the next thing you know you’ve got a bridge. But when you’re building software the same person is most likely doing both jobs, and they’re most likely doing them at the same time.
Perhaps it sounds foolhardy to design a bridge while you build it. Joel Spolsky will tell you that you should design the bridge before anyone starts putting bricks on top of each other. And for the most part that’s true. Without good engineering you’ll either end up with ungainly code, or you end up doing lots of rewrites.
But you can’t write a spec if you don’t know what what you want to build or how you want to build it. This isn’t a bridge-building project, this is a skunkworks. We’re looking for fun gameplay and you can’t really appraise the fun of something until you’ve got a working prototype.
So we’re coding with no spec. This leads to interesting problems.
|Most bugs are boring, but this one was funny: I was adding a feature to make robots fire in volleys instead of single shots. I messed up the cooldown timing and the bot began shooting at me every single frame - 60 shots a second.|
Hm. Let’s see. I’m adding this code that will create this animated character for the player. All the controls are in player.cpp, so it makes sense to put this character-drawing stuff in there with it. I mean, one controls the position of the character and the other controls the appearance of the character. Those are basically related ideas, right?
Now what about this code that tracks player stats like shields, energy, XP, level, etc? Well, previously the only player data was the player position and momentum in player.cpp. This level-tracking stuff is arguably just more player data, so it belongs in player.cpp.
For testing purposes, I need to be able to see the player health. I’ll just have it print the raw values right to the screen. Just some quick-and-dirty rendering so I can get back to testing.
Now I’m adding code to restrict what the player can see. Well, that ties into player position data, so I guess that should go into player.cpp.
I need to see more than just player health. I need to see my supply of missiles and energy. And you know what would help? If I replaced these ugly numbers with proper status bars so I can get a feel for the values without needing to take my eyes off the action.
I want to draw a blue sphere around the player when their shields go up or down. Oh, I can just attach that to the rendering of the player character here in player.cpp.
The animated player character needs to generate particle effects as it flies around. That’s directly tied to the character, so that belongs in player.cpp.
I need to handle input from the keyboard or the Xbox controller. Well, all the controls are already in player.cpp, so this is a natural extension of that.
Taken alone, any one of these decisions is pretty reasonable. But taken together, they are madness. One source file has inputs, game mechanics, particle effects, animation code, HUD rendering, line-of-sight checking, messing with the stencil buffer, polygon rendering, collision detection, mouse controls, and half a dozen other things. Going back to our bridge analogy: We’ve successfully built a structure that can let travelers cross the valley. But this is not a bridge, it’s a giant shapeless mound of bricks. It does what it needs to do, but with no sense of order, clarity, or efficiency.
|As suggested in the comments, I restored the flashlight mode, but now the flashlight is purely cosmetic / psychological. You can see all around you, but where you're aiming is brighter. I like it.|
So now that we’ve made a mess, let’s do some proper software engineering and give this thing some proper shape. (One advantage we have over bridge-builders is that we can sometimes get away with this sort of slapdash “build first, design second” mentality.)
When you’re designing code you have to start thinking about interfaces. Not like, text boxes or buttons. I’m talking about programming interfaces. The whole point of cutting up your program into distinct sections is to manage complexity and try to control side-effects.
Side-effects is a tough problem to explain. Let me take a stab at it:
Let’s say I’m doing processing on a missile object in the game. I move it, check for collisions, and find it has just smacked into a bomb robot. So I pause processing this missile for a second to send a “you take damage” message to the robot. The robot’s health goes below zero. This type of robot is supposed to explode when it does, so it spawns an explosion. The explosion initializes itself by looking at everything within its blast radius and sending it a damage packet. Let’s see… there’s the robot that spawned us, but he’s already dead. Then we have the player, so let’s send them a damage packet. Oh! and there’s this missile in the blast radius. I’ll send it a damage packet. The missile, upon getting the damage, sees that its hitpoints are below zero and so it destroys itself and deletes itself from memory.
Ok, great. What were we doing before all that? Oh, right. We were updating that missile. But isn’t that the missile we just deleted from memory? But that means-
|Would you like to check online for a solution to the crash bug you just created? No, thanks Microsoft. I've looked, and "online" is the worst place to look for those kinds of solutions.|
To be clear: This is a contrived and simplified example that will hopefully help you grasp this problem. I didn’t actually make this mistake. This would be a crash that would happen only when the player used a missile to kill a robot that exploded on death. Depending on how the game works, that might be hard to identify and track down. Players would just report “a crash”. After a while someone might notice that the crash only happens when using missiles, but only sometimes. (If the blast radius of the missile is larger than the blast radius of the robot, then this crash will only happen sometimes.) Eventually you’ll be able to replicate the bug and follow this snarl of logic down to where things go wrong.
When you’ve got a program with 40 different systems running (not a made up number in this case) the number of ways that different systems can interact becomes kind of terrifying. The most expedient way to solve the above problem would be for the missile to set a flag for itself saying, “I’m in the middle of processing, so don’t delete me.” That would fix this particular crash bug, but it wouldn’t fix the larger problem that these interfaces are badly designed and these systems don’t interact in a safe way.
If you design the interface properly, then when you’re writing the explosion-making code you might notice that you can’t directly destroy other objects. All you can do is send them damage packets, which they will process in their own time, thus preventing large-scale interactions.
You can take this idea even further, where individual objects are not allowed to change anything but themselves. This is what John Carmack was talking about when he got into functional programming in his Quakecon keynote. In a functional programming paradigm, an explosion couldn’t deal damage to other objects. Some super-object that owns both of them would need to offer a snapshot of the explosion to the missile saying, “Here is an explosion. Use it to damage yourself and let me know what happens to you.”
|Some people suggested that it should be pure darkness where your light isn't shining. Here is what that looks like. It might be fun for a brief gameplay section, but I think this sort of thing would wear out its welcome quickly.|
It sounds pretty restrictive, and it is. But languages have gradually been getting more strict for decades. As we make larger and more complex software, we need ways to mitigate that complexity and make it comprehensible. In the old days we had imperative programming. (Also called procedural programming.) I did imperative programming for over a decade before I even knew it was called that. There wasn’t a commonly used name for it because all languages were basically imperative. (At least in the areas where I operated. I never got near programming theorists or academics. I was just a kid figuring stuff out on his own, without the help of an internet.)
The imperative approach to the above problem would have program execution jumping recklessly from missile-process to robot-processing to explosion-processing, where at any moment any system could make changes to any other. Object-oriented design built some walls around those systems and forces the programmer to deliberately place turnstile doorway for data moving in and out. An impatient programmer might be writing a bit of code to make an explosion do its thing and get annoyed that they can’t just directly destroy some other object. Hey! Why can’t I call
missile->Destroy ()? I know explosions ALWAYS do more than the base hitpoints of a missile, so there’s no need to waste time calling
missile->Damage (N). It will be way more efficient* to just add a way to destroy the missile directly.”
* “Efficient” in this case means, “I have no idea how it might impact performance, but it’s way more convenient.
|The lava level! The water level! The... lots-of-green-plants level! The purple one because shut up it's cool!|
Object-oriented design lives or dies based on how smart your interface is. You can build a turnstile at every point in the wall, at which point the wall is no longer in your way but also no longer protecting you from dangerous levels of complexity. You’re effectively back to imperative programming, but with really screwy syntax. If you happen to get paid by lines of code, you might do okay for yourself.
Functional languages make the wall impenetrable, and will only let you build ONE door. I wouldn’t want to program anything complicated like that, but it’s an interesting approach to what is clearly a growing problem: Eventually we won’t be smart enough to work on our own software.