[Cocos Creator] 制作简版消消乐(二):实现基础组件和管理脚本


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

前言

在上一篇文章中我们初步建立了项目并搭建好了场景,那么本篇文章将和大家一起实现部分基础组件和管理脚本

温馨提醒:本文含有大量代码和注释,请提前做好心理准备并认真阅读

话不多说,Let's go!

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

正文

代码实现

前排提示:本项目已经上传至码云,点击文章底部阅读原文即可获取。

1. 新建文件 Enum ,用来存放所有自定义枚举,方便管理。这里我先创建了名为 TileType 的枚举来表示方块的类型(同时我建议给值为 number 类型的枚举的初始值设为 1 ,避免使用时出现歧义):

// 以下为 Enum.ts 文件内容
/**
 * 方块类型
 */
export enum TileType {
    A = 1,
    B,
    C,
    D,
    E,
}

/**
 * 方块点击事件
 */
export enum TileEvent {
    TouchStart = 'tile_touchstart',
    TouchEnd = 'tile_touchend',
    TouchCancel = 'tile_touchcancel',
}

2. 新建脚本 GameConfig ,用来储存游戏配置。我这里定义了一些方块的基本配置:

import { TileType } from "../game/type/Enum";

export default class GameConfig {

    public static row: number = 8; // 行数

    public static col: number = 8; // 列数

    public static size: number = 70; // 方块的尺寸

    public static spacing: number = 5; // 间隔

    public static padding: number = 5; // 边距

    public static types: TileType[] = [1, 2, 3, 4, 5]; // 方格类型集合

}

3. 新建文件 DataStructure ,用来存放自定义的类和类型。这里我定义了名为 Coordinate 的类,用来表示方块的坐标,并且实现一些内置的函数;另外我还实现了一个快速创建坐标对象的函数 Coord :

// 以下为 DataStructure.ts 文件内容
/**
 * 坐标
 */
export class Coordinate {

    public x: number; // 横坐标

    public y: number; // 纵坐标

    /**
     * 构造函数
     * @param x 横坐标
     * @param y 纵坐标
     */
    constructor(x: number = 0, y: number = 0) {
        this.x = x;
        this.y = y;
    }

    /**
     * 更新赋值
     * @param x 横坐标
     * @param y 纵坐标
     */
    public set(x: number | Coordinate, y?: number) {
        if (typeof x === 'number') {
            this.x = x;
            this.y = y;
        } else {
            this.x = x.x;
            this.y = x.y;
        }
    }

    /**
     * 复制
     */
    public copy(): Coordinate {
        return new Coordinate(this.x, this.y);
    }

    /**
     * 对比
     * @param x 比较对象
     */
    public compare(x: number | Coordinate, y?: number): boolean {
        if (typeof x === 'number') return this.x === x && this.y === y;
        else return this.x === x.x && this.y === x.y;
    }

    /**
     * 是否相邻
     * @param coord 比较对象
     */
    public isAdjacent(coord: Coordinate): boolean {
        if (this.x === coord.x && (this.y === coord.y + 1 || this.y === coord.y - 1)) return true;
        else if (this.y === coord.y && (this.x === coord.x + 1 || this.x === coord.x - 1)) return true;
        else return false;
    }

    /**
     * 转换为方便阅读的字符串
     */
    public toString(): string {
        return '(x:' + this.x + ', ' + 'y:' + this.y + ')';
    }
}

/**
 * 创建坐标对象
 * @param x 横坐标
 * @param y 纵坐标
 */
export function Coord(x: number = 0, y: number = 0) {
    return new Coordinate(x, y);
}

4. 新建脚本 MapManager ,关于地图的实现都在这里完成。简版不需要动态生成不同的地图,只用来生成和储存每个方块位置,所以我直接做成静态脚本了:

import GameConfig from "../../common/GameConfig";
import { Coordinate } from "../type/DataStructure";

export default class MapManager {

    private static _posMap: cc.Vec2[][] = null;
    public static getPos(x: number | Coordinate, y?: number): cc.Vec2 {
        if (typeof x === 'number') return this._posMap[x][y];
        else return this._posMap[x.x][x.y];
    }

