A lesser known, but powerful feature of JavaFX is its Production Suite. The production suite allows graphics designers to produce designs using tools familiar to them that are readily consumable by JavaFX programs. Using the production suite leads to simpler programs that focus only on the logic and effects and leaves the heavy lifting of the design to the graphic artists who know how to do that best. In fact, incorporating the production suite into your toolbox provides you with a JavaFX UI design tool that rivals any other. In this post, I'll show you how to create a simple dice game using an elegant UI produced by a real designer. Accomplishing a design like this in pure JavaFX would be almost unthinkable. Using the production suite though, this task becomes not only possible, but almost trivial.

Before we dissect our Production Suite powered game, let’s take a look at the demo first. Here’s a screen shot and a Web Start link:

Webstart.small2

The rules for the game are pretty straight-forward. From Wikipedia:

Each turn, a player repeatedly rolls a die until either a 1 is rolled or the player holds and scores the sum of the rolls (i.e. the turn total). At any time during a player’s turn, the player is faced with two options:

  • roll – If the player rolls a
    • 1: the player scores nothing and it becomes the next player’s turn.
    • 2 – 6: the number is added to the player’s turn total and the player’s turn continues.
  • hold – The turn total is added to the player’s score and it becomes the next player’s turn.

The first player to score 100 or more points wins.

The entire look & feel of the game was created in Adobe Illustrator by Jonathan Wei. What remained was merely wiring up the interactive elements of the game using JavaFX script. During development, we had a couple short design iterations that were greatly simplified by using the production suite.

Before Starting

If you’d like to follow along, make sure you’ve installed the following software:

  • NetBeans IDE 6.5 for JavaFX
  • JavaFX 1.1 Production Suite
  • (Optional) Adobe Illustrator CS3. If you’d like to open the asset and manipulate it yourself. You can also gain limited viewing of the assets by double-clicking on DesignerBoard.fxz in NetBeans.

Both NetBeans and the Production Suite are available at JavaFX.com.

Structure of the Application

The source code for the application is available at Git Hub. The structure of our app is a standard NetBeans project layout, with the addition of a directory to hold the artwork.

  • art The assets for the game in their native formats
    • PIG_theDiceGame_cs3.ai The game assets in Illustrator format
  • src
    • net
      • newfoo
        • pig Main package for the game code and the design in JavaFX format.
          • Button Reusable control for the buttons on screen
          • DesignerBoard.fxz Assets converted from Illustrator to native JavaFX
          • DesignerBoardAdapter.fx Helper class to enhance the auto-generated UI stub
          • DesignerBoardUI.fx Auto-generated helper providing access to named JavaFX nodes in the design
          • Game.fxz The logic for the game
          • Main.fxz The main program and controller
          • PlayerMarker.fxz Provides UI transitions for player switching
          • ScoreControl.fxz Manages UI changes to player scores

The application structure is illustrated below:

Dissecting the Code

DesignerBoardUI.fx

This file is maintained through the production suite. As the design is updated with new named nodes, we can re-generate this file by right-clicking on DesignerBoard.fxz and selecting the “Generate UI Stub” menu item.

DesignerBoardAdapter

The Production Suite allows us to conveniently connect to assets designed in illustrator through the UI stub. Often though, we’ll want to modify the design programatically before using it in our application. We might also want to include some small animations without making full-fledged classes for each animation and effect. In this game for instance, we need to apply some translations and also hook up the button states. One way to isolate the changes you need to make from the DesignerBoardUI stub, is to place them in an adapter. Our adapter will be responsible for picking which nodes go where.

Using an adapter also has the added benefit that we could use multiple designs in the same application even if the names of the nodes don’t quite match in each design. This way we could potentially add more than one design to the application. As long as each adapter provides the same interface to the rest of the application, we can simply plug in the appropriate adapter on the fly. Using this approach we could produce designs targeted for specific devices such as the Desktop or Mobile.

Controls

