WhiteOutGame: A Fun Puzzle Game
This sample code shows how to build a version of the Lights Out game with the JavaFX technology. It shows manual layout, multiple scenes, buttons, highlighting, and transitional effects.
Understanding the Code
The code is designed so that it adapts to the window size and orientation(i.e. landscape or portrait).
The Env class uses the window size to compute the sizes and positions of the various GUI gadgets, as shown in Figure 1.
public def bigFont =
if (screenWidth < 320)
Font {size: 30}
else if (screenWidth < 450)
Font {size: 60}
else
Font {size: 70}
// Compute height / width of the blue buttons based on the size of "Reset"
def resetSize = Text {
content: "Reset"
font: smallFont
};
// Leave a bit of space around "Reset"
public def blueButtonHeight = resetSize.layoutBounds.height + 8;
public def blueButtonWidth = resetSize.layoutBounds.width + 10;
Figure 1: Computing Sizes of GUI Gadgets
Also, notice the computation of the size of the game buttons (the buttons in the 5 x 5 array) in Env.fx. This computation computes the desired size of a button based on the width/height of the window and its orientation.The classes that create a layout format use the values from Env. For example, Figure 2 shows the BlueButton class setting the size of a BlueButton.
public class BlueButton extends Group {
public-init var env: Env;
public-init var text: String;
// We will add a 2 pixel shadow.
public-init var width = env.blueButtonWidth - 2;
public-init var height = env.blueButtonHeight - 2;
Figure 2: BlueButton Class Setting the Size of a BlueButton
You can experiment with changing the Env settings to see how that changes the layout of the GUI gadgets. Be sure to note the setting of translateX and translateY, and the use of layoutBounds in creating the layout. For example, see the following code in Canvas.fx:
// the Next Level items in the shaded rectangle.
finishedText.translateX = (shadedRect.width - finishedText.layoutBounds.width) / 2;
finishedText.translateY = shadedRect.translateY + shadedRect.layoutBounds.minY +
.3 * nextButton.height;
levelText.translateX = finishedText.translateX + finishedText.layoutBounds.minX + 5;
levelText.translateY = finishedText.translateY + finishedText.layoutBounds.maxY + 5;
Figure 3: Setting the Coordinates of "Finished" and "Level"
The code in Figure 3 sets the coordinates of the words "Finished" and "Level" that appears just below when all the white squares have been eliminated. Also in Canvas.fx are uses of the new-for-JavaFX 1.2 class javafx.scene.layout.Tile. This class is used to layout the items:
Reset Button
"Moves"
move-count
Quit Button
These items run vertically down the right side of the screen in landscape mode and horizontally across the bottom
of the screen in portrait mode. The Tile class allows the layout to adjusted for these two modes, e.g:
Tile {
translateX: if (env.isPortrait) 0 else env.gameSize
translateY: if (env.isPortrait) env.gameSize + 5 else env.gameButtonGap
columns: if (env.isPortrait) 3 else 1
rows: if (env.isPortrait) 1 else 3
// In portrait mode, we allocate 1/3 the width for each button and the
// "move count".
tileWidth: if (env.isPortrait) env.gameSize / 3 else env.screenWidth - env.gameSize
tileHeight: if (env.isPortrait) env.blueButtonHeight else env.gameSize / 3
content: [
BlueButton {
env: env
text: "Reset"
onMouseReleased: function(e:MouseEvent) {
model.reset();
}
}
Figure 4: Setting up a Tile
The buttons in the game have drop shadows and borders that fade in or fade out as the mouse enters or leaves the button. These effects are achieved by creating two rectangles, one offset down and right by two pixels, and with a dark color. This is the drop shadow. The other rectangle is the button proper. It has a "stroke," the line that draws the rectangle. Note that the opacity of this line is "bound" to a variable named strokeAlpha that changes as the mouse enters or leaves. See the following code in BlueButton.fx. (This code could be simplified by using the new-for-JavaFX 1.2 class javafx.scene.control.Button).
content = [
// This creates a shadow on the bottom and right of the button
Rectangle {
fill: Color.rgb(0x30, 0x30, 0x30)
translateX: 2
translateY: 2
width: width
height: height
arcWidth: 6
arcHeight: 6
}
Rectangle {
fill: env.blueGrad
width: width
height: height
arcWidth: 6
arcHeight: 6
stroke: bind Color{red: 1, green: 1, blue: 1, opacity: strokeAlpha}
onMouseEntered: function(e) {
fade.rate = 10.0;
fade.play();
}
onMouseExited: function(e) {
fade.rate = -10.0;
fade.play();
}
}
Figure 5: Creating Drop Shadows and Borders in Buttons
Note the fade.play() calls in Figure 5. 'fade' is a Timeline variable:
def fade = Timeline {
keyFrames: [
at(0s) { strokeAlpha => 0.3 tween Interpolator.LINEAR }
at(0.5s) { strokeAlpha => 1.0 tween Interpolator.LINEAR }
]
};
When this timeline is activated by the fade.play() statement, the value of the strokeAlpha variable is linearly varied from .3 to 1.0 over the
course of .5 seconds. This variation changes the rectangle stroke to opaque. Similarly, when the mouse exits, the
timeline is played in reverse and the rectangle stroke becomes nearly transparent.
The Splash class shows the splash screen and contains an example of nested Timelines.
When the splash screen is displayed, the following occurs:
textWhite.x = -(textWhite.layoutBounds.width + 1);
textWhite.visible = true;
textOut.x = env.screenWidth + 1;
textOut.visible = true;
opacity = 0.0;
def opa = Timeline {
keyFrames: [
at (.5s) {opacity => 1.0 tween Interpolator.LINEAR}
KeyFrame {
time: .4s
timelines: Timeline {
keyFrames: [
at (.4s) {textWhite.x => finalWhiteX tween Interpolator.LINEAR}
at (.4s) {textOut.x => finalOutX tween Interpolator.LINEAR}
]
}
}
KeyFrame {
time: 1s
timelines: Timeline {
rate: 1.0
keyFrames: [
at (.5s) {buttonY => finalButtonY tween spring}
]
}
}
]
}
opa.play();
Figure 6: Creating a Timeline
So we see that:Customizing the Code
The main customizations you can make to this code are:
In addition, you could do some experimenting:
