Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Sunday, December 13, 2015

Sprite Programming with JavaFX and Scene Builder

This post demonstrates Sprite programming using JavaFX and Scene Builder.  Sprite programming involves displaying and manipulating small images on a background scene.  Development platforms like MIT's Scratch and Apple's SpriteKit have been warmly embraced by the next generation of computer programmers.  This blog post adopts the Sprite paradigm -- a Sprite Sheet is created in Scene Builder -- in the form of a demonstration game.


All of the code in this post is available on GitHub here.

The Game


The object of the game is to correctly order the Sprites which are labeled A through G.  See the following video for a demonstration.


You can play the game for yourself at https://www.bekwam.net/sortme/sortme.jnlp.

Graphic Design

To build the game, I used JavaFX and the Gluon-distributed Scene Builder app.  The Sprite technique is implemented in Scene Builder as a composite Pane which holds several views for the Sprite, one for each state a Sprite might become.

In the following screenshot from Scene Builder, the top row of components all belong to a single template Sprite.
Scene Builder Breaking Out a Sprite in the Top Row
 The Rectangles containing the letter A are
  1. The normal view of the Sprite at rest,
  2. The highlighted view when the Sprite gets the focus,
  3. The transparent view for when the Sprite is moving, and
  4. The temporary error view for when the Sprite is out of order.
As you may recall from the video or URL, you don't see more than one view of a Sprite at a time.  I've shifted each item while designing so that I can see all of the views together to make sure that they'll look consistent.  In the code, I'll replace the shifted layoutX properties with zeros to stack them and toggle the visible properties.

In the game, a Sprite is a Pane and each view is a Group: normal, highlighted, transparent, temporary.  Each Group is a pairing of a Rectangle and a Text.  The Rectangles vary by opacity, effect, and border.

Controller Code

All of the code in this post is available on GitHub here.  These sections omit boilerplate code like imports and the trivial Application that launches the app.

The main code is in the Controller (SortMeController.java) and in a special purpose "Sprite" class that encapsulates the FX objects.
The Controller code starts with @FXML annotations that let me access what I'm calling a template Sprite -- what I defined in Scene Builder -- and build other Sprites from the template.  In this header, there is also a Comparator to determine the stacking order of the Sprites, a list of Sprites kept convenience, and a list of the expectedResults which assists the terminal check() operation and sets up the game.

  @FXML
  Pane background;
 
  @FXML
  Pane container;
 
  @FXML
  Group normal;
 
  @FXML
  Group highlight;
 
  @FXML
  Group drag;

  @FXML
  Group error;

  private SpriteXComparator spriteYComparator = new SpriteXComparator(); 
  private final List<Sprite> sprites = new ArrayList<>(); 
  private final List<String> expectedResults = 
    Arrays.asList( new String[]{"A", "B", "C", "D", "E", "F", "G"} );


"background" is the container on which the game is played.  The Sprites are arranged on this.  "container" refers to the root FX object in a Sprite.  The four Groups are the different views all belonging to the root.

This is the @FXML initialize() method.  I'm deferring the presentation of the Sprite class, but it uses the FXML-bound fields in a constructor.  Then, in a loop, that object is used to create other objects (templateSprite.create(s)).

Notice that I disconnect the initial object added to background.  Everything added to background is added from the streams loop.

  @FXML
  public void initialize() {
 
    Sprite templateSprite = new Sprite( container, normal, highlight, drag, error, "A" );
  
    background.getChildren().remove(container);
  
    expectedResults
 .stream()
 .forEach((s) -> {
  Sprite sp = templateSprite.create( s );
  background.getChildren().add( sp.container );
  sprites.add( sp );
 });
    shuffle();
}

Sprite Class Code

I'll present the Sprite class code in segments.  The full code is available on GitHub.

Here are the fields of the Sprite class.  They are the familiar Pane and Group objects plus a textProperty.  There are also two fields that assist with the movement of the Sprite.

  final Pane container;
  final Group normal;
  final Group highlight;
  final Group drag;
  final Group error;
  final StringProperty textProperty;
  private double mouseInSpriteX = -1.0d;
  private double mouseInSpriteY = -1.0d;

