Today I’m going to once again talk about building the systems in Project ONE, a side scrolling shooter game in development under the direction of Benjamin Tovar. This time, I was going to get into the gritty details of spawning of making bad guys for our faux-moving environment, but after some consideration I think that section will be more fun if we can actually blow them up, and to do that we need a player.
So, we shall begin there, but there is one more important thing we have to do first…
The most important thing when dealing with complex software systems is to make sure you can decipher them some time later. Or perhaps even more importantly, that someone else could be handed the source code and decipher it without your help. My experience has been that one of the best ways to do this is to design to an interface, and not specific implementations.
If you’ve ever read the introduction to ye ol’ Gang of Fours work, Design Patterns, that might sound familiar.
For a project as simple as this one ultimately is, though, we’ll skip making an actual Interface file and instead take a moment to seriously consider every function we’re going to need to use over and over again. In a side scrolling shooter, the one thing you can count on is stuff taking damage, so lets decide that the function to handle damage will always be called ApplyDamage(). We will use specific implementations of this function in four different scripts in this Dev Diary, and the final game required quite a few more than that, but since they are all the same interface we shouldn’t have any trouble remembering the function we need.
Similarly, we’ll have several instances of FireWeapon() to handle projectile firing. While we’re thinking about what we need to do, it might also become apparent that the game is meant for mobile devices, but deploying to them in a test environment is a bit of a hassle – and the web player is readily available to everyone. Therefore we shall write our Player_Controller_Script so that it’s simple to implement an additional input interface and disable the testing one.
If you’re following along, now would be the best time to sit down and try to name every function you think you will need, and describe what it does in comments. From this, start a list of variables as well. When you’re ready, we’ll find out how close yours is to mine. Ready? Here we go:
The first thing that you might have noticed is how heavily marked up this file is with comments. This is because I am using this code for instructional purposes – it’s overkill for anything else.
Anyway, as is traditional, first we have the variables. In the first group I have gathered all the variables related to dealing damage: the amount of damage to be dealt if the player collides with something, the projectile prefab that shall be spawned when the player fires, an empty gameobject that we will parent to the player to use to offset the projectiles spawn from the players ship, and the delay in between shots the player fires. These variables are all public, set in the inspector by the designer as they tweak the games numbers. The last variable, playerShotTimer, is private because we use it internally to count time and nobody should ever be adjusting it outside of that.
Next we have the section for health and lives. For the moment we’re not doing anything with this, but because Project ONE is the sort of game where the player survives more than one hit most of the time, we’ll need them later and having them in there now reminds us why it’s ApplyDamage() and not AsplodePlayer() or something.
Finally, we have the players movement speed and four boundary variables. We’ll use these to prevent the player from moving off the edge of the screen, since our camera isn’t able to follow them at all.
Next we have the first half of our functions. ApplyDamage() should do what it sounds like it will do, but for the moment lets leave it empty – it’ll be easier to check out our waves of bad guys later if we’re invulnerable! Next is CountShotTimer(), which we will call in our beloved Update() function in order to make the timer go down and thus allow the player to fire again. Speaking of which, FireWeapon() is next on the list. This function first makes sure there is no time left on the shot clock, and if there isn’t, it puts time on the clock and then spawns the projectile. We’ll let each projectile handle it’s own movement and collisions in their script. An empty GameOver() function that we’ll leave blank since we can’t die yet, and then the fun part.
KeyboardInput() is an example of how you can break apart elements in order to achieve more modular code. In this case, we’ll simply listen for the WASD and arrow keys, along with the space bar to be pushed and if we hear them we call the function for what those buttons do. This function is also called every frame by Update(), meaning that all we have to do to disable it when we build for touch screens is drop our //comment marker in front of that line in Update(). Easy, peezy.
First up in the second assortment of functions, is our collection of Move() functions that KeyboardInput() and any other input systems we create, will call. These each check to make sure that the player isn’t out of bounds in the corresponding direction, and if they aren’t moves them one frames worth of distance that way. Next up, OnTriggerEnter() is another of Unitys provided functions that makes our lives a lot simpler. This one is automatically called by the Unity engine whenever the object enters a trigger box, and passes it a connection to that trigger boxes gameobject. For the moment, the only thing that goes in here is the enemy itself, so that the collision damages them. In the Enemy_Controller_Script we’ll have the exact same thing happen in reverse, damaging the player when the enemy runs into them.
Of course, in any actual collision, these will both fire at the same time, but doing it this way allows each object to inform the other of how much damage to take, instead of every object in the game needing to remember how much damage it should take from impacting with every other object. Moving along we have our placeholder PlayerDied() function for when we implement death, and then our wonderful friend Update(), who dutifully calls the CountShotTimer() and KeyboardInput() functions for us every single frame.
Now the player script is ready to be fiddled with in the inspector, but it needs an object to move around. For this we can simply use a primitive. I happen to be fond of blue cubes for the player, myself, but you can use just about anything. We’ll also need a Prefab for the players shot, and it will need its own script as shown above. Lets break this down for completeness, shall we?
We’ll need variables for the amount of damage this projectile does, any particle effect we want to spawn when it hits something, how fast it should move and the line at which point the projectile is removed from play. Our first function is our good friend, OnTriggerEnter() again, and again we’re just going to look for the enemy right now and inform it that it has been dealt damage. If we had one, we’d spawn our particle effect next, because the next line destroys the projectile – if we don’t spawn it now, we won’t be able to tell where to spawn it at all.
Our second function, our dear friend Update(), bears a few similarities to the Move() functions the player has, but things have been rearranged somewhat. For the projectile, which shoots out in a straight line, we’ll just go ahead and always move it forward one frames worth. Then, we check to see if it has crossed the termination line yet and if it has we destroy it immediately, without any fanfare. Enemy_Shot_Script merely reverses all of these, and is good to go.
Finally, in order to make Unity happy enough to let us test these scripts, we need to write at least a little bit of the Enemy_Controller_Script. This little bit is all that it takes right now to make Unity happy, though it produces an enemy that sits there and waits to be killed – Mmm, target practice! I used some red cubes for this, myself.
That’s enough of the player for now. What we have will allow us to interact with the waves of enemies we’ll create in my next Dev Diary – and it’s really next this time, I promise! In the mean time, good luck, and have fun!