之前总结了一个用pixi 实现的人物换装游戏,没看过的可以看 PIXI 实现人物换装 今天继续总结用 pixi 实现一个 红包雨 H5 游戏,可以来体验下
相信大家对这个游戏应该不陌生了,支付宝 QQ 啥的春节的时候都有这种游戏。
作为一个前端,做界面相关的实现肯定是我们应有的能力
学一些游戏的实现也可以帮助我们自己,自己开发多一些游戏,可以让朋友间互动,比如结婚的时候,要让我们的能力为我们的生活服务嘛哈哈
不废话了,下面开始讲解这个游戏的具体实现
1、游戏简介
2、游戏实现
3、代码仓库
游戏简介
游戏玩法很简单,去体验下就知道了
红包雨使用的游戏引擎依然是pixi,还不懂PIXI的可以看 PIXI 需求级入门
另外我们还使用了一个动画库来让属性变化动画更加丝滑,比如坐标位置的移动变化,透明度的变化,他就是
gsap
gsap 介绍他是
1、高性能js 动画工具库
2、超强浏览器兼容
3、支持多种实现方式(React、Vue、css、canvas,svg)
4、Chrome 推荐的动画库
5、行业动画标准
6、超千万网站使用
反正就是很牛逼的动画库
官方文档可以看
https://greensock.com/docs/
主要它可以配合pixi 完成动画,简直是不二选择,只要注册 pixi 插件就行了
import { PixiPlugin } from 'gsap/PixiPlugin.js';
gsap.registerPlugin(PixiPlugin);
虽然可能没用过,但是这次涉及的 gsap api 不多,就三个 from 、to、fromTo,都很简单
fromTo ,从某个状态开始,到某个状态结束,需要设置两个状态
// html
<div class="fromTo">1111</div>
//js
gsap.fromTo(".fromTo", { autoAlpha: 0, x: 0 }, { autoAlpha: 1, x: 100 });
from ,从某个状态开始,设置的是起始状态
<div class="from">2222</div>
gsap.from(".from", { opacity: 0, y: 100, duration: 1 });
to , 过渡到某个状态,设置的是结束状态
<div class="to">3333</div>
gsap.to(".to", { opacity: 0, x: 100, duration: 1 });
游戏实现
游戏实现分了 7个部分来详细说明,本文主要讲解主逻辑,边边角角不会太详细,详细的可以看具体仓库代码
1、总览
2、数据配置
3、数据通信
4、代码详解 - 红包
5、代码详解 - 倒计时
1总览
看下整个游戏的流程图 和 代码架构图
流程图
代码架构图
App
功能的入口,控制整个游戏的生命流程,包括其中 红包的定时生成,启动倒计时,监听倒计时结束后清空
class App{
constructor(container) {
super();
this.app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight - 195,
antialias: true, // default: false 反锯齿
autoDensity: true, // canvas视图的CSS尺寸是否应自动调整为屏幕尺寸。
resolution:2, // default: 1 分辨率
backgroundColor: 0x000000,
});
// 挂载到DOM 上
container?.appendChild(this.app.view);
}
start(){ ... } // 开始游戏
createRedPkg(){ ... }
onRedPkgClick(){ ... }
createTimer(){ ... }
onTimeEnd(){ ... }
}
RedPkg
负责红包具体的绘制细节,销毁,监听点击,具体看下面的讲解
class RedPackage {
create(){ ... }
destry(){ ... }
onClick(){ ... }
}
DropBase
把元素坠落动画的功能抽离了出来,主要是这部分是通用能力,游戏中坠落的元素可以有很多种,比如我们的游戏就有 红包和 星星,所以把能力抽离出来,让他们去继承从而拥有这些能力
Timer 、Score
倒计时和 分数 功能比较简单,只是绘制的内容比较复杂
2数据配置
把游戏的一些基础配置数据都抽离出来,单独管理
import RedPkgImg from './img/redpkg.jpg';
export const GAME_CONFIG = {
assets: {
redPackageImg: RedPkgImg, // 红包图片
},
animations: {
redPackageFrequency: 300, // 红包生成频率
countdownTotal: 5000, // 游戏时长
redPackageDuration: 4000, // 红包坠落时长
redPackageEaseOut: 200, // 红包消失动画时长
},
// 奖品列表
lotteryList: ['久旱逢甘霖', '金榜题名时', '洞房花烛夜', '他乡遇故知'],
}
3数据通信
App 作为入口,负责元素的控制,包括红包生成,倒计时、分数的绘制等等,而具体的细节则交给对应的类去完成
比如 App 只负责调用 RedPkg 的create 方法,具体怎么 create 由 RedPkg 去负责
App 想要知道 什么时候 create 完毕,就需要 RedPkg 去通知App,所以这里就用了 eventemitter3 这个库去实现监听通信
比如这样
class App {
start(){
while(true){
this.createRedPkg()
}
}
createRedPkg(){
const rpg = new RedPkg()
rpg.create()
rpg.on("createEnd",()=>{
console.log("绘制结束")
})
}
}
class RedPkg extends eventemitter3{
create(){
.....
this.emit("createEnd")
}
}
4代码详解 - 红包
红包生成逻辑
绘制也没什么复杂的,不过我们需要随机设定他坠落的起始位置,毕竟不能所有红包都从一个位置下来把
class RedPkg {
element = null
create() {
this.element = PIXI.Sprite.from("红包url..");
this.element.width = 60;
this.element.height = 90;
this.element.anchor.set(0.5);
this.setInitPos()
// 监听点击事件
this.element.interactive = true;
this.element.on('pointerdown', (e) => {
this.onClick(e);
});
// 添加进容器
this.app.stage.addChild(this.element);
}
setInitPos(){ ... }
}
随机位置从 设定好的三个方向中 随机抽取
主要就是通过 Math.random *3 ,这样就得到 1-3 之前的随机数字
class RedPkg {
initPositions = [
[window.innerWidth * 0.25, 0],
[window.innerWidth * 0.5, 0],
[window.innerWidth * 0.75, 0],
];
setInitPos(){
// 设置随机初始坐标
const [x, y] = this.getRandomPosition();
this.element.x = x;
this.element.y = y;
}
getRandomPosition() {
const randomNum = Math.random() * this.initPositions.length
return this.initPositions[ Math.floor(randomNum)];
}
}
除了设置初始的位置,我们还会设置一个随机初始的 角度,代码是一样的
而 红包什么时候开始生成 ,是由App 控制的
当调用游戏开始的时候,App会使用一个 setInterval 去循环生成 红包
class App {
start(){
this.redPkgTimer = setInterval(() => {
this.createRedPkg();
},250);
}
createRedPkg(){
const redPkg = new RedPkg()
redPkg.create()
}
}
红包掉落逻辑
这里的内容主要是红包坠落的动画,观察这个动画,一个是从上至下的坠落动画,一个是左右摇晃的动画,毕竟是模拟雨嘛,并不是直上直下的
这里就用了前面说的动画库 gasp,控制的动画是 红包元素 的 y 坐标 和 x 坐标 变化
先看最基础的坠落动画,使用 gasp.to 设置 结束状态的坐标是 【屏幕高度+50】
然后 gasp 就可以控制 红包元素 从起始位置一直移动到结束位置,从而达到 坠落的效果
import gsap from 'gsap/all';
class DropBase {
element = null
drop() {
this.animateY();
this.animateX();
}
animateY() {
gsap.to(this.element, {
y: window.innerHeight + 50,
duration: 5000, // 坠落的时间,前面抽离出来的配置
ease: 'none',
onComplete: () => {
this.app.stage?.removeChild?.(this.element);
},
});
}
}
class RedPkg extends DropBase{ ... }
class App {
createRedPkg(){
const redPkg = new RedPkg()
redPkg.create()
redPkg.drop() // 手动控制坠落
}
}
下面来看下 X 坐标的动画,其实就是横向的偏移动画
这里的逻辑主要是几点
1、设置横向偏移的幅度值,比如这里设置的是 左右最大偏移25
2、偏移是从一端到另一端,所以使用 gasp.fromTo 设定起始状态 和 结束状态
3、为了防止元素偏移到屏幕之后,计算出偏移值之后会 进行比较
先简单看下代码
class DropBase {
animateX() {
const initX = this.element.x
// 起始位置是 位置左偏移X 25
const from = {
x: Math.max(0, initX - 25),
};
// 结束位置是 位置右偏移X 25
const to = {
x: Math.min(initX + 25, window.innerWidth),
};
// gasp 配置
const option = {
duration: 2,
repeat: -1,
yoyo: true, //
ease: 'power1.inOut',
};
// 左右随机一下,以免都是同一个方向的偏移
if (Math.random() > 0.5) {
gsap.fromTo(this.element, from, {
...to,
...option,
});
} else {
gsap.fromTo(this.element, to, {
...from,
...option,
});
}
}
}
上面代码需要解释两个地方
1、配置中出现的 repeat 和 yoyo
可想而知,左右横移动画应该是重复的,而不是执行一次就结束了,所以这里设置 repeat = -1,表示为无限循环
yoyo 类似于 css 中的 animation-direction:alternate
作用是 动画在奇数次(1、3、5...)正向播放,在偶数次(2、4、6...)反向播放。
就像这样循环往复的效果
不然每完成一次动画都从头开始
2、偏移方向随机
为了防止所有红包 都往一个方向偏移,所以这里会随机处理一下,有的往左,有的往右
也就是调换一下 from 和 to
红包点击逻辑
红包点击之后需要做几个事情
1、红包移除+消失动画
2、分数+1
在 监听到 红包元素点击的时候,就需要通知到总控室 App,控制分数+1
class RedPkg {
element = null
create() {
this.element = PIXI.Sprite.from("红包url..");
// 监听点击事件
this.element.interactive = true;
this.element.on('pointerdown', (e) => {
this.onClick(e);
});
}
onClick(){
this.emit("redPkgClick",()=>{
// 红包消失动画...
})
}
}
class App {
createRedPkg(){
const redPkg = new RedPkg()
redPackage.on('redPkgClick', this.onRedPackageClick);
}
onRedPackageClick(){ ... 分数+1 逻辑 }
}
另外点击之后还有一个红包消失的动画,这部分内容主要是 ,不复杂,但是挺麻烦的,不过不属于主体逻辑,所以不放在这里说,具体可以看仓库代码
5代码详解 - 倒计时
倒计时内容主要有两部分
1、绘制的逻辑
2、通信逻辑
其中绘制的内容也不会细说,主要看仓库代码,这里讲讲 倒计时 和 App 的关系
倒计时内部,主要就是使用 setInterval 完成时间计算,然后在倒计时结束的时候触发 timeEnd 事件
class Timer extends EventEmitter {
remainTime = 5000 // 剩余时长
duration=1000 // 单位时长
start() {
this.draw();
const timer = setInterval(() => {
this.remainTime -= this.duration;
if (this.remainTime <= 0) {
clearInterval(timer);
this.emit('timeEnd');
}
}, this.duration);
}
// 绘制倒计时
draw(){...}
}
App 则是负责初始化倒计时,监听倒计时事件,在倒计时结束的时候,完成收尾动作
class App {
start(){
this.createTimer()
this.redPkgTimer = setInterval(() => {
this.createRedPkg();
},250);
}
createTimer(){
this.timer = new Timer();
this.timer.start();
this.timer.on('timeEnd', () => {
// ....移除所有元素,销毁 pixi 容器
this.destroy()
});
}
}
倒计时结束的收尾动作,主要是
1、销毁红包生成定时器
2、销毁所有 pixi 元素
3、销毁 pixi 容器
class App{
destroy() {
clearInterval(this.redPkgTimer as number);
this.redPkgTimer = null;
while (this.app?.stage.children[0]) {
this.app.stage.removeChild(this.app.stage.children[0]);
}
this.app?.destroy(true);
this.app?.renderer?.destroy(true);
}
}
代码仓库
https://gitee.com/hoholove/study-code-snippet/tree/master/PIXI/HongBaoYu