本文由“壹伴编辑器”提供技术支

 

前言在上一篇文章中我们实现部分基础组件和管理脚本,那么本篇文章将和大家一起实现方块的生成与交换的逻辑。温馨提醒:本文含有大量代码和注释,请提前做好心理准备并认真阅读。话不多说,冲鸭!!!本文由“壹伴编辑器”提供技术支正文

生成方块

1. 新建脚本 GameUtil ,用来实现游戏中的各种算法,是游戏中最重要的模块之一。我这里暂时只实现了一个获取随机类型的函数:import { TileType } from "../type/Enum";
import GameConfig from "../../data/GameConfig";

export default class GameUtil {

    /**
     * 获取随机类型
     * @param exclude 需排除的类型
     */
    public static getRandomType(exclude: TileType[] = []):TileType {
        let types = GameConfig.types.concat();
        for (let i = 0; i < exclude.length; i++) {
            types.splice(types.indexOf(exclude[i]), 1);
        }
          return types[Math.floor(types.length * Math.random())];
    }

}2. 新建脚本 TileManager ,用来管理所有方块逻辑和操作实现,也是游戏最重要的模块之一。 2-1. 用二维数组变量 typeMap 和 tileMap 分别来装所有方块类型和组件,我们就可以根据二维坐标来获取特定的类型和组件;2-2. 根据 GameConfig 中的配置遍历生成类型表,然后再根据类型表生成方块:import Tile from "../component/Tile";
import { TileType } from "../type/Enum";
import GameConfig from "../../data/GameConfig";
import GameUtil from "../util/GameUtil";
import PoolManager from "./PoolManager";
import MapManager from "./MapManager";

const { ccclass, property } = cc._decorator;

@ccclass
export default class TileManager extends cc.Component {

    @property(cc.Node)
    private container: cc.Node = null; // 所有方块的容器

    private typeMap: TileType[][] = null; // 类型表:二维数组,保存所有方块的类型,方便计算

    private tileMap: Tile[][] = null; // 组件表:二维数组,保存所有方块 Tile 组件,方便读取

    private static instance: TileManager = null

    protected onLoad() {
        TileManager.instance = this;
    }

    public static init() {
        this.instance.generateInitTypeMap();
        this.instance.generateTiles();
    }

    /**
     * 生成初始的类型表
     */
    private generateInitTypeMap() {
        this.typeMap = [];
        for (let c = 0; c < GameConfig.col; c++) {
            let colSet: TileType[] = [];
            for (let r = 0; r < GameConfig.row; r++) {
                colSet.push(GameUtil.getRandomType());
            }
            this.typeMap.push(colSet);
        }
    }

    /**
     * 根据类型表生成方块
     */
    private generateTiles() {
        this.tileMap = [];
        for (let c = 0; c < GameConfig.col; c++) {
            let colTileSet: Tile[] = [];
            for (let r = 0; r < GameConfig.row; r++) {
                colTileSet.push(this.getTile(c, r, this.typeMap[c][r]));
            }
            this.tileMap.push(colTileSet);
        }
    }

