前言
在上一篇文章中我们初步建立了项目并搭建好了场景,那么本篇文章将和大家一起实现部分基础组件和管理脚本。
温馨提醒:本文含有大量代码和注释,请提前做好心理准备并认真阅读。
话不多说,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 为之前文章提到过的事件监听发射系统
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();
}
}