The PlayerMarker, ScoreControl and Button controls encapsulate the behavior and animation functionality for those visual elements. The PlayerMarker and ScoreControl use triggers to kick off visual changes in their state. For instance, the ScoreControl has a trigger on its value attribute. Whenever that value changes, we kick off a small animation to temporarily grow the score on the screen. The code for animating updates to scores looks like this:

 public var value : Integer = -1 on replace {
     if (value != -1) {
         if (value < 10) {
             text.content = "00{value}";
         } else if (value < 100) {
             text.content = "0{value}";
         } else {
             text.content = "{value}";
         }
         // when the value is updated, create a small scaling animation
         var t = ScaleTransition {
             repeatCount: 2
             autoReverse: true
             node: text
             fromX: 1, toX: 1.3, byX: .05
             fromY: 1, toY: 1.3, byY: .05
             interpolate: Interpolator.EASEIN
             duration: 175ms
         }
         t.playFromStart();
     }
 }
 

The above example demonstrates how we can apply a JavaFX Scale Transition to a node. Unlike many other animation examples on the web though, we applied this transition to a node created in Production Suite.

The button code is the same generic button code used in a previous tutorial. There are two differences though in this application worth calling out. The first is that the visual button states were defined in one spot on the design to save the designer from having to copy the button states to every button. In the final design, we’ll see only the normal state of the button in the correct spot to make it easier on the designer. This isn’t a problem as we can introduce some JavaFX code to do the heavy lifting of copying the button states over to our buttons after we’ve loaded our design. The second difference to the original tutorial is that these buttons only have normal, hover and pressed states in the design. To get around this, we’ll simply copy the normal state button and darken its colors to represent our disabled button.

To create all of our buttons, we’ll introduce a createButton function inside our DesignerBoardAdapter to do all the heavy lifting. Inside the function, it’ll 1.) duplicate the other button state nodes so that we can use copies throughout our game. 2.) visually align the duplicate copies with the “normal” state buttons 3.) replace the text on the prototype button states with text from the real buttons. The final step is to create a Group of nodes that contain all of the states of the button as expected by our control: the active area and the disabled, pressed, hovered and normal states of the button. The code looks like this:

    function createButton( normal:Node, hoveredSource:Node, pressedSource:Node ) {
        var hovered = Duplicator.duplicate(hoveredSource);
        align(normal, hovered);
        hovered.visible = true;
        var pressed = Duplicator.duplicate(pressedSource);
        align(normal, pressed);
        pressed.visible = true;
        var disabled = Duplicator.duplicate(normal);
        align(normal, disabled);
        // active area could have been any node with the same shape
        var active = Duplicator.duplicate(normal);
        align(normal, active);

        replaceText(normal, hovered);
        replaceText(normal, pressed);
        Group {
            content: [
                active,
                disabled,
                pressed,
                hovered,
                normal
            ]
        }
    }

    function replaceText( text:Node, dest:Node ) {
        var group = (dest as Group);
        var newText = Duplicator.duplicate((text as Group).content[2]);
        newText.translateX = -1 * group.translateX;
        newText.translateY = -1 * group.translateY;
        (dest as Group).content[2] = newText;
    }

    function align( target:Node, dest:Node ) {
        var destBounds = dest.boundsInScene;
        var targetBounds = target.boundsInScene;

        dest.translateX = (targetBounds.minX - destBounds.minX) - dest.translateX;
        dest.translateY = dest.translateY + (targetBounds.minY - destBounds.minY);
    }
 

Game Logic

The entire game logic is implemented in the net.newfoo.pig.Game class. The Game#roll method has all of the logic for determining what to do next. The Game class itself contains two callback functions it uses to communicate after a roll has occurred and when the game is over. This design, typical in Model View Controller designs, allows us to define the game logic in one spot without having to worry about any UI repercussions.

 package net.newfoo.pig;

 import java.lang.Math;

 public class Game {
     public-read var player1Score : Integer;
     public-read var player2Score : Integer;
     public-read var turnScore : Integer;
 
     public-read var isPlayerOne = true;
 
     public-init var gameOver : function( isPlayerOne : Boolean ) : Void;
     public-init var afterRoll : function( lastRoll : Integer ) : Void;
 
     public function roll() : Boolean {
         var r = ((Math.random() * 6) as Integer) + 1;
         if (r == 1) {
             turnScore = 0;
         } else {
             turnScore += r;
         }
         afterRoll(r);
         if (isPlayerOne and turnScore + player1Score >= 100) {
             player1Score = turnScore + player1Score;
             gameOver(true);
             return false;
         } else if (not isPlayerOne and turnScore + player2Score >= 100) {
             player2Score = turnScore + player2Score;
             gameOver(false);
             return false;
         } else {
             return r != 1;
         }
     }
 
     public function switchPlayer() {
         if (isPlayerOne) {
             player1Score += turnScore;
         } else {
             player2Score += turnScore;
         }
         turnScore = 0;
         isPlayerOne = not isPlayerOne;
     }
 
     public function reset() {
         turnScore = player1Score = player2Score = 0;
     }
 }
 

