Egret 小游戏实战教程 跳一跳(搬运二)
开始页面的逻辑
由于开始页面比较单调,只有一个开始游戏的按钮,所以我们只需要在按钮上添加一个事件监听即可
具体逻辑就是当触摸事件发生时,我们将把 SceneGame 添加到舞台中,同时把 BeginScene 从舞台中移除
SceneBegin.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 中声明一堆变量,具体可以去看文章末尾的源码,就不全部复制过来了
// 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.computDistance 这个方法,不明白?没关系,假想成 setInterval 就好了。
this.computDistance 这个方法的主要目的就是通过按压时间来计算出跳跃的水平距离,看下面的代码应该不难理解:
// 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) {} ,小人的坐标也将跟着改变,于是小人就动起来了,好好体会一下
也许你又会问,那小人的坐标为什么那样写呢?其实说白了,这就是一个公式,啥公式呢,如下图:
这公式又是啥呢,就是下面这个:
是不是稍微熟悉了点呢,等等,P0,P1,P2 又是啥?就是三个坐标点啦,我们看下面这幅图会好理解点:
由上图可以看出 P0 是小人的坐标,P2 是目标点的坐标,P1 则是二者中间上方的某一个点(可自己修改),然后带入上述公式即可
如果你还是一头雾水,没关系,不重要,你只要知道若是我们已知三个点(起点,中间点和终点),带入上述公式,就能画出一条贝塞尔曲线就行
再次好好体会一下,有余力的话可以百度了解一下贝塞尔曲线
要是实在理解不了呢,也不打紧,你就不要用它,直接把小人平移到终点就好,不要什么跳跃效果了,就像下面这样:
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
原文:尤水就下
*** | 以上内容仅为学习参考、学习笔记使用 | ***