Trial play address, currently only suitable for PC. The source code

The originator of hexagon game should be this HEX-FRVR. The original author developed the game using piXI game engine. In line with the concept of rapid development, this game adopts Cocos Creator and uses HEX-FRVR as UI. In the process of learning, there are various ways to achieve reference. This source code is for learning use only, thank you.

preview

Function is introduced

Hexagon game is tetris in essence, understanding this for the next development will be very helpful.

The functions of the game are as follows:

  • [X] Hexagon checkerboard drawing, square random generation
  • The determination of whether [x] squares can fall into the board
  • [X] Block elimination and end of game determination
  • [x] various animation effects
  • [x] Game scoring

cocos creator

Before talking about game development ideas, it is recommended to learn about Cocos Creator

  • The document
  • API

The apis you must know are:

  • Game
  • Canvas
  • Scene
  • Node
  • Component
  • Sprite
  • Texture2D
  • Director
  • loader
  • Event
  • Touch
  • Action
  • Vec2
  • Animation
  • AnimationClip
  • Prefab
  • sys

Among them, Node, Event and Vec2 are the focus of this game development.

The development train of thought

The following describes the development ideas one by one from the functions.

Drawing board

The board uses a hexagonal grid layout, and hexagonal grids are not as common in video games as square grids. Let’s take a look at the hexagonal grid.

Hexagonal grid

Hexagonal meshes discussed in this paper use regular hexagons. There are two typical orientations for hexagonal meshes: horizontal (with vertices up) and vertical square (with edges up). In this game, the vertex is up.

If you’re careful, you’ll notice that there’s something like a coordinate system, called axial coordinates.

Axis coordinates

An axial coordinate system, sometimes called a trapezoidal coordinate system, is a coordinate system constructed by taking two of the three coordinates of a cubic coordinate system. And since we have the constraint x plus y plus z is equal to 0, the third coordinate is actually redundant. Axis coordinates are suitable for map data storage as well as display coordinates for players. Like cubic coordinates, you can also use basic operations like addition, subtraction, multiplication, and division in Cartesian coordinates.

There are many kinds of cubic coordinate systems, and therefore, of course, many kinds of axial coordinate systems derived from them. In this game, q = x and r = z are selected. Here Q stands for columns and R stands for rows.

Offset coordinates are the first coordinate system that comes to mind because they can directly use the Cartesian coordinates of the square grid. Unfortunately, an axis in an offset coordinate system always looks out of place and can eventually complicate matters. Cubic and axial coordinates work well together and the algorithm is simpler, although map storage becomes slightly more complicated. Therefore, using cubic/axial coordinates is relatively simple.

From hexagonal mesh to pixels

Now that you have a general idea of what a hexagonal grid is, see how to convert a hexagonal grid into pixels.

If you use axial coordinates, you can first observe the unit vector shown in the figure below. In the figure below, the arrow A→Q represents the unit vector of the Q axis and A→R represents the unit vector of the R axis. The pixel coordinates are q_basis _ q + R_basis _ R. For example, point B at (1, 1) is equal to the sum of the unit vectors q and R.

When the grid is horizontal, the height of the hexagon is height = size * 2. The vertical distance between adjacent hexagons is vertical = height * 3/4.

The width of a hexagon is width = SQRT (3)/2 * height. The horizontal distance between adjacent hexagons is horizontal = width.

For this game, the center point of the board is (0,0). From the known hexagonal grid coordinates (regular hexagon) and the height of the hexagon, the coordinates of each regular hexagon can be obtained. The following pixel conversion code can be obtained:

  hex2pixel(hex, h) {
    let size = h / 2;
    let x = size * Math.sqrt(3) * (hex.q + hex.r / 2);
    let y = ((size * 3) / 2) * hex.r;
    return cc.p(x, y);
  }
Copy the code

Grid coordinate system generation

Coordinate system to pixel problem solved, next, need to obtain the game in the hexagonal grid layout of the corresponding coordinate system.

