Creating a Simple Game in JavaFX (Part 6)

So far we have a working game but we are still missing an essential ingredient: score keeping.

Displaying the Score


...
var scoreTime = 0;
var scoreMoves = 0;
var scoreBonus = 0;
var scoreTotal = 0;
var scoreHigh = 0;

var scoreArea: Group = Group {

    content : [
        Rectangle {
            arcWidth: 20  arcHeight: 20
            x: 290 y: 0
            width: 120, height: 200
            fill: Color.LIGHTGOLDENRODYELLOW
            stroke: Color.DARKORANGE
            strokeWidth: 3
        },
        Text {
            font : Font {
                size: 24
            }
            layoutX: 320 layoutY: 30
            content: "Score"
        },
        Tile {
            columns: 2
            rows: 5
            hgap: 5
            vgap: 5
            tileWidth: 50
            hpos: HPos.LEADING
            layoutX: 290 layoutY: 50
            content: [
                Text {
                    font : Font {
                        size: 12
                    }
                    content: "Time:"
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: bind scoreTime.toString()
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: "Moves:"
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: bind scoreMoves.toString()
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: "Bonus:"
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: bind scoreBonus.toString()
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: "Total:"
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: bind scoreTotal.toString()
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: "High:"
                },
                Text {
                    font : Font {
                        size: 12
                    }
                    content: bind scoreHigh.toString()
                }
            ]
        }
    ]
}

Stage {
    ...
    scene: Scene {
        ...
        content: Group {
            ...
            content: [
                playingField,
                player,
                enemies,
                gameOverScreen,
                scoreArea
            ]
        }
    }
}

At this point you should be comfortable with the new code above. Nonethless there are two new aspects that we have not covered earlier. I will discuss them as we go through the code.
At the top I have added different variables for the different types of score keeping. ScoreTime keeps the score collected for running of the game. With each second passed the player automatically receives points. ScoreMoves collects the score when the player is moving. When the player is not moving, points will be deducted from scoreMoves. ScoreBonus is the score received when a player touches an enemy in bonus mode. Not touching this enemy will result in points being deducted from the scoreBonus score. Finally the total score adds up all the scores mentioned above and the high score persists the highest achieved score for the duration of the applications life. This means if you close the game, the high score is lost. This behaviour is of course not desirable, and would need fixing in future versions of the game.

The scoreArea is a familiar Group object that encompasses a Rectangle which is the background of the score area, a title object and then a new type of object called a Tile. A tile is one of several layout objects added to JavaFX in version 1.2 and is basically a grid in our case of two columns and 5 rows. All tiles or cells are of the same size. The objects are added in the content[] sequence, with the sequence position determining which position in the grid the object will have.

All we are adding to the Tile object are Text objects. The first Text object of each row is the label and the second Text object is the corresponding score. Now for the second new aspect. The value of the score is defined as bind scoreTotal.toString(). What this does is bind the value of the Text object to the value of the scoreTotal variable (as String (.toString())). This is an incredibly simple way of having your score Text object updated each time the score changes. No need for extra code, extra listeners etc. etc.

Don’t forget to add the new scoreArea to the main Group.

Our game screen now looks like this:

img 5

Adding Time Score

Now that we have the graphics in place lets calculate the time score.


                ...
                // checkCollision
                for (enemy in enemies) {
                    if (checkCircleCollision(player.translateX,player.translateY,player.radius,enemy.translateX,enemy.translateY,10.0)) {
                        gameOver();
                    } else {
                        calculateNewScore();
                    }

                }
            }
        }
    ]
}

function calculateNewScore() {
    scoreTime += 5;
    scoreTotal = scoreTime + scoreMoves + scoreBonus;

}

As always it is in our game play Timeline object that score calculations take place. After we have checked that no collision with enemy players has taken place we call the method calculateNewScore(). Notice that we do not only update the scoreTime variable with 5 points every cycle but that we are already calculating the total score which is made up of scoreTime, scoreMoves, scoreBonus.
Also notice that just as we have pointed out before, through the binding mechanism, once we update the scoreTime and totalScore variables, the corresponding text objects are also updated.

There is still a little bug in the above code. Each time you reach game over, the score is not reset. Lets make the necessary changes to start each game with a fresh new score:


function initGame():Void {
    initActors();
    initScore();
    mode = MODE_RUNNING;
    gameOverScreen.visible = false
}

function initScore() {
    scoreBonus = scoreMoves = scoreTime = scoreTotal = 0
}

All it took was a small change in initGame() and a new function called initScore().

Adding Move Score


var timeline: Timeline = Timeline {
    ...
    keyFrames : [
        KeyFrame {
            ...
            action: function() {
                var playerMoved = false;
                ...
                // Player
                if (moveRight) {
                    var newX = player.translateX + PLAYER_BALL_STEP;
                    if (newX < playingField.boundsInLocal.maxX - player.radius - playingField.strokeWidth) {
                        playerMoved = true;
                        player.translateX = newX
                    }
                }
                if (moveLeft) {
                    var newX = player.translateX - PLAYER_BALL_STEP;
                    if (newX > playingField.boundsInLocal.minX + player.radius + playingField.strokeWidth) {
                        playerMoved = true;
                        player.translateX = newX
                    }
                }
                if (moveUp) {
                    var newY= player.translateY - PLAYER_BALL_STEP;
                    if (newY > playingField.boundsInLocal.minY + player.radius + playingField.strokeWidth) {
                        playerMoved = true;
                        player.translateY = newY
                    }
                }
                if (moveDown) {
                    var newY= player.translateY + PLAYER_BALL_STEP;
                    if (newY < playingField.boundsInLocal.maxY - player.radius - playingField.strokeWidth) {
                        playerMoved = true;
                        player.translateY = newY
                    }
                }
                // checkCollision
                for (enemy in enemies) {
                    if (checkCircleCollision(player.translateX,player.translateY,player.radius,enemy.translateX,enemy.translateY,10.0)) {
                        gameOver();
                    } else {
                        calculateNewScore(playerMoved);
                    }

                }
            }
        }
    ]
}
...
function calculateNewScore(addMoveScore:Boolean) {
    scoreTime += 5;
    scoreMoves = if (addMoveScore) scoreMoves+5 else scoreMoves-10;
    scoreTotal = scoreTime + scoreMoves + scoreBonus;

}

Calculating a move score only requires very subtle changes. In the main loop we introduce a variable called playerMoved of type Boolean. If a new position is calculated for our player object, then playerMoved is set to true.
We have also updated the function calculateNewScore to accept a parameter indicating whether the player has moved or not. If the player has moved we add 5 points to the move score otherwise we subtract 10 points.

Adding Bonus Score

Adding the bonus score is a bit trickier. Here are the code changes in Main.fx:


var bonusTimeline: Timeline = Timeline {
    repeatCount: 1
    keyFrames : [
        KeyFrame {
            time : 3s
            canSkip: false
        }
    ]
}
...
var timeline: Timeline = Timeline {
    ...
    keyFrames : [
        KeyFrame {
            ...
            action: function() {
                var playerMoved = false;

                calculateBonusEnemy();
                for (enemy in enemies) {
                    if (checkCircleCollision(player.translateX,player.translateY,player.radius,enemy.translateX,enemy.translateY,10.0)) {
                        if (not enemy.bonusMode) {
                            gameOver();
                        } else {
                            calculateNewScore(playerMoved,true);
                        }
                    } else {
                        calculateNewScore(playerMoved);
                    }
                }
            }
        }
    ]
}
function calculateNewScore(addMoveScore:Boolean,addBonusScore:Boolean) {
    scoreBonus += 1000;
    calculateNewScore(addMoveScore);
}
function calculateBonusEnemy() {
    var bEnemy:Enemy = null;
    for (enemy in enemies) {
        if (enemy.bonusMode) {
            bEnemy = enemy;
        }
    }

    // None Bonus
    if (bEnemy==null) {
        if (javafx.util.Math.random()>0.95) {
            // Change one to bonus
            var i = (javafx.util.Math.ceil( enemies.size()*javafx.util.Math.random() )).intValue();
            println("changing to bonus {i}");
            enemies[i].fill(Color.CYAN);
            enemies[i].bonusMode = true;
            bonusTimeline.playFromStart();
        }
    } else {
        // One Bonus
        if (not bonusTimeline.running) { // min 3s in Bonus
            if (javafx.util.Math.random()>0.5) { // Turn off bonus
                bEnemy.fill(Enemy.DEFAULT_FILL);
                bEnemy.bonusMode = false;
                if (not bEnemy.bonusWasHit) {
                    scoreBonus -= 5000
                }
                bEnemy.bonusWasHit = false
            } else {
                // prolong bonus
                bonusTimeline.playFromStart();
            }

       }
    }
}
...

And the code changes in Enemy.fx:


...
public var bonusMode = false;
public var bonusWasHit = false; // Player hit in Bonus Mode
...
public function fill(color:Color) {
    enemy.fill = color;
}

In Enemy.fx all we do is make our Enemy objects aware that they could be in bonus mode (bonusMode) and whether they were hit in bonus mode (bonusWasHit). Furthermore we add fill function that allows us to change the fill color of the enemy objects.

The bigger changes are in Main.fx. In each cycle we now have a function calculateBonusEnemy() being called. This method first checks if an enemy is in bonus mode. If no enemy is in bonus mode it randomly decides to change one enemy to bonus mode (if random number > 0.95). Which enemy is set to bonus mode is also randomly determined. The bonus enemy gets its color changed and the bonusMode variable set to true. Furthermore a bonusTimeline is played which serves the sole purpose of running for three seconds.

If no enemy is in bonus mode (not bonusTimeline.running) there is a 50/50 chance that the bonus mode will be turned off or that the bonus mode will be prolonged. When turning of the bonus mode we also check if the enemy was hit during its bonus time and if not, 5000 points are subtracted from the bonus score. It pays to hit enemies in bonus mode.

The other changes in Main.fx are quite simple. If a player collides with a bonus enemy, 1000 points are added to the players score for each cycle.

Adding High Score


// Function called after collision
function gameOver():Void {
    timeline.stop();
    mode = MODE_GAME_OVER;
    gameOverScreen.visible = true;
    if (scoreTotal > scoreHigh)
        scoreHigh = scoreTotal;
}

Adding the High Score functionality is as simple as checking if the current total score when the gameOver() function is called is higher than the previous High Score.

We're done! We now have the final version of our game. Please check part 7 for some final thoughts, a functioning version of our game and the complete tutorial code.

Tags: , ,

Leave a Reply