Skip to content

Tag: openjfx

JavaFX, Defuse the Bomb

I continue to develop simple games demos to feel better the strengths and weakness of JavaFX for game development.

Preview:

Click to play via Java Web Start:

There’s a little JavaFX game demo where you have to transport a bomb to a defuse point without touching in the walls. I’m using the collision detection methods I described early in this post to detect when the bomb hits a wall and then explode or when a bomb is inside the defuse point and the game ends. As it’s only a demo, it’s just one single level, but adding more levels would be easy.

Basically we have this four images:


bomb.png


goal.png


floor.png


wall.png

The code is petty simple. A little bit more than 300 lines with even with all comments and declarations. I transform the bomb image into a draggable node, create a list of collidable nodes and a especial node, the goal. I check the collisions when the bomb is dragged by mouse, if it hits something, it blows up.

I use extensively the TimeLine class from the animation framework (javafx.animation) to create chained animations and even to control some game logic.

As I focused in the simplicity, I don’t declared any classes to after instantiate their objects. I just was using common classes from JavaFX and putting logic on ir throught event and binding to external variables.

import javafx.application.Frame;
import javafx.application.Stage;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.animation.Interpolator;
import javafx.scene.image.ImageView;
import javafx.scene.image.Image;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.geometry.Circle;
import javafx.scene.geometry.Rectangle;
import javafx.scene.geometry.Shape;
import javafx.scene.text.Text;
import javafx.scene.Font;
import javafx.scene.FontStyle;
import javafx.input.MouseEvent;

/* Fade variable modified in some animations and used in the fadescreen */
var fade = 0.0;

/* The Bomb */
var lock = false;
var tx = 0.0;
var ty = 0.0;
var bomb:Node = Group{
    opacity: bind bombfade;
    content: [
        ImageView {
            image: Image {
                url: "{__DIR__}/bomb.png"
            }
        },
        Circle {
            centerX: 45, centerY: 21, radius: 7, fill: Color.LIME
            opacity: bind led
        },
        Circle {
            centerX: 30, centerY: 30, fill: Color.WHITE
            radius: bind fireradius
        },
    ],
    var startX = 0.0;
    var startY = 0.0;
    translateX: bind tx
    translateY: bind ty

    onMousePressed: function( e: MouseEvent ):Void {
        if (lock) {return;}
        startX = e.getDragX() - tx;
        startY = e.getDragY() - ty;
    }

    onMouseDragged: function(e:MouseEvent):Void {
        if (lock) {return;}
        tx = e.getDragX() - startX;
        ty = e.getDragY() - startY;
        checkcollissions();
    }
}

/* Big rectangle that covers all the screen (bomb explosion or game end) */
var fadescreen = Rectangle {
    x: 0, y: 0, width: 640, height: 480, fill: Color.WHITE
    opacity: bind fade
}

/* The wood floor image for the scenario. */
var floor = ImageView {
    image: Image {
        url: "{__DIR__}/floor.png"
    }
}

/* The goal image where the bomb should be placed. */
var goal = ImageView {
    x: 470, y: 360
    image: Image {
        url: "{__DIR__}/goal.png"
    }
}

/* List of obstacles nodes that the bomb can collide with. */
var obstacles = [
    Rectangle { x: 120, y: 0, width: 100, height: 300, fill: Color.BLACK},
    Rectangle { x: 350, y: 200, width: 100, height: 300, fill: Color.BLACK},
    Rectangle { x: 370, y: 50, width: 50, height: 50, fill: Color.BLACK},
    Rectangle {
        x: 250, y: 120, translateX: bind move, width: 100, height: 50
        fill: Color.BLACK
    },
];

/* Visible representations of obstacles */
var wallimage = Image {
    url: "{__DIR__}/wall.png"
}
var walls = for(obs in obstacles){
    ImageView {
        x: obs.x, y: obs.y, translateX: bind obs.translateX
        clip: obs, image: wallimage
    }
}

/* Animation for a blinking green led */
var led = 0.0;
var bombclock = Timeline {
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
    keyFrames : [
        KeyFrame {
            time : 0s
            values : led => 0.0 tween Interpolator.LINEAR
        },
        KeyFrame {
            time : 1s
            values : led => 1.0 tween Interpolator.LINEAR
        }
    ]
}

