引言
好了,上一章节(egret实战教程之跳一跳(一))我们已经把静态场景做好了✌,一切准备就绪,就差写逻辑(写 bug)了?。废话不多说,赶紧直奔主题吧。
开始页面的逻辑
由于开始页面比较单调,只有一个开始游戏的按钮,所以我们只需要在按钮上添加一个事件监听即可。具体逻辑就是当触摸事件发生时,我们将把 SceneGame
添加到舞台中,同时把 BeginScene
从舞台中移除,代码如下:
// SceneGame.ts
public beginBtn:eui.Button;
private init() {
// 这里的 once 其实就是 addEventListner 的意思,只不过它只监听一次
this.beginBtn.once(egret.TouchEvent.TOUCH_TAP, this.start, this);
}
private start() {
// 在舞台中添加游戏场景
this.parent.addChild(new SceneGame());
// 在舞台中移除初始场景
this.parent.removeChild(this);
}
复制代码
游戏页面的逻辑
现在我们点击开始按钮就能够跳到游戏界面,接下来就是高大上?的游戏逻辑了,也是本文的精髓所在?,撸起袖子加油敲吧!
变量声明
这里要声明一堆变量,具体可以去看文章末尾的源码,就不全部复制过来了。
// SceneGame.ts 中主要的变量声明
// 当前的盒子(最新出现的盒子,就是准备要跳过去的目标盒子)
private currentBlock: eui.Image;
// 下一个的盒子方向(1向右,-1向左)
public direction: number = 1;
// tanθ 角度值(可自己微调),和 direction 配合计算出下一个盒子的坐标
public tanAngle: number = 0.556047197640118;
// 随机盒子的最大最小(水平)距离
private minDistance = 220;
private maxDistance = 320;
// 跳的距离(也就是根据你按压时间算出来的),这里指的是水平方向上的距离
public jumpDistance: number = 0;
// 左侧跳跃点(固定的,可自己微调)
private leftOrigin = { "x": 180, "y": 350 };
// 右侧跳跃点(固定的,可自己微调)
private rightOrigin = { "x": 505, "y": 350 };
复制代码
初始化界面
首先我们要看下游戏的初始界面长什么样,才知道 init
函数里面写什么:
ok,我们先简要看下一个方块是如何生成并添加到舞台上(重复的东西我们一般会写成一个类或者方法,这里写的是方法),其实就是贴图,好比我们用
new Image()
一样,再设置
src
等属性即可,具体请看下面代码:
// 创建一个方块
private createBlock(): eui.Image {✌
// 随机背景图
let n = Math.floor(Math.random() * this.blockSourceNames.length);
// 实例化并添加到舞台中
let blockNode = new eui.Image();
blockNode.source = this.blockSourceNames[n];
this.blockPanel.addChild(blockNode);
// 设置方块的锚点(之前说过的不是图片的中心点,而是图中盒子的中心点)
blockNode.anchorOffsetX = 222;
blockNode.anchorOffsetY = 78;
blockNode.touchEnabled = false;
// 把新创建的方块添加进入 blockArr 里,统一管理
this.blockArr.push(blockNode);
return blockNode;
}
// 添加一个方块并设置 xy 值
private addBlock() {
// 创建一个方块
let blockNode = this.createBlock();
// 随机水平位置(在最大最小值之间的一个数,毕竟屏幕就那么大)
let distance = this.minDistance + Math.random() * (this.maxDistance - this.minDistance);
if (this.direction > 0) { // 向右跳
blockNode.x = this.currentBlock.x + distance;
blockNode.y = this.currentBlock.y - distance * this.tanAngle;
} else { // 向左跳
blockNode.x = this.currentBlock.x - distance;
blockNode.y = this.currentBlock.y - distance * this.tanAngle;
}
this.currentBlock = blockNode;
}
复制代码
ok,现在我们知道了怎么创建方块,接下来就看看 init
函数里面的代码吧,瞅瞅初始化的时候都做了啥:
// SceneGame.ts
private init() {
// 所有盒子资源
this.blockSourceNames = ["block1_png", "block2_png", "block3_png"];
// 加载按下和跳跃的声音
this.pushVoice = RES.getRes('push_mp3');
this.jumpVoice = RES.getRes('jump_mp3');
// 初始化场景(方块和小人)
this.initBlock();
// 添加触摸事件
this.blockPanel.touchEnabled = true;
this.blockPanel.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.onTapDown, this);
this.blockPanel.addEventListener(egret.TouchEvent.TOUCH_END, this.onTapUp, this);
// 心跳计时器(目的:计算按的时长,推算出跳的距离)
egret.startTick(this.computeDistance, this);
}
private initBlock() {
// 初始化第一个方块,并设置相关的属性(主要就是在舞台中的位置也就是xy值)
this.currentBlock = this.createBlock();
this.currentBlock.x = this.leftOrigin.x;
this.currentBlock.y = this.stage.stageHeight - this.leftOrigin.y;
this.blockPanel.addChild(this.currentBlock);
// 初始化小人(小人的锚点在底部的中间)
this.player.y = this.currentBlock.y;
this.player.x = this.currentBlock.x;
this.player.anchorOffsetX = this.player.width / 2;
this.player.anchorOffsetY = this.player.height - 20;
this.blockPanel.addChild(this.player);
// 初始化得分
this.score = 0;
this.scoreLabel.text = this.score.toString();
this.blockPanel.addChild(this.scoreLabel);
// 初始化方向
this.direction = 1;
// 添加下一个盒子
this.addBlock();
}
复制代码
上面的代码注释应该都写得挺清楚了,我们主要讲一下其中的难点 egret.startTick(this.computeDistance, this)
。startTick
这个 api 将会以 60 帧速率来调用 this.computeDistance
这个方法,不明白?没关系,假想成 setInterval
就好了。this.computeDistance
这个方法的主要目的就是通过按压时间来计算出跳跃的水平距离,看下面的代码应该不难理解:
// SceneGame.ts
// 这个函数需要返回布尔值(规定),具体还不是很清楚它的作用,但不影响我们写代码
private computeDistance(timeStamp:number):boolean {
// timeStamp 是一个自增的时间(执行到当前所逝去的时间,比如0,500,1000...,单位 ms)
let now = timeStamp;
let time = this.time;
let pass = now - time;
pass /= 1000;
if (this.isReadyJump) {
// 通过按压时间(就是 s = vt)来计算出跳的距离(这里指的是水平位移)
this.jumpDistance += 300 * pass; // 300 是调试出来的参数,可自行更改
}
this.time = now;
return true;
}
复制代码
其实上面的内容都是铺垫,下面才正式开始写游戏部分的逻辑???,深吸一口气,心态要稳,车还是要继续开的。
起跳前
也就是当我们触摸界面的时候,需要做什么呢,先在脑海中回忆一下?。。。。
没错,就两件事情,播放一下按下的音效,然后为了逼真一点,给小人加上 y 轴上的形变即可(简单到爆),代码如下:
// SceneGame.ts
private onTapDown() {
// 播放按下音效,参数为(从哪里开始播放,播放次数)
this.pushSoundChannel = this.pushVoice.play(0, 1);
// 使小人变矮做出积蓄能量的效果,就是缩放Y轴
egret.Tween.get(this.player).to({scaleY: 0.5}, 3000);
// 起跳的标记
this.isReadyJump = true;
}
复制代码
起跳时
也就是当我们手指离开界面的时候,总共要做以下几件事情:
1、将舞台置为不可点击状态;
2、切换声音;
3、通过按压时间来计算跳跃的水平距离;
4、小人沿曲线起跳并旋转;
先上代码再解释:
// SceneGame.ts
private onTapUp() {
if (!this.isReadyJump) return;
if (!this.targetPos) this.targetPos = new egret.Point(); // point 就是个点,有 xy 值等
// 一松手小人就该起跳,此时应先禁止点击屏幕,并切换声音
this.blockPanel.touchEnabled = false;
this.pushSoundChannel.stop();
this.jumpVoice.play(0, 1);
// 清除所有动画
egret.Tween.removeAllTweens();
this.isReadyJump = false;
// 计算落点坐标
this.targetPos.x = this.player.x + this.direction * this.jumpDistance;
this.targetPos.y = this.player.y + this.direction * this.jumpDistance * (this.currentBlock.y - this.player.y) / (this.currentBlock.x - this.player.x);
// 执行跳跃动画
egret.Tween.get(this).to({ factor: 1 }, 400).call(() => { // 这表示贝塞尔曲线,在 400 毫秒内,this 的 factor 属性将会缓慢趋近1这个值,这里的 factor 就是曲线中的 t 属性,它是从 0 到 1 的闭区间。
this.player.scaleY = 1;
this.jumpDistance = 0;
// 判断跳跃是否成功
this.checkResult();
});
// 执行小人空翻动画,先处理旋转中心点
this.player.anchorOffsetY = this.player.height / 2;
egret.Tween.get(this.player)
.to({ rotation: this.direction > 0 ? 360 : -360 }, 200)
.call(() => { this.player.rotation = 0 })
.call(() => { this.player.anchorOffsetY = this.player.height - 20; });
}
// 添加 factor 的 set、get 方法
public get factor():number {
return 0;
}
// 这里的 getter 使 factor 属性从 0 开始,结合刚才 tween 中传入的 1,使其符合公式中的 t 的取值区间。
// 而重点是这里的 setter,里面的 player 对象是我们要应用二次贝塞尔曲线的显示对象,而在 setter 中给 player 对象的 xy 属性赋值的公式正是之前列出的二次贝塞尔曲线公式。
public set factor(t:number) {
// 仅仅是个公式
this.player.x = (1 - t) * (1 - t) * this.player.x + 2 * t * (1 - t) * (this.player.x + this.targetPos.x) / 2 + t * t * (this.targetPos.x);
this.player.y = (1 - t) * (1 - t) * this.player.y + 2 * t * (1 - t) * (this.targetPos.y - 300) + t * t * (this.targetPos.y);
}
复制代码
比较难理解的应该是?♀️小人?怎么沿贝塞尔曲线(这种令人迷茫的数学名词)运动了,这里我们就小小剖析(扯淡?)一下。看下 egret.Tween.get(this).to({ factor: 1 }, 400)
里面的 factor
,这是什么意思呢?factor
是一个属性,你就当做是个变量吧,它的初始值为 0,我们用 egret.Tween
这个缓动函数让 factor
的值在 400 ms 内从 0 变成 1,factor
的值改变了,根据 public set factor(t:number) {}
,小人的坐标也将跟着改变,于是小人就动起来了。好好体会一下。
也许你又会问,那小人的坐标为什么那样写呢?其实说白了,这就是一个公式,啥公式呢,如下图:
egret.Tween.get(this.player).to({ x: this.targetPos.x, y: this.targetPos.y }, 400).call(() => {})
复制代码
看到这里真是不容易啊,跨过了一个坎,得给自己鼓个掌?????
起跳后
马不停蹄,同志们请继续加油,黎明就在眼前?,fighting。 现在,小人已经跳到目标方块上了,此时我们需要判断一下,小人落地的位置是不是在允许误差范围内,以此来判断成功和失败,先来看下下面这张图:
我们知道了小人和方块的位置,就可以求出二者的误差是多少(就是求斜边),如果小于一定范围我们就认为此次跳跃是成功的,代码如下:// SceneGame.ts
private checkResult() {
// 实际误差
let err = Math.pow(this.player.x - this.currentBlock.x, 2) + Math.pow(this.player.y - this.currentBlock.y, 2)
// 允许的最大误差
const MAX_ERR_LEN = 90 * 90;
if (err <= MAX_ERR_LEN) { // 跳跃成功
// 更新分数
this.score++;
this.scoreLabel.text = this.score.toString();
// 要跳动的方向
this.direction = Math.random() > 0.5 ? 1 : -1;
// 当前方块要移动到相应跳跃点的距离
let blockX, blockY;
blockX = this.direction > 0 ? this.leftOrigin.x : this.rightOrigin.x;
blockY = this.stage.stageHeight / 2 + this.currentBlock.height;
// 小人要移动到的点
let diffX = this.currentBlock.x - blockX;
let diffY = this.currentBlock.y - blockY;
let playerX, playerY;
playerX = this.player.x - diffX;
playerY = this.player.y - diffY;
// 更新页面,更新所有方块位置
this.updateAll(diffX, diffY);
// 更新小人的位置
egret.Tween.get(this.player).to({
x: playerX,
y: playerY
}, 800).call(() => {
// 开始创建下一个方块
this.addBlock();
// 让屏幕重新可点;
this.blockPanel.touchEnabled = true;
})
} else { // 跳跃失败
this.restartBtn.addEventListener(egret.TouchEvent.TOUCH_TAP, this.reset, this);
this.overPanel.visible = true;
this.overScoreLabel.text = this.score.toString();
}
}
private updateAll(x, y) {
egret.Tween.removeAllTweens();
for (var i: number = this.blockArr.length - 1; i >= 0; i--) {
var blockNode = this.blockArr[i];
// 盒子的中心点(不是图片的中心点)在屏幕左侧 或者在 屏幕右侧 或者在 屏幕下方
if (blockNode.x + blockNode.width - 222 < 0 || blockNode.x - this.stage.stageWidth - 222 > 0 || blockNode.y - this.stage.stageHeight - 78 > 0) {
// 方块超出屏幕,从显示列表中移除
if (blockNode) this.blockPanel.removeChild(blockNode);
this.blockArr.splice(i, 1);
} else {
// 没有超出屏幕的话,则移动
egret.Tween.get(blockNode).to({
x: blockNode.x - x,
y: blockNode.y - y
}, 800)
}
}
}
复制代码
先说下跳跃失败的情况,很显然,我们需要更新分数到结束界面中,并显示结束界面,同时还要在结束界面中的按钮上添加事件监听,对于重置也就是把各种变量重新初始化一遍,这个比较简单,就不细说了。再来看下跳跃成功的情况,我们需要做的有更新分数,随机下一个方向,移动所有的方块和小人,创建下一个方块。这步的难点就在于如何移动画面,所以又要好好装 13 一番?。
我们先这样想,所有的东西一起移动有个共同的地方就是:大家都会往左或往右移动一样的距离,往上或往下移动一样的距离,所以我们只需要知道其中一个方块怎么移,往 x 轴移动多少,往 y 轴移动多少,其他方块和小人也跟着移动一样的距离不就可以了?又可以好好体会一番?。
So 现在我们的首要任务就是知道其中一个方块怎么移,先来看看下面这张图:
diffY
和
diffX
,然后对方块数组进行一个 for 循环,都移动同样的距离即可,小人也是一样的。移动完后再生成下一个方块,就大功告成了✌。
能看到这里实属不易,给读者们几个大大的掌声????,真的优秀!
运行游戏
我们打开 Egret Launcher,点击发布设置,如下图:
选择微信小游戏,并点击设为默认发布,输入自己的 AppID(自己在小程序官网上注册一个,很快的),再写个名称,点击确定即可: 然后会弹出如下一个弹窗: 我们点击使用微信开发者工具打开(前提是你要安装微信开发者工具),就可以预览自己写的游戏效果了,就像下面这样。耶耶耶! 这里补充一点,就是玩的时候你可能会觉得卡,没关系,我们只要改一下游戏的默认帧率就行,就像下面这样:结语
至此,要多粗糙有多粗糙的跳一跳小游戏就可以跑起来了,还等啥,赶紧动手玩玩吧,玩自己写的游戏,感觉还真是非(废)一般呢。当然,问题还是有很多的,比如适配啊,有些手机尺寸容易出现黑边(就很丑);或者细节不到位啊(自己可以试着改改);功能不够完善啊(自己试着加下)。但这些都不重要,重要的是游戏思路,思路才是最值钱的,人与人之间的差别又体现出来了?。
后续呢,我会在这个游戏的基础上加上分享和排行等功能,主要是针对微信小游戏跑一套较为完整的流程,所以就有了第三篇章?(总共应该就三篇了,不能再多了),那么就下期再见撒?????。
跳一跳源码地址:github.com/lgq627628/e…