How to handle Bejeweled-type game with group of sprites and 2D array?

I’m working on a sliding puzzle game with a 2D checkerboard layout. The game will not use physics, because it’s a tile based game with simple interactions, similar to Bejeweled or Sokoban.


I’m using a Phaser.group and add all the sprites to this group. This is convenient, because during the game there will be lots of adding and removing objects from the board.

But on the other hand, having it all in a 2D array would make it easier to check which objects are next to each other, adjacent or in a row etc. For example, you can do a simple for-loop to detect a row or column of same colored balls.

See my code below:

create: function ()
{
		// background
		this.background = this.add.image(48, 128, 'sprites', 'checkerboard');
  
		// group of game objects
		this.grpballs = this.add.group();

		// array of grid board layout
		this.mygrid = [];

		// initialise random objects
		for (var y=0; y < GRID_HEIGHT; y++) {
			this.mygrid[y] = [];
			for (var x=0; x < GRID_WIDTH; x++) {
				// initialise empty
				this.mygrid[y][x] = 0;

				// add random objects
				var ball = Phaser.Math.RND.between(0, 5);

				if (ball != 0) {
					// keep track in array
					this.mygrid[y][x] = ball;

					// add sprite to group
					var xpos = (x * 48) + 48;
					var ypos = (y * 48) + 128;
					var sprball = this.grpballs.create(xpos, ypos, 'sprites', 'ball_'+ball);
				};
			};
		};

The problem with the above code is that the mygrid array and the grpballs group of sprites are 2 separate things.

Ideally I would add the sprites to the 2D array, but then when the player moves for example the red ball from position 5,5 down to 5,8 that sprite should also move in the array from position 5,5, to 5,8.

So my question is:

  • What s the best way to organise the code and variables in a game with this type of 2D grid layout?
  • Or, is there an easy way to detect adjacent groups of sprites on a grid, using only a Phaser.group?

I personally love 2D arrays and would suggest using them in this scenario, since Phaser.groups are seemingly one dimensional and add slightly more overhead to your game.

With that being said, you appear to have a great start given your code. My only gripe with it is that I always perceive coordinates as (x, y), but you are initializing your grid as (y, x).

Now that you have the basic setup complete you can start adding convenience methods such as getPixelPos(gridPos), getClosestGridPos(pixelPos), getNeighbors(gridPos, radius = 1), swapGridPos(gridPos, gridPos), getGridPos(x, y) and more. I would supply you with pseudo code, but there is a bit too much to cover. Let me know if there is something more specific you need help with.

@Jake.Caron Are you suggesting to use the on-screen pixel positions of the sprite to figure out which grid position it is? That is not a good idea I think, because the balls will also be moving to slide, or shaking left right when removed etc.

I was just looking at the data element. Each sprite/gameobject had a data element, although I can’t find an example of how to use that. I guess spr.data = 3 or spr.data = {"objtyp": 3} or something?

I thought of another way, instead of just storing integers in the array, I guess I could store the sprites as array items? And then have helper functions to move them around.

// in a loop, add to array
var sprball = this.grpballs.create(xpos, ypos, 'sprites', 'ball_'+ball); 
this.mygrid[y][x] = sprball;

// helper function to move objects around
function SwitchBalls(x1, y1, x2, y2) {
	// update sprites in array
    var tmp = this.mygrid[y1][x1];
    this.mygrid[y1][x1] = this.mygrid[y2][x2];
    this.mygrid[y2][x2] = tmp;

	// also update screen positions
	this.mygrid[y1][x1].x = x1 * TILESIZE;
	this.mygrid[y1][x1].y = y1 * TILESIZE;
	
	this.mygrid[y2][x2].x = x2 * TILESIZE;
	this.mygrid[y2][x2].y = y2 * TILESIZE;
};

But will that also work for empty array items, where there is no sprite at all?

I always confuse the swap between (x, y) and (y, x) you get with 2d arrays. Recently I’ve been using a custom Grid class that looks something like (pseudocode incoming)

class Grid {
  constructor() {
    this.tiles = {};
  }

  set(x, y, value) {
    this.tiles[this._tileIndexName(x, y)] = value;
  }

  get(x, y) {
    return this.tiles[this._tileIndexName(x, y)];
  }

  _tileIndexName(x, y) {
    return `${x},${y}`;
  }
}

It lets me still think in/use an (x, y) notation for tiles. I didn’t include a method for iterating over all the tiles, but you could write one.

@Jake.Caron Are you suggesting to use the on-screen pixel positions of the sprite to figure out which grid position it is? That is not a good idea I think, because the balls will also be moving to slide, or shaking left right when removed etc.

Not quite. I am saying that you may want a convenience method that allows you to snap a draggable object to a location on the grid. For instance, if your user drags a sprite somewhere on the screen, you could call the method getClosestGridPos(pixelPosX, pixelPosY), which would return the closest point on your 2D grid. This may not be necessary depending on what you are trying to accomplish.

As far as storing sprites in your 2D array, I would change that to storing Nodes and your nodes can have properties such as: indexX, indexY, sprite, pixelX, pixelY.

This would allow you to change your SwitchBalls(x1, y1, x2, y2) function to:

// helper function to move objects around
function SwitchBalls(node1, node2) {
	// update sprites in array
    var tmp = node1.sprite;
    node1.sprite = node2.sprite;
    node2.sprite = node1.sprite;

	// also update screen positions
    node1.sprite.setPosition(node1.pixelX, node1.pixelY);
    node2.sprite.setPosition(node2.pixelX, node2.pixelY);
};