    private static width: number = null;

    private static height: number = null;

    private static beginX: number = null;

    private static beginY: number = null;

    /**
     * 初始化
     */
    public static init() {
        this.generatePosMap();
    }

    /**
     * 生成位置表
     */
    private static generatePosMap() {
        this._posMap = [];
        // 计算宽高
        this.width = (GameConfig.padding * 2) + (GameConfig.size * GameConfig.col) + (GameConfig.spacing * (GameConfig.col - 1));
        this.height = (GameConfig.padding * 2) + (GameConfig.size * GameConfig.row) + (GameConfig.spacing * (GameConfig.row - 1));
        // 以左下角为原点,计算第一个方块的位置
        this.beginX = -(this.width / 2) + GameConfig.padding + (GameConfig.size / 2);
        this.beginY = -(this.height / 2) + GameConfig.padding + (GameConfig.size / 2);
        // 计算所有方块的位置
        // 从左到右计算每一列方块的位置
        for (let c = 0; c < GameConfig.col; c++) {
            let colSet = [];
            let x = this.beginX + c * (GameConfig.size + GameConfig.spacing);
            // 从下到上计算该列的每一个方块的位置
            for (let r = 0; r < GameConfig.row; r++) {
                let y = this.beginY + r * (GameConfig.size + GameConfig.spacing);
                colSet.push(cc.v2(x, y));
            }
            this._posMap.push(colSet);
        }
    }
}

5. 新建脚本 ResManager ,此脚本用来存放游戏中用到的方块图片资源,方便运行中快速读取(简版只有固定的5种方块类型,所以我选择直接将图片资源挂载到该组件上):

import { TileType } from "../type/Enum";

const { ccclass, property } = cc._decorator;

@ccclass
export default class ResManager extends cc.Component {

    @property(cc.SpriteFrame)
    private a: cc.SpriteFrame = null;

    @property(cc.SpriteFrame)
    private b: cc.SpriteFrame = null;

    @property(cc.SpriteFrame)
    private c: cc.SpriteFrame = null;

    @property(cc.SpriteFrame)
    private d: cc.SpriteFrame = null;

    @property(cc.SpriteFrame)
    private e: cc.SpriteFrame = null;

    private static instance: ResManager = null;

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

    /**
     * 获取方块图片资源
     * @param tileType 方块类型
     */
    public static getTileSpriteFrame(tileType: TileType): cc.SpriteFrame {
        switch (tileType) {
            case TileType.A:
                return this.instance.a;
            case TileType.B:
                return this.instance.b;
            case TileType.C:
                return this.instance.c;
            case TileType.D:
                return this.instance.d;
            case TileType.E:
                return this.instance.e;
        }
    }
}

6. 新建脚本 Tile ,此为方块的关键组件,每个方块上都需要挂载该组件。具体内置函数和作用看下面代码与注释(其中 GameEvent 为之前文章提到过的事件监听发射系统,没看过的同学可以 点这里 查看,而 PoolManager 将在下面进行讲解):

import { TileType, TileEvent } from "../type/Enum";
import { Coordinate, Coord } from "../type/DataStructure";
import ResManager from "../manager/ResManager";
import PoolManager from "../manager/PoolManager";
import { GameEvent } from "../../common/GameEvent";

const { ccclass, property } = cc._decorator;

@ccclass
export default class Tile extends cc.Component {

    @property(cc.Sprite)
    private sprite: cc.Sprite = null; // 显示图片的组件

    private _type: TileType = null; // 类型
    /**
     * 获取该方块的类型
     */
    public get type() { return this._type; }

    private _coord: Coordinate = null; // 坐标
    /**
     * 获取该方块的坐标
     */
    public get coord() { return this._coord; }

    protected onLoad() {
        this.bindTouchEvents();
    }

    protected onDestroy() {
        this.unbindTouchEvents();
    }

    /**
     * 节点池复用回调
     */
    public reuse() {
        this.bindTouchEvents();
    }
    /**
     * 节点池回收回调
     */
    public unuse() {
        this.unbindTouchEvents();
    }

