Programming Games in JavaFX - Part 3
September 10th, 2009
In this lesson we'll complete the code that controls all the movement of our ship. Right now the left and right arrow keys will turn the ship but we need to apply thrust in the direction that the ship is facing. Just for fun, we'll add a reverse gear that is triggered when the down arrow key is pressed.
I must warn you that there is some trigonometry in the code that determines how to move the ship depending on it's velocity and faceAngle properties, but it's not important that you really understand the math. I have to admit that until recently, it made no sense to me; I just understood that it was an algorithm that accomplished my goal. If you want to understand the details, I recommned a book by Keith Peters called ActionScript 3.0 Animation - Making Things Move! Although it is not written for javaFX, it is an easy-to-understand explanation of the math used to animate objects.
One other issue that we need to resolve is what to do if the ship goes outside the boundries of the game board. In this case we'll use a technique called 'wrapping', which means that if the ship moves beyond the top edge of the screen, it will re-appear on the bottom. Likewise, if the ship moves off the left edge, it will re-appear on the right.
Let's get right into the code, the additions from the last lesson are highlighted in bold.
We'll start with Config.fx, where we add just one constant to control how fast our ship can accelerate:
package blasteroids;
public def SCREEN_HEIGHT:Integer = 800;
public def SCREEN_WIDTH:Integer = 1200;
public def SHIP_ROTATION_VELOCITY = 10;
public def REFRESH_RATE = .04s;
public def SHIP_ACCELERATION = 1;
Now we'll add some new properties to our Ship.fx class that will allow us to control its position on the game board. Notice that we add a 'moveAngle' property. There is a difference between this property and the 'faceAngle' property that we added in the previous lesson. The move angle controls the ship's direction of actual movement, while the face angle controls the direction that the ship is facing. So our ship can move in one direction but pivot without affecting it's directional movement. I spent some time playing with different ways to control the ship's movement and if you'd like to see one model that might be better suited for a race car game, you can download the alternate project here. I've also added a height and width property to the Ship class, which allows me to access the dimensions of the image that serves as our ship. These properities will come in handy when we handle the ship moving beyond the borders of the screen and 'wrapping' to the other side.
Here's the Ship.fx class with our additions highlighted in bold:
package blasteroids;
import javafx.scene.CustomNode;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.Node;
import javafx.scene.transform.Rotate;
public class Ship extends CustomNode {
public var posX:Number = 0;
public var posY:Number = 0;
public var offsetX:Number = bind posX - img.width/2;
public var offsetY:Number = bind posY - img.height/2;
public var faceAngle: Number=0;
public var moveAngle: Number = 0;
public var velocityX: Number=0;
public var velocityY: Number=0;
public var width:Number = bind img.width;
public var height:Number = bind img.height;
var img = Image{
url: "{__DIR__}ship.png"
}
var imgView = ImageView{
image: img
x: bind offsetX
y: bind offsetY
transforms: Rotate {
angle: bind faceAngle
pivotX: bind posX
pivotY: bind posY
}
}
override public function create():Node{
return imgView
}
}
Finally, we'll make some significant additions to the Container.fx class. Note that we add code to respond when the up and down arrow keys are pressed (and this is where we add that tricky math to re-calculate the velocity of the ship along both the X and Y axis). Then, as the gameLoop cycles and the updateShip() function gets called, it repositions the ship based on the velocityX and velocityY properties that were changed when the up and down arrows were pressed.
Here's the code for our game engine class, Container.fx:
package blasteroids;
import blasteroids.Config;
import blasteroids.Ship;
import java.lang.Math;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
public class Container extends CustomNode {
public var turnLeft:Boolean = false;
public var turnRight:Boolean = false;
public var ship: Ship = Ship{
posX: Config.SCREEN_WIDTH / 2
posY: Config.SCREEN_HEIGHT /2
}
public var txtInfo:Text = Text{
x:10
y:20
wrappingWidth: Config.SCREEN_WIDTH - 20
font: Font { size: 18 }
fill: Color.WHITE
//Just updating the text that was set in the last lesson...
content:"Use the left and right arrow keys to turn the ship."
"\nUse the up and down arrow keys to move the ship "
"forward or reverse it."
"\nPress ENTER to start..."
}
public var gameBoard: Group = Group{
content: [
//backgroud rectangle...
Rectangle{
width: Config.SCREEN_WIDTH;
height: Config.SCREEN_HEIGHT;
fill: Color.BLACK
}
//add the info text to the scene...
txtInfo,
//add the ship to the scene...
ship
]
focusTraversable:true
//setting focusTraversable to true allows us to
//listen for keyboard events
onKeyPressed: function(e:KeyEvent):Void{
if(e.code == KeyCode.VK_ENTER){
if(not gameLoop.running or gameLoop.paused){
startGame();
}
}
if(e.code == KeyCode.VK_LEFT){
turnLeft = true;
}
if(e.code == KeyCode.VK_RIGHT){
turnRight = true;
}
if(e.code == KeyCode.VK_UP){
//when thrust is applied, make sure that it is applied
//in the direction that the ship is FACING, so adjust
//the moveAngle to equal the faceAngel...
ship.moveAngle = ship.faceAngle;
//increase the X velocity when the up arrow is pressed...
ship.velocityX += Math.sin(Math.toRadians(ship.moveAngle))
* Config.SHIP_ACCELERATION;
//increase the Y velocity when the up arrow is pressed...
ship.velocityY += -Math .cos(Math.toRadians(ship.moveAngle))
* Config.SHIP_ACCELERATION;
}
if(e.code == KeyCode.VK_DOWN){
ship.moveAngle = ship.faceAngle;
//decrease the X velocity when the up arrow is pressed...
ship.velocityX += Math.sin(Math.toRadians(ship.moveAngle))
* -Config.SHIP_ACCELERATION;
//decrease the Y velocity when the up arrow is pressed...
ship.velocityY += -Math .cos(Math.toRadians(ship.moveAngle))
* -Config.SHIP_ACCELERATION;
}
}
onKeyReleased:function(e:KeyEvent):Void{
if(e.code == KeyCode.VK_LEFT){
turnLeft = false;
}
if(e.code == KeyCode.VK_RIGHT){
turnRight = false;
}
}
}
override public function create():Node{
return gameBoard;
}
def gameLoop:Timeline = Timeline{
repeatCount: Timeline.INDEFINITE
keyFrames: [
KeyFrame{
time: Config.REFRESH_RATE
action: function(){
gameUpdate();
}
}
]
}
public function startGame():Void{
txtInfo.visible = false;
gameLoop.play();
}
public function gameUpdate():Void{
updateShip();
}
public function updateShip():Void{
if(turnLeft){
ship.faceAngle -=Config.SHIP_ROTATION_VELOCITY;
}
if(turnRight){
ship.faceAngle +=Config.SHIP_ROTATION_VELOCITY;
}
//update the ship's position...
ship.posX += ship.velocityX;
ship.posY += ship.velocityY;
//handle wrapping for when ship goes out of bounds...
if(ship.posX < 0 - ship.width/2 ) {
//ship has moved off the left side,
//so make it wrap to the right side...
ship.posX = Config.SCREEN_WIDTH;
}
if(ship.posX > Config.SCREEN_WIDTH + ship.width/2) {
//ship has moved off the right side,
//so make it wrap to the left side...
ship.posX = 0;
}
if(ship.posY < 0 - ship.height/2){
//ship has moved off the top of the scene,
//so make it wrap to the bottom...
ship.posY = Config.SCREEN_HEIGHT;
}
if(ship.posY > Config.SCREEN_HEIGHT + ship.height/2 )
{
//ship has moved off the bottom of the scene,
//so make it wrap to the top...
ship.posY = 0;
}
}
}
Tha's it! The code for controlling the movement of our ship is complete. To continue on to the next lesson, go to Programming Games in JavaFX - Part 4