1玩法说明
游戏开始后,点击屏幕左右两侧,机器人朝左上方或右上方跳一步,如果下一步有石块,成功得1分,否则游戏结束。
2模块介绍
游戏场景分为2个:主页场景(home)、游戏场景(game)。
主页场景(home)作为游戏入口,没有其他功能,单纯提供游戏入口。
游戏场景(game)实现游戏玩法以及游戏逻辑控制,界面如下:
游戏的主体功能,都在游戏场景内,游戏场景的主要功能结构如下图:
3开发说明
这里主要介绍游戏场景的逻辑,按照上面功能结构进行介绍,先看一下游戏场景内的所有课件UI组件:
下面分模块介绍:
1石块逻辑(Box)
脚本挂载在石块预制上,实现石块相关逻辑,主要有2个:
1. 石块往下运动
根据机器人当前屏幕位置,机器人跳动后,无论成功还是失败,让石块往下方运动,运动到屏幕外,对应代码如下:
down(y: number){
this.node.runAction(cc.sequence(
cc.moveBy(0.4, 0, y),
cc.callFunc( () => {
NodeMgr.putBox(this.node);
})
));
}
2. 记录数据
private mPrevBox: cc.Node = null; // 上一个石块
private mNextBox: cc.Node = null; // 下一个石块
private mOffset: number = 0; // 左右偏移量 [-4,4]
上下石块主要是为了提供给机器人使用,让机器知道下一个跳过去的位置在哪里,而偏移量则记录石块在屏幕水平方向上的位置,从左到右,取值[-4,4]整数。
2节点管理逻辑(NodeMgr)
游戏中的石块,最多的时候,只铺满3个屏幕高度,超出了以后,幕布会移动到最下,石块重绘,如此循环,以达到一直玩下去的目的,所以石块是反复的移除和添加的,使用节电池,能让游戏有更好的表现。
1. 获取石块节点
判断节电池中是否已经有,有就去现成的,没有则返回空,让游戏逻辑自己生成一个新的节点,代码如下:
public static putBox(box: cc.Node){
if(this.mBoxNodePool == null){
this.mBoxNodePool = new cc.NodePool('boxs');
}
if(box != null){
this.mBoxNodePool.put(box);
}
}
2. 回收石块节点
移除节点时,直接把节点放入节点池,提供下次需要时使用,代码如下:
public static getBox(){
if(this.mBoxNodePool != null && this.mBoxNodePool.size() > 0){
let box = this.mBoxNodePool.get();
box.stopAllActions();
return box;
}else{
return null;
}
}
3游戏逻辑(Game)
Game脚本组件挂载在游戏场景的根节点上,石块管理脚本组件也一样,如下图:
主要功能有3个:
1. 点击事件逻辑
根据点击位置的x坐标判断,在屏幕左边往左跳,在屏幕右边往右跳。能不能跳之前,需要判断机器人现在是否正在跳,需要注意,代码如下:
onTouchCallback(event: any){
if(!this.mIsPlaying){
return;
}
if(this.nodeRobot.getComponent('Robot').isJumping()){
return;
}
this.bgDown();
this.mIsPlaying = true;
let location = event.getLocation();
if(location.x < cc.winSize.width / 2){
this.robotJumpLeft();
}else{
this.robotJumpRight();
}
}
2.游戏背景运动控制
游戏开始时,计算背景运动的最大y坐标,运动前,判定跳以后是否超过最大坐标,移动到第一屏位置,类似石块摆放逻辑,主要代码如下:
bgDown(){
let maxY = -cc.winSize.height / 2 - 2 * cc.winSize.height;
let interval = this.node.getComponent('BoxMgr').getIntervalY();
// 超出了,刷屏
if(this.nodeBg.y - interval <= maxY){
this.nodeBg.y += 2 * cc.winSize.height;
this.reloadBoxs();
}
// 下移
this.nodeBg.runAction(cc.sequence(
cc.moveBy(0.2, 0, -interval),
cc.callFunc( () => {
})
));
}
3.控制石块重绘
结合游戏背景控制逻辑,判断所有石块是否需要重绘制。
4石块管理逻辑(BoxMgr)
挂载在游戏场景根节点,主要完成以下3项功能:
1. 生成新的石块
对应代码中的 reloadNew函数,代码太多,就不贴代码了,需要的话,下载工程代码看。
2. 加载所有石块
先判断有没有上一屏保留的,有的话,先绘制上一屏,再绘制新的,新的在第三屏能显示的,需要保留,用来下次切屏的时候绘制。
3. 清理石块
清理所有石块,保留在NodeMgr中,代码如下:
clearAll(){
if(this.mMemBoxs != null){
for(let i = 0; i < this.mMemBoxs.length; i++){
this.putBox(this.mMemBoxs[i]);
}
this.mMemBoxs = [];
}
if(this.mNewBoxs != null){
for(let i = 0; i < this.mNewBoxs.length; i++){
this.putBox(this.mNewBoxs[i]);
}
this.mNewBoxs = [];
}
}
5机器人逻辑(Robot)
主要功能
根据下一跳方向,判断机器人能否跳过去,对应代码中的jump函数。
//------------Box.ts--------------------
import NodeMgr from "./NodeMgr";
/*
* @Copyright: Copyright (c) 2019
* @Author:
* @Version: 1.0
* @Date: 2019-11-22 19:05:49
*/
// Learn TypeScript:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/typescript.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/typescript.html
// Learn Attribute:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/life-cycle-callbacks.html
const {ccclass, property} = cc._decorator;
@ccclass
export default class Box extends cc.Component {
@property(cc.Label)
txtNum: cc.Label = null;
private mPrevBox: cc.Node = null;
private mNextBox: cc.Node = null;
private mOffset: number = 0; // [-4,4]
// LIFE-CYCLE CALLBACKS:
// onLoad () {}
start () {
}
// update (dt) {}
setOffset(offset: number){
this.mOffset = offset;
}
getOffset(){
return this.mOffset;
}
setPrev(prev: cc.Node){
this.mPrevBox = prev;
}
getPrev(){
return this.mPrevBox;
}
setNext(next: cc.Node){
this.mNextBox = next;
}
getNext(){
return this.mNextBox;
}
setNum(num: number){
this.txtNum.string = `${num}`;
}
down(y: number){
this.node.runAction(cc.sequence(
cc.moveBy(0.4, 0, y),
cc.callFunc( () => {
NodeMgr.putBox(this.node);
})
));
}
}
//-------------BoxMgr.ts-------------------
import NodeMgr from "./NodeMgr";
/*
* @Copyright: Copyright (c) 2019
* @Author:
* @Version: 1.0
* @Date:
*/
// Learn TypeScript:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/typescript.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/typescript.html
// Learn Attribute:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/life-cycle-callbacks.html
const {ccclass, property} = cc._decorator;
@ccclass
export default class BoxMgr extends cc.Component {
// 障碍物父节点
@property(cc.Node)
nodeParent: cc.Node = null;
// 障碍物预制
@property(cc.Prefab)
prefabBox: cc.Prefab = null;
private mMaxY: number = 0; // 最大Y坐标
private mMemMinY: number = 0; // 需要保留的最小Y值
private mStartY: number = 200; // 起始位置
private mNewBoxs: cc.Node[] = []; // 所有的障碍物
private mMemBoxs: cc.Node[] = []; // 需要保存的障碍物
private mMaxOffset: number = 3; // 最大偏移量
private mCurrOffset: number = 0; // 当前偏移量
private mMaxZIndex:number = 10000; // 最大层级
private mCurrZIndex: number = 0; // 当前层级
private mIntervalX: number = 0; // X间距
private mIntervalY: number = 0; // Y间距
private mStandBox: cc.Node = null; // 障碍物链头
private mIsNew: boolean = true;
// LIFE-CYCLE CALLBACKS:
onLoad () {
this.mMaxY = 3 * cc.winSize.height + this.prefabBox.data.height / 2;
this.mMemMinY = 2* cc.winSize.height - this.prefabBox.data.height / 2;
this.mIntervalX = this.prefabBox.data.width * 0.5;
this.mIntervalY = this.prefabBox.data.height * 0.5;
}
start () {
// this.reloadAll();
}
/**
* @description: 生成一个障碍物
* @param {type}
* @return:
*/
getBox(){
let box = NodeMgr.getBox();
if(box == null){
box = cc.instantiate(this.prefabBox);
}
return box;
}
/**
* @description: 回收一个障碍物
* @param {type}
* @return:
*/
putBox(box: cc.Node){
if(box != null){
NodeMgr.putBox(box);
}
}
clearAll(){
if(this.mMemBoxs != null){
for(let i = 0; i < this.mMemBoxs.length; i++){
this.putBox(this.mMemBoxs[i]);
}
this.mMemBoxs = [];
}
if(this.mNewBoxs != null){
for(let i = 0; i < this.mNewBoxs.length; i++){
this.putBox(this.mNewBoxs[i]);
}
this.mNewBoxs = [];
}
}
/**
* @description: 重新加载所有障碍物
* @param {type}
* @return:
*/
reloadAll(){
for(let i = 0; i < this.mNewBoxs.length; i++){
this.putBox(this.mNewBoxs[i]);
}
this.mNewBoxs = [];
this.mCurrZIndex = this.mMaxZIndex;
if(this.mMemBoxs.length <= 0){
this.mIsNew = true;
this.mCurrOffset = 0;
this.reloadNew(this.mStartY);
}else{
this.mIsNew = false;
let i = 0;
while(i < this.mMemBoxs.length){
this.mMemBoxs[i].y -= (2 * cc.winSize.height);
this.mMemBoxs[i].zIndex = this.mCurrZIndex;
this.mMemBoxs[i].getComponent('Box').setNum(this.mCurrZIndex);
this.mCurrZIndex--;
this.mNewBoxs.push(this.mMemBoxs[i]);
i++;
}
let last = this.mMemBoxs[this.mMemBoxs.length - 1];
this.mMemBoxs = [];
this.mCurrOffset = last.getComponent('Box').getOffset();
cc.log('CurrOffset', this.mCurrOffset);
this.reloadNew(last.y);
}
}
/**
* @description: 重新加载新的障碍物(不包含换屏的)
* @param {type}
* @return:
*/
reloadNew(startY: number){
let y = startY + this.mIntervalY;
while(y < this.mMaxY){
let box = this.getBox();
// 障碍物链
if(this.mStandBox == null){
this.mStandBox = box;
}
let random = this.getRandom();
// 第一个居中
if(!this.mIsNew){
// 最右
if(this.mCurrOffset + random > this.mMaxOffset){
this.mCurrOffset -= random;
}
// 最左
else if(this.mCurrOffset + random < -this.mMaxOffset){
this.mCurrOffset -= random;
}
// 合法
else{
this.mCurrOffset += random;
}
}
this.mIsNew = false;
let js = box.getComponent('Box');
box.x = this.mCurrOffset * this.mIntervalX;
box.y = y;
box.zIndex = this.mCurrZIndex;
js.setNum(this.mCurrZIndex);
this.mCurrZIndex--;
box.parent = this.nodeParent;
// 切屏保留
if(y >= this.mMemMinY){
if(this.mMemBoxs.length > 0){
this.mMemBoxs[this.mMemBoxs.length - 1].getComponent('Box').setNext(box);
js.setPrev(this.mMemBoxs[this.mMemBoxs.length - 1]);
}else{
this.mNewBoxs[this.mNewBoxs.length - 1].getComponent('Box').setNext(box);
js.setPrev(this.mNewBoxs[this.mNewBoxs.length - 1]);
}
this.mMemBoxs.push(box);
}else{
if(this.mNewBoxs.length > 0){
this.mNewBoxs[this.mNewBoxs.length - 1].getComponent('Box').setNext(box);
js.setPrev(this.mNewBoxs[this.mNewBoxs.length - 1]);
}else{
js.setPrev(null);
}
this.mNewBoxs.push(box);
}
y += this.mIntervalY;
}
}
/**
* @description: 获取随机数
* @param {type}
* @return:
*/
getRandom(){
let ran = Math.random(); //[0,1)
return (ran < 0.5)? -1: 1;
}
getIntervalX(){
return this.mIntervalX;
}
getIntervalY(){
return this.mIntervalY;
}
getStandBox(){
return this.mStandBox;
}
// update (dt) {}
}
//-----------Game.ts---------------------
// Learn TypeScript:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/typescript.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/typescript.html
// Learn Attribute:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/life-cycle-callbacks.html
const {ccclass, property} = cc._decorator;
@ccclass
export default class Game extends cc.Component {
@property(cc.Node)
nodeBg: cc.Node = null;
@property(cc.Node)
nodeRobot: cc.Node = null;
@property(cc.Node)
nodeEnd: cc.Node = null;
@property(cc.Label)
txtPoints: cc.Label = null;
// 是否正在移动
private mIsPlaying: boolean = false;
private mPoints: number = 0;
// LIFE-CYCLE CALLBACKS:
onLoad () {
this.node.on(cc.Node.EventType.TOUCH_START, (event: any) => {
event.stopPropagation();
this.onTouchCallback(event);
});
this.mPoints = 0;
}
start () {
this.reloadBoxs();
let robot = this.nodeRobot.getComponent('Robot');
robot.setAddCallback( () => {
this.onAddPoints();
});
robot.setFailedCallback( () => {
this.onGameEnd();
});
this.mIsPlaying = true;
}
onAddPoints(){
this.mPoints++;
}
onGameEnd(){
this.mIsPlaying = false;
this.txtPoints.string = `得分:${this.mPoints}`;
this.nodeEnd.active = true;
}
reloadBoxs(){
let boxMgr = this.getComponent('BoxMgr');
boxMgr.reloadAll();
let robot = this.nodeRobot.getComponent('Robot');
if(robot.getCurrent() == null){
let standBox = boxMgr.getStandBox();
let nextBox = standBox.getComponent('Box').getNext();
robot.setCurrent(standBox);
robot.setNext(nextBox);
if(nextBox.x > standBox.x){
robot.turnRight();
}else{
robot.turnLeft();
}
}else{
robot.setCurrent(robot.getCurrent());
}
}
robotJumpLeft(){
let js = this.nodeRobot.getComponent('Robot');
js.turnLeft();
js.jump();
}
robotJumpRight(){
let js = this.nodeRobot.getComponent('Robot');
js.turnRight();
js.jump();
}
// update (dt) {}
/**
* @description: 背景下移
* @param {type}
* @return:
*/
bgDown(){
let maxY = -cc.winSize.height / 2 - 2 * cc.winSize.height;
let interval = this.node.getComponent('BoxMgr').getIntervalY();
// 超出了,刷屏
if(this.nodeBg.y - interval <= maxY){
this.nodeBg.y += 2 * cc.winSize.height;
this.reloadBoxs();
}
// 下移
this.nodeBg.runAction(cc.sequence(
cc.moveBy(0.2, 0, -interval),
cc.callFunc( () => {
})
));
}
/**
* @description: 点击屏幕
* @param {type}
* @return:
*/
onTouchCallback(event: any){
if(!this.mIsPlaying){
return;
}
if(this.nodeRobot.getComponent('Robot').isJumping()){
return;
}
this.bgDown();
this.mIsPlaying = true;
let location = event.getLocation();
if(location.x < cc.winSize.width / 2){
this.robotJumpLeft();
}else{
this.robotJumpRight();
}
}
onClick(){
this.nodeEnd.active = false;
cc.director.loadScene('game');
}
}
//------------Home.ts--------------------
// Learn TypeScript:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/typescript.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/typescript.html
// Learn Attribute:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/life-cycle-callbacks.html
const {ccclass, property} = cc._decorator;
@ccclass
export default class NewClass extends cc.Component {
@property(cc.Label)
label: cc.Label = null;
@property
text: string = 'hello';
// LIFE-CYCLE CALLBACKS:
// onLoad () {}
start () {
}
// update (dt) {}
onClick(event, data){
switch(data){
case 'start':{
cc.director.loadScene('game');
break;
}
}
}
}
//-------------NodeMgr.ts-------------------
// Learn TypeScript:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/typescript.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/typescript.html
// Learn Attribute:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/life-cycle-callbacks.html
const {ccclass, property} = cc._decorator;
@ccclass
export default class NodeMgr {
private static mBoxNodePool: cc.NodePool = null;
public static putBox(box: cc.Node){
if(this.mBoxNodePool == null){
this.mBoxNodePool = new cc.NodePool('boxs');
}
if(box != null){
this.mBoxNodePool.put(box);
}
}
public static getBox(){
if(this.mBoxNodePool != null && this.mBoxNodePool.size() > 0){
let box = this.mBoxNodePool.get();
box.stopAllActions();
return box;
}else{
return null;
}
}
}
//--------------Robot.ts------------------
// Learn TypeScript:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/typescript.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/typescript.html
// Learn Attribute:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/reference/attributes.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - [Chinese] https://docs.cocos.com/creator/manual/zh/scripting/life-cycle-callbacks.html
// - [English] http://www.cocos2d-x.org/docs/creator/manual/en/scripting/life-cycle-callbacks.html
const {ccclass, property} = cc._decorator;
enum RobotFace{
Left,
Right
}
@ccclass
export default class Robot extends cc.Component {
@property(cc.SpriteFrame)
spfLeft: cc.SpriteFrame = null;
@property(cc.SpriteFrame)
spfRight: cc.SpriteFrame = null;
@property(cc.Label)
txtPoints: cc.Label = null;
private mPrevNode: cc.Node = null; // 上一次站着的节点
private mCurrNode: cc.Node = null; // 当前站着的节点
private mNextNode: cc.Node = null; // 下一个节点
private mOffset: cc.Vec2 = cc.v2(11, 120); // 机器人与障碍物中心间隔
private mRobotFace:number = -1; // -1朝左,1朝右
private mAddCallback: any = null;
private mFailedCallback: any = null;
private mJumping: boolean = false;
private mPoints: number = 0;
// LIFE-CYCLE CALLBACKS:
// onLoad () {}
start () {
this.node.zIndex = 20000;
this.txtPoints.string = `${this.mPoints}`;
}
// update (dt) {}
/**
* @description: 向左转
* @param {type}
* @return:
*/
turnLeft(){
this.mRobotFace = RobotFace.Left;
this.node.getComponent(cc.Sprite).spriteFrame = this.spfLeft;
}
/**
* @description: 向右转
* @param {type}
* @return:
*/
turnRight(){
this.mRobotFace = RobotFace.Right;
this.node.getComponent(cc.Sprite).spriteFrame = this.spfRight;
}
/**
* @description: 设置下一个节点
* @param {type}
* @return:
*/
setNext(node: cc.Node){
this.mNextNode = node;
}
/**
* @description: 设置当前节点
* @param {type}
* @return:
*/
setCurrent(node: cc.Node){
this.mCurrNode = node;
this.node.position = this.mCurrNode.position.add(this.mOffset);
}
getCurrent(){
return this.mCurrNode;
}
setAddCallback(callback: any){
this.mAddCallback = callback;
}
setFailedCallback(callback: any){
this.mFailedCallback = callback;
}
/**
* @description: 跳
* @param {type}
* @return:
*/
jump(){
if(this.mNextNode == null){
return;
}
this.mJumping = true;
let curPos = this.node.position;
let nextPos = this.mNextNode.position;
nextPos = nextPos.add(this.mOffset);
// 能跳上去
if((this.mRobotFace == RobotFace.Left && nextPos.x < curPos.x) ||
(this.mRobotFace == RobotFace.Right && nextPos.x > curPos.x)){
this.node.runAction(cc.sequence(
cc.jumpTo(0.2, nextPos, 30, 1),
cc.callFunc( () => {
this.mPrevNode = this.mCurrNode;
this.mCurrNode = this.mNextNode;
this.mNextNode = this.mCurrNode.getComponent('Box').getNext();
this.mJumping = false;
}),
cc.callFunc( () => {
if(this.mPrevNode != null){
this.mPrevNode.getComponent('Box').down(-this.getDownY(this.mPrevNode));
this.mPrevNode = null;
}
if(this.mAddCallback != null){
this.mAddCallback();
}
this.mPoints++;
this.txtPoints.string = `${this.mPoints}`;
})
));
}
// 跳不上去
else{
let targetPos = curPos;
if(nextPos.x > curPos.x){
targetPos.x -= 130;
}else{
targetPos.x += 130;
}
targetPos.y += 65;
this.node.runAction(cc.sequence(
cc.jumpTo(0.2, targetPos, 30, 1),
cc.callFunc( () => {
this.mJumping = false;
if(this.mPrevNode != null){
this.mPrevNode.getComponent('Box').down(-this.getDownY(this.mPrevNode));
this.mPrevNode = null;
}
this.node.runAction(cc.sequence(
cc.moveBy(0.5, 0, -this.getDownY(this.node)),
cc.callFunc( () => {
if(this.mFailedCallback != null){
this.mFailedCallback();
}
})
));
}),
));
}
}
getDownY(node: cc.Node){
let pos = node.parent.convertToWorldSpace(node.position);
let y = pos.y + node.height;
return y;
}
isJumping(){
return this.mJumping;
}
}
感谢:
本文转载自https://mp.weixin.qq.com/s/34vJUZR7XIfkZDsBATqJGQ这篇文章,这里感谢原作者对于技术的分享。
下载: