C H A P T E R 11
■ ■ ■
215
Pulling It All Together:
Clown Cannon
Throughout this book I have presented techniques and examples in isolation so that you can examine
details of the implementation. They represent the experience that I have gained from trial and error
when working with JavaFX. But an application is more than just the sum of its features and effects, which
is why, in this chapter, we will explore an entire application from start to finish. We will look at the
design process, the workflow, and the implementation of an example application.
Design Phase
I wanted to find a way to bring the examples in this book together, and I thought an example application
would do the job. While some of the techniques in this book could be used in many different types of
applications, a game is the only application where it makes sense to use all of them. It seemed each
chapter could add something to a game that contributed to specific design goals: Physics, for example,
quickly creates compelling game play. Animated lighting gives a unique and interesting look to a game.
What about animated gradients? There must be some use for them in a game.
Game Design
So I followed my own advice from Chapter 1 and opened up Adobe Illustrator and started designing a
game from scratch. My goal was to use as many examples from the book as I could without it seeming
contrived, but upon reflection I gave up worrying about that. Let me present to you Clown Cannon, a
game where the goal is to fire a clown out of a cannon and into a bucket of water. Figures 11-1 and 11-2
show the initial design concept.
In Figure 11-1 a very simple start screen is described with a thematic background, a title, and two
buttons. The four notes are self-explanatory. But I want to point out that the use of transitions is nearly
identical to the case presented in Chapter 3—using transitions to move from one screen to another.
CHAPTER 11 ■ PULLING IT ALL TOGETHER: CLOWN CANNON
in the game is laid over each other. This is intentional, because for this chapter I decided to use a single
Illustrator file to store all of the assets in the game. There are advantages and disadvantages to using a
single file instead of multiple files, but before we discuss that, let me explain how the Illustrator file is
CHAPTER 11 ■ PULLING IT ALL TOGETHER: CLOWN CANNON
218
organized. (By the way, the Illustrator file used to create the assets in this game is included with the
source code, so you can inspect it. The file is saved as a CS3 file.)
On the right of Figure 11-3 we see the Layer tool from Illustrator, which displays each component in
the file. Each of those items will become a JavaFX Node when exported. For example, the item named
jfx:score is the graphic that says “Score: 00000.” This will become a Text node named score in the
JavaFX code and will enable the code to change the displayed text dynamically at runtime. In fact, each
component updated at runtime is given a name with the prefix jfx:, which allows the export tool, in
conjunction with NetBeans, to create a JavaFX class that represents this content. This class will be called
GameAssetsUI. Chapter 1 describes working with the JavaFX Production Suite in more detail.
The game is composed of three screens—the start screen, the welcome screen, and the game screen.
Each of these screens will be an instance of GameAssetsUI. Since each screen does not require all of the
content found in each GameAssetsUI, the game code must prune nodes to create exactly the right
content. For example, neither the start screen nor the game screen require the about panel, just as the
welcome screen and the about screen don’t require the text “Game Over,” as this is only used by the
game screen. When each screen is initialized, all unneeded nodes will be removed.
It might make sense to simply create an Illustrator file for each screen, removing the need to delete
unwanted nodes. You could also create one master Illustrator file or a number of smaller Illustrator files
this is a question of workflow. For this game, however, I decided to create a single file because all of the
screens shared a background; I did not want to update three different illustrator files every time I
changed the color of the background. I could have also chosen to create a background Illustrator file and
then three other Illustrator files for each screen. This, of course, would work. But once we get to the code
we will see that initializing each GameAssetsUI for use as three different screens is not all that
complicated. Let me say this: The Illustrator to JavaFX workflow is not perfect. In most cases there will be
clown.
CHAPTER 11 ■ PULLING IT ALL TOGETHER: CLOWN CANNON
221 Figure 11-6. The game screen
The power level on the upper left shows that the user clicked the mouse when the meter was at
about 80%. Note that the power level is a gradient. We will use the animated gradient technique from
Chapter 8 to implement this.
If the clown makes it to the water bucket on the right, points are awarded and there is a small
fireworks display. Figure 11-7 shows the firework display.
CHAPTER 11 ■ PULLING IT ALL TOGETHER: CLOWN CANNON
222 Figure 11-7. Fireworks
In Figure 11-7 there are two dots that came out of the launchers below them. The dots represent a
firework shell, and when they reach the top of their animation a bunch of star particles are created. Each
star particle moves outward in a random direction to create a firework effect.
Implementation
You learned how to implement the effects used in this game in previous chapters; the following code
examples will focus on how these effects are used in an application. We will also look at the code that
glues these effects together to create a complete game and some tricks you can use when working with
content created in Illustrator.
CHAPTER 11 ■ PULLING IT ALL TOGETHER: CLOWN CANNON
223
224
public var blockInput = false;
public var lightAnim:Timeline;
function run():Void{
initStartScreen();
initAboutScreen();
Stage {
title: "Clown Cannon"
resizable: false;
scene: scene
}
rootGroup.requestFocus();
lightAnim.play();
}
function keyReleased(event:KeyEvent){
gameModel.keyReleased(event);
}
public function addLights(gameAsset:GameAssetsUI):Timeline{
var yCenter = gameAsset.backPanelGroup2.boundsInParent.height/2.0;
var spotLight = SpotLight{
x: 320
y: yCenter
z: 50;
pointsAtZ: 0
pointsAtX: 320
},
KeyFrame{
time: 3s
values: spotLight.pointsAtY => yCenter-100 tween Interpolator.EASEBOTH
},
KeyFrame{
time: 4s
values: spotLight.pointsAtX => 320 tween Interpolator.EASEBOTH
},
KeyFrame{
time: 5s
values: spotLight.pointsAtY => yCenter+100 tween Interpolator.EASEBOTH
},
KeyFrame{
time: 6s
values: spotLight.pointsAtX => 610 tween Interpolator.EASEBOTH
},
KeyFrame{
time: 7s
values: spotLight.pointsAtY => yCenter-100 tween Interpolator.EASEBOTH
},
KeyFrame{
time: 8s
values: [spotLight.pointsAtX => 320 tween Interpolator.EASEBOTH,
spotLight.pointsAtY => yCenter tween Interpolator.EASEBOTH]
}
]
}
return anim;
}
public function removeFromParent(node:Node):Void{
var parent:Object = node.parent;
if (parent instanceof Group){
delete node from (parent as Group).content;
} else if (parent instanceof Scene){
delete node from (parent as Scene).content
}
}
public function makeButton(node:Node,action:function()){
node.blocksMouse = true;
node.onMouseClicked = function(event:MouseEvent):Void{
if (not blockInput){
action();
}
}
node.onMouseEntered = function(event:MouseEvent):Void{
node.effect = Glow{}
}
node.onMouseExited = function(event:MouseEvent):Void{
node.effect = null;
}
}
public function allowInput():Void{
blockInput = false;
}
function startGame():Void{
lightAnim.stop();
gameModel = GameModel{}
FlipReplace.doReplace(startScreen, gameModel.screen, gameModel.startingAnimationOver);
translateY: yOffset;
content: node;
}
insert group before parent.content[index];
return group;
}
public function createLinearGradient(stops:Stop[]):LinearGradient{
return LinearGradient{
startX: 1
endX: 1
startY: 0
endY: 1
proportional: true
stops: sortStops(stops);
}
}
public function sortStops(stops:Stop[]):Stop[]{
var result:Stop[] = Sequences.sort(stops, Comparator{
public override function compare(obj1:Object, obj2: Object):Integer{
var stop1 = (obj1 as Stop);
var stop2 = (obj2 as Stop);
if (stop1.offset > stop2.offset){
return 1;
} else if (stop1.offset < stop2.offset){
return -1;
} else {
var newGradient = LinearGradient{
endX: linearGradient.endX
endY: linearGradient.endY
proportional: linearGradient.proportional;
startX: linearGradient.startX
startY: linearGradient.startY
stops: newStops;
}
shape.fill = newGradient;
}
}
}
if (node instanceof Group){
for(n in (node as Group).content){
simplifyGradients(n);
}
}
}
In Listing 11-1 the variables startScreen and aboutScreen are instances of GameAssetsUI. Each
GameAssetsUI is a complete set of Nodes from the original Illustrator file. The functions initStartScreen
and initAboutScreen prepare startScreen and aboutScreen for use in the game. The function
initStartScreen simplifies the gradients, creates an animation for the spotlight, removes a number of
unwanted Nodes and turns the Nodes startScreen.startButton and startScreen.aboutButton into
buttons. Let’s take a look at each of these steps.
The gradients generated when exporting from Illustrator are oddly complex. Listing 11-2 shows one
of these gradients.
Listing 11-2. GameAssets.fxz (partial)
SVGPath {
with just 2 colors. I am not exactly sure why all of the extra Stops are included. Perhaps the algorithm
Illustrator used for tweening colors is different than that of JavaFX. Since gradients are a performance
pain point in JavaFX, it makes sense to simplify these gradients to use just 2 Stops. There might be a
fidelity issue with doing this, but I couldn’t tell the difference between the LinearGradient with 17 Stops
and the simplified LinearGradient with only 2 Stops. In Listing 11-1, the functions that initialize the two
GameAssetUIs use the function simplifyGradient to recursively traverse the Node tree and simplify all
LinearGradients. Be warned that if your Illustrator file uses gradients, which should have more than 2
Stops, the simplifyGradients function will not correctly preserve the intended look.
The function initStartScreen creates a Timeline for animating the SpotLight by calling the function
addLights. The function addLights creates a Lighting effect with a SpotLight and applies it to the Group
backPanelGroup2. The Group backPanelGroup2 contains the ceiling and wall of the circus tent. The
SpotLight that is created is positioned in the center of the Group backPanelGroup2, and the Timeline anim
is then created to change the location where the SpotLight is pointing. The Timeline anim is returned
from the function addLights to allow the animation to be started and stopped. This is important because
applying lighting effects is computationally expensive and should be turned off when not in use.
The functions initStartScreen and initAboutScreen use the function removeFromParent to get rid of
unwanted content. This is a simple utility function found in Listing 11-1 that I find handy, because
Node.parent returns a Node of type Parent, which is not very useful. Both of the classes Scene and Group
extend Parent, since these are the two types that might contain a Node. Unfortunately the class Parent
does not require an attribute named content. Rather it requires the function removeFromParent to cast
node.parent to the correct class before deleting it from the content that contains it.
The last thing the functions initStartScreen and initAboutScreen do is create buttons out of some
of the Nodes in the fxz content. The function makeButton does not create an instance of
javafx.scene.control.Button, but instead just adds button-like functionality to the Node passed to the
function. Adding some event listeners to the Node does this. The onMouseClicked attribute is used to call
the function action when the user clicks on the Node, and setting blocksMouse to true prevents the click
from being processed by some other listening node. The two properties onMouseEntered and
CHAPTER 11 ■ PULLING IT ALL TOGETHER: CLOWN CANNON
Main.removeFromParent(screen.startButton);
Main.removeFromParent(screen.title);
screen.powerLevel.visible = true;
screen.backFromPlayButton.visible = false;
screen.playAgainButton.visible = false;
screen.gameOverText.visible = false;
Main.makeButton(screen.backFromPlayButton, goBack);
Main.makeButton(screen.playAgainButton, playAgain);
screen.onMouseWheelMoved = mouseWheelMoved;
screen.onMouseClicked = mouseButtonClicked;
clownNode = Main.offsetFromZero(screen.flyingClown);
cannonNode = Main.offsetFromZero(screen.cannon);
bucketNode = Main.offsetFromZero(screen.waterBucket);
balloonNode = Main.offsetFromZero(screen.bonusBalloon);
net = Main.offsetFromZero(screen.net);