Programming Games in JavaFX - Part 5
June 30th, 2009
Now that our ship can shoot bullets, we'll need to give it some targets. We'll be adding a class called Asteroid.fx. You'll notice that it is very similar to both the Ship and Bullet classes. One major difference is that we will use a Timeline inside of the Asteroid class so that our asteroids will continuously rotate. We'll also use a lot of randomly generated numbers to set the properties of the asteroids, but this code will be put into our game engine, Container.fx, when it creates the asteroids.
Let's start with our updates to Config.fx (as usual, new code is highlighted in bold):
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;
public def BULLET_VELOCITY=15;
public def BULLET_COUNT=10;
public def ASTEROID_COUNT = 10;
public def ASTEROID_MAX_VELOCITY = 5;
We'll limit the amount of asteroids to 10 and make sure that they move quite a bit slower than the bullets.
Now we'll create our new Asteroid.fx class. Here's a link to the image that we'll use for for rendering it, rock.png. You can simply paste it right next to all the class files in the project. Here's the code for the Asteroid class:
package blasteroids;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.Node;
import javafx.scene.transform.Rotate;
import blasteroids.Config;
public class Asteroid 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;
public var rotation_increment: Number = 0;
public var active:Boolean = false;
var img = Image{
url: "{__DIR__}rock.png"
preserveRatio: true
}
var imgView = ImageView{
image: img
x: bind offsetX
y: bind offsetY
transforms: Rotate {
angle: bind faceAngle
pivotX: bind posX
pivotY: bind posY
}
}
def timeline = Timeline{
repeatCount: Timeline.INDEFINITE
keyFrames: [
KeyFrame{
time: Config.REFRESH_RATE
action: function():Void{
faceAngle += rotation_increment
}
}
]
}
override public function create():Node{
timeline.play();
return imgView
}
}
Notice that we start the timeline by calling the play() function from inside the create() function.
Next, in our game engine, Container.fx, we'll add some code to create a sequence of asteroids and to control the movement of each active asteroid. Here's the code with updates highlighted in bold:
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;
import java.util.Random;
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
}
var bullets: Bullet[];
var currentBullet:Integer = 0;
function initializeBullets():Void{
//make sure the bullets sequence is empty before reloading...
delete bullets;
for(i in [1..Config.BULLET_COUNT]){
var b=Bullet{};
b.active = false;
b.visible = false;
//put the bullet onto the gameBoard,
//even though it's not active yete...
insert b into gameBoard.content;
//put the bullet into the bullets sequence...
insert b into bullets;
}
}
var asteroids: Asteroid[];
function initializeAsteroids():Void{
for(i in [1..Config.ASTEROID_COUNT]){
var a = Asteroid{
}
insert a into asteroids;
var rand: Random = Random{
};
//generate random staring x and y positions for the asteroid...
a.posX = Math.abs(rand.nextInt() mod Config.SCREEN_WIDTH)+1;
a.posY = Math.abs(rand.nextInt() mod Config.SCREEN_HEIGHT)+1;
//generate a random angle of movement for the asteroid
//(between 0-360 degreess)...
a.moveAngle = Math.abs(rand.nextInt() mod 360) + 1;
//generate a random number between 1 and 8 that
//will determine how fast the asteroid spins...
a.rotation_increment = rand.nextInt() mod 8 + 1;
//set the velocity of the asteroid...
//but first generate a random velocity between 0 and the
//max velocity that's specified in the config file...
var randomVelocity:Number = Math.abs(rand.nextInt()
mod Config.ASTEROID_MAX_VELOCITY) + 1;
a.velocityX = Math.sin(Math.toRadians(a.moveAngle))
* randomVelocity;
a.velocityY = -Math .cos(Math.toRadians(a.moveAngle))
* randomVelocity;
//set the asteroid to be active...
a.active = true;
//insert the asteroid into the group
//(between the background and the ship)...
insert a before gameBoard.content[1];
}
}
public var txtInfo:Text = Text{
x:10
y:20
wrappingWidth: Config.SCREEN_WIDTH - 20
font: Font { size: 18 }
fill: Color.WHITE
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 the space key to fire bullets"
"\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){
ship.moveAngle = ship.faceAngle;
ship.velocityX += Math.sin(Math.toRadians(ship.moveAngle)) * Config.SHIP_ACCELERATION;
ship.velocityY += -Math .cos(Math.toRadians(ship.moveAngle)) * Config.SHIP_ACCELERATION;
}
if(e.code == KeyCode.VK_DOWN){
ship.moveAngle = ship.faceAngle;
ship.velocityX += Math.sin(Math.toRadians(ship.moveAngle)) * -Config.SHIP_ACCELERATION;
ship.velocityY += -Math .cos(Math.toRadians(ship.moveAngle)) * -Config.SHIP_ACCELERATION;
}
if(e.code == KeyCode.VK_SPACE){
//launch a bullet...
currentBullet++;
//if we reach the end of the bullets sequence, then
//we start recycling bullets...
if(currentBullet > Config.BULLET_COUNT - 1){
currentBullet = 0
}
//get a handle on the current bullet...
var b = bullets[currentBullet];
//make the bullet active...
b.active = true;
//make sure the bullet is visible...
b.visible = true;
//set the bullet's position to the position of the ship...
b.posX = ship.posX;
b.posY = ship.posY;
//set the bullet's move angle equal to the face angle of the ship...
b.moveAngle = ship.faceAngle;
//set the velocity of the bullet...
b.velocityX = Math.sin(Math.toRadians(b.moveAngle)) * Config.BULLET_VELOCITY;
b.velocityY = -Math .cos(Math.toRadians(b.moveAngle)) * Config.BULLET_VELOCITY;
}
}
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;
initializeBullets();
initializeAsteroids();
gameLoop.play();
}
public function gameUpdate():Void{
updateShip();
updateBullets();
updateAsteroids();
}
public function updateBullets():Void{
for(b in bullets){
if(b.active){
//update the active bullet's position...
b.posX += b.velocityX;
b.posY += b.velocityY;
//if the bullet goes off the screen, set it to inactive...
if(b.posX < 0 or b.posY > Config.SCREEN_WIDTH){
b.active = false;
}
if(b.posY < 0 or b.posY > Config.SCREEN_HEIGHT){
b.active = false;
}
}
}
}
public function updateAsteroids():Void{
for(a in asteroids){
if(a.active){
//update the active asteroid's position...
a.posX += a.velocityX;
a.posY += a.velocityY;
//handle wrapping for when the asteroid goes out of bounds...
if(a.posX < 0 - a.width / 2 ) {
a.posX = Config.SCREEN_WIDTH;
}
if(a.posX > Config.SCREEN_WIDTH + a.width / 2) {
a.posX = 0;
}
if(a.posY < 0 - a.height / 2){
a.posY = Config.SCREEN_HEIGHT;
}
if(a.posY > Config.SCREEN_HEIGHT + a.height / 2 )
{
a.posY = 0;
}
}
}
}
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 bottom of the scene,
//so make it wrap to the top...
ship.posY = Config.SCREEN_HEIGHT;
}
if(ship.posY > Config.SCREEN_HEIGHT + ship.height/2 )
{
//ship has moved off the top of the scene,
//so make it wrap to the bottom...
ship.posY = 0;
}
}
}
Now, when you run the game, you'll see bullets flying and asteroids spinning around all over the game board, but to add collision detection to the game, continue to the next lesson Programming Games in JavaFX - Part 6.