Programming Games in JavaFX - Part 6
June 30th, 2009
By the end of this lesson we'll have a complete game, although it will be crude and there will be plenty of room for refinement. In this step we'll add collision detection so that we can determine when a bullet has hit an asteroid, and when the ship collides with an asteroid. The only changes that we'll make will be to our game engine, Container.fx. In the gameUpdate() function, after all the 'update' functions have been called, add a call to a function named collisionDetection(). The collisionDetection() function will loop through each active bullet to see if it's X and Y position falls within the boundaries of any of the active asteroids. If so, we'll remove both the bullet and the asteroid from the game board by setting it's visible and active properties to false. The next step is to loop through all the active asteroids to see if any of their boundaries intersect with the boundaries of the ship. There is a subtle difference in how we check for asteroid collisions with bullets, and how we check for asteriod collisions with the ship. Because the bullet is so small, we'll just check to see if its position (a single point on the screen that is determined by it's posX and posY variables) is contained within the boundaries of each asteroid. But because the ship is much bigger than a bullet, we'll have to see if it's boundaries intersect with the boundaries of each active asteroid. You'll notice that this collision detection is far from perfect because the boundary of the ship and each asteroid is a rectangle, while neither the ship nor the asteroids are rectangular in shape. In another tutorial, we'll go over some ways you can improve the collision detection code. But for now, here are the updates to Container.fx (highlighted in bold as usual, you'll have to scroll down the screen a bit before you get the the new code):
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 yet...
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();
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;
}
}
}
}
}
//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,
//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){
//no asteroids remain, so start the game over...
startGame();
}
}
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;
}
}
}
Although we have a fully functional game, and by now you've learned the basics of game programming with JavaFX, our game leaves something to be desired. So to make it more interesting I'll do a bonus lesson in which we'll overhaul some of the code so that when an asteroid is hit by a bullet, it will split into two smaller asteroids. So if you're interested, continue to the first bonus lesson, Programming Games in JavaFX - Part 7.