Monday, January 12, 2009

Tutorial 7: new ships and Artifical Intelligence

Right, now that preloader nonsense is out of the way, let's get another ship up on the screen.

This is what we'll have by the end of this tutorial:


press A and D to rotate, and W to apply thrust.


Get the whole zip file here.

New ship types. To inherit or not to inherit?

Here's an interesting issue I've had. I want to make different kinds of ships. In the example above I have blueLightFighter and redLightFighter. Now, how do I define these?

Before the Tragedy, I had them as classes that extended the Ship class, so I'd just write var myShip:Ship = new blueLightFighter();

But since then, reading about OOP has got me worried about using inheritance when I don't need to. The thing is, blueLightFigher and redLightFighter have totally identical methods. In fact, the only difference they have is in the initial values for their attributes (such as maxSpeed or acceleration). Even with more complex ships, that may have multiple cannons and missile launchers, the only thing that's different is their constructor.

This led me to think that maybe I should simply pass in a variable which tells the Ship constructor what values it should use for the ship attributes. Something like var myShip:Ship = new Ship(Ship.BLUE_LIGHT_FIGHTER). The Ship.BLUE_LIGHT_FIGHTER is a public static constant (which means it can be accessed through the class definition) which will then call a function within the Ship class that sets the attributes.

But now we have another issue. I know for a fact that later on I'm going to start writing ship classes that do extend the methods of Ship - for example a great big carrier will actually have a rotationAcceleration value as well (as in it eases into rotation). Also, I'll be overriding collision detection methods. So then things would get complicated if I have to start writing:

var myFighter:Ship = new Ship(Ship.BLUE_LIGHT_FIGHTER); //just change the ship attributes
var myCarrier:Ship = new carrierShip(Ship.BLUE_CARRIER); //carrierShip extends the Ship class

urgh. I prefer simply calling a new class each time. As in:

myShip:Ship = new blueLightFighter();
myOtherShip:Ship = new blueCarrier();

Keeps things consistent. So even though my smaller ships don't need to extend the Ship class, they will do, to keep things simple when I start scripting my levels later on. All clear? Good, let's make some new ships.

Making a few changes to the Ship class.

