Skip to content

Tag: game

JavaFX, Defuse the Bomb

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

Preview:

[youtube]hR2LiKiBUgE[/youtube]

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

[youtube]NRwRTHPGg6M[/youtube]

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, Side Scrolling Gaming

I started to make several small JavaFX game demos. I’m doing that to fell where JavaFX is good to make this sort of game and what patterns would be frequently needed to implement, where I will place a little framework for fast development of simple casual games. What I’m calling now just ‘GameFX’. My first experiment was to creating a side scrolling animation that’s is usefull to create the parallax effect in side scrolling games. For that I created the class Slidding. You create an Slidding with a set of nodes and they will slide from right to left and when a node reaches the left side it back to the right side.

Example:

Slidding {
    content: [
       Circle {
           centerX: 100, centerY: 100, radius: 40
           fill: Color.RED
       },
       Circle {
           centerX: 300, centerY: 100, radius: 40
           fill: Color.BLUE
       }
    ]
    clock: 0.05s
 }

That produces:

You create a Slidding with a list of Nodes at content, a clock (that will determine the speed of that animation) and a width. If you don’t provide a width, the slidding will do the best effort to determine one. You can use this approach to create more complex scenarios, using more Slidding groups.

This is a example of that:

import javafx.application.*;
import javafx.animation.*;
import javafx.scene.geometry.*;
import javafx.scene.paint.*;
import javafx.scene.*;

import gamefx.Slidding;

var SCREENW = 500;
var SCREENH = 400;

/* the sky is a light blue rectangle */
var sky = Rectangle {
    width: SCREENW, height: SCREENH
    fill: LinearGradient {
        startX: 0.0 , startY: 0.0
        endX: 0.0, endY: 1.0
        proportional: true
        stops: [
            Stop { offset: 0.0 color: Color.LIGHTBLUE },
            Stop { offset: 0.7 color: Color.LIGHTYELLOW },
            Stop { offset: 1.0 color: Color.YELLOW }
        ]
    }
}

/* the ground is a olive rectangle */
var ground = Rectangle {
    translateY: 300
    width: 500, height: 100
    fill: LinearGradient {
        startX: 0.0 , startY: 0.0
        endX: 0.0, endY: 1.0
        proportional: true
        stops: [
            Stop { offset: 0.2 color: Color.OLIVE },
            Stop { offset: 1.0 color: Color.DARKOLIVEGREEN }
        ]
    }
}

/* a clod cloud is like an ellipse */
class Cloud extends Ellipse {
    override attribute radiusX = 50;
    override attribute radiusY = 25;
    override attribute fill = Color.WHITESMOKE;
    override attribute opacity = 0.5;
}

/* we create a slidding of clouds */
var clouds = Slidding {
    content: [
        Cloud{centerX: 100, centerY: 100},
        Cloud{centerX: 150, centerY:  20},
        Cloud{centerX: 220, centerY: 150},
        Cloud{centerX: 260, centerY: 200},
        Cloud{centerX: 310, centerY:  40},
        Cloud{centerX: 390, centerY: 150},
        Cloud{centerX: 450, centerY:  30},
        Cloud{centerX: 550, centerY: 100},
    ]
    clock: 0.2s
}

var SUNX = 100;
var SUNY = 300;
var rotation = 0;

/* the sun, with it's corona */
var sun = Group {
    rotate: bind rotation
    anchorX: SUNX, anchorY: SUNY
    content: [
        for (i in [0..11]) {
            Arc {
                centerX: SUNX, centerY: SUNY
                radiusX: 500, radiusY: 500
                startAngle: 2 * i * (360 / 24), length: 360 / 24
                type: ArcType.ROUND
                fill: Color.YELLOW
                opacity: 0.3
            }
        },
        Circle {
            centerX: SUNX, centerY: SUNY, radius: 60
            fill: Color.YELLOW
        },
    ]
}

/* animate the corona changing the it rotation angle */
var anim = Timeline {
    repeatCount: Timeline.INDEFINITE
    keyFrames : [
        KeyFrame {
            time : 0s
            values: rotation => 0.0 tween Interpolator.LINEAR
        },
        KeyFrame {
            time : 2s
            values: rotation => (360.0/12) tween Interpolator.LINEAR
        },
    ]
}
anim.start();

/* a tree is a simple polygon */
class Tree extends Polygon{
    public attribute x = 0;
    public attribute y = 0;
    override attribute points = [0,0, 10,30, -10,30];
    override attribute fill = Color.DARKOLIVEGREEN;
    init{
        translateX = x;
        translateY = y;
    }
}

/* a forest is a lot of trees */
var forest = Slidding{
    content: [
        Tree{x: 20, y: 320}, Tree{x: 80, y: 280}, Tree{x:120, y: 330},
        Tree{x:140, y: 280}, Tree{x:180, y: 310}, Tree{x:220, y: 320},
        Tree{x:260, y: 280}, Tree{x:280, y: 320}, Tree{x:300, y: 300},
        Tree{x:400, y: 320}, Tree{x:500, y: 280}, Tree{x:500, y: 320}
    ]
    clock: 0.1s
    width: SCREENW
}

Frame {
    title: "Side Scrolling"
    width: SCREENW
    height: SCREENH
    closeAction: function() {
        java.lang.System.exit( 0 );
    }
    visible: true

    stage: Stage {
        content: [sky, sun, clouds, ground, forest]
    }
}

Producing:

If you want to try these examples, place this Slidding implementation as Slidding.fx in a directory named gamefx, or grab here the NetBeans project.

package gamefx;

import javafx.scene.CustomNode;
import javafx.scene.Node;
import javafx.scene.Group;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;

/*
 * The slidding group of nodes for side scrolling animations.
 *
 * @example
 * Slidding {
 *    width: 300
 *    content: [
 *       Circle { centerX: 100, centerY: 100, radius: 40, fill: Color.RED },
 *       Circle { centerX: 200, centerY: 100, radius: 40, fill: Color.BLUE },
 *    ]
 *    clock: 0.05s
 * }
 */
public class Slidding extends CustomNode {
    public attribute content: Node[];
    public attribute clock = 0.1s;
    public attribute width: Number;

    public attribute autostart = true;
    public attribute cycle = true;

    public attribute anim = Timeline {
        repeatCount: Timeline.INDEFINITE
        keyFrames : [
            KeyFrame {
                time : clock
                action: function() {
                    for(node in content){
                        node.translateX--;
                        if (node.getX() + node.translateX + node.getWidth() <= 0){
                            if(cycle){
                                node.translateX = width - node.getX();
                            } else {
                                delete node from content;
                            }
                        }
                    }
                } // action
            } // keyframe
        ]
    } // timeline 

    public function create(): Node {
        // if width is not setted, we try to figure out
        if(width == 0) {
            for(node in content) {
                if(node.getX() + node.getWidth() > width) {
                    width = node.getX() + node.getWidth();
                }
            }
        }

        // normaly the slidding will start automaticaly
        if(autostart){
            anim.start();
        }

        // just a Group of Nodes
        return Group {
            content: content
        };
    }
}

Is not the final implementation but it’s a idea. Soon I’ll show a demo game I did using theses codes.

Java key listening example

This post continues a serie of posts I’m writing about 2D game development in Java.
A simple example of an JPanel that implements KeyListener (and a little trick) to handle KeyEvents to move a white square.

Java KeyListening Example

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class KeyPanel extends JPanel implements KeyListener{
    private int x=50,y=50;
    public KeyPanel() {
        JTextField textfield = new JTextField();
        textfield.addKeyListener(this);
        add(textfield);
        textfield.setPreferredSize(new Dimension(0,0));
    }

    public void keyTyped(KeyEvent e) {}

    public void keyReleased(KeyEvent e) {}

    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_LEFT)
            x-=5;
        if (e.getKeyCode() == KeyEvent.VK_RIGHT)
            x+=5;
        if (e.getKeyCode() == KeyEvent.VK_DOWN)
            y+=5;
        if (e.getKeyCode() == KeyEvent.VK_UP)
            y-=5;
        this.repaint();
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.black);
        g.fillRect(0, 0, 500, 500);
        g.setColor(Color.white);
        g.fillRect(x, y, 50, 50);
    }
}

Download the complete NetBeans source project files: KeyTest.tar.bz2.

JavaFX: Side-scrolling

An side-scrolling game attempt.

an plane

I used two images, this mountain background made with Gimp (xcf sources here) and that ship above made with Inkscape (svg sources here).

[youtube]5F4STuluSiM[/youtube]

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

var scroll;
scroll = [1..800] dur 60000 linear continue if true;

var mountains = Clip{
    transform: bind translate(-scroll,0)
    shape: Rect {x:bind scroll, y:0, width:400, height:200}
    content: [ImageView {
            transform: translate(0,0)
            image: Image { url: "http://silveiraneto.net/downloads/mountains.png"}
        },
        ImageView {
            transform: translate(800,0)
            image: Image { url: "http://silveiraneto.net/downloads/mountains.png"}
        }
    ]
};

var h = 50;

var ship = ImageView {
    cursor: HAND
    transform: bind translate(0,h)
    image: Image { url: "http://silveiraneto.net/downloads/jfx_plane.png"}
    onMouseDragged: operation(e) {
        h += e.localDragTranslation.y;
    }
};

Canvas {
    content: [mountains, ship]
}