Monday, March 28, 2011

Javafx: avoid the red balls

This weekend i took a look at javafx. After watching some video tutorials I felt able to develop my own application. I decided to write a simple game. It’s all about avoiding the red balls with your black square. And see how long you can escape.

The game covers a lot of things from collision detection to visual effects like inner shadows. If you want to play the game: click here. It’s probably a good idea having a look at the game before stepping through the code so you already know what I am talking about.

So let’s step through the code. You can also download the code as Netbeans project.
/*
 * Copyright 2011, Benny Gächter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package javafxapp;

import javafx.animation.KeyFrame;
import javafx.scene.paint.Color;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Flow;
import javafx.scene.text.Text;
import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.scene.CustomNode;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.transform.Translate;
import javafx.scene.text.Font;
import javafx.scene.Group;
import javafx.scene.control.Button;
import javafx.scene.effect.InnerShadow;
import javafx.scene.control.Hyperlink;
import javafx.stage.AppletStageExtension;

// Height and Width of this Application
def APP_WIDTH = 500;
def APP_HEIGHT = APP_WIDTH;
//Variables for mouse coordinates
var mouseX = 0.0;
var mouseY = 0.0;
//We need this to get a random place for the balls
var random = new java.util.Random();
//state of the game 1 is “running” 2 is “game over”
var state = 1;
//not used at the moment
var counter = 0;
//player time
var currentRuntime: Number = 0;


//create a background and track mouse movements on it
def background = Rectangle {
            x: 0 y: 0 width: APP_WIDTH
            height: APP_HEIGHT
            fill: Color.web("#E6E6E6");
            onMouseMoved: function(e: MouseEvent) {
                mouseX = e.x;
                mouseY = e.y;
            }

        }

 //create player 
def player = Rectangle {
            x: bind mouseX - 10
            y: bind mouseY - 10
            width: 20
            height: 20
 //create a nice visual effect
            effect: InnerShadow {
                choke: 0.2
                offsetX: 1
                offsetY: 1
                radius: 5
                color: Color.WHITE
            }
        }

//create balls
var AIs: Ball[];
for (i in [0..16]) {
    insert Ball {
        //set balls to a random place
        x: random.nextInt(450), y: random.nextInt(450)
       //create a nice visual effect for the balls
        effect: InnerShadow {
            choke: 0.5
            offsetX: 1
            offsetY: 1
            radius: 5
            color: Color.BLACK
        };


    } into AIs;

    //let the balls go to random directions
    if(random.nextInt(100) mod 2 == 0){
        AIs[i].vy = 1
    }
    if(random.nextInt(100) mod 2 == 0){
        AIs[i].vx = -1
    }

}

//t1 is for game logic
def t1 = Timeline {
            repeatCount: Timeline.INDEFINITE
            keyFrames: [
                KeyFrame {
//17ms is better than 25…
                    time: 17ms //25ms
                    values: []
                    action: function() {
                        for (AI in AIs) {
                            //bounce from walls
                            if (AI.x + AI.vx < AI.radius) { AI.vx = -AI.vx;
                            }
                            if (AI.x + AI.vx > APP_WIDTH - 7) { AI.vx = -AI.vx;
                            }

                            if (AI.y + AI.vy < AI.radius) { AI.vy = -AI.vy;
                            }
                            if (AI.y + AI.vy > APP_WIDTH - 7) { AI.vy = -AI.vy
                            }

                            //bounce from other balls
                            /*
                            *not yet implemented
                            */

                            
                            //detect collissions
                            if (AI.x >= mouseX and AI.x <= mouseX + 10) {
                                if (AI.y + AI.radius >= mouseY - 10 and AI.y - AI.radius <= mouseY) {
                                    AI.vy = -AI.vy;
                                    state = 2;
                                }
                            } else if (AI.y >= mouseY - 10 and AI.y <= mouseY) {
                                if (AI.x + AI.radius >= mouseX and AI.x - AI.radius <= mouseX + 10) {
                                    AI.vx = -AI.vx;
                                    state = 2;

                                }
                            }

                            //calculate new position
                            AI.x += AI.vx;
                            AI.y += AI.vy;

                            //increase speed
                            AI.vx = AI.vx * 1.002;
                            AI.vy = AI.vy * 1.002;

                        }
                       // not used at the moment
                        counter++;
                    }
                }
            ]
        };
        //t1 observes the game state and reacts on it