The essence of this problem is the map storage of the axis coordinate system. 8)

For a hexagonal layout with radius N, if N = Max (abs(x), abs(y), abs(z), first_column[r] == -n-min (0, r). Finally you will access array[r][q + N + min(0, r)]. However, since we may use some r < 0 positions as starting points, we must also deviate, with array[r + N][q + N + min(0, r)].

For example, in this game, the board is a hexagonal grid layout with 5 boundary hexagons, and the generated coordinate system storage code is as follows:

  setHexagonGrid() {
    this.hexSide = 5;
    this.hexSide--;
    for (let q = -this.hexSide; q <= this.hexSide; q++) {
      let r1 = Math.max(-this.hexSide, -q - this.hexSide);
      let r2 = Math.min(this.hexSide, -q + this.hexSide);
      for (let r = r1; r <= r2; r++) {
        let col = q + this.hexSide;
        let row = r - r1;
        if (!this.hexes[col]) {
          this.hexes[col] = [];
        }
        this.hexes[col][row] = this.hex2pixel({ q, r }, this.tileH); }}}Copy the code

Hexagonal grid layout with 6 boundaries and 61 hexagons in total. Then, you just need to iterate over the background to complete the drawing of the board.

  setSpriteFrame(hexes) {
    for (let index = 0; index < hexes.length; index++) {
      let node = new cc.Node('frame');
      let sprite = node.addComponent(cc.Sprite);
      sprite.spriteFrame = this.tilePic;
      node.x = hexes[index].x;
      node.y = hexes[index].y;
      node.parent = this.node;
      hexes[index].spriteFrame = node;
      this.setShadowNode(node);
      this.setFillNode(node);
      this.boardFrameList.push(node); }}Copy the code

At this point, the checkerboard is finished.

Cube random generation

The shape of the block can be varied, first look at the game agreed in advance of 23 shapes.

It is not difficult to implement these 23 shapes based on the knowledge of the previous hexagonal mesh. You just need to agree on the axis coordinates for each shape.

The code configuration is as follows:

const Tiles = [
  {
    type: 1.list[[[:0.0]]]}, {type: 2.list[[[:1.- 1], [0.0], [1.0], [0.1]],
      [[0.0], [1.0], [- 1.1], [0.1]],
      [[0.0], [1.0], [0.1], [1.1]]]}, {type: 3.list[[[:0.- 1], [0.0], [0.1], [0.2]],
      [[0.0], [1.- 1], [- 1.1], [2 -.2]],
      [[- 1.0], [0.0], [1.0], [2.0]]]}, {type: 4.list[[[:0.0], [0.1], [0.- 1], [- 1.0]],
      [[0.0], [0.- 1], [1.- 1], [- 1.1]],
      [[0.0], [0.1], [0.- 1], [1.0]],
      [[0.0], [1.0], [- 1.0], [1.- 1]],
      [[0.0], [1.0], [- 1.0], [- 1.1]]]}, {type: 5.list[[[:0.0], [0.1], [0.- 1], [1.- 1]],
      [[0.0], [1.- 1], [- 1.1], [- 1.0]],
      [[0.0], [1.- 1], [- 1.1], [1.0]],
      [[0.0], [1.0], [- 1.0], [0.- 1]],
      [[0.0], [1.0], [- 1.0], [0.1]]]}, {type: 6.list[[[:0.- 1], [- 1.0], [- 1.1], [0.1]],
      [[- 1.0], [0.- 1], [1.- 1], [1.0]],
      [[0.- 1], [1.- 1], [1.0], [0.1]],
      [[- 1.1], [0.1], [1.0], [1.- 1]],
      [[- 1.0], [- 1.1], [0.- 1], [1.- 1]],
      [[- 1.0], [- 1.1], [0.1], [1.0}]]]];Copy the code

Since the probability of the occurrence of squares is not involved, random is simply used to achieve the random generation of squares.

const getRandomInt = function(min, max) {
  let ratio = cc.random0To1();
  return min + Math.floor((max - min) * ratio);
};
Copy the code

I like the simple UI style. It is very suitable for the beginning of game development. Next, deal with the game interaction logic.

Squares fall into checkerboard logic

The interaction between block and board is Drag and Drop. Currently, there is no Drag related component in Cocos Creator, which is simulated by touch events. In the process of block touchmove, we need to deal with two things. First, we need to detect whether the Chinese block crosses with the board during the dragging process, which is the so-called collision detection in the game. Cc provides corresponding collision components, but it is not flexible enough, because what we need to get is the coincidence relation between block and board (PS: Cc provides a number of apis for this, mostly related to VEC2. Second, check whether squares can fall into the board.

Collision detection (coincidence determination)

Square and checkerboard are actually composed of regular hexagons, there is a relatively simple way to judge whether there is any overlap, that is, to judge the distance between the two hexagons, if less than the set value, it is considered that there is overlap.

For simplicity, the origin of the coordinate system of the parent node of the checkerboard and the square is set to be the same (center point). The cocos coordinate system is a reference to this

Since the square is positioned relative to its parent center point, and its parent is positioned relative to the Canvas, we can use cc.padd (this.node.position, tile.position) to get the square’s coordinate value relative to the origin of the board. The hexagon coordinates are then iterated through the board to check which hexagon coordinates are overlaps with the board. The relevant codes are as follows:

  checkCollision(event) {
    const tiles = this.node.children; // this.node is the parent of the block, and dragging changes the coordinates of the node
    this.boardTiles = []; // Save the overlap between the board and the square.
    this.fillTiles = []; // Save the currently overlapped portion of the box.
    for (let i = 0; i < tiles.length; i++) {
      const tile = tiles[i];
      const pos = cc.pAdd(this.node.position, tile.position); // pAdd is an early API provided by CC, which can be replaced by vector addition in VEC2
      const boardTile = this.checkDistance(pos);
      if (boardTile) {
        this.fillTiles.push(tile);
        this.boardTiles.push(boardTile);
      }
    }
  },
  checkDistance(pos) {
    const distance = 50;
    const boardFrameList = this.board.boardFrameList;
    for (let i = 0; i < boardFrameList.length; i++) {
      const frameNode = boardFrameList[i];
      const nodeDistance = cc.pDistance(frameNode.position, pos);
      if (nodeDistance <= distance) {
        returnframeNode; }}},Copy the code

In the process of dragging, the real-time preservation of the board has a coincidence of the hexagon, used to determine whether the square can fall into the board

Move later determine

Pieces are considered playable as long as the number of squares matches the number of fillable parts of the board (there are no squares).

checkCanDrop() {
    const boardTiles = this.boardTiles; // The current board overlaps with the square.
    const fillTiles = this.node.children; // The total number of squares currently being dragged
    const boardTilesLength = boardTiles.length;
    const fillTilesLength = fillTiles.length;

    // If the current board overlaps zero squares and the number of squares does not match, then the board cannot be played.
    if (boardTilesLength === 0|| boardTilesLength ! = fillTilesLength) {return false;
    }

    // If there are squares in the box, it is judged that the box cannot be played.
    for (let i = 0; i < boardTilesLength; i++) {
      if (this.boardTiles[i].isFulled) {
        return false; }}return true;
  },
Copy the code

Move later hints

After obtaining the value of falling or not, the user needs to be given a hint of falling. One approach here is to create a child node named shadowNode for each checkerboard node before the checkerboard is generated. Then all you need to do is change the spriteFrame of the eligible node to the spriteFrame of the current drag block and reduce the opacity. The code is as follows:

  dropPrompt(canDrop) {
    const boardTiles = this.boardTiles;
    const boardTilesLength = boardTiles.length;
    const fillTiles = this.fillTiles;

    this.resetBoardFrames();
    if (canDrop) {
      for (let i = 0; i < boardTilesLength; i++) {
        const shadowNode = boardTiles[i].getChildByName('shadowNode');
        shadowNode.opacity = 100;
        constspriteFrame = fillTiles[i].getComponent(cc.Sprite).spriteFrame; shadowNode.getComponent(cc.Sprite).spriteFrame = spriteFrame; }}}Copy the code

Fall into the logic

At this point, the block’s TouchMove event is added. The next thing you need to do is do the logic after the drag is done.

There are two cases where a cube can fall, and a cube can’t fall. The decision whether to fall in has already been obtained. The next step is to add the appropriate processing.

All you need to do is add squares to the board, and then randomly generate new squares. Can’t fall to return the dragged square to its original position.

The method of adding squares is similar to the method of dropping hints mentioned before. A new node named fillNode is added under each grid node in the board, and squares are related to this node.

  tileDrop() {
    this.resetBoardFrames();
    if (this.checkCanDrop()) {
      const boardTiles = this.boardTiles;
      const fillTiles = this.fillTiles;
      const fillTilesLength = fillTiles.length;

      for (let i = 0; i < fillTilesLength; i++) {
        const boardTile = boardTiles[i];
        const fillTile = fillTiles[i];
        const fillNode = boardTile.getChildByName('fillNode');
        const spriteFrame = fillTile.getComponent(cc.Sprite).spriteFrame;

        boardTile.isFulled = true;
        fillNode.getComponent(cc.Sprite).spriteFrame = spriteFrame;
        this.resetTile();
      }
      this.board.curTileLength = fillTiles.length;
      this.board.node.emit('dropSuccess');
    } else {
      this.backSourcePos();
    }
    this.board.checkLose();
  }
Copy the code

Eliminate the logic

The board has, can also judge whether the square can fall into the board. The next thing to do is to eliminate logic processing, before said, hexagonal elimination game is a derivative version of Tetris, in fact, there are several elimination directions, look at the picture:

If you think of the checkerboard as an array, add [0,1,2…..] from the left. , the following elimination rules can finally be obtained:

const DelRules = [
  / / left oblique
  [0.1.2.3.4],
  [5.6.7.8.9.10],
  [11.12.13.14.15.16.17],
  [18.19.20.21.22.23.24.25],
  [26.27.28.29.30.31.32.33.34],
  [35.36.37.38.39.40.41.42],
  [43.44.45.46.47.48.49],
  [50.51.52.53.54.55],
  [56.57.58.59.60]./ / right Angle
  [26.35.43.50.56],
  [18.27.36.44.51.57],
  [11.19.28.37.45.52.58],
  [5.12.20.29.38.46.53.59],
  [0.6.13.21.30.39.47.54.60],
  [1.7.14.22.31.40.48.55],
  [2.8.15.23.32.41.49],
  [3.9.16.24.33.42],
  [4.10.17.25.34]./ / level
  [0.5.11.18.26],
  [1.6.12.19.27.35],
  [2.7.13.20.28.36.43],
  [3.8.14.21.29.37.44.50],
  [4.9.15.22.30.38.45.51.56],
  [10.16.23.31.39.46.52.57],
  [17.24.32.40.47.53.58],
  [25.33.41.48.54.59],
  [34.42.49.55.60]].Copy the code

Now that you have the rule, add the elimination logic and look directly at the code:

  deleteTile() {
    let fulledTilesIndex = []; // Store the index of squares on the board
    let readyDelTiles = []; // Store blocks to be eliminated
    const boardFrameList = this.boardFrameList;
    this.isDeleting = true; // The block is being cleared to act as an asynchronous status lock when animation is added later
    this.addScore(this.curTileLength, true);

    // Get the information about the squares in the checkerboard
    for (let i = 0; i < boardFrameList.length; i++) {
      const boardFrame = boardFrameList[i];
      if(boardFrame.isFulled) { fulledTilesIndex.push(i); }}for (let i = 0; i < DelRules.length; i++) {
      const delRule = DelRules[i]; // Eliminate rule fetching
      // Get the intersection of the array of rules and the array of existing squares
      let intersectArr = _.arrIntersect(fulledTilesIndex, delRule);
      if (intersectArr.length > 0) {
        // Check whether the two arrays are the same. If they are, add squares to the array to be eliminated
        const isReadyDel = _.checkArrIsEqual(delRule, intersectArr);
        if(isReadyDel) { readyDelTiles.push(delRule); }}}// Start elimination
    let count = 0;
    for (let i = 0; i < readyDelTiles.length; i++) {
      const readyDelTile = readyDelTiles[i];
      for (let j = 0; j < readyDelTile.length; j++) {
        const delTileIndex = readyDelTile[j];
        const boardFrame = this.boardFrameList[delTileIndex];
        const delNode = boardFrame.getChildByName('fillNode');
        boardFrame.isFulled = false;
        // You can add the corresponding elimination animation here
        const finished = cc.callFunc((a)= > {
          delNode.getComponent(cc.Sprite).spriteFrame = null;
          delNode.opacity = 255;
          count++;
        }, this);
        delNode.runAction(cc.sequence(cc.fadeOut(0.3), finished)); }}if(count ! = =0) {
      this.addScore(count);
      this.checkLose();
    }

    this.isDeleting = false;
  }
Copy the code

End of game logic

If all three squares fail to fit on the board, the game is considered over.

Firstly, the information of the unfilled checkerboard grid is obtained, and then the three squares are put into the unfilled area one by one to judge whether it can be put in. The code is as follows:

  checkLose() {
    let canDropCount = 0;
    const tiles = this.node.children;
    const tilesLength = tiles.length;
    const boardFrameList = this.board.boardFrameList;
    const boardFrameListLength = boardFrameList.length;

    // TODO:Invalid detection exists, which can be optimized
    for (let i = 0; i < boardFrameListLength; i++) {
      const boardNode = boardFrameList[i];
      let srcPos = cc.p(boardNode.x, boardNode.y);
      let count = 0;
      if(! boardNode.isFulled) {// Filter out the unfilled checkerboard grid
        for (let j = 0; j < tilesLength; j++) {
          let len = 27; // Set the minimum spacing for overlap

          // Move the square to the origin of the unfilled checkerboard grid and get the current block coordinates of each party
          let tilePos = cc.pAdd(srcPos, cc.p(tiles[j].x, tiles[j].y));

          // Walk through the checkerboard to determine whether the hexagon can fit into the square
          for (let k = 0; k < boardFrameListLength; k++) {
            const boardNode = boardFrameList[k];
            let dis = cc.pDistance(cc.p(boardNode.x, boardNode.y), tilePos);
            if(dis <= len && ! boardNode.isFulled) { count++; }}}if(count === tilesLength) { canDropCount++; }}}if (canDropCount === 0) {
      return true;
    } else {
      return false; }}Copy the code

The scoring system

The scoring rules vary depending on your needs. General square into and eliminate can add extra points.

  scoreRule(count, isDropAdd) {
    let x = count + 1;
    let addScoreCount = isDropAdd ? x : 2 * x * x;
    return addScoreCount;
  }
Copy the code

Thank you

The project belongs to the entry level. I got to know the game development of Cocos Creator for the first time, mostly referring to some open source hexagonal games on the Internet. Thanks to open source, the project has incorporated some of its own methods, such as dealing with hexagonal grids, but eliminating rules requires more knowledge to improve. Write such an entry level article first, and then further, I hope to some people like me just contact game development can have some help. Later, we may talk about cocos Creator animation, particle systems, physics systems, WebGL, etc., with appropriate examples.

The source code

reference

  • hexagons
  • Hexagonal grid
  • LBXGame