Sunday, January 4, 2009

Tutorial 3: getting user input and making the ship rotate. Creating a pilot class, and having fun with inheritance!

So we have a ship on the screen, big whoop-di-doo. We want this thing to actually react to our input, right? By the end of this tutorial we should have this:


press A and D to rotate the ship. Oh and click on the flash file for it to become active

So, here's how we're going to do it...

First of all, if we're going to get things moving in this game, we need to decide how. I like things nice and smooth, so in the spaceGame.fla file I set the frames per second to 25. Also, I'm not going to bother having the game work in real time – so if the computer is slow, the game is slower too. This isn't very professional of me, but I'm doing it to allow me to have complete control over how things move, allowing for simpler collision detection and other such things later on.


In AS3, we get user input by adding special Event Listeners to the stage. In a later tutorial I'm going to go into great detail on event listeners and dispatchers (when we start to create our own ones, that is), but for now we'll just look at how they are used to get keyboard input.

So the stage, as far as I can tell, is the main MovieClip which all the visual elements of the game reside in. It's here that we'll be “listening” for key-strokes. To do this we use the addEventListener function. We need to add the event listener to the stage, and it will happily sit there waiting for keyboard input. Once that input arrives, it will then call the keyDownHandler() function within ship, so within the Ship class we might have the line:

stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);

But unfortunately in AS3 you can only access the stage from an object which has been added to the display list. So, if we write this in the ship class:

//THE SHIP CLASS
package ships
{
import flash.display.*;
import flash.events.*;

public class Ship extends Sprite
{
private var _shipImage:MovieClip;

public function Ship()
{
//add event listener to stage
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);

_shipImage = new blueLightFighterImage();
addChild(_shipImage);

x = 275;
y = 200;
}

//function to be called whenever event listener detects user input
public function keyDownHandler(event:Event)
{
trace("I have pressed a key!");
}

}
}

We get this error:

TypeError: Error #1009: Cannot access a property or method of a null object reference.
at ships::Ship$iinit()
at spaceGame$iinit()


This is because the Ship hasn't been added to the display list (as in, we haven't called addChild(myShip) yet), and it has no access to the stage. Also, notice that to be able to use addEventListener() I've imported flash.events.*. Again laziness with the wildcard...

Also, I've started commenting my code. You really want to do this, even when you work on your own - I've often hated/loved my former self based on how much information I had decided to leave me about the weird stuff I'd programmed.

So what do we do?

Well, the document class (defined in SpaceGame.as) has access to the stage, so we could add keyboard event listeners from the document class to the Ship class...

//THE DOCUMENT CLASS
package
{
import flash.display.*;
import ships.Ship;
import flash.events.*;

public class spaceGame extends MovieClip
{
public function spaceGame()
{
//create new ship
var myShip:Ship = new Ship();
//add event listener to stage which calls function within myShip
stage.addEventListener(KeyboardEvent.KEY_DOWN, myShip.keyDownHandler);

//add ship to display list
addChild(myShip);
}
}
}

And now it works! Whenever the eventListener detects a key-press on the stage, it calls the keydownHandler function in myShip. So far, all this does is trace an asinine message.

yes, dear, very good

But this is really awkward, as it would oblige me to write three lines every time I create a ship:

 
allShips[i] = new Ship();
addChild(allShips[i]);
allShips[i].addEventListeners();

Yuk. This is pretty much the way I did things Before the Tragedy (BT) but it's crap. The whole point of OOP is that I don't have to think about this nonsense...

Anyway, this is not how I want to do things – the ship isn't going to be the entity which detects key-strokes from the user. This is why:

We need to think about things in an object orientated way. The ship is just an automaton, a machine with certain attributes and behaviours, but it doesn't think for itself. Just like their real-life counterparts, these ships need a pilot to handle them. So that's what we're going to do: create a pilot class.

Why is the Pilot class separate from the Ship Class? In this game there will obviously be the human player controlling a ship, but several other ships controlled by the computer. The point is that the ship doesn't care who is controlling it. Whether it's an AI pilot or a human telling it to turn left, the effect is the same. So by separating the entity giving the commands and the entity responding to the commands, we're keeping things simple.

Similarly, the Pilot class will never directly modify the ship class. It will only give commands, which the ship can ignore or process.

There are going to be several different pilot types. Obviously there is a person playing the game, but also the AI Pilots, of which there will be several types depending on what ship they're controlling (FighterAIPilot, CarrierAIPilot, ArtilleryAIPilot etc.). There might also be a PuppetMasterPilot used for cut scenes, or background ships…

Each of these can pass commands to the ship they control – turn, thrust, fire, select next enemy and so on – but the way these commands are decided differs for each one. Here's a good situation in which to use inheritance. We'll have one base class Pilot which is the one core of functionality all Pilots share, then several classes that extend this class with their own specific attributes and methods. The end result will always be a simple list of commands to be sent to the ship.


First we need to create the Pilot class. Once again I'm going to start by making it a package, so I've got a new directory called pilots, and inside a new .as document called Pilot.as, and write the following code:

//PILOT CLASS - sets commands for ship under pilot control
package pilots
{
import ships.*;

public class Pilot
{
//pilot can never modify attribute of Ship, only pass commands
//The available commands:
protected var _commandRotation:Number = 0.0;
protected var _commandThrust:Boolean = false;

//The ship being controlled
protected var _ship:Ship;

public function get commandRotation():Number
{
return _commandRotation;
}

public function get commandThrust():Boolean
{
return _commandThrust;
}

public function set ship(ship:Ship):void
{
_ship = ship;
}

public function Pilot()
{
}
}
}

So we have a class that doesn't do much at the moment. All we have are two private variables that define commands given by the player: _commandRotate and _commandThrust. They are protected, which means only this class and its children can access them, but I do want the ship to be able to read these commands, so I make "getter" functions. This means that within the ship class I could write:

Rotation += myPilot.commandRotate;

And this will work, even though _commandRotate is protected. But because I haven't written a "setter" function, the following line from within the ship class would cause an error:

myPilot.commandRotate = 5.0;

I only want Pilot classes to be able to change the command variables. But notice I have written a "setter" for the _ship variable. This will be particularly useful for the HumanPilot and PupperMasterPilot classes because they will jump in and out of ships at will, but certain things need to be done to keep the game from breaking. At the moment the setter doesn't do much, but some extra functionality will be added later.

If you're confused about the whole getter setter thing there're lots of tutorials online. Unfortunately though, my inadequacies as a programmer come out once again as it turns out that getters and setters break the all important encapsulation which I think one day I might begin to understand. Whatever. Let's make a game. But this is getting on a bit so I'll divide this tutorial into two parts...

On to part 2!

No comments:

Post a Comment