def t2 = Timeline {
            repeatCount: Timeline.INDEFINITE
            keyFrames: [
                KeyFrame {
                    time: 25ms
                    values: []
                    action: function() {
                        /* insert Ball {
                        x: random.nextInt(500), y: random.nextInt(500)
                        } into AIs;*/
                        //insert AIs into stage.scene.content;


                        //stage.scene.impl_setStage(stage);
                        if (state != 1) {
                            t1.stop();
                            tlTimer.stop();
                            gameOverScreen.visible = true;

                        }
                    }
                }
            ]
        };
        //tlTimer is for displaying the current time
def tlTimer = Timeline {
            repeatCount: Timeline.INDEFINITE
            keyFrames: [
                KeyFrame {
                    time: 100ms
                    values: []
                    action: function() {
                        currentRuntime += 0.1;

                    }
                }
            ]
        };
//start all timers
tlTimer.play();
t1.play();
t2.play();

//create the text object for timer
def timer = Flow {
            content: [Text { content: bind currentRuntime.toString()
                    font: Font { size: 30 } }
            ];
        }

        //create the game over screen
var gameOverScreen: Group = Group {
            visible: false
            var r: Rectangle = Rectangle {
                        arcWidth: APP_WIDTH arcHeight: APP_WIDTH
                        width: APP_WIDTH, height: APP_WIDTH
                        fill: Color.ROSYBROWN
                        stroke: Color.DARKRED
                        strokeWidth: APP_WIDTH
                    }
            content: [
                r,
                Text {
                    font: Font {
                        size: 28
                    }
                    x: 20, y: 50
                    content: "Game Over! Time: ";
                },
                Text {
                    font: Font {
                        size: 28
                    }
                    x: 20, y: 80
                    content: bind String.valueOf(currentRuntime).concat(" Seconds")
                },
                Text {
                    font: Font {
                        size: 28
                    }
                    x: 20, y: 200
                    content: "By Benny Gächter ";
                },
                Hyperlink {
                    layoutX: 100,
                    layoutY: 230,
                    scaleX:3
                    scaleY:3
                    text: "www.bgaechter.ch"
                    action: function(): Void {
                        AppletStageExtension.showDocument("http://www.bgaechter.ch");
                    }
                }

                Button {
                    layoutX: 250
                    layoutY: 120
                    scaleX: 3
                    scaleY: 3
                    text: "Restart"
                    action: function() {
                        restart();

                    },
                }
            ]

        }
// Display everything
var stage: Stage = Stage {
            title: "Test App"
            width: 500
            height: 500

            scene: Scene {
                content: [
                    background,
                    player,
                    AIs,
                    timer,
                    gameOverScreen
                ]
            }
        }

//the ball class derived from CustomNode
class Ball extends CustomNode {
    // Ball position
    public var x: Number = 100;
    public var y: Number = 100;
    // Ball radius
    public var radius: Number = 10;
    // Ball velocity vectors
    public var vx: Number = 1.0;
    public var vy: Number = -1.0;

    public override function create(): Node {
        return Circle {
                    transforms: Translate { x: bind x, y: bind y }
                    radius: bind radius
                    fill: Color.RED
                }
    }

}
//The restart function, may be called after game over
function restart(): Void {
    state = 1;
    currentRuntime = 0;
    t1.playFromStart();
    tlTimer.playFromStart();
    gameOverScreen.visible = false;
    for (AI in AIs) {
        AI.vx = 1;
        AI.vy = -1;
    }
}

2 comments:

  1. You did that in 2011? Sorry to disappoint you, but JavaFX 1.x syntax (JavaFX Script) has been dropped by Oracle months ago, and they released an Early Access to JavaFX 2.0 using Java syntax (or Groovy, or Scala, or... any other JVM language).
    Well, JavaFX Script is still something fun to learn and hopefully what you learned there will still be useful in 2.0, but I suggest not to spend too much time on the fine details of the language... (also see the Visage project, reusing this syntax/the compiler).

    ReplyDelete
  2. Hi, sorry for this late answer. Yes i knew i am using an outdated technology and i didn't intend to do something serious. It was just playing around with some java related technology.

    I didn't look at javafx 2.0 but i think that my experience with java 1.x will help a bit.

    ReplyDelete