Bubble Shooter Game in Unity: Part 8

As I mentioned before, a bubble shooter game looks better with a hex grid, specially when the targets are circles. A square grid populated with circles should not run matches diagonally, because it looks weird. So with a square grid you can have a maximum of four neighbors for each cell (diagonal neighbors look too far away when using circles). But with a hex grid you can have up to six neighbors.

Note: If you are building the type of bubble shooter where the “grid” responds to the impact of each shot, meaning there is some physics applied to it, you might consider dropping a grid altogether and use a graph tree instead.

Converting the square grid to a hex grid is super easy though.

I decided to offset the columns, because I thought it looked better, but you have the choice of offsetting either the columns or the rows.

The first change is in the Ball class.

public void SetBallPosition (Grid grid, int column, int row) {

this.grid = grid;
this.column = column;
this.row = row;

ballPosition = new Vector3 ( (column * grid.TILE_SIZE) - grid.GRID_OFFSET_X , grid.GRID_OFFSET_Y + (-row * grid.TILE_SIZE) , 0);

if (column % 2 == 0) {
ballPosition.y -= grid.TILE_SIZE * 0.5f;
}

transform.localPosition = ballPosition;

foreach (var go in colorsGO) {
go.SetActive(false);
}
}

That condition checking the column index... It shifts the Y of each cell by half its size.

Then we change the logic in the Grid class which collects a cell's neighbors:

List BallActiveNeighbors (Ball ball) {
var result = new List ();
if (ball.column + 1 < COLUMNS) { if (gridBalls [ball.row] [ball.column + 1].gameObject.activeSelf) result.Add (gridBalls [ball.row] [ball.column + 1]); } //left if (ball.column - 1 >= 0) {
if (gridBalls [ball.row] [ball.column - 1].gameObject.activeSelf)
result.Add (gridBalls [ball.row] [ball.column - 1]);
}
//top
if (ball.row - 1 >= 0) {
if (gridBalls [ball.row - 1] [ball.column].gameObject.activeSelf)
result.Add (gridBalls [ball.row - 1] [ball.column]);
}

//bottom
if (ball.row + 1 < ROWS) { if (gridBalls [ball.row + 1] [ball.column].gameObject.activeSelf) result.Add (gridBalls [ball.row + 1] [ball.column]); } if (ball.column % 2 == 0) { //bottom-left if (ball.row + 1 < ROWS && ball.column - 1 >= 0) {
if (gridBalls [ball.row + 1] [ball.column - 1].gameObject.activeSelf)
result.Add (gridBalls [ball.row + 1] [ball.column - 1]);
}

//bottom-right
if (ball.row + 1 < ROWS && ball.column + 1 < COLUMNS) { if (gridBalls [ball.row + 1] [ball.column + 1].gameObject.activeSelf) result.Add (gridBalls [ball.row + 1] [ball.column + 1]); } } else { //top-left if (ball.row - 1 >= 0 && ball.column - 1 >= 0) {
if (gridBalls [ball.row - 1] [ball.column - 1].gameObject.activeSelf)
result.Add (gridBalls [ball.row - 1] [ball.column - 1]);
}

//top-right
if (ball.row - 1 >= 0 && ball.column + 1 < COLUMNS) { if (gridBalls [ball.row - 1] [ball.column + 1].gameObject.activeSelf) result.Add (gridBalls [ball.row - 1] [ball.column + 1]); } } return result; }

If we are collecting the neighbors from a cell located in the shifted column we collect TOP, BOTTOM, LEFT, RIGHT, BOTTOM LEFT and BOTTOM RIGHT.

If the cell is in a regular column, we collect TOP LEFT and TOP RIGHT instead.

The same update in the logic is added to the method collecting the empty neighbors around a cell (used when we add a new Ball to the grid).

And that's it. I'm sure I'm forgetting something major in order to make this a proper game (like identifying when the grid is clear!) but the main chunk of the game logic is here. Have fun!

In the next few posts I'll cover some odd options and changes to the main engine.

Here is the final project.