游戏引擎
- cocos creator 3.6
项目地址
核心逻辑代码
- assets/script/game/GameScene.ts
import { _decorator, Component, Node, Prefab, assetManager, NodePool, instantiate, v3, Vec3, UITransform, game, tween, Animation, PageView, Label } from 'cc';
import { Fw } from '../../framework/manager/Fw';
import { ArrayUtil } from '../../framework/util/ArrayUtil';
import { LoadUtil } from '../../framework/util/LoadUtil';
import { GameStep, TileStatus, TileUnit } from '../config/BaseConf';
import { PageConf } from '../config/PageConf';
import { GameManager } from '../manager/GameManager';
import { Tile } from './Tile';
const { ccclass, property } = _decorator;
@ccclass('GameScene')
export class GameScene extends Component {
public static I: GameScene
@property(Node)
Map: Node = null;
@property(Node)
Put: Node = null;
@property(Prefab)
TilePrefab: Prefab = null;
@property(Prefab)
FxTishiPrefab: Prefab = null;
@property(Label)
NumRefresh: Label = null;
@property(Label)
NumTips: Label = null;
@property(Label)
NumBack: Label = null;
lvd: { type: number, cell: number[][][], idList: number[], total: number } = null;
tilePool: NodePool = new NodePool();
fxTishiPool: NodePool = new NodePool();
start() {
GameScene.I = this;
this.preLoadLevel(() => {
this.initLevel();
})
this.refreshUI();//刷新UI
}
update(deltaTime: number) {
}
//预加载关卡配置与对应的预制体
preLoadLevel(complete: Function, progress: Function = null): void {
let skinChildIdList: number[] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
];
let finished: number = 0;
let total: number = 2;
LoadUtil.load(`level:${GameManager.I.level}`, (err0, res0) => {
console.log(err0, res0)
let lvd: any = res0.json;
this.lvd = lvd;
this.lvd.idList = [];
this.lvd.total = 0;
for (let row of this.lvd.cell)
this.lvd.total += row.length;
total = lvd.type + 1 + 1;
finished++;
progress && progress(finished, total);
this.lvd.idList = ArrayUtil.randomElementList(skinChildIdList, lvd.type);
let arr = [];
for (let id of this.lvd.idList) {
arr.push(`icon:skin/skin${GameManager.I.curSkinId}/${id}/spriteFrame`);
}
arr.push(`icon:skin/kuai${GameManager.I.curSkinId}/spriteFrame`);
LoadUtil.loadList(arr, complete, () => {
finished++;
progress && progress(finished, total);
})
})
}
//初始化关卡
initLevel(): void {
//移除Tile
for (let i: number = this.Map.children.length - 1; i >= 0; i--) {
for (let j: number = this.Map.children[i].children.length - 1; j >= 0; j--) {
this.removeTile(this.Map.children[i].children[j]);
}
}
//配置
let groupTotal: number = this.lvd.total / 3;
let idList: number[] = [];
for (let i: number = 0; i < groupTotal; i++) {
let id = ArrayUtil.randomElement(this.lvd.idList);
for (let j: number = 0; j < 3; j++)
idList.push(id);
}
idList = ArrayUtil.randomSort(idList);
let tileList = [];
for (let i: number = 0; i < this.lvd.cell.length; i++) {
this.lvd.cell[i] = this.lvd.cell[i].sort((a, b) => { return a[1] !== b[1] ? b[1] - a[1] : a[0] - b[0] })
for (let cell of this.lvd.cell[i])
tileList.push(v3(...cell, i));
}
//生成
let index: number = 0;
this.schedule(() => {
let id = idList[index];
let pos = tileList[index];
let node: Node = this.createTile();
let sc: Tile = node.getComponent(Tile);
sc.id = id;
// sc.index = index;
sc.pos = pos;
sc.status = TileStatus.Show;
node.active = true;
index++;
//向上检测是否被覆盖 所有显示的块
this.checkUpAll();
}, 0.03, this.lvd.total - 1);
}
//创建块
createTile(): Node {
let node: Node = this.tilePool.get();
if (node) return node;
node = instantiate(this.TilePrefab);
let sc: Tile = node.getComponent(Tile);
sc.status = TileStatus.Recover;
node['TileId'] = -1;
node['IsMask'] = false;
return node;
}
//移除块
removeTile(node: Node): void {
node.setParent(this.node);
node.active = false;
let sc: Tile = node.getComponent(Tile);
sc.status = TileStatus.Recover;
// sc.index = -1;
this.tilePool.put(node);
}
//创建提示粒子
createFxTishi(): Node {
let node: Node = this.fxTishiPool.get();
if (node) return node;
node = instantiate(this.FxTishiPrefab);
return node;
}
//移除提示粒子
removeFxTishi(node: Node): void {
node.setParent(this.node);
node.active = false;
this.fxTishiPool.put(node);
}
//加入队列的顺序
putSortList: Node[] = [];
//加入待消除的队列
putTile(node: Node): void {
this.putSortList.push(node);//加入队列的顺序
Fw.Audio.playEffect("tile_click");
let sc: Tile = node.getComponent(Tile);
sc.status = TileStatus.Put;
let index = 0;
let len: number = this.Put.children.length;
if (len) {
for (let i: number = 0; i < len; i++) {
if (this.Put.children[i]['TileId'] === node['TileId']) {
index = i + 1;
}
}
if (index === 0) index = len;
}
let position: Vec3 = node.worldPosition;
this.Put.insertChild(node, index);
node.worldPosition = position;
//向上检测是否被覆盖 所有显示的块
this.checkUpAll();
//检测消除列表
this.scheduleOnce(this.checkPutTile, 0.3);
}
PutMax: number = 7;//最大待消除数量
PutAniDuration: number = 0.3;//每一次动画持续时长(单位:秒)
nextPutAniTime: number = 0;
putAniList: [string, Node, any][][] = [];
//检测消除列表
checkPutTile(): void {
let len: number = this.Put.children.length;
if (len < 3) return;
let preTileId: number = this.Put.children[0]['TileId'];
let total: number = 1;
let index: number = -1;
for (let i: number = 1; i < len; i++) {
if (preTileId === this.Put.children[i]['TileId']) {
total++;
if (total >= 3) {
index = i;
break;
}
}
else {
preTileId = this.Put.children[i]['TileId'];
total = 1;
}
}
if (index === -1) return this.checkLevelFail();
Fw.Audio.playEffect("tile_match");
for (let i: number = index; i >= index - 2; i--) {
let node: Node = this.Put.children[i];
//播放粒子
let fx: Node = this.createFxTishi();
this.node.addChild(fx);
fx.worldPosition = node.worldPosition;
fx.active = true;
fx.getComponent(Animation).play('fxtishi');
this.scheduleOnce(() => {
this.removeFxTishi(fx);
}, 1)
//移除块
this.removeTile(node);
}
//检测是否通关
this.checkLevelWin();
}
//检测是否失败
checkLevelFail(): void {
if (this.Put.children.length < this.PutMax) return;
console.log("失败")
GameManager.I.gameStep = GameStep.End;
GameManager.I.levelRes = false;
Fw.View.open(PageConf.EndView);
}
//检测是否胜利
checkLevelWin(): void {
for (let child of this.Map.children)
if (child.children.length) return;
console.log("胜利");
GameManager.I.gameStep = GameStep.End;
GameManager.I.levelRes = true;
Fw.View.open(PageConf.EndView);
}
//向上检测是否被覆盖 所有显示的块
checkUpAll(): void {
for (let row of this.Map.children) {
for (let node of row.children) {
let sc: Tile = node.getComponent(Tile);
sc.checkUp();
}
}
}
//刷新道具
onBtnRefresh(): void {
if (GameManager.I.refreshTool < 1) {
GameManager.I.refreshTool += 3;
this.refreshUI();//刷新UI
Fw.msg('数量不足');
return;
}
GameManager.I.refreshTool--;
this.refreshUI();//刷新UI
//配置
let idList: number[] = [];
let tileList = [];
for (let row of this.Map.children) {
for (let node of row.children) {
let sc: Tile = node.getComponent(Tile);
idList.push(sc.id);
tileList.push(sc.pos);
}
}
idList = ArrayUtil.randomSort(idList);
//移除之前的Tile节点
for (let i: number = this.Map.children.length - 1; i >= 0; i--) {
for (let j: number = this.Map.children[i].children.length - 1; j >= 0; j--) {
this.removeTile(this.Map.children[i].children[j]);
}
}
//生成
let index: number = 0;
this.schedule(() => {
let id = idList[index];
let pos = tileList[index];
let node: Node = this.createTile();
let sc: Tile = node.getComponent(Tile);
sc.id = id;
// sc.index = index;
sc.pos = pos;
sc.status = TileStatus.Show;
node.active = true;
index++;
//向上检测是否被覆盖 所有显示的块
this.checkUpAll();
}, 0.017, idList.length - 1);
}
//回退道具
onBtnBack(): void {
if (GameManager.I.backTool < 1) {
GameManager.I.backTool += 3;
this.refreshUI();//刷新UI
Fw.msg('数量不足');
return;
}
for (let i: number = this.putSortList.length - 1; i >= 0; i--) {
let sc: Tile = this.putSortList[i].getComponent(Tile);
if (sc.status !== TileStatus.Put) continue;
GameManager.I.backTool--;
this.refreshUI();//刷新UI
sc.reset();
return;
}
Fw.msg("暂无可回退的对象");
}
//提示道具
onBtnTips(): void {
if (GameManager.I.tipsTool < 1) {
GameManager.I.tipsTool += 3;
this.refreshUI();//刷新UI
Fw.msg('数量不足');
return;
}
GameManager.I.tipsTool--;
this.refreshUI();//刷新UI
//PS:当前的推荐方法还有很大的优化空间,不支持多层堆叠推荐
//显示的块
let showMap = {};
let showIdList = [];
for (let i: number = this.Map.children.length - 1; i >= 0; i--)
for (let node of this.Map.children[i].children) {
if (node['IsMask']) continue;
let id = node['TileId'];
if (!showMap[id]) showMap[id] = [];
showMap[id].push(node);
if (showIdList.indexOf(id) === -1) showIdList.push(id);
}
showIdList = showIdList.sort((a, b) => { return showMap[b].length - showMap[a].length })
//已推的块
let putIdList = [];
let putMap = {}
for (let node of this.Put.children) {
let id = node['TileId'];
if (!putMap[id]) putMap[id] = [];
putMap[id].push(node);
if (putIdList.indexOf(id) === -1) putIdList.push(id);
}
putIdList = putIdList.sort((a, b) => { return putMap[b].length - putMap[a].length })
//剩余的格子数
let disPutNum: number = this.PutMax - this.Put.children.length;
//情况一:已推的块+显示的块>=3
for (let id of putIdList) {
if (putMap[id].length + disPutNum < 3) continue;//剩余的格子数无法满足消除的需求
if (!showMap[id]) continue;
if (putMap[id].length + showMap[id].length < 3) continue;
for (let i: number = 0, len: number = 3 - putMap[id].length; i < len; i++)
this.scheduleOnce(() => { this.putTile(showMap[id][i]) }, 0.3 * i);
return;
}
//情况二:显示的块>=3
if (disPutNum >= 3 && showMap[showIdList[0]].length >= 3) {
for (let i: number = 0; i < 3; i++)
this.scheduleOnce(() => { this.putTile(showMap[showIdList[0]][i]) }, 0.3 * i);
return;
}
//没有满足3个待消除的情况
if (disPutNum <= 1) {
GameManager.I.tipsTool++;
this.refreshUI();//刷新UI
Fw.msg("没有可提示的选项");
return;
}
//情况三:推已推中有相同的块
for (let id of putIdList) {
if (!showMap[id]) continue;
this.putTile(showMap[id][0]);
return;
}
//情况四:推显示的第一个
this.putTile(showMap[showIdList[0]][0]);
}
//刷新UI
refreshUI(): void {
this.NumBack.string = GameManager.I.backTool + '';
this.NumRefresh.string = GameManager.I.refreshTool + '';
this.NumTips.string = GameManager.I.tipsTool + '';
}
}
- assets/script/game/Tile.ts
import { _decorator, Component, Node, Sprite, v3, Vec3, game, director, tween, UITransform } from 'cc';
import { LoadUtil } from '../../framework/util/LoadUtil';
import { GameStep, TileStatus, TileUnit } from '../config/BaseConf';
import { GameManager } from '../manager/GameManager';
import { GameScene } from './GameScene';
const { ccclass, property } = _decorator;
@ccclass('Tile')
export class Tile extends Component {
v3_0: Vec3 = v3();
v3_1: Vec3 = v3();
@property(Sprite)
Bg: Sprite = null;
@property(Sprite)
Icon: Sprite = null;
@property(Node)
Mask: Node = null;
start() {
}
onClick() {
if (GameManager.I.gameStep !== GameStep.Game) return;
switch (this.status) {
case TileStatus.Show: {
if (this.Mask.active) return;
GameScene.I.putTile(this.node);
break;
}
case TileStatus.Put: {
break;
}
}
}
update(deltaTime: number) {
switch (this.status) {
case TileStatus.Put: {
let siblingIndex: number = this.node.getSiblingIndex();
this.v3_0.y = 0;
this.v3_0.x = TileUnit * 2 * siblingIndex;
Vec3.lerp(this.v3_1, this.node.position, this.v3_0, 0.2);
this.node.position = this.v3_1;
break;
}
case TileStatus.Show: {
//向上检测是否被覆盖
// if (director.getTotalFrames() % GameScene.I.lvd.total !== this.index) return;
// this.checkUp();
break;
}
}
}
//向上检测是否被覆盖
checkUp(): void {
for (let z: number = this.z + 1; z < GameScene.I.Map.children.length; z++) {
for (let node of GameScene.I.Map.children[z].children) {
if (Math.abs(node.position.x - this.node.position.x) <= TileUnit && Math.abs(node.position.y - this.node.position.y) <= TileUnit) {
this.Mask.active = this.node['IsMask'] = true;
return;
}
}
}
this.Mask.active = this.node['IsMask'] = false;
}
//-----------------------------------状态-----------------------------------
status: TileStatus = TileStatus.Recover;
//-----------------------------------编号-----------------------------------
get id(): number { return this.node['TileId'] }
set id(val: number) {
this.node['TileId'] = val;
this.Bg.spriteFrame = LoadUtil.get(`icon:skin/kuai${GameManager.I.curSkinId}/spriteFrame`);
this.Icon.spriteFrame = LoadUtil.get(`icon:skin/skin${GameManager.I.curSkinId}/${this.id}/spriteFrame`);
}
// index: number = -1;
//-----------------------------------位置-----------------------------------
_x: number = 0;
get x(): number { return this._x }
set x(val: number) {
this._x = val;
this.node.position = v3(this.x * TileUnit, this.y * TileUnit);
}
_y: number = 0;
get y(): number { return this._y }
set y(val: number) { this._y = val; this.node.position = v3(this.x * TileUnit, this.y * TileUnit); }
_z: number = 0;
get z(): number { return this._z }
set z(val: number) {
this._z = val;
this.node.setParent(GameScene.I.Map.children[val]);
this.node.position = v3(this.x * TileUnit, this.y * TileUnit);
}
get pos(): Vec3 { return v3(this.x, this.y, this.z) }
set pos(val: Vec3) {
this._x = val.x;
this._y = val.y;
this.z = val.z;
}
reset(): void {
let position: Vec3 = this.node.worldPosition;
let parent: Node = GameScene.I.Map.children[this.z];
let index: number = 0;
for (let i: number = 0; i < parent.children.length; i++) {
let sc: Tile = parent.children[i].getComponent(Tile);
if (this.y < sc.y) continue;
if (this.y === sc.y && this.x > sc.x) continue;
index = i;
break;
}
parent.insertChild(this.node, index);
this.node.worldPosition = position;
tween(this.node).to(0.6, { position: v3(this.x * TileUnit, this.y * TileUnit) }, { easing: 'circInOut' }).start();
this.status = TileStatus.Show;
}
}
内容说明
- (非业务)基础框架
- 核心玩法
- 道具(刷新、回退、提示)
提示:本项目如标题所示仅提供核心玩法的逻辑,不计划补充周边功能
页面效果
实现流程
1、根据配置(配置中包含种类数量、块位置列表)实例化关卡
2、每步操作后,【检测】所有块的被遮挡状态
3、点击(未遮挡的)块,将其移动到待消除的列表中
- 【检测】满3个执行消除
- 【检测】待消除的列表长度>=7 游戏失败
- 【检测】消除所有的块 游戏通关
比较麻烦的点:
1、块的排列(没有什么捷径,addChild前优先做好前后排序)
2、块的移动(采用lerp的方式)