/* Animation for the bomb explosion and game reset */
var fireradius = 0.0;
var explosion:Timeline = Timeline {
    repeatCount: 1
    keyFrames : [
        KeyFrame {
            time : 0s
            values : [
                fireradius => 0.0,
                fade => 0.0
            ]
        },
        KeyFrame {
            time : 2s
            values : [
                fireradius => 200.0 tween Interpolator.LINEAR,
                fade => 1.0 tween Interpolator.LINEAR
            ]
            action: gamereset
        },
        KeyFrame {
            time : 3s
            values: fade => 0.0 tween Interpolator.LINEAR
        },
    ]
}

/* Reset variables for initial values */
function gamereset(){
    lock = false;
    fireradius = 0.0;
    tx = 0.0;
    ty = 0.0;
    bombfade = 1.0;

    moveblock.start();
    specialcollison.start();
    bombclock.start();
}

/* Animation when the bomb reaches the goal. Bomb disapear. */
var bombfade = 1.0;
var bomdisapear = Timeline {
    repeatCount: 1
    keyFrames : [
        KeyFrame {
            time : 1s
            values: [
                        bombfade => 0.0 tween Interpolator.EASEBOTH,
                        fade => 0.0
            ]
        },
        KeyFrame {
            time : 2s
            values:
                    fade => 1.0 tween Interpolator.LINEAR;
            action: gamereset
        },
        KeyFrame {
            time : 3s
            values:
                    fade => 0.0 tween Interpolator.LINEAR;
        },
    ]
}

/* Animation for a moving block. */
var move = 0.0;
var moveblock = Timeline {
    repeatCount: Timeline.INDEFINITE
    autoReverse: true
    keyFrames : [
        KeyFrame {
            time : 0s
            values :
                    move => 0.0
        },
        KeyFrame {
            time : 3s
            values :
                    move => 200.0 tween Interpolator.EASEBOTH
        },
    ]
}

/* Check and handle possible collisions. */
function checkcollissions(): Void {
    if(checkobstacles()){
        lock = true;
        specialcollison.stop();
        moveblock.stop();
        explosion.start();
    }

    if (insidenode(bomb,goal)) {
        lock = true;
        moveblock.stop();
        bomdisapear.start();
    }
}

/* There was a bug, when the bomb is stopped, not been gragged, in front of
the moving block, it could pass through it because checkcollissions() was
only called on mouse moving. This make sure checking this special case. */
var specialcollison:Timeline = Timeline {
    repeatCount: Timeline.INDEFINITE
    keyFrames : [
        KeyFrame {
            time : 1s/5
            action: function(){
                if(hitnode(obstacles[sizeof obstacles-1], bomb)){
                    lock = true;
                    moveblock.stop();
                    explosion.start();
                    specialcollison.stop();
                }
            }
        }
    ]
}

/*
* The next four functions are for collision detection.
* @See http://silveiraneto.net/2008/10/30/javafx-rectangular-collision-detection/
*/

/*
 * Check collision between two rectangles.
 */
function collission(ax, ay, bx, by, cx, cy, dx, dy): Boolean {
    return not ((ax > dx)or(bx < cx)or(ay > dy)or(by < cy));
}

/*
 * Check if the first rectangle are inside the second.
 */
function inside (ax, ay, bx, by, cx, cy, dx, dy):Boolean{
    return ((ax > cx) and (bx < dx) and (ay > cy) and (by < dy));
}

function hitnode(a: Node, b:Node): Boolean {
    return (collission(
        a.getBoundsX(), a.getBoundsY(),
        a.getBoundsX() + a.getWidth(), a.getBoundsY() + a.getHeight(),
        b.getBoundsX(), b.getBoundsY(),
        b.getBoundsX() + b.getWidth(), b.getBoundsY() + b.getHeight()
    ));
}