Main

Tying everything together is the Controller class inside net.newfoo.pig.Main.fx. The controller is responsible for creating instances of the UI, the controls and the game logic. The Controller then wires everything together. For instance, the game is hooked up with afterRoll and gameOver callbacks. The roll button triggers the Game#roll method to be called. Once the controller is setup and has wired the controls and game logic, we merely have to add it to our main stage. The JavaFX event loop will take care of the rest for us.

Below are a couple of important snippets from the Controller class. This snippet configures the game class with its callbacks. The gameOver callback in this case will lighten the rest of the game board and display the game over dialog. The afterRoll will update the scores and kickoff the dice roll animation.

 var game: Game = Game{
     afterRoll: function rolled( lastRoll:Integer ) : Void {
         hideSides();
         var side = ui.sides.content[lastRoll - 1];
         side.visible = true;
         ui.rollDice(side);
         if (game.isPlayerOne) {
             player1Score.value = player1Score.value + lastRoll;
         } else {
             player2Score.value  = player2Score.value  + lastRoll;
         }
     }

     gameOver: function( player1Wins:Boolean ) {
         println("Game over Player {if (player1Wins) 1 else 2}");
         ui.pigWinsScore.content = "{game.player1Score} vs. {game.player2Score}";
         var name = if (player1Wins) ui.player1NameText.content else ui.player2NameText.content;
         ui.pigWinsText.content = "{name} Wins!";
         rollButton.disable = true;
         holdButton.disable = true;
         ui.gameOverDialog.visible = true;
         ui.boardGroup.opacity = .4;
     }
 }
 

In this game, there is no real benefit to creating a controller class to group together all of this work other than for organization. In this example game, the Controller shows that a game could be grouped together in one class and easily added to main stage. For larger games however, the controller class would likely exist in its own class file. To add the game to our main stage, we simply have to add the following code:

 Stage {
     title: "Pig the Dice Game"
     width: 640
     height: 500
     scene: Scene {
         content: controller.ui
     }
 }
 

Since the controller orchestrated all the event handling, we don’t need to do anything else when we add the UI to the stage. The JavaFX event handling loop will take care of the rest for us.

The Power of the Production Suite

The JavaFX code for the entire application is around 800 lines of code. If you consider that number includes all of the comments, the automatically generated DesignerBoardUI.fx and the re-usable Button code, the source code would be less than 500 lines of code! So with approximately 500 lines of “new” code, we were able to put together a complete game. Of course the game was pretty simple, but you’d be hard-pressed to achieve something similar in any other free platform right now! The production suite freed us from the burden of laying out the UI. It allowed us to go through several designer/developer iterations with almost no effort. Our code merely focused on the game logic and the dynamic aspects of the game itself.

Though this example was fairly simple for a real application, you can see that constructing elegant JavaFX UIs in the Production Suite is significantly easier than manually writing JavaFX code for everything. Since the Production Suite also generates JavaFX code, you also have the flexibility to produce portions of the UI manually in JavaFX code should you ever need that option. If you also consider that the Production Suite works with the industry standard design tools, the choice to use it becomes almost automatic. In addition, should your team prefer an all free toolkit, you can even use the Production Suite with free graphical editors capable of producing SVG such as Inkscape. In the end, choosing the production suite in your graphically oriented JavaFX application should be automatic.

comments powered by Disqus