We now have a player object we can control and we have enemy objects moving around the playing field. What we are still missing is code to deal with enemy player collisions and score keeping. This article will deal with calculating collisions.
Calculating Collisions
Once again it is in the Timeline responsible for managing our play cycle where we need to check for collisions.
var timeline: Timeline = Timeline {
...
keyFrames : [
KeyFrame {
...
action: function() {
// move enemy and player to new positions
...
// checkCollision
for (enemy in enemies) {
if (checkCircleCollision(player.translateX,player.translateY,player.radius,enemy.translateX,enemy.translateY,10.0)) {
gameOver();
}
}
}
}
]
}
// Function called after collision
function gameOver():Void {
timeline.stop();
}
function checkCircleCollision(c1X:Number,c1Y:Number,c1R:Number,c2X:Number,c2Y:Number,c2R:Number):Boolean {
var distanceSquared = ((c1X - c2X) * (c1X - c2X)) + ((c1Y - c2Y) * (c1Y - c2Y));
var radiiSquared = (c1R + c2R) * (c1R + c2R);
if (radiiSquared > distanceSquared) {
return true
}
return false
}
After we have moved our enemies and player to new positions we need to check in each game play cycle, if they have collided. This is done in the function checkCircleCollision(…). The parameters of this function are the x and y coordinates and the radius of the player and of one enemy circle. More details about the mathematics of calculating such a collision can be found here: http://gpwiki.org/index.php/C:Collision_detection_between_two_circles.
If the player collides with any of the enemy objects the method gameOver() is called and gameplay is stopped by halting the Timeline object responsible for our game play.
Restarting the Game after Game Over
Of course we would like to let players of our game restart the game without reloading or restarting our JavaFX application. These are the necessary code changes:
def MODE_GAME_OVER: String = "GAME_OVER";
def MODE_RUNNING: String = "RUNNING";
var mode:String = MODE_RUNNING;
var gameOverScreen: Group = Group{
visible:false
var r:Rectangle = Rectangle {
arcWidth: playingField.arcWidth arcHeight: playingField.arcHeight
width: playingField.width, height: playingField.height
fill: Color.ROSYBROWN
stroke: Color.DARKRED
strokeWidth: playingField.strokeWidth
}
content: [
r,
Text {
font : Font {
size: 24
}
x: 10, y: 50
content: "Game Over"
},
Text {
font : Font {
size: 20
}
x: 10, y: 80
content: "(press Enter to restart)"
}
]
}
Stage {
...
scene: Scene {
...
content: Group {
...
content: [
playingField,
player,
enemies,
gameOverScreen
]
onKeyPressed : function (e: KeyEvent){
if (mode==MODE_GAME_OVER) {
if (e.code == KeyCode.VK_ENTER) {
initGame();
timeline.playFromStart();
}
}
}
...
}
// Function called after collision
function gameOver():Void {
timeline.stop();
mode = MODE_GAME_OVER;
gameOverScreen.visible = true;
}
function initGame():Void {
initActors();
mode = MODE_RUNNING;
gameOverScreen.visible = false
}
// Function is used to initialize the actors i.e. player and enemies
// on the playing field
function initActors() {
player.translateX = 250;
player.translateY = 250;
for (enemy in enemies) {
enemy.translateX = randomEnemyInitPosition();
enemy.translateY = randomEnemyInitPosition();
}
}
This code is easy to understand. We introduce a variable called “mode” and define two constants MODE_GAME_OVER and MODE_RUNNING. These are the two states that are possible in our game.
We then define a Game Over screen which is a simple rectangle which is the same size as our playing field and whose visibility property is initially set to “false”. Remember that the game starts running, so we do not want to see the game over screen.
We then add the gameOverScreen object to the visible elements of our Scene. Notice how it gets added as the last object. This is necessary because once the gameOverScreen becomes visible it should appear on top of everything else.
Notice how in the gameOver() function we now set the mode to MODE_GAME_OVER.
In our KeyListener we now check if the mode of our game is MODE_GAME_OVER if so and the ENTER key has been pressed we reinitialize the game by calling the init() function. The init() function resets the player and enemy positions, changes the state or mode of our game to MODE_RUNNING and hides the game over screen. Finally our main game loop is restarted by calling timeline.playFromStart().
That concludes this part of our tutorial. In the next part we will add all of the scoring functionality. Below is a listing of the complete code so far:
Main.fx
/*
* Main.fx
*
* Created on 15.10.2009, 17:36:19
*/
package myballgame;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.Group;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Circle;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
/**
* @author Alexander Gnodtke
*/
def PLAYER_BALL_STEP = 4;
def MODE_GAME_OVER: String = "GAME_OVER";
def MODE_RUNNING: String = "RUNNING";
var mode:String = MODE_RUNNING;
var moveUp = false;
var moveDown = false;
var moveLeft = false;
var moveRight = false;
var playingField: Rectangle = Rectangle {
arcWidth: 20 arcHeight: 20
width: 280, height: 380
fill: Color.ANTIQUEWHITE
stroke: Color.DARKORANGE
strokeWidth: 3
}
var gameOverScreen: Group = Group{
visible:false
var r:Rectangle = Rectangle {
arcWidth: playingField.arcWidth arcHeight: playingField.arcHeight
width: playingField.width, height: playingField.height
fill: Color.ROSYBROWN
stroke: Color.DARKRED
strokeWidth: playingField.strokeWidth
}
content: [
r,
Text {
font : Font {
size: 24
}
x: 10, y: 50
content: "Game Over"
},
Text {
font : Font {
size: 20
}
x: 10, y: 80
content: "(press Enter to restart)"
}
]
}
var player: Circle = Circle {
translateX: 250, translateY:250
radius: 10
fill: Color.BLACK
}
var enemies : Enemy[] = for (i in [0..3]) {
var enemy:Enemy = Enemy {
translateX : randomEnemyInitPosition(), translateY:randomEnemyInitPosition()
}
enemy
};
var timeline: Timeline = Timeline {
repeatCount: Timeline.INDEFINITE
keyFrames : [
KeyFrame {
time : 30ms
canSkip: false
action: function() {
// Enemy
for (enemy in enemies)
enemy.calcPosition(playingField.boundsInLocal.minX,playingField.boundsInLocal.maxX,playingField.boundsInLocal.minY,playingField.boundsInLocal.maxY);
// Player
if (moveRight) {
var newX = player.translateX + PLAYER_BALL_STEP;
if (newX < playingField.boundsInLocal.maxX - player.radius - playingField.strokeWidth) {
player.translateX = newX
}
}
if (moveLeft) {
var newX = player.translateX - PLAYER_BALL_STEP;
if (newX > playingField.boundsInLocal.minX + player.radius + playingField.strokeWidth) {
player.translateX = newX
}
}
if (moveUp) {
var newY= player.translateY - PLAYER_BALL_STEP;
if (newY > playingField.boundsInLocal.minY + player.radius + playingField.strokeWidth) {
player.translateY = newY
}
}
if (moveDown) {
var newY= player.translateY + PLAYER_BALL_STEP;
if (newY < playingField.boundsInLocal.maxY - player.radius - playingField.strokeWidth) {
player.translateY = newY
}
}
// checkCollision
for (enemy in enemies) {
if (checkCircleCollision(player.translateX,player.translateY,player.radius,enemy.translateX,enemy.translateY,10.0)) {
gameOver();
}
}
}
}
]
}
// Function called after collision
function gameOver():Void {
timeline.stop();
mode = MODE_GAME_OVER;
gameOverScreen.visible = true;
}
function initGame():Void {
initActors();
mode = MODE_RUNNING;
gameOverScreen.visible = false
}
// Function is used to initialize the actors i.e. player and enemies
// on the playing field
function initActors() {
player.translateX = 250;
player.translateY = 250;
for (enemy in enemies) {
enemy.translateX = randomEnemyInitPosition();
enemy.translateY = randomEnemyInitPosition();
}
}
// http://gpwiki.org/index.php/C:Collision_detection_between_two_circles
function checkCircleCollision(c1X:Number,c1Y:Number,c1R:Number,c2X:Number,c2Y:Number,c2R:Number):Boolean {
var distanceSquared = ((c1X - c2X) * (c1X - c2X)) + ((c1Y - c2Y) * (c1Y - c2Y));
var radiiSquared = (c1R + c2R) * (c1R + c2R);
if (radiiSquared > distanceSquared) {
return true
}
return false
}
function randomEnemyInitPosition():Integer {
var r = javafx.util.Math.random();
var r2 = javafx.util.Math.random();
((r * 100)+(r2*100)) as Integer
}
Stage {
title: "Ballgame"
scene: Scene {
fill: Color.CHOCOLATE;
width: 430, height: 400
content: Group {
focusTraversable: true
translateX: 10, translateY:10
content: [
playingField,
player,
enemies,
gameOverScreen
]
onKeyPressed : function (e: KeyEvent){
if (mode==MODE_GAME_OVER) {
if (e.code == KeyCode.VK_ENTER) {
initGame();
timeline.playFromStart();
}
}
if (e.code == KeyCode.VK_LEFT) {
moveLeft = true;
}
if (e.code == KeyCode.VK_RIGHT) {
moveRight = true;
}
if (e.code == KeyCode.VK_UP) {
moveUp = true;
}
if (e.code == KeyCode.VK_DOWN) {
moveDown = true;
}
}
onKeyReleased : function (e: KeyEvent){
if (e.code == KeyCode.VK_LEFT) {
moveLeft = false;
}
if (e.code == KeyCode.VK_RIGHT) {
moveRight = false;
}
if (e.code == KeyCode.VK_UP) {
moveUp = false;
}
if (e.code == KeyCode.VK_DOWN) {
moveDown = false;
}
}
}
}
}
// Start game
timeline.playFromStart();
Enemy.fx
/*
* Enemy.fx
*
* Created on 03.10.2009, 13:59:33
*/
package myballgame;
import javafx.scene.CustomNode;
import javafx.scene.Node;
import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;
/**
* @author Alexander Gnodtke
*/
public static def DEFAULT_FILL = Color.CRIMSON;
public class Enemy extends CustomNode {
var radius = 10;
var moveUp = false;
var moveLeft = false;
var speedX = 0;
var speedY = 0;
var enemy:Circle;
override function create():Node {
// Enemy Speed
speedX = calcSpeed();
speedY = calcSpeed();
// Enemy initial Direction
moveUp = calcRandomBoolean();
moveLeft = calcRandomBoolean();
// Enemy figure
enemy = Circle {
radius: radius
fill: Color.CRIMSON
}
return enemy
}
function calcSpeed():Integer {
var r = javafx.util.Math.random();
if (r>0.66){
return 3
} else if (r<0.33) {
return 2
} else {
return 1
}
}
function calcRandomBoolean():Boolean {
if (javafx.util.Math.random()>0.5)
return true
else
return false
}
public function calcPosition(xMin:Integer,xMax:Integer,yMin:Integer,yMax:Integer) {
if (moveUp) {
var newY= translateY - speedY;
if (newY > yMin + radius)
translateY = newY
else
moveUp = false
} else {
var newY= translateY + speedY;
if (newY < yMax - radius)
translateY = newY
else
moveUp = true
}
if (moveLeft) {
var newX = translateX - speedX;
if (newX > xMin + radius)
translateX = newX
else
moveLeft = false
} else {
var newX = translateX + speedX;
if (newX