Programming Games in JavaFX - Part 7
July 1st, 2009
Let's make our game more interesting by splitting the asteroids into smaller ones when they are hit by a bullet. We'll be overhauling some of the code we've already written in the Asteroid.fx class and in the Conainer.fx class.
Let's start with the Asteroid class. Rather than show you the changes in bold, I thought it might be easier to have you rebuild this class from the gound up. So I'll give you all the code for the new version and you can paste it over the previous version.
You'll note that we are adding 3 constants that will determine the size of the asteroid being created. So we'll also be adding two more images to our project (the links to them are provided after the code). You'll also notice that when an Asteroid is created (in the create() function), we will check the 'type' property to determine if we should return a 'normal', 'smaller', or 'smallest' asteroid.
Here's the new Asteroid.fx 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;
//Note that public constants are declared outside of the class
//in JavaFX
public def NORMAL:Integer = 1;
public def SMALLER:Integer = 2;
public def SMALLEST:Integer = 3;
public class Asteroid extends CustomNode {
public var type: Integer;
public var img: Image;
public var imgView: ImageView;
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;
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();
var tempImg: Image;
if(type == NORMAL){
tempImg = Image{
url: "{__DIR__}rock.png"
};
}else if (type == SMALLER){
tempImg = Image{
url: "{__DIR__}rock-smaller.png"
};
}else if (type == SMALLEST){
tempImg = Image{
url: "{__DIR__}rock-smallest.png"
};
}
this.img = tempImg;
this.imgView = ImageView{
image: img
x: bind offsetX
y: bind offsetY
transforms: Rotate {
angle: bind faceAngle
pivotX: bind posX
pivotY: bind posY
}
}
return imgView
}
}
If you'd like to use my images for the 'smaller' and 'smallest' asteroids, here they are: smaller asteroid, smallest asteroid. You can just paste them in the same place as all the class files in the project. But don't forget to include images for the asteroids in your project. You won't get an error, but you might see some other strange things.
Now we'll have to overhaul some of the code in our game engine, Container.fx. We'll create a new function called createAsteroid() which will take parameters to determine the position and the type of asteroid to create (NORMAL, SMALLER, or SMALLEST). This function will be used when we first initialize the sequence of asteroids, and when an asteroid is hit by a bullet (in the collisionDetection() function). If a NORMAL asteroid is hit we'll call the splitAsteroid() function, which will create two other asteroids. One of the new asteroids will be SMALLER, and the other will be a SMALLEST asteroid.
Here's the code for Container.fx (the changes are 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]){
//NOTE:replace all the code that was previously inside this
//for loop with what you see here...
//create random numbers to use for placing the asteroid
//on the screen...
var rand: Random = Random{};
var x: Number = Math.abs(rand.nextInt()
mod Config.SCREEN_WIDTH)+1;
var y: Number = Math.abs(rand.nextInt()
mod Config.SCREEN_HEIGHT)+1;
//create the asteroid...
var a = createAsteroid(x,y,Asteroid.NORMAL);
//insert it into our asteroid sequence...
insert a into asteroids;
//put it on the scene...
insert a before gameBoard.content[1];
}
}
function createAsteroid(x:Number,y:Number,type:Integer):Asteroid{
var rand: Random = Random{};
//get a random number between 0-360 to set the
//angle of movement of the asteroid...
var moveAngle = Math.abs(rand.nextInt() mod 360) + 1;
//get a random number to use as the velocity of the asteroid...
var randomVelocity: Number = Math.abs(rand.nextInt()
mod Config.ASTEROID_MAX_VELOCITY) + 1;
//create a random rotation increment between 1 and 8...
var rotation_increment = rand.nextInt() mod 8 + 1;
//declare and initialize a new asteroid...
var a: Asteroid = Asteroid{
type: type
posX: x
posY: y
moveAngle: moveAngle
velocityX: Math.sin(Math.toRadians(moveAngle))
* randomVelocity
velocityY: -Math.cos(Math.toRadians(moveAngle))
* randomVelocity
rotation_increment: rotation_increment
active: true;
}
return a;
}
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();
collisionDetection();
}
public function collisionDetection():Void{
//check for bullets colliding with asteriods...
for(b in bullets){
if(b.active){
//for each 'active' bullet, loop through the active
//asteriods and check for collision...
for(a in asteroids){
if(a.active){
if(a.contains(b.posX, b.posY)){
//we have a hit, so make the asteroid disappear...
a.visible = false;
a.active = false;
//make the bullet disappear...
b.active = false;
b.visible = false;
//if it's a NORMAL asteroid, then split it
//into two smaller asteroids...
if(a.type == Asteroid.NORMAL){
splitAsteroid(a.posX,a.posY);
}
}
}
}
}
}
//use a boolean flag to determine if active asteroids
//are remaining on the game board...
var asteroidsRemain:Boolean = false;
//check for ship colliding with asteroids...
for(a in asteroids){
if(a.active){
asteroidsRemain = true;
if(a.intersects(ship.offsetX, ship.offsetY, ship.width, ship.height)){
//the ship has collided with an asteroid,
//set reset it to the middle of the screen...
ship.posX = Config.SCREEN_WIDTH / 2;
ship.posY = Config.SCREEN_HEIGHT / 2;
ship.velocityX =0;
ship.velocityY=0;
}
}
}
//if asteroidsRemain is false then the game is over....
if(not asteroidsRemain){
startGame();
}
}
public function splitAsteroid(x:Number, y:Number):Void{
//create a SMALLER asteroid and a SMALLEST asteroid...
var a1: Asteroid = createAsteroid(x,y,Asteroid.SMALLER);
var a2: Asteroid = createAsteroid(x,y,Asteroid.SMALLEST);
//add each one to the asteroids sequence...
insert a1 into asteroids;
insert a2 into asteroids;
//add them to the scene...
insert a1 before gameBoard.content[1];
insert a2 before gameBoard.content[1];
}
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;
}
}
}
We're done! By now you should be able to modify the game in an infinite number of directions. I hope you enjoyed this tutorial. In the future I hope to add more tutorials that will make our game more interesting, such as adding sound and improving the collision detection.