So now in my game I'll never actually call new Ship(), but the name of a ship type that extends the Ship class. In the Ship constructor, I'll contain a call to a setShipAttributes() function (I hope it's purpose is self-explanatory). Within the Ship class, setShipAttributes is empty - it will be defined in the classes that inherit from it.

I'm also going to add a few things - each ship will have a _shipType variable. This will be useful in the future for things like AI. I plan to use the classic rock-paper-sizzors form of game design regarding units - as in shipA is deadly against shipB, but vulnerable to shipC (which in turn might be vulnerable to shipB etc. etc.). So I don't want the AI just attacking the nearest ship - that ship might be the worst kind of ship to fight. Instead, it will look for the ship types it is most effective against...

Also, let's add some arguments for the ship constructor - it's initial position and rotation.

//--------------------------------------------------------------------------
//
// CONSTRUCTOR
//
//--------------------------------------------------------------------------

public function Ship(x:Number = 0, y:Number = 0, rotation:Number = 0)
{
this.x = x;
this.y = y;
this.rotation = rotation;

setShipAttributes();

_aiPilot = new AIPilot(this);
_controllingPilot = _aiPilot;

//see bottom of file for updateShip
addEventListener(Event.ENTER_FRAME, updateShip)
}


A nice thing about flash is that if you set a value for the arguments of a function, those arguments are now optional. So I could say var myShip:Ship = new blueLightFighter(20,50,45) to have a ship at position [20,50] and facing and angle of 45 degrees, but if I just write new blueLightFighter(20,50) there won't be an error - the constructor will merely use the default value of 0 degrees for rotation. Cool, eh?

I've also added the public static consts for shipTypes. I've made them integers, so that I can classify them (as in all ship types below 10 are fighters, between 10 and 20 are corvettes, between 20 and 30 are frigates etc.). Also, integer comparisons are faster than string comparisons...

So now let's create two new .as documents: blueLightFighter.as and redLightFighter.as. These will both be very simple classes, that inherit from Ship and simply override the setShipAttributes function. Here's blueLightFighter for example:

////////////////////////////////////////////////////////////////////////////////
//
// AWOOGAMUFFIN
// Copyright er... I don't understand copyright. I think uploading this code
// possibly robs me of all rights. Just be nice - if you use my code, mention
// where you got it from, yeah?
//
// NOTICE: Awoogamuffin permits you to use, modify, and distribute this file
// in accordance with the terms of the license agreement accompanying it.
//
// Hahahaha, I just copied that from the conventions page (like everything I
// do). There's no license agreement. Stop looking for it.
//
// BLUE LIGHT FIGHTER
//
////////////////////////////////////////////////////////////////////////////////

package ships
{
import pilots.*;

public class BlueLightFighter extends Ship
{
//--------------------------------------------------------------------------
//
// CONSTRUCTOR
//
//--------------------------------------------------------------------------

public function BlueLightFighter(x:Number = 0, y:Number = 0, rotation:Number = 0.0)
{
super(x, y, rotation);
}

//--------------------------------------------------------------------------
//
// OVERRIDDEN METHODS
//
//--------------------------------------------------------------------------

override protected function setShipAttributes()
{
_shipType = Ship.BLUE_LIGHT_FIGHTER;

//attributes specific to the blue light fighter class
shipImage = new blueLightFighterImage();
_turnRate = 300.0/25.0;
_maxSpeed = 450.0/25.0;
_acceleration = 100.0/(25.0 * 25.0);

//give the ship a mind of its own:
_aiPilot = new AIPilot(this);

addChild(shipImage);
}
}
}

You might be full of wonder and excitement at the line _aiPilot = new AIPilot(), as indeed you should be.

Writing the AIPilot class:

Now it's ridiculous how much time I sank into the AI before the tragedy. You get hooked up on all these tricky little details that any average user would probably never notice, but that grate against your soul. Keeping things simple and realistic is key, but ever so difficult to enforce.

So we're going to write the AIPilot class. This will extend the Pilot class, so all it will do is interpret the situation, then set commandRotation and commandThrust to the valid setting - it will never actually modify the ship itself (hence the use of private variables). For now, all I'm going to have the AIPilot do is turn to face the targeted ship, and once it is facing the ship, apply thrust.

So first of all, back in the Pilot class, we add the variable:

private var _target:Ship;

And we give this a getter and setter. The setter is important - because later whenever you set a new target for a Pilot, this will modify settings in the Pilot's previous target, as well at the new target. All this will happen in the setter, and I won't have to worry about it. For now, it doesn't do anything other than set the Pilot's _target variable...

Right, so we add an updatePilot() function in our AIPilot class, and within it we do a little trigonometry to figure out the angle between the ship and its target - we can use this with the angle getter function for our Vector2D class. We then write a function faceAngle which will turn the ship towards the desired angle, and it returns a Boolean: true if the ship is now facing its target, false otherwise. So we'll simply have the AIPilot apply thrust when faceAngle returns true. Brilliant.

Here's the AIPilot class:

////////////////////////////////////////////////////////////////////////////////
//
// AWOOGAMUFFIN
// Copyright er... I don't understand copyright. I think uploading this code
// possibly robs me of all rights. Just be nice - if you use my code, mention
// where you got it from, yeah?
//
// NOTICE: Awoogamuffin permits you to use, modify, and distribute this file
// in accordance with the terms of the license agreement accompanying it.
//
// Hahahaha, I just copied that from the conventions page (like everything I
// do). There's no license agreement. Stop looking for it.
//
// AIPilot - the A stands for "Aaaaaaargh!"
//
////////////////////////////////////////////////////////////////////////////////

package pilots
{
import ships.*;
import Maths.Vector2D;

public class AIPilot extends Pilot
{
public function AIPilot(ship:Ship)
{
super();
_ship = ship;
}

//--------------------------------------------------------------------------
//
// METHODS
//
//--------------------------------------------------------------------------

//called from the ship class
public function updatePilot()
{
//start by reseting all commands
resetCommands();

//for now, we'll just have the AIPilot chase after its target
if(_target != null)
{
/* calculate angle between ship and target. I should probably put this sort of thing
in a trigonometry class */
var targetAngle:Number = 0.0;
var relativePosition:Vector2D = new Vector2D(_target.x - _ship.x, _target.y - _ship.y);
targetAngle = relativePosition.angle;

//so turn ship towards this angle, and if it reaches it, apply thrust
if(faceAngle(targetAngle)) _commandThrust = true;
}
}

//faceAngle - will turn ship towards targetAngle, and if it reaches said angle it returns true
public function faceAngle(angle:Number):Boolean
{
//calculate the difference between target angle and the ship's angle
var difference:Number = angle - _ship.rotation;
var absDifference:Number = Math.abs(difference);

//next part is a bit tricky due to the fact that we have a switch between 180 and -180
//first, if target is within turn rate of AI ship it'll go for it!
if(absDifference <= _ship.turnRate || absDifference >= (360 - _ship.turnRate))
{
//now the ship only turns the required number of degrees - which is less than the ship's
//turn rate
_commandRotation = difference;
return true; //ship facing angle
}

//otherwise it will rotate to face target ship
else
{
//so first check if it needs to turn right
if(difference > 0 && absDifference < 180 || absDifference > 180 && difference < 0)
{
_commandRotation = _ship.turnRate;
}
//ok, so turn left then
else
{
_commandRotation = -_ship.turnRate;
}
}
return false; //ship not facing angle yet
}

public function resetCommands()
{
_commandRotation = 0.0;
_commandThrust = false;
}
}
}


Now I think this AIPilot class is so wonderful that every ship should have one. Remember in Ship.as we had _controllingPilot? Well let's add an _aiPilot variable as well. This will be added to every ship in the constructor (as we saw earlier). This will be the default controllingPilot for all ships, so back in the Ship class, we add the line _controllingPilot = _aiPilot.

Why don't we just put _aiPilot = new AIPilot() in the Ship class? It would be more efficient than having to write it in each ship class I make from now on, right? Well, the thing is I know that the AIPilot for a little fighter will function very differently from that AIPilot for an artillery ship. Yes, in the future, we will be extending AIPilot as well, and in each ship class, we'll be telling it what kind of AIPilot to create in the setShipAttributes function. See? Thinking ahead...

In the updateShip function we also need to add: aiPilot.updatePilot().

Fine, here's the whole Ship class... I'll just make it so you have to scroll through it - that way it doesn't take up most of this blog entry:

////////////////////////////////////////////////////////////////////////////////
//
// AWOOGAMUFFIN
// Copyright er... I don't understand copyright. I think uploading this code
// possibly robs me of all rights. Just be nice - if you use my code, mention
// where you got it from, yeah?
//
// NOTICE: Awoogamuffin permits you to use, modify, and distribute this file
// in accordance with the terms of the license agreement accompanying it.
//
// Hahahaha, I just copied that from the conventions page (like everything I
// do). There's no license agreement. Stop looking for it.
//
// SHIP CLASS - chaos and craziness and head-aches
//
////////////////////////////////////////////////////////////////////////////////

package ships
{
import flash.display.*;
import flash.events.*;
import pilots.*;
import Maths.Vector2D;

public class Ship extends Sprite
{
//--------------------------------------------------------------------------
//
// CLASS VARIABLES
//
//--------------------------------------------------------------------------

public static const BLUE_LIGHT_FIGHTER = 1;
public static const RED_LIGHT_FIGHTER = 2;

//--------------------------------------------------------------------------
//
// VARIABLES
//
//--------------------------------------------------------------------------

//----------------------------------
// ship movement
//----------------------------------
protected var _turnRate:Number = 300.0/25.0;
protected var _maxSpeed:Number = 400.0/25.0;
protected var _acceleration:Number = 250.0/(25.0 * 25.0);
protected var _velocity:Vector2D = new Vector2D();

//----------------------------------
// identifiers
//----------------------------------
protected var _shipType:int;

//----------------------------------
// visual
//----------------------------------
protected var shipImage:Sprite;

//----------------------------------
// pilots
//----------------------------------
private var _controllingPilot:Pilot;
protected var _aiPilot:AIPilot;

//--------------------------------------------------------------------------
//
// CONSTRUCTOR
//
//--------------------------------------------------------------------------

public function Ship(x:Number = 0, y:Number = 0, rotation:Number = 0)
{
this.x = x;
this.y = y;
this.rotation = rotation;

setShipAttributes();

_controllingPilot = _aiPilot;

//see bottom of file for updateShip
addEventListener(Event.ENTER_FRAME, updateShip)
}

//--------------------------------------------------------------------------
//
// GETTERS AND SETTERS
//
//--------------------------------------------------------------------------

public function get turnRate():Number
{
return _turnRate;
}

public function get velocity():Vector2D
{
return _velocity;
}

public function get aiPilot():AIPilot
{
return _aiPilot;
}

public function get shiptype():int
{
return _shipType;
}

public function set controllingPilot(pilot:Pilot)
{
_controllingPilot = pilot;
pilot.ship = this;
}

//--------------------------------------------------------------------------
//
// METHODS
//
//--------------------------------------------------------------------------

public function applyThrust()
{
var thrust:Vector2D = Vector2D.newVectorFromAngle(rotation);
thrust.length = _acceleration;
_velocity.addVector(thrust);
}

public function moveShip()
{
//first check that velocity is not above maxSpeed
if(_velocity.lengthSquared > _maxSpeed * _maxSpeed)
{
_velocity.length = _maxSpeed;
}

x += _velocity.x;
y += _velocity.y;

//this next bit is just to make the ship bounce off the edges off the
//stage
if(x < 10 || x > 540)
{
x < 10 ? x = 10 : x = 540;
_velocity.x *= -0.3;
}
if(y < 10 || y > 390)
{
y < 10 ? y = 10 : y = 390;
_velocity.y *= -0.3;
}
}

protected function setShipAttributes()
{
//this is performed by child classes.
}

//--------------------------------------------------------------------------
//
// EVENT HANDLERS
//
//--------------------------------------------------------------------------

public function updateShip(event:Event)
{
_aiPilot.updatePilot();

rotation += _controllingPilot.commandRotation;
if(_controllingPilot.commandThrust) applyThrust();

moveShip();
}
}
}

Now back in the document class, in our startGame function we add these lines:

public function startGame()
{
//create the player ship
var myShip:Ship = new BlueLightFighter(100,300);
addChild(myShip);

//assign the human player as the ship's pilot
var humanPilot:HumanPilot = new HumanPilot();
myShip.controllingPilot = humanPilot;

//now let's make a red fighter
var enemyShip:Ship = new RedLightFighter(300,100);
addChild(enemyShip);

//this is silly - in the future part of that aiPilot's functionality will be to select it's own targets
enemyShip.aiPilot.target = myShip;
}


One more thing, we need the HumanPilot class to know that it is controlling myShip, so in the Ship class, for the set controllingPilot() function we now have:

public function set controllingPilot(pilot:Pilot)
{
_controllingPilot = pilot;
pilot.ship = this;
}


And the result is an angry little red ship chasing after the blue ship and bumping against the edges of the screen. Not much of a game I admit, but things are coming along...