Last time I figured out a way to solve our Tic Tac Toe board. Any winning move was printed out to the console. BORING!
This time I'm going to take a look at a couple of new things. First I want to change out the usage of a SGText node to use some images instead. The text node is nice and simple, but it just looks awful. Secondly, I want to signify the winner in a much nicer way.
Switching to Images
First things first. If I'm going to switch out the text node for images, I need to create my images.


K, done!
To switch out the text nodes for images, I'm going to update our GridCell. Values enum to contain the Image value.
public static enum Values { O, X; private Image image; private Values() { int imageWidth = (TicTacToe.GAME_SIZE.width / 3) - 1; int imageHeight = (TicTacToe.GAME_SIZE.height / 3) - 1; try { image = ImageUtils.loadImageFromClassPath("/images/" + toString() + ".png", imageWidth, imageHeight); } catch (Exception e) { e.printStackTrace(); } } private Image getImage() { return image; } }
As you can see, I've added a private image object, it's getter, and an empty constructor to the enum. In the constructor I use my ImageUtils to load the image from the classpath and resize it to fit into the cell size.
All that's left now is to update the GridCell to use a SGImage node instead of a SGText node, and update the setValue method to use the image instead of the string value.
public class GridCell extends SGGroup { // ... Values enum and other instance variables ... // private SGImage image; private Values value; public GridCell(int row, int column, Dimension size) { // ... // // Create the SGImage node. image = new SGImage(); // ... remove the text node translation ... // // add the image. add(image); } // ... Other getter methods ... // public SGImage getImage() { return image; } public void setValue(Values value) { this.value = value; // set the image node's image to the value's. image.setImage(value.getImage()); } }
As you can see the changes are very minor here, most of the work is done in the enum.
At this point you can see the image use in action, and it should look like this.

Prettifying the Winner
Now that we have images instead of text nodes, I really want to let the user know who has won in a much prettier way than some message in the console. I am going to split up the scene into two nodes. The first node will be the normal node which everything gets originally set in. The second node will be our "Winners" node. When a winner is found, I'll move the winning nodes into the winner's node, and then apply a gaussian blur on the normal node. Now, everything except the winning nodes will be blurred.
Splitting the Scene
First, I've updated the TicTacToe main class to have a static gameScene object and made it public so that anyone can access it within our game. I've also made the Grid static and globally available as well.
public class TicTacToe { // This is going to be the size of the game panel. public static Dimension GAME_SIZE = new Dimension(400, 400); // This is the game's scene. All nodes are added here. public static SGGroup gameScene = new SGGroup(); // The Tic Tac Toe game grid. public static Grid grid = new Grid(); public static void main(String[] args) { // ... rest of the main method ... // // add the grid to the scene node. gameScene.add(grid); // set the gameScene in our game panel. gamePanel.setScene(gameScene); gameFrame.setVisible(true); } }
Simple enough I hope. I also need to update the Grid to keep track of the GridCells it contains. This makes it easier later to get the GridCells which are winners. I'll explain in a bit.
public class Grid extends SGGroup { // ... Create the GridCellMouseListener ... // private GridCell[][] gridCells; public Grid() { // initialize the grid cells double array. gridCells = new GridCell[3][3]; for (int row = 0; row < 3; row++) { for (int column = 0; column < 3; column++) { // ... Create the GridCell and add the MouseListener ... // // add the new GridCell to the array. gridCells[row][column] = gridCell; // ... rest of the loop ... // } } } /** * Helpful method to get the GridCell objects contained in this Grid. */ public GridCell[][] getGridCells() { return gridCells; } }
If you remember from earlier the GridCells are actually translated over to their correct positions (by adding them to a SGTransform.Translate object) before they are actually added to the Grid object. When I start adding the GridCell's images to the winner's node, it will be very difficult if I am unable to get at the GridCells themselves.
One option to keeping track of the GridCells in a separate array within the Grid object is to use SGParent.getChildren. Technically I could use the Grid.getChildren method to find all of it's siblings. However because the GridCell is actually part of a translation node, I would need to get the child of that to get to the actual GridCell.
Basically, by adding the array, I've removed the need to do something like this.
Grid grid = TicTacToe.grid; List<SGNode> transforms = grid.getChildren(); List<GridCell> siblings = new ArrayList<GridCell>(); for(SGNode transform : transforms) { siblings.addAll(((SGParent)transform).getChildren()); }
Besides that, later when I'm adding winning cells to the winner's node, I'd have to loop through the siblings to find the right ones. The double array lets me grab those cells by index.
Now I update the GridCellMouseListener to create a winner's node in the game scene, and move the image node from each winning GridCell to it.
public class GridCellMouseListener extends SGMouseAdapter { // ... Instance variables and Constructor ... // @Override public synchronized void mouseClicked(MouseEvent event, SGNode node) { // ... No changes here ... // } /** * Essentially removes the GridCell's image node and adds it to a translate object to move it over to it's proper position. */ private SGNode externalizeGridCellImage(GridCell gridCell) { // the GridCells technically belong to a parentTranslation. Clone it and use it for the image. SGTransform.Translate parentTranslation = (SGTransform.Translate) gridCell.getParent(); // create a new translation for the image. SGTransform.Translate imageTranslation = SGTransform.createTranslation(0, 0, gridCell.getImage()); // set the image translation's location to the parent's location. imageTranslation.setTranslateX(parentTranslation.getTranslateX()); imageTranslation.setTranslateY(parentTranslation.getTranslateY()); return imageTranslation; } private void solve(GridCell gridCell) { int row = gridCell.getRow(); int column = gridCell.getColumn(); Grid grid = TicTacToe.grid; SGGroup winnersNode = null; if ((rowCounts[row] == 3) && (Math.abs(rowValues[row]) == 3)) { // there's 3 values at this row and the value is either 3 or -3 // we have a winnerFound. winnerFound = true; System.out.println("Winner " + gridCell.getValue() + " at row[" + (row + 1) + "]"); winnersNode = new SGGroup(); // Add all the images from GridCells in the same row to the winners node. for (GridCell sibling : grid.getGridCells()[row]) { winnersNode.add(externalizeGridCellImage(sibling)); } } else if ((columnCounts[column] == 3) && (Math.abs(columnValues[column]) == 3)) { // there's 3 values at this column and the value is either 3 or -3 // we have a winnerFound. winnerFound = true; System.out.println("Winner " + gridCell.getValue() + " at column[" + (column + 1) + "]"); winnersNode = new SGGroup(); // Add all the images from GridCells in the same column to the winners node. for (GridCell[] rowCells : grid.getGridCells()) { GridCell sibling = rowCells[column]; winnersNode.add(externalizeGridCellImage(sibling)); } } else if ((diagonalCounts[0] == 3) && (Math.abs(diagonalValues[0]) == 3)) { // there's 3 values at this diagonal and the value is either 3 or -3 // we have a winnerFound winnerFound = true; System.out.println("Winner " + gridCell.getValue() + " at diagonal[1]"); winnersNode = new SGGroup(); GridCell[][] gridCells = grid.getGridCells(); // Add all the images from GridCells in the first diagonal to the winners node. for (int rowCol = 0; rowCol < 3; rowCol++) { winnersNode.add(externalizeGridCellImage(gridCells[rowCol][rowCol])); } } else if ((diagonalCounts[1] == 3) && (Math.abs(diagonalValues[1]) == 3)) { // there's 3 values at this diagonal and the value is either 3 or -3 // we have a winnerFound winnerFound = true; System.out.println("Winner " + gridCell.getValue() + " at diagonal[2]"); winnersNode = new SGGroup(); GridCell[][] gridCells = grid.getGridCells(); // Add all the images from GridCells in the second diagonal to the winners node. for (int _row = 2, _col = 0; _col < 3; _row--, _col++) { winnersNode.add(externalizeGridCellImage(gridCells[_row][_col])); } } // if a winner was found, the winners node will not be null. if (winnersNode != null) { // add it to the gameScene. TicTacToe.gameScene.add(winnersNode); } } // ... Update methods ... // }
The externalizeGridCellImage method basically takes the GridCell's image node and moves it over to a SGTransform.Translate object that is equal to the GridCell's translate object.
The rest should be pretty simple to understand. When a winner is found, I simply loop through the winning cells and move their images to the winner's node. Now you can see why that double array in the Grid object is a necessity.
Focus?
Now that we have the winner's in their very own node and the rest of the game in it's own node it will be very trivial to actually add the Gaussian blur.
Update the solve method to move the Grid into a SGEffect object. Then simply set the effect to be a GaussianBlur effect.
private void solve(GridCell gridCell) { // ... Rest of the solve method ... // // if a winner was found, the winners node will not be null. if (winnersNode != null) { SGEffect gaussianBlur = new SGEffect(); gaussianBlur.setChild(grid); gaussianBlur.setEffect(new GaussianBlur()); TicTacToe.gameScene.add(gaussianBlur); // add it to the gameScene. TicTacToe.gameScene.add(winnersNode); } }
Done!

When I started this task I honestly thought that the Gaussian blur part would be the most difficult. It turned out that moving the nodes around turned out to be the most taxing. Adding a Gaussian blur effect was minimal. There are obviously things I can improve on - adding the blur there makes the last move very slow, I could easily move that into a worker thread which would make it much smoother. I could also add some animation - I think I'll do this in the next post.
Well, the code is attached at the end of this post. Enjoy!
Eric
| Attachment | Size |
|---|---|
| townsfolkdesigns.com_.zip | 21.95 KB |