最近开始研究微信小游戏,有兴趣的 可以关注一下 公众号, 记录一些心路历程和源代码。
定义 一个CupMgr class 管理 cup 。这个系统允许玩家通过拖动杯子来倒水,直到所有杯子都达到目标状态。以下是代码的主要部分及其功能解释:
主要功能
-
初始化配置:
- 从本地存储中读取当前关卡和上一次的操作记录。
- 如果没有记录,则初始化配置。
-
创建杯子:
- 根据当前关卡的配置创建杯子,并设置其初始状态。
- 使用布局组件(Layout)来排列这些杯子。
-
处理杯子交互:
- 玩家点击杯子时,检查是否可以倒水,并执行倒水动画。
- 支持撤销上一步操作。
-
关卡管理:
- 玩家完成当前关卡后,自动进入下一关。
- 提供获取当前关卡和操作记录数量的方法。
关键组件和类
- CupMgr:管理整个杯子系统的类。
- Cup:表示单个杯子的类,包含杯子的状态和操作方法。
- WaterFlow:表示水流动画的类,用于实现倒水效果。
主要方法和属性
- onLoad():组件加载时调用,初始化关卡和配置。
- initCfg():初始化当前关卡的配置。
- createCups():创建并排列杯子。
- onClickCup():处理杯子点击事件,检查并执行倒水操作。
- startPour():开始倒水动画。
- onPourOneFinished():处理一次倒水完成后的逻辑,如更新关卡状态。
- undoAction():撤销上一步操作。
- nextLevel():进入下一关。
- getLevel():获取当前关卡数。
- checkIsAllFinished():检查所有杯子是否都达到目标状态。
注意事项
- 代码中使用了
sys.localStorage
来存储和读取关卡和操作记录,确保游戏状态在玩家下次进入时可以恢复。 - 使用了
tween
来实现动画效果,使杯子移动和倒水过程更加平滑。 - 代码中包含了一些调试和编辑器相关的逻辑,如
executeInEditMode
和DEV
、EDITOR
常量,这些在发布时通常会被移除。
这段代码是一个典型的游戏逻辑实现,展示了如何使用Cocos Creator引擎来创建和管理游戏中的交互元素。
import { Component, JsonAsset, Layout, Prefab, _decorator, instantiate, sys,Node, UITransform, v2, v3, view, Color, tween, log } from "cc";
import Cup, { _CupInfo } from "./cup";
import { DEV, EDITOR } from "cc/env";
const { ccclass, property,executeInEditMode } = _decorator;
const COOKIE_LEVEL = "level"
const COOKIE_LAST_CFG = "last_cfg"
const COOKIE_ACTION_HISTORY = "action_history"
// spacesArr 是一个在 CupMgr 类中定义的对象,用于存储不同数量杯子时,水平布局的间距和缩放比例。具体来说,它是一个键值对对象,其中键是杯子的数量,值是一个数组,包含两个或三个元素:
// 第一个元素是水平间距 spacingX。
// 第二个元素是垂直缩放比例 scale。
// 第三个元素是垂直间距 spacingY(可选)。
const spacesArr = {
[1] : [0,1],
[2] : [80,1],
[3] : [50,1],
[4] : [40,0.9],
[5] : [30,0.8],
[6] : [30,0.65],
[7] : [20,0.6,60],
[8] : [10,0.55,80],
}
@ccclass
@executeInEditMode
export class CupMgr extends Component{
@property(JsonAsset)
private levelCfg:JsonAsset = null;
@property(Prefab)
private pfb:Prefab = null;
/**当前等级 */
private _level = 1;
private curCfg:Array<_CupInfo> = [];
onLoad(){
if(EDITOR){
return
}
this._level = checkint(sys.localStorage.getItem(COOKIE_LEVEL)||1);
// this._level = 50
let str = sys.localStorage.getItem(COOKIE_LAST_CFG);
if(str){
try{
this.curCfg = JSON.parse(str);
}catch(e){
this.initCfg()
}
}else{
this.initCfg()
}
str = sys.localStorage.getItem(COOKIE_ACTION_HISTORY);
if(str){
try{
this._actions = JSON.parse(str);
}catch(e){
}
}
this.createCups();
}
private initCfg(){
this.curCfg = [];
let cfgArr:Array<number> = this.levelCfg.json[this._level-1];//每一关的数据,都是数组,每四个数字代表一杯水
let acc = 0;
while(acc<cfgArr.length){
let info = {
colorIds:[cfgArr[acc],cfgArr[acc+1]||0,cfgArr[acc+2]||0,cfgArr[acc+3]||0]
}
this.curCfg.push(info);
acc+=4;
}
}
@property private _debugLevel: number = 0;
@property({ tooltip: DEV && '调试关卡' })
public get debugLevel() { return this._debugLevel; }
public set debugLevel(value: number) {
this._debugLevel = value;
this._level = value;
this.nextLevel()
}
private _cups:Array<Cup> = [];
private layout_v:Layout = null;
private async createCups(){
if(this.layout_v){
this.layout_v.node.destroyAllChildren();
}
this._cups = [];
this.selected = null;
this._actions = [];
// await wait(1);
let arr = this.curCfg;
const len = this.curCfg.length;
if(len==0){
return;
}
for(let i=0;i<len;i++){
let info = arr[i];
let _node = instantiate(this.pfb);
_node.parent = this.node;
let _cup = _node.getComponent(Cup)
_cup.setCupInfo(info,this.onClickCup.bind(this));
this._cups.push(_cup)
}
function _createLayout(type:any,parent:Node,name?:string) {
let node = new Node(name);
node.parent = parent;
node.addComponent(UITransform)
let layout = node.addComponent(Layout);
layout.type = type;
layout.resizeMode = Layout.ResizeMode.CONTAINER;
return layout
}
if(this.layout_v==null){
this.layout_v = _createLayout(Layout.Type.VERTICAL,this.node,"layout_v");
this.layout_v.node.setSiblingIndex(1);
}
let cupSize = this._cups[0].node.getComponent(UITransform).contentSize;
let cupIdxGroups:Array<Array<number>> = [];
if(len<=4){
let idGroup:Array<number> = [];
for(let i=0;i<this._cups.length;i++){
idGroup.push(i);
}
cupIdxGroups.push(idGroup);
}else if(len<=15){
let idGroup:Array<number> = [];
let i=0;
let middleId = (len)/2;
for(;i<middleId;i++){
idGroup.push(i);
}
cupIdxGroups.push(idGroup);
idGroup = [];
for(;i<len;i++){
idGroup.push(i);
}
cupIdxGroups.push(idGroup);
idGroup = [];
}
let layoutArr:Array<Layout> = [];
let maxNum = 1;
for(let i = 0;i<cupIdxGroups.length;i++){
let node_layout_h = _createLayout(Layout.Type.HORIZONTAL,this.layout_v.node,`layout_h_${i}`);
node_layout_h.node.getComponent(UITransform).height = cupSize.height;
let idGroup = cupIdxGroups[i];
for(let j=0;j<idGroup.length;j++){
let id = idGroup[j];
this._cups[id].node.parent = node_layout_h.node;
}
maxNum = Math.max(maxNum,node_layout_h.node.children.length);
let spaceX = spacesArr[maxNum][0];
if(spaceX!=node_layout_h.spacingX){
node_layout_h.spacingX = spaceX;
}
layoutArr.push(node_layout_h);
}
this.layout_v.enabled = true;
let _scale = spacesArr[maxNum][1];
this.layout_v.node.scale = v3(_scale,_scale,_scale)
this.layout_v.spacingY = spacesArr[maxNum][2]||40;
for(let layout of layoutArr){
layout.updateLayout();
layout.enabled = false;
}
this.layout_v.updateLayout();
this.layout_v.enabled = false;
for(let cup of this._cups){
(cup as any).orignPt = cup.node.position.clone();
}
}
private selected:Cup = null;
private onClickCup(cup:Cup){
if(this.selected){
if(this.selected==cup){
this.doSelect(cup,false);
this.selected = null;
}else if(this.checkPour(this.selected,cup)){
this.startPour(this.selected,cup);
}else{
this.doSelect(this.selected,false);
this.selected = null;
}
}else{
this.selected = cup;
this.doSelect(cup,true);
}
}
/**检查两个杯子是否能倒水 */
private checkPour(src:Cup,dst:Cup){
let srcTop = src.getTop();
let dstTop = dst.getTop();
if(srcTop.topColorId==0){
return false;
}
if(dstTop.topColorId==0){
return true;
}
return srcTop.topColorNum<=dstTop.emptyNum;
}
/**开始倒水 */
private startPour(src:Cup,dst:Cup){
dst.node.setSiblingIndex(0)
dst.node.parent.setSiblingIndex(0)
src.node.setSiblingIndex(10)
src.node.parent.setSiblingIndex(10)
let srcTop = src.getTop();
let dstPt = v3(dst.node.position);
let dstGlobal = dst.node.parent.getComponent(UITransform).convertToWorldSpaceAR(dstPt)
let viewSize = view.getVisibleSize()
let isRight = dstGlobal.x>viewSize.width*0.5;//标记目标是否在屏幕右侧
if(Math.abs(dstGlobal.x-viewSize.width*0.5)<2){//目标在中间
let srcPt = src.node.parent.getComponent(UITransform).convertToWorldSpaceAR(v3(src.node.position));
isRight = srcPt.x<viewSize.width*0.5;
}
dstPt.y += 60 + dst.node.getComponent(UITransform).height*0.5;
let offsetX = 0//dst.node.width*0.5-20;
dstPt.x = dstPt.x + (isRight?-offsetX:offsetX);
dstPt = dst.node.parent.getComponent(UITransform).convertToWorldSpaceAR(dstPt);
//将瓶口设置为锚点
src.setPourAnchor(isRight)
dstPt = src.node.parent.getComponent(UITransform).convertToNodeSpaceAR(dstPt);
// log("---------src.x",src.node.x,dstPt.x)
const flow = src.getFlow();
flow.node.parent = this.node;
flow.setLineScale(this.layout_v.node.scale.x)
const onPourStart = ()=>{
let startPt = src.node.getComponent(UITransform).convertToWorldSpaceAR(v3())
startPt = flow.node.parent.getComponent(UITransform).convertToNodeSpaceAR(startPt);
let endPt = v3(startPt.x,dst.getWaterSurfacePosY());
endPt = flow.node.parent.getComponent(UITransform).convertToNodeSpaceAR(endPt);
endPt.x = startPt.x
flow.strokeColor = new Color().fromHEX(srcTop.colorHex);
flow.playFlowAni(startPt,endPt,0.2,false,()=>{
dst.startAddWater(srcTop.topColorId,srcTop.topColorNum,(cup:Cup,isFinished:boolean)=>{
this.onPourOneFinished(src,dst,srcTop.topColorId,srcTop.topColorNum);
});
})
}
//倒完水就收回去
function onPourFinish() {
let startPt = src.node.getComponent(UITransform).convertToWorldSpaceAR(v3())
startPt = flow.node.parent.getComponent(UITransform).convertToNodeSpaceAR(startPt);
let endPt = v3(startPt.x,dst.getWaterSurfacePosY(true));
endPt = flow.node.parent.getComponent(UITransform).convertToNodeSpaceAR(endPt);
endPt.x = startPt.x
flow.playFlowAni(startPt,endPt,0.2,true,()=>{
flow.clear();
})
src.setNormalAnchor();
let pt = (src as any).orignPt;
let moveBack = tween(src.node)
.delay(0.7)
.to(0.5,{position:pt,angle:0},{easing:"sineOut"})
.call(()=>{
src.node.setSiblingIndex(0);
src.node.parent.setSiblingIndex(0);
})
moveBack.start();
}
this.selected = null;
src.moveToPour(dstPt,isRight,onPourStart.bind(this),onPourFinish.bind(this));
}
private doSelect(cup:Cup,bool:boolean){
let pt = (cup as any).orignPt;
let y = pt.y+(bool?cup.node.getComponent(UITransform).height*0.2:0);
tween(cup.node).stop();
tween(cup.node).to(0.2,{position:v3(pt.x,y)}).start();
}
private _actions:Array<Action> = [];
/**一次倒水完成(以加水那个杯子水面升到最高为界限) */
private onPourOneFinished(from:Cup,to:Cup,colorId:number,num:number){
let fromCupIdx = this._cups.indexOf(from);
let toCupIdx = this._cups.indexOf(to);
if(this._actions.length==5){
this._actions.shift()
}
this._actions.push({
from:fromCupIdx,
to:toCupIdx,
colorId:colorId,
num:num
})
let isAllFinished = this.checkIsAllFinished();
if(isAllFinished){
this._level++;
sys.localStorage.setItem(COOKIE_LEVEL,this._level+"");
this.node.emit("level_finish")
}else{
this.node.emit("do_pour")
}
sys.localStorage.setItem(COOKIE_LAST_CFG,JSON.stringify(this.curCfg));
sys.localStorage.setItem(COOKIE_ACTION_HISTORY,JSON.stringify(this._actions));
}
public getActionNum(){
return this._actions.length;
}
/**恢复上一次的操作 */
public undoAction(){
let action = this._actions.pop();
if(action==null){
return false;
}
let {from,to,num,colorId} = action;
let toCup = this._cups[to];
let fromCup = this._cups[from];
if(toCup.isPouring()||fromCup.isPouring()){
return false;
}
toCup.removeTopWaterImmediately(num);
fromCup.addWaterImmediately(colorId,num);
return true;
}
public nextLevel(){
this.initCfg();
this.createCups();
}
public getLevel(){
return this._level;
}
private checkIsAllFinished(){
for(let cup of this._cups){
if(!cup.checkIsFinshed()){
return false
}
}
return true;
}
}
interface Action{
from:number,
to:number,
num:number,
colorId:number,
}
async function wait(sec:number) {
return new Promise(function (resolve,reject) {
setTimeout(() => {
resolve(null);
}, sec*1000);
})
}
function checkint(val){
if(val==null){
return 0;
}
let ret = parseInt(val);
if(Number.isNaN(ret)){
ret = 0;
}
return ret;
}