    /**
     * 生成并初始化方块
     * @param x 横坐标
     * @param y 纵坐标
     * @param type 类型
     */
    private getTile(x: number, y: number, type: TileType): Tile {
        let node = PoolManager.get();
        node.setParent(this.container);
        node.setPosition(MapManager.getPos(x, y));
        let tile = node.getComponent(Tile);
        tile.init();
        tile.setCoord(x, y);
        tile.setType(type);
        tile.appear();
        return tile;
    }
}3. 将我们写好的 Tile 组件挂在 tile 预制体上,并将子节点 sprite 拖到 Tile 组件上,然后保存: [Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_c++预制体 tile4. 在场景中新建一个节点 managers ,然后把 ResManager 、 PoolManager 和 TileManager 脚本拖到 managers 节点下,并设置好他们各自的属性: [Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_数据_02managers 节点  [Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_c++_03  [Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_子节点_04  [Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_二维数组_05左:ResManager | 中:PoolManager | 右:TileManager5. 新建脚本 Game ,这个脚本为游戏的入口,启动游戏就靠它:import MapManager from "./manager/MapManager";
import TileManager from "./manager/TileManager";

const { ccclass, property } = cc._decorator;

@ccclass
export default class Game extends cc.Component {

    protected start() {
        MapManager.init();
        TileManager.init();
    }

}6. 在场景下新建 main 节点,并将 Game 组件拖到该节点上。保存场景,点击预览,就可以看到已经成功生成方块了: [Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_c++_06     [Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_二维数组_07左:main 节点 | 右:预览画面

交换方块

1. 我们交换方块有两种方式:点击和滑动。我们先在 Enum 文件中添加一个枚举 SlidDirection 来表示滑动方向:/**
 * 滑动方向
 */
export enum SlidDirection {
    Up = 1, // 上
    Down, // 下
    Left, // 左
    Right, // 右
}2. 向 GameUtil 中添加一个计算滑动方向的函数和一个根据坐标和方向计算目标坐标的函数:/**
 * 获取滑动的方向
 * @param startPos 开始位置
 * @param endPos 结束位置
 */
public static getSlidDirection(startPos: cc.Vec2, endPos: cc.Vec2): SlidDirection {
    let offsetX = endPos.x - startPos.x; // x 偏移
    let offsetY = endPos.y - startPos.y; // y 偏移

    if (Math.abs(offsetX) < Math.abs(offsetY)) {
        return offsetY > 0 ? SlidDirection.Up : SlidDirection.Down
    } else {
        return offsetX > 0 ? SlidDirection.Right : SlidDirection.Left;
    }
}

/**
 * 获取指定方向的坐标
 * @param coord 坐标
 * @param direction 方向
 */
public static getCoordByDirection(coord: Coordinate, direction: SlidDirection) {
    switch (direction) {
        case SlidDirection.Up:
            return coord.y === GameConfig.row - 1 ? null : Coord(coord.x, coord.y + 1);
        case SlidDirection.Down:
            return coord.y === 0 ? null : Coord(coord.x, coord.y - 1);
        case SlidDirection.Left:
            return coord.x === 0 ? null : Coord(coord.x - 1, coord.y);
        case SlidDirection.Right:
            return coord.x === GameConfig.col - 1 ? null : Coord(coord.x + 1, coord.y);
    }
}3. 接下来在 TileManager 中更新并添加了很多变量和函数来实现方块交换的逻辑。高能预警!!!接下来有大量代码和注释,虽然我每个函数都加了注释,但是我还是建议你直接拉到底部阅读完整文件,那样会比较好理解:
private selectedCoord: Coordinate = null; // 当前已经选中的方块坐标

private tileTouchStartPos: cc.Vec2 = null; // 滑动开始位置

protected onLoad() {
    TileManager.instance = this;

    GameEvent.on(TileEvent.TouchStart, this.onTileTouchStart, this);
    GameEvent.on(TileEvent.TouchEnd, this.onTileTouchEnd, this);
    GameEvent.on(TileEvent.TouchCancel, this.onTileTouchCancel, this);
}

protected onDestroy() {
    GameEvent.off(TileEvent.TouchStart, this.onTileTouchStart, this);
    GameEvent.off(TileEvent.TouchEnd, this.onTileTouchEnd, this);
    GameEvent.off(TileEvent.TouchCancel, this.onTileTouchCancel, this);
}

/**
 * 方块的 touchstart 回调
 * @param coord 坐标
 * @param pos 点击位置
 */
private onTileTouchStart(coord: Coordinate, pos: cc.Vec2) {
    cc.log('点击 | coord: ' + coord.toString() + ' | type: ' + this.getTypeMap(coord));
    // 是否已经选中了方块
    if (this.selectedCoord) {
        // 是否同一个方块
        if (!this.selectedCoord.compare(coord)) {
            // 判断两个方块是否相邻
            if (this.selectedCoord.isAdjacent(coord)) {
                this.tryExchangeByTouch(this.selectedCoord, coord);
                this.setSelectedTile(null); // 交换后重置
            } else {
                this.tileTouchStartPos = pos;
                this.setSelectedTile(coord); // 更新选中的方块坐标
            }
        } else {
            this.tileTouchStartPos = pos;
        }
    } else {
        this.tileTouchStartPos = pos;
        this.setSelectedTile(coord);
    }
}

/**
 * 方块的 touchend 回调
 */
private onTileTouchEnd() {
    this.tileTouchStartPos = null;
}

/**
 * 方块的 touchcancel 回调
 * @param coord 坐标
 * @param cancelPos 位置
 */
private onTileTouchCancel(coord: Coordinate, cancelPos: cc.Vec2) {
    if (!this.tileTouchStartPos) return;
    this.tryExchangeBySlid(coord, GameUtil.getSlidDirection(this.tileTouchStartPos, cancelPos));
    this.tileTouchStartPos = null;
}

/**
 * 设置选中的方块
 * @param coord 坐标
 */
private setSelectedTile(coord: Coordinate) {
    this.selectedCoord = coord;
}

/**
 * 尝试点击交换方块
 * @param coord1 1
 * @param coord2 2
 */
private tryExchangeByTouch(coord1: Coordinate, coord2: Coordinate) {
    cc.log('尝试点击交换方块 | coord1: ' + coord1.toString() + ' | coord2: ' + coord2.toString());
    this.tryExchange(coord1, coord2);
}

/**
 * 尝试滑动交换方块
 * @param coord 坐标
 * @param direction 方向
 */
private tryExchangeBySlid(coord: Coordinate, direction: SlidDirection) {
    cc.log('点击交换方块 | coord1: ' + coord.toString() + ' | direction: ' + direction);
    let targetCoord = GameUtil.getCoordByDirection(coord, direction);
    if (targetCoord) {
        this.tryExchange(coord, targetCoord);
        this.setSelectedTile(null);
    }
}

/**
 * 尝试交换方块
 * @param coord1 1
 * @param coord2 2
 */
private async tryExchange(coord1: Coordinate, coord2: Coordinate) {
    await this.exchangeTiles(coord1, coord2);
}

/**
 * 交换方块
 * @param coord1 1
 * @param coord2 2
 */
private async exchangeTiles(coord1: Coordinate, coord2: Coordinate) {
    // 保存变量
    let tile1 = this.getTileMap(coord1);
    let tile2 = this.getTileMap(coord2);
    let tile1Type = this.getTypeMap(coord1);
    let tile2Type = this.getTypeMap(coord2);
    // 交换数据
    tile1.setCoord(coord2);
    tile2.setCoord(coord1);
    this.setTypeMap(coord1, tile2Type);
    this.setTypeMap(coord2, tile1Type);
    this.setTileMap(coord1, tile2);
    this.setTileMap(coord2, tile1);
    // 交换方块
    cc.tween(tile1.node).to(0.1, { position: MapManager.getPos(coord2) }).start();
    cc.tween(tile2.node).to(0.1, { position: MapManager.getPos(coord1) }).start();
    await new Promise(res => setTimeout(res, 100));
}

/**
 * 设置类型表
 * @param x 横坐标
 * @param y 纵坐标
 */
private getTypeMap(x: number | Coordinate, y?: number): TileType {
    return typeof x === 'number' ? this.typeMap[x][y] : this.typeMap[x.x][x.y];
}

/**
 * 获取类型
 * @param x 横坐标
 * @param y 纵坐标
 * @param type 类型
 */
private setTypeMap(x: number | Coordinate, y: number | TileType, type?: TileType) {
    if (typeof x === 'number') this.typeMap[x][y] = type;
    else this.typeMap[x.x][x.y] = <TileType>y;
}

/**
 * 获取组件
 * @param x 横坐标
 * @param y 纵坐标
 */
private getTileMap(x: number | Coordinate, y?: number): Tile {
    return typeof x === 'number' ? this.tileMap[x][y] : this.tileMap[x.x][x.y]; }

/**
 * 设置组件表
 * @param x 横坐标
 * @param y 纵坐标
 * @param type 组件
 */
private setTileMap(x: number | Coordinate, y: number | Tile, tile?: Tile) {
    if (typeof x === 'number') this.tileMap[x][<number>y] = tile;
    else this.tileMap[x.x][x.y] = <Tile>y;
}

4. 以上逻辑写好之后,我们就已经实现了交换方块的逻辑了:

[Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_初始化_08点点点,滑滑滑

5. 为了更直观的让我们知道当前选中了哪个方块,我们接着作。

5-1.  在 tileContainer 节点上方添加一个空节点 underLayer ,将资源中的 selectFrame 图片拖到 underLayer 节点下,调整为合适的大小,然后将 selectFrame 节点关闭:

[Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_c++_09selectFrame

5-2. 给 TileContainer 添加一个属性,并对 setSelectedTile 函数进行升级:

@property(cc.Node)
private selectFrame: cc.Node = null; // 选中框

/**
 * 设置选中的方块
 * @param coord 坐标
 */
private setSelectedTile(coord: Coordinate) {
    this.selectedCoord = coord;
    if (coord) {
        this.selectFrame.active = true;
        this.selectFrame.setPosition(MapManager.getPos(coord));
    } else {
        this.selectFrame.active = false;
    }
}

5-3. 将 selectFrame 节点拖到 TileContainer 组件上之后,再预览游戏:

[Cocos Creator] 制作简版消消乐(三):实现方块的生成与交换_二维数组_10wow awsome

★ 到这里本篇文章内容就结束了,不知道你看懵了没,反正我是写懵了。下篇文章是将会是这个系列最重要的一篇,消消乐消除算法的实现!!!

本文由“壹伴编辑器”提供技术支结束语以上皆为本菜鸡的个人观点,文采实在不太好,如果写得不好还请各位见谅。如果有哪些地方说的不对,还请各位指出,大家共同进步。接下来我会持续分享自己所学的知识与见解,欢迎各位关注本公众号。我们,下次见!