function insidenode(a:Node,b:Node):Boolean {
    return (inside(
        a.getBoundsX(), a.getBoundsY(),
        a.getBoundsX() + a.getWidth(), a.getBoundsY() + a.getHeight(),
        b.getBoundsX(), b.getBoundsY(),
        b.getBoundsX() + b.getWidth(), b.getBoundsY() + b.getHeight()
    ));
}

/*
* Check collision of bomb against each obstacle.
*/
function checkobstacles(): Boolean{
    for(obst in obstacles){
        if (hitnode(obst, bomb)){
            return true;
        }
    }
    return false;
}

/* Pack visual game elements in a Frame's Stage, unresizable. */
Frame {
    title: "Defuse the Bomb"
    width: 640
    height: 480
    resizable: false
    closeAction: function() {
        java.lang.System.exit( 0 );
    }
    visible: true

    stage: Stage {
        content: bind [floor, goal, walls, bomb, fadescreen]
    }
}

/* Call gamereset to set initial values and start animations */
gamereset();

Downloads:

JavaFX, rectangular collision detection

In a game I wrote some years ago we handled simple rectangular collisions. Given the points:

We did:

// returning 0 means collision
int collision(int ax, int ay, int bx, int by, int cx, int cy, int dx, int dy){
	return ((ax > dx)||(bx < cx)||(ay > dy)||(by < cy));
}

I'll show here a little demo about how implement simple rectangular collisions on JavaFX.
First I created a movable rectangle using the same idea of draggable nodes I already had posted before.

import javafx.input.MouseEvent;
import javafx.scene.geometry.Rectangle;

public class MovableRectangle extends Rectangle {
    private attribute startX = 0.0;
    private attribute startY = 0.0;

    public attribute onMove = function(e:MouseEvent):Void {}

    override attribute onMousePressed = function(e:MouseEvent):Void {
        startX = e.getDragX()-translateX;
        startY = e.getDragY()-translateY;
        onMove(e);
    }

    override attribute onMouseDragged = function(e:MouseEvent):Void {
        translateX = e.getDragX()-startX;
        translateY = e.getDragY()-startY;
        onMove(e);
    }
}

In the main code I some important things:

  • colide, a color that represents the collision effect. White means no collision and gray means collision.
  • rec1 and rec2, the two rectangles that can collide.
  • checkcollision() the function that checks and handles a possible collision.

Here is the main code:

import javafx.application.Frame;
import javafx.application.Stage;
import javafx.scene.geometry.Rectangle;
import javafx.scene.paint.Color;
import javafx.input.MouseEvent;

var colide = Color.WHITE;

function checkcollision():Void {
    if (
        (rec1.getBoundsX() > rec2.getBoundsX() + rec2.getWidth()) or
        (rec1.getBoundsX() + rec1.getWidth() < rec2.getBoundsX()) or 
        (rec1.getBoundsY() > rec2.getBoundsY() + rec2.getHeight()) or 
        (rec1.getBoundsY() + rec1.getHeight() < rec2.getBoundsY())
    ) {
        colide = Color.WHITE
    } else {
        colide = Color.LIGHTGRAY
    }
}

var rec1: MovableRectangle = MovableRectangle {
    x: 10, y: 10, width: 50, height: 60, fill: Color.RED
    onMove: function(e:MouseEvent):Void {
        checkcollision()
    }
}

var rec2: MovableRectangle = MovableRectangle {
    x: 100, y: 100, width: 70, height: 30, fill: Color.BLUE
    onMove: function(MouseEvent):Void {
        checkcollision()
    }
}
Frame {
    title: "Rectangular Collisions", width: 300, height: 300
    closeAction: function() { 
        java.lang.System.exit( 0 ); 
    }
    visible: true

    stage: Stage {
        fill: bind colide
        content: [rec1, rec2]
    }
}

Try it via Java Web Start:

Java Web Start

Some considerations:

  • You can use rectangular collisions to create bounding boxes to handle collisions in more complex shapes or sprites. Is a common approach in 2d games to avoid more expensive calculations.
  • There are space for optimizations.
  • In this case I'm using only two objects. Some problems raises when I have N objects to handle.

More generally, we can code:

function collission(ax, ay, bx, by, cx, cy, dx, dy): Boolean {
    return not ((ax > dx)or(bx < cx)or(ay > dy)or(by < cy));
}

function hitnode(a: Node, b:Node): Boolean{
    return (collission(
        a.getBoundsX(), a.getBoundsY(),
        a.getBoundsX() + a.getWidth(), a.getBoundsY() + a.getHeight(),
        b.getX(), b.getY(),
        b.getX() + b.getWidth(), b.getY() + b.getHeight()
    ));
}

This way we can pass just two bounding boxes to hitnode and easily check collision of a node against a list of bounding boxes nodes.
Using the same approach I also wrote this function to test if a Node is inside another Node:

function inside (ax, ay, bx, by, cx, cy, dx, dy):Boolean{
    return ((ax > cx) and (bx < dx) and (ay > cy) and (by < dy));
}

function insidenode(a:Node,b:Node):Boolean{
    return (inside(
        a.getBoundsX(), a.getBoundsY(),
        a.getBoundsX() + a.getWidth(), a.getBoundsY() + a.getHeight(),
        b.getBoundsX(), b.getBoundsY(),
        b.getBoundsX() + b.getWidth(), b.getBoundsY() + b.getHeight()
    ));
}

Soon I'll post game examples showing how to use this method and others collission detection methods.

Downloads:

JavaFX: Color picker

An simple color picker that can be also used as a gadget.

import javafx.ui.*;
import javafx.ui.canvas.*;

var colors = [red:Color, orange:Color, yellow:Color, green:Color,
     cyan:Color,blue:Color, magenta:Color, gray:Color];

var chosenColor: Paint;
chosenColor = black:Color;

var x = 120;
var y = 70;

Canvas{
    content: Group{
        transform: bind translate(x,y)
        content: [Star{
            points: sizeof colors
            rin: 30
            rout: 50
            fill: bind chosenColor
            onMouseDragged: operation(e) {
                x += e.localDragTranslation.x;
                y += e.localDragTranslation.y;
            }
        },
        foreach (i in [1..sizeof colors]) Circle {
            var: self
            transform: [rotate(i*360/sizeof colors,0,0), translate(50,0)]
            radius: 10
            fill: colors[i%sizeof colors]
            onMouseClicked: operation (e){
                chosenColor = self.fill;
            }
        }]
    }
}

Draggable and Growable Ball in JavaFX

Two simple JavaFX code handling onMouseDragged event.

import javafx.ui.*;
import javafx.ui.canvas.*;

Canvas {
    content: Circle {
        var x = 50
        var y = 50
        transform: bind translate(x, y)
        radius: 30
        fill: red
        onMouseDragged: operation(e) {
                x += e.localDragTranslation.x;
                y += e.localDragTranslation.y;

        }
    }
}

import javafx.ui.*;
import javafx.ui.canvas.*;

Canvas {
    content: Circle {
        var x = 50
        var y = 50
        var radius = 30
        transform: bind translate(x, y)
        radius: bind radius
        fill: red
        onMouseDragged: operation(e) {
            if (e.button == 1){
                x += e.localDragTranslation.x;
                y += e.localDragTranslation.y;
            }
            if (e.button == 3) {
                radius += e.localDragTranslation.x;
            }
        }
    }
}

import javafx.ui.*;
import javafx.ui.canvas.*;

Canvas {
    content: [
    Rect {x: 50, y: 50, width: 50, height: 50, fill: orange },
    Circle {
        var x = 50
        var y = 50
        var radius = 30
        var color = red:Color
        transform: bind translate(x, y)
        radius: bind radius
        fill: bind color
        onMouseDragged: operation(e) {
            if (e.button == 1){
                x += e.localDragTranslation.x;
                y += e.localDragTranslation.y;
            }
            if (e.button == 3) {
                radius += e.localDragTranslation.x;
            }
        }
        onMousePressed: operation(e){
            color = Color {blue: 0.0, green: 0.0, red: 1.0, opacity: 0.5};
        }
        onMouseReleased: operation(e){
            color = red:Color;
        }
    }]
}

You can test this examples with thhe JavaFX Pad or using Netbeans with the JavaFX Plugin.