Ok, now that we've created a simple grid, let's add some action and get our X's and O's going. Let's also take a little time to clean up the code and make things a bit more Object Oriented.
Objectization
Previously the tic tac toe grid was made out of 9 SGShape nodes (Rectangles) that were added to a SGGroup. Looking back at this, it's a quick and dirty approach to drawing the tic tac toe grid. To achieve the end goal of creating a game, we'll need to take it a step further. We're going to need be able to click on our grid and put up an X or an O depending on who's turn it is.
Let's remove the grid creation from the main method and move it to a Grid class. We're also going to create a GridCell class which will contain the value of the cell - basically null (not set), X, or O. We're also going to introduce MouseEvent handling to our game.
The Grid
To help objectize the Grid, let's give our game a size and make it available to all our game components.
public class TicTacToe { // This is going to be the size of the game panel. public static Dimension GAME_SIZE = new Dimension(400, 400); public static void main(String[] args) { // Build our Game Panel. JSGPanel gamePanel = new JSGPanel(); gamePanel.setPreferredSize(GAME_SIZE); // Finish setting up the game panel color, and the JFrame as before. } // Rest of the main class. }
Now that all our components can quickly find out how big our game is the Grid class can easily create it's own grid. Here is the Grid class constructor.
public Grid() { Dimension gameSize = TicTacToe.GAME_SIZE; // the cell size is equal to 1/3 the game size minus 1 pixel for padding. Dimension cellSize = new Dimension((gameSize.width / 3) - 1, (gameSize.height / 3) - 1); for (int row = 0; row < 3; row++) { for (int column = 0; column < 3; column++) { GridCell gridCell = new GridCell(row, column, cellSize); gridCell.addMouseListener(gridCellMouseListener); // translate the whole grid cell group over. SGTransform.Translate translation = SGTransform.createTranslation(column * (cellSize.width + 2), row * (cellSize.height + 2), gridCell); add(translation); } } }
The grid cell sizing should look familiar. One thing to point out that's different is the use of the SGTransform.Translate class. This is one of the greatest features of the Scenegraph data structure. The GridCell is a SGGroup which consists of the Rectangle used as it's background (the white square) and a text node which will be used to show the X and O when the cell's value is set. We need all the contents of the GridCell to be together right? Previously I set the location of the cell's Rectangle which works great for the shape. However, if I wanted the text node to also be in the same location as the Rectangle, I would need to set the location on it as well. This would get very tedious and annoying really quick if my group had a lot of children.
Enter the SGTransform.Translate, by translating the entire group, I'm translating all the children as well.
Continuing on, let's take a look at the GridCell class, here are the important details.
public class GridCell extends SGGroup { public static enum Values { O, X } // instance variables - initialized in constructor. public GridCell(int row, int column, Dimension size) { this.row = row; this.column = column; rectangle = new Rectangle(size); SGShape rectShape = new SGShape(); rectShape.setShape(rectangle); rectShape.setFillPaint(Color.white); add(rectShape); text = new SGText(); // resize the font. Font textFont = new Font("default", Font.PLAIN, size.height); text.setFont(textFont); // translate the text node to the bottom since text 0x0 location is at it's bottom. SGTransform.Translate textTransform = SGTransform.createTranslation(0, size.height, text); add(textTransform); } // Other accessors. public void setValue(Values value) { this.value = value; text.setText(value.toString()); } }
Hopefully this all looks pretty straight forward. Our GridCell consists of the Rectangle used to represent it's square in the Grid, the row and column it sits in, and it's current value which is also represented in the SGText node. I'm using a SGTransform.Translate here to translate the text node to the bottom of the rectangle because text is rendered upwards making it's 0x0 location at the bottom left of the first character not the top left. I'm also creating a much larger font to be used.
I've also decided to use an enum for the possible values because there can really only ever be two, excluding "not set" or null.
Adding Action
If you are familiar with Swing at all, then you're familiar the event listener classes. Scenegraph is slightly different in that the SGMouseListener interface combines the MouseListener, MouseMotionListener, and the MouseWheelListener of Swing into one class.
All we need to do is create our GridCellMouseListener class and add it to the GridCells.
// excerpt from the Grid constructor above. gridCell.addMouseListener(gridCellMouseListener);
public class GridCellMouseListener extends SGMouseAdapter { // X = true, O = false; private boolean turn; public GridCellMouseListener() { } @Override public synchronized void mouseClicked(MouseEvent event, SGNode node) { GridCell gridCell = (GridCell) node; if (gridCell.getValue() == null) { // if the value isn't null, then the value is already set, ignore click. if (turn) { // it's X's turn. gridCell.setValue(GridCell.Values.X); } else { // it's O's turn. gridCell.setValue(GridCell.Values.O); } // change turns. turn = !turn; } } }
Again, this should be pretty self explanatory. I'm using a boolean to determine who's turn it is, true is equal to X's turn and false means it's O's turn. When the mouse is clicked in the we first check to make sure the cell hasn't been clicked before, if it has it will have a value. If it hasn't, we then check who's turn it is and set the cell's value accordingly. We then switch turns.
Here's a quick screencast of what this looks like:

Here's the source for today:
K, that was actually quite a bit today. Well, I hope it all made sense and you learned something.
Cheers!