The final fields are set in a constructor.  The constructor also registers a MouseEvent handler and binds the Text object's textProperty to the class variable (also called "textProperty").  A combination of setVisible and setLayout results in the "normal view" bing shown first.

  public Sprite(Pane container, Group normal, Group highlight, Group drag, Group error, String text) {
  
    this.container = container;   
    this.normal = normal;
    this.highlight = highlight;
    this.drag = drag;  
    this.error = error;
  
    this.container.addEventHandler(MouseEvent.ANY, mouseHandler);
  
    this.normal.setVisible(true);
    this.normal.setLayoutX( 0.0d );
  
    this.highlight.setVisible(false);
    this.highlight.setLayoutX( 0.0d );
  
    this.drag.setVisible(false);
    this.drag.setLayoutX( 0.0d );
  
    this.error.setVisible(false);
    this.error.setLayoutX( 0.0d );
  
    textProperty = new SimpleStringProperty( text );

    ((Text)this.normal.getChildren().get(1)).textProperty().bind( textProperty );
    ((Text)this.highlight.getChildren().get(1)).textProperty().bind( textProperty );
    ((Text)this.drag.getChildren().get(1)).textProperty().bind( textProperty );
    ((Text)this.error.getChildren().get(1)).textProperty().bind( textProperty );
}

I have a public create() method that's called to duplicate a Sprite from another Sprite (what I've called the template Sprite).  A better implementation would involve recursion and possibly reflection to do the deep copy, however this code-heavy version works and is clearer in its intent.

  public Sprite create(String text) {
 
    Pane copyContainer = new Pane();
    copyContainer.getStyleClass().addAll( container.getStyleClass() );
  
    Group copyNormal = copyGroup( normal );
    Group copyHighlight = copyGroup( highlight );
    Group copyDrag = copyGroup( drag );
    Group copyError = copyGroup( error );
  
    copyContainer.getChildren().addAll( copyNormal, copyHighlight, copyDrag, copyError );
 
    Sprite copySprite = new Sprite( copyContainer, copyNormal, copyHighlight, copyDrag, copyError, text );
 
    return copySprite;
}

The copyGroup() method digs into to the Rectangle and Text and creates objects based on them.

  private Group copyGroup(Group sourceGroup) {
  
    Group copyGroup = new Group();
    copyGroup.getStyleClass().addAll( sourceGroup.getStyleClass() );

    Rectangle sourceBackground = (Rectangle)sourceGroup.getChildren().get(0);
  
    Rectangle copyBackground = new Rectangle();
    copyBackground.setWidth( sourceBackground.getWidth() );
    copyBackground.setHeight( sourceBackground.getHeight() );
    copyBackground.getStyleClass().addAll( sourceBackground.getStyleClass() );
    copyBackground.setEffect( sourceBackground.getEffect() );
    copyBackground.setOpacity( sourceBackground.getOpacity() );
  
    Text sourceText = (Text)sourceGroup.getChildren().get(1);
  
    Text copyText = new Text();
    copyText.getStyleClass().addAll( sourceText.getStyleClass() );
    copyText.setLayoutX( sourceText.getLayoutX() );
    copyText.setLayoutY( sourceText.getLayoutY() );
    copyText.setText( sourceText.getText() );
    copyText.setOpacity( sourceBackground.getOpacity() );
  
    copyGroup.getChildren().addAll( copyBackground, copyText );
 
    return copyGroup;
}

The preceding sections, Controller and Sprite Class, set up the game.  The next section will focus on the movement of the Sprites.

Movement

Back to the Sprite class, I publish a relocate() method that will move the Sprite.   This is used in the shuffle() operation that will randomize the starting display.  There is also a MouseEvent EventHandler implemented as a Lambda that moves the pieces in response to drag events.  This also controls the highlighting and view changes of the Sprite in response to events.

EventHandler<MouseEvent> mouseHandler = (evt) -> {
  
  if( evt.getEventType() == MouseEvent.MOUSE_ENTERED && !evt.isPrimaryButtonDown()) {
   
    if( !Sprite.this.highlight.isVisible() ) {
      Sprite.this.normal.setVisible(false);
      Sprite.this.highlight.setVisible(true);
      Sprite.this.drag.setVisible(false);
      Sprite.this.error.setVisible(false);
    }
   
  } else if( evt.getEventType() == MouseEvent.MOUSE_EXITED && !evt.isPrimaryButtonDown() ) {

    if( !Sprite.this.normal.isVisible() ) {
      Sprite.this.normal.setVisible(true);
      Sprite.this.highlight.setVisible(false);
      Sprite.this.drag.setVisible(false);
      Sprite.this.error.setVisible(false);
    }
   
  } else if( evt.getEventType() == MouseEvent.MOUSE_DRAGGED ) {
   
    if( !Sprite.this.drag.isVisible() ) {

      Sprite.this.normal.setVisible( false );
      Sprite.this.highlight.setVisible(false);
      Sprite.this.drag.setVisible(true);
      Sprite.this.error.setVisible(false);
    }

    if( mouseInSpriteX == -1.0d || mouseInSpriteY == -1.0d ) {
    
      Point2D spriteInParent = Sprite.this.container.localToParent( 
        Sprite.this.container.getLayoutBounds().getMinX(), 
        Sprite.this.container.getLayoutBounds().getMinY() );
    
      double spriteMinX = spriteInParent.getX();     
      double spriteMinY = spriteInParent.getY();
    
      mouseInSpriteX = evt.getSceneX() - spriteMinX;
      mouseInSpriteY = evt.getSceneY() - spriteMinY;
    
    } else {
   
      Sprite.this.container.relocate(
        evt.getSceneX() - mouseInSpriteX, 
        evt.getSceneY() - mouseInSpriteY      
       );
    }
   
  } else if( evt.getEventType() == MouseEvent.MOUSE_RELEASED ) {
   
    if( mouseInSpriteX != -1.0d && mouseInSpriteY != -1.0d ) {
    
      mouseInSpriteX = -1.0d;
      mouseInSpriteY = -1.0d;
    
      Sprite.this.normal.setVisible(true);
      Sprite.this.highlight.setVisible(false);
      Sprite.this.drag.setVisible(false);
      Sprite.this.error.setVisible(false);

      }
    }
  };

Entered and Exited control the normal to highlight transition.  The drag event will show the drag view (transparent).  The relocate() method is called once the relocation is offset by where in the Sprite the mouse was initially placed.

Scoring

In the Controller class, there is a check() @FXML method tied to the bottom Button of the same name.  check() marches through the expected results and a sorted -- by position -- list of Sprites.  If each iteration matches (ex "A" is the top most, "B" is after "A", etc), the check passes.  Otherwise, the check fails.  In both cases a dialog is presented.  An animation of the error view on the Sprite is used to highlight the problem Sprites.

@FXML
public void check() { 

  Collections.sort( sprites, spriteYComparator );
  
  boolean allMatch = true;
  
  List spritesToFlag = new ArrayList<>();
  for( int i=0; i {
      if (response == ButtonType.YES) {
         shuffle();
      } else { 
       Platform.exit();
      }
    });  
    
  } else {
   
    Alert alert = new Alert(
      AlertType.WARNING, 
      "Incorrect. Try again?"
    );
   
    alert.showAndWait();
   
    spritesToFlag
       .stream()
       .forEach((sp)->sp.flagAsError());
  
  }
}

Finally, the shuffle() method produces the starting display.  Sprites are scattered across the background via a relocate() call.

@FXML
public void shuffle() {
  
  Bounds bounds = background.getLayoutBounds();
  
  double minx = bounds.getMinX();
  double miny = bounds.getMinY();

  Random random = new Random();
  
  for( Sprite sp : sprites ) {   
    double maxx = bounds.getMaxX() - sp.container.getWidth();
    double maxy = bounds.getMaxY() - sp.container.getHeight();
    double x = random.nextDouble() * (maxx - minx);
    double y = random.nextDouble() * (maxy - miny);
    sp.relocate(x, y);
  } 
}

This blog post demonstrated how to make a simple game using JavaFX and Scene Builder.  Although the whole app could be coded exclusively in Java (no FXML), I'm in a better position from a graphic design perspective to maintain the shapes. For example, I can apply different stylesheets to Scene Builder to give my app a different skin.  And I can quickly make adjustments since I see all the views at once.

As developers, we're often focused on delivering the functionality behind the controls.  But in today's app-heavy world, we're going to be increasingly judged on style points.


No comments:

Post a Comment