    /**
     * touchstart 回调
     * @param e 参数
     */
    private onTouchStart(e: cc.Event.EventTouch) {
        GameEvent.emit(TileEvent.TouchStart, this._coord.copy(), e.getLocation());
    }

    /**
     * touchend 回调
     */
    private onTouchEnd() {
        GameEvent.emit(TileEvent.TouchEnd);
    }

    /**
     * touchcancel 回调
     * @param e 参数
     */
    private onTouchCancel(e: cc.Event.EventTouch) {
        GameEvent.emit(TileEvent.TouchCancel, this._coord.copy(), e.getLocation());
    }

    /**
     * 绑定点击事件
     */
    private bindTouchEvents() {
        this.node.on('touchstart', this.onTouchStart, this);
        this.node.on('touchcancel', this.onTouchCancel, this);
        this.node.on('touchend', this.onTouchEnd, this);
    }

    /**
     * 解绑点击事件
     */
    private unbindTouchEvents() {
        this.node.off('touchstart', this.onTouchStart, this);
        this.node.off('touchcancel', this.onTouchCancel, this);
        this.node.off('touchend', this.onTouchEnd, this);
    }

    /**
     * 初始化
     */
    public init() {
        this._type = null;
        this.sprite.spriteFrame = null;
        this.setCoord(-1, -1);
        this.node.setScale(0);
    }

    /**
     * 设置类型
     * @param type 类型
     */
    public setType(type: TileType) {
        this._type = type;
        this.updateDisplay();
    }

    /**
     * 更新方块图片
     */
    private updateDisplay() {
        this.sprite.spriteFrame = ResManager.getTileSpriteFrame(this._type);
    }

    /**
     * 设置坐标
     * @param x 横坐标
     * @param y 纵坐标
     */
    public setCoord(x: number | Coordinate, y?: number) {
        if (!this._coord) this._coord = Coord();
        if (typeof x === 'number') this._coord.set(x, y);
        else this._coord.set(x);
    }

    /**
     * 显示方块
     */
    public appear() {
        cc.tween(this.node)
            .to(0.075, { scale: 1.1 })
            .to(0.025, { scale: 1 })
            .start();
    }

    /**
     * 消失并回收
     */
    public disappear() {
        cc.tween(this.node)
            .to(0.1, { scale: 0 })
            .call(() => PoolManager.put(this.node))
            .start();
    }

}

7. 新建脚本 PoolManager ,用来管理游戏中频繁用到的预制体。我这里只内置了 tile 节点池 tilePool ,并且在实例化节点池时将 Tile 组件作为参数传入,目的是让节点池复用和回收时自动调用 Tile 组件上的 reuse 和 unuse 函数:

import Tile from "../component/Tile";

const { ccclass, property } = cc._decorator;

@ccclass
export default class PoolManager extends cc.Component {

    @property(cc.Prefab)
    private tilePrefab: cc.Prefab = null;

    private tilePool: cc.NodePool = new cc.NodePool(Tile);

    private static instance: PoolManager = null;

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

    /**
     * 获取节点
     */
    public static get() {
        if (this.instance.tilePool.size() > 0) return this.instance.tilePool.get();
        else return cc.instantiate(this.instance.tilePrefab);
    }

    /**
     * 存入节点
     * @param node
     */
    public static put(node: cc.Node) {
        cc.Tween.stopAllByTarget(node);
        if (this.instance.tilePool.size() < 30) this.instance.tilePool.put(node);
        else node.destroy();
    }
}

★ 以上就是之后游戏中需要用到的部分基础组件和管理脚本,为了避免文章过于冗长,我只能在这里先暂停一下,更多代码和讲解请持续关注本公众号。

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

结束语

以上皆为本菜鸡的个人观点,文采实在不太好,如果写得不好还请各位见谅。如果有哪些地方说的不对,还请各位指出,大家共同进步。

接下来我会持续分享自己所学的知识与见解,欢迎各位关注本公众号。

我们,下次见!

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

扫描二维码

获取更多精彩

文弱书生陈皮皮

点击 阅读原文 获取本项目

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值