掘金:egret 实战教程之跳一跳(二)

2019-04-16 H5案例分享 H5案例分享

引言

 

好了,上一章节(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) {},小人的坐标也将跟着改变,于是小人就动起来了。好好体会一下。

也许你又会问,那小人的坐标为什么那样写呢?其实说白了,这就是一个公式,啥公式呢,如下图:

这公式又是啥呢,就是下面这个:
 

 
是不是稍微熟悉了点呢,等等✋,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 现在我们的首要任务就是知道其中一个方块怎么移,先来看看下面这张图:

左图的意思是:我们小人始终是在这两个固定点(可自己微调位置)的其中一个位置起跳的,这很重要。开头也说到了,为什么一开始的方块会固定在左边,其实放右边也可以,但必须是这两个固定点的其中一个,这样我们移动方块的时候才有一个参考点,如上图中的右图所示,小人跳完后所在的那个方块需要移动到其中一个跳跃点上,因为下一个方向向左,所以我们将把小人所在的方块移动到右边的跳跃点上。这样一来,事情就变得简单了。小人所在的方块和右侧跳跃点的坐标值都有了,一减就可以算出 diffYdiffX,然后对方块数组进行一个 for 循环,都移动同样的距离即可,小人也是一样的。移动完后再生成下一个方块,就大功告成了✌。

能看到这里实属不易,给读者们几个大大的掌声👏👏👏👏,真的优秀!

 

运行游戏

 

我们打开 Egret Launcher,点击发布设置,如下图:


选择微信小游戏,并点击设为默认发布,输入自己的 AppID(自己在小程序官网上注册一个,很快的),再写个名称,点击确定即可:
 

 
然后会弹出如下一个弹窗:
 

 
我们点击使用微信开发者工具打开(前提是你要安装微信开发者工具),就可以预览自己写的游戏效果了,就像下面这样。耶耶耶!
 

 
这里补充一点,就是玩的时候你可能会觉得卡,没关系,我们只要改一下游戏的默认帧率就行,就像下面这样:
 

 

结语

 

至此,要多粗糙有多粗糙的跳一跳小游戏就可以跑起来了,还等啥,赶紧动手玩玩吧,玩自己写的游戏,感觉还真是非(废)一般呢。当然,问题还是有很多的,比如适配啊,有些手机尺寸容易出现黑边(就很丑);或者细节不到位啊(自己可以试着改改);功能不够完善啊(自己试着加下)。但这些都不重要,重要的是游戏思路,思路才是最值钱的,人与人之间的差别又体现出来了😂。


后续呢,我会在这个游戏的基础上加上分享和排行等功能,主要是针对微信小游戏跑一套较为完整的流程,所以就有了第三篇章😳(总共应该就三篇了,不能再多了),那么就下期再见撒👋👋👋👋👋。


跳一跳源码地址:github.com/lgq627628/e…




作者:尤水就下也
链接:https://juejin.im/post/5c919e6f5188252d96380308
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 
 
(转自掘金的egret 实战教程之跳一跳(二),作者:尤水就下也,链接:https://juejin.im/post/5c90b4175188252d7a5c4c15
 
 
 
以上为“H5案例分享”团队转载文章,版权归原作者所有。

以上为“H5案例分享”团队原创文章,转载请注明出处!

标签

分享:
海报
案列链接
下载截图
收藏案例
已收藏
分享到微信朋友圈
打开微信,点击底部的“发现”,
使用“扫一扫”即可将网页分享至朋友圈。

相关推荐:

DANIEL WELLINGTON×京东闪购:请查收,你的2019自在态度清单

掘金:egret 实战教程之跳一跳(一)

关注微信号:h5-share,获取更多创意H5案例分享!也可访问H5案例分享网站www.h5anli.com,搜索查阅~

阅读原文

微信扫一扫
关注该公众号

收藏

微信极速登录/注册

对汉语支持不错,别对小五说英文

微信公众号

提交案例

请pc端输入网址

"h5anli.com"

下载截图压缩包~

标签已提交成功

小编正在审核

请等待一下下啦

哎呀

标签已存在

换个新的呗~