使用 Pixi.js 创建 HTML5 游戏
今天(2017.2.14)的 Google 涂鸦是一个小游戏,抛开单身狗不应该玩这个游戏来说,我对这个游戏的实现还是很好奇的,看了一下,是用 pixi.js
来实现的,于是花了两个小时学了一些简单的皮毛,大家可以试试,说不定哪天就用上了。这个地址有几个基础的教程,有兴趣可以自己查看。
了解 pixi.js
pixi.js - The HTML5 Creation Engine,Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.
- Fast PixiJS' strength is speed. When it comes to 2D rendering, PixiJS is the fastest there is.
- Flexible Friendly, feature-rich API lets PixiJS take care of the fundamentals whilst you focus on producing incredible multiplatform experiences.
- Free PixiJS is and always will be Open Source, with a large and supportive community pushing its growth and evolution.
英语太渣,不知道怎么翻译好,直接把官网的介绍 copy 过来.
创建简单的地牢狩猎游戏
首先看看最终效果。
左上角是地牢门,右上角是探险家的血条。地牢里面有一个探险家和 6 只怪物,在地牢的右边有一个宝箱。
游戏规则是:
- 可以用上下左右控制探险家移动
- 怪物一直在上下移动
- 探险家碰到怪物,则血条就一直下降,当血条变为 0 时游戏 game over
- 探险家取得宝箱并跑出地牢的门,则获胜
初始化项目
pixi.js 与我们常用的 jquery 等 js 库的使用没什么区别,可以使用 Bower、NPM 安装,也可以用 CDN 。
Bower 安装:
$> bower install pixi.js
NPM 安装:
$> npm install pixi.js
CDN(cdnjs):
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.2.2/pixi.min.js"></script>
这里我使用 npm 来安装。
首先创建文件夹。
$> mkdir demo
$> cd demo
添加 package.json,安装 pixi.js
$> echo "{}" > package.json
$> npm install pixi.js
这个时候已经安装好了.
添加 html 页面
添加 index.html,这个作为游戏的页面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>pixi game demo</title>
<script src="node_modules/pixi.js/dist/pixi.min.js"></script>
</head>
<body>
<script src="demo.js"></script>
</body>
</html>
其中 demo.js
放置我们的游戏脚本代码。我们在 demo.js
里面添加一些测试代码:
var type = "WebGL";
if (!PIXI.utils.isWebGLSupported()) {
type = "canvas";
}
PIXI.utils.sayHello(type);
现在访问页面,查看控制台,输出如下信息:
Pixi.js 4.3.5 - ✰ WebGL ✰ http://www.pixijs.com/ ♥♥♥
到现在为止基本的设置就完成了,接下来就是添加代码了。
创建渲染器
在页面上创建动画的第一步就是需要添加一个渲染器(renderer),也就是创建一个可以播放动画的区域,相当于 html 页面上的 <canvas>
元素。PIXI 有一个 renderer 类来自动生成 html 元素以及处理图像的显示。代码如下:
var renderer = PIXI.autoDetectRenderer(512, 512);
document.body.appendChild(renderer.view);
访问页面,可以看到 html 中已经有一个 512*512 的 canvas 元素了。
除了 autoDetectRenderer
接口,还有 CanvasRenderer
和 WebGLRenderer
接口,autoDetectRenderer
可以根据客户端对 WebGL 的支持自动创建 WebGL 或 Canvas renderer。
创建舞台
渲染器(renderer)创建之后就是创建一个舞台(stage),舞台相当于一个容器(Container),里面可以添加不同的元素,甚至也可以添加容器,最后由渲染器(renderer)渲染舞台。相当于一个顶级的容器。
在 pixi.js 中有个 Container()
类,这个类就是一个容器。
var stage = new PIXI.Container();
添加舞台之后可以由渲染器(renderer)渲染。
renderer.render(stage);
现在还不需要这么做,因为我们的舞台还没搭建完成。
创建材质集
在动画中最重要的元素是图片(材质),这一类特殊的图片对象在 pixi.js 中称为精灵(sprite),通过控制 sprite 的大小,位置以及一些其他的属性,达到动画的效果。学会创建以及控制精灵(sprite),是学习 pixi.js 最重要的一个技能,也是你学会制作游戏或动画的第一步。
在 pixi 中有一个 sprite 类,可以根据外部的图片(材质)来创建一个可以在 pixi 中使用的 sprite 对象。有三种不同的方式创建:
- 从某个单独的图片创建
- 从整个材质图片创建,根据材质上不同的位置和大小截取某部分来创建 sprite
- 从材质集创建
材质集是一个 json 文件,定义了某个材质图片里面图片的位置和大小等等,这样一方面不用每次创建 sprite 都要定义位置和大小,另外一方面修改了材质图片的时候不用修改代码。
在这个游戏中有五个材质,分别是 blob(怪物),door(大门),dungeon(地牢),explorer(探险家),treasure(宝箱),这几个图片都可以在这里下载。
可以使用 Texture Packer 来创建一个材质 atlas,创建后会生成一张包含所有图片的图片以及定义了图片位置的 json 文件。在 github 上面的 images 文件夹里面也可以下载这两个文件。我们来看看。
treasureHunter.png:
treasureHunter.json:
{"frames": {
"blob.png":
{
"frame": {"x":35,"y":515,"w":32,"h":24},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":32,"h":24},
"sourceSize": {"w":32,"h":24},
"pivot": {"x":0.5,"y":0.5}
},
"door.png":
{
"frame": {"x":1,"y":515,"w":32,"h":32},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":32,"h":32},
"sourceSize": {"w":32,"h":32},
"pivot": {"x":0.5,"y":0.5}
},
"dungeon.png":
{
"frame": {"x":1,"y":1,"w":512,"h":512},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":512,"h":512},
"sourceSize": {"w":512,"h":512},
"pivot": {"x":0.5,"y":0.5}
},
"explorer.png":
{
"frame": {"x":69,"y":515,"w":21,"h":32},
"rotated": true,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":21,"h":32},
"sourceSize": {"w":21,"h":32},
"pivot": {"x":0.5,"y":0.5}
},
"treasure.png":
{
"frame": {"x":103,"y":515,"w":28,"h":24},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":28,"h":24},
"sourceSize": {"w":28,"h":24},
"pivot": {"x":0.5,"y":0.5}
}},
"meta": {
"app": "http://www.codeandweb.com/texturepacker",
"version": "1.0",
"image": "treasureHunter.png",
"format": "RGBA8888",
"size": {"w":514,"h":548},
"scale": "1",
"smartupdate": "$TexturePacker:SmartUpdate:3c5094e02c8477b61f5692e6cac9d0f1:3923663e59fb40b578d66a492a2cda2d:9995f8b4db1ac3cb75651b1542df8ee2$"
}
}
根据材质集加载图片
在 pixi 中有一个 loader 类来管理图片的加载,并且在加载完成后调用回调函数处理。
PIXI.loader
.add("images/treasureHunter.json")
.load(setup);
treasureHunter.json
是材质集的配置文件,setup 是在完成图片加载后调用的回调函数。PIXI.loader
在加载完成后可以通过 PIXI.loader.resources
来获取加载的图片。
回调函数
在完成图片加载后,PIXI.loader 会自动调用 setup 函数来进行下一步的处理。我们先定义一个测试方法,看看是否跟预期一样。
function setup() {
console.log("加载完成.");
}
访问页面,查看控制器,显示如下:
Pixi.js 4.3.5 - ✰ WebGL ✰ http://www.pixijs.com/ ♥♥♥
demo.js:19 加载完成.
删掉 setup 里面的内容,加载完图片后接下来要完善我们的舞台了。
创建场景
这个游戏要创建两个场景,一个场景(gameScene)用来显示正常游戏画面,一个场景(gameOverScene)显示游戏结果。在 gameScene 场景中要显示所有的图片,在 gameOverScene 中显示一些文字。
场景很简单,跟舞台一样是一个容器。我们先创建 gameScene。
var gameScene;
function setup() {
gameScene = new PIXI.Container();
}
在容器中要添加所有的材质并创建对应的 sprite,如何添加?通过 PIXI.loader.resources
可以访问加载的素材。
id = PIXI.loader.resources["images/treasureHunter.json"].textures;
dungeon = new Sprite(id["dungeon.png"]);
创建了 dungeon 的 sprite 之后,需要把 sprite 添加到场景中。
gameScene.addChild(dungeon);
同样的,把 door(大门),explorer(探险家),treasure(宝箱) 等几个材质创建 sprite 并添加到场景中。可以通过 sprite 的 x, y 属性来定义他们在场景中的位置。添加完成后渲染一下,完整的代码以及效果。
function setup() {
gameScene = new PIXI.Container();
stage.addChild(gameScene);
// 获取所有加载的素材
id = PIXI.loader.resources["images/treasureHunter.json"].textures;
// 获取地牢素材并创建对应的 sprite
dungeon = new PIXI.Sprite(id["dungeon.png"]);
// 添加到场景中
gameScene.addChild(dungeon);
// 添加门
door = new PIXI.Sprite(id["door.png"]);
door.position.set(32, 0);
gameScene.addChild(door);
// 添加探险家
explorer = new PIXI.Sprite(id["explorer.png"]);
explorer.x = 68;
explorer.y = gameScene.height / 2 - explorer.height / 2;
explorer.vx = 0;
explorer.vy = 0;
gameScene.addChild(explorer);
// 添加宝箱
treasure = new PIXI.Sprite(id["treasure.png"]);
treasure.x = gameScene.width - treasure.width - 48;
treasure.y = gameScene.height / 2 - treasure.height / 2;
gameScene.addChild(treasure);
// 渲染舞台,暂时为了查看效果,后面会把这个移到其他的地方
renderer.render(stage);
}
接下来添加怪物(blob),用 foreach 循环添加 6 个怪物。
var numberOfBlobs = 6,
spacing = 48, // 怪物之间的间隔
xOffset = 150, // x 坐标 offset
speed = 2, // 运动时的速度
direction = 1; // 运动方向
blobs = [];
for (var i = 0; i < numberOfBlobs; i++) {
var blob = new PIXI.Sprite(id["blob.png"]);
var x = spacing * i + xOffset;
var y = randomInt(0, stage.height - blob.height); // randomInt 随机生成指定范围的整数,自定义
blob.x = x;
blob.y = y;
blob.vy = speed * direction;
direction *= -1;
blobs.push(blob);
gameScene.addChild(blob);
}
randomInt 方法:
return Math.floor(Math.random() * (max - min + 1)) + min;
最后,这个场景还差一个血条。血条的创建原理很简单,创建两个不同颜色的矩形然后添加到场景中就行。
// 添加血条,创建一个容器
healthBar = new PIXI.Container();
healthBar.position.set(stage.width - 170, 6);
gameScene.addChild(healthBar);
// 添加底层黑色矩形,在血条不断降低时显示这个
var innerBar = new PIXI.Graphics();
innerBar.beginFill(0x000000);
innerBar.drawRect(0, 0, 128, 8);
innerBar.endFill();
healthBar.addChild(innerBar);
// 添加外层红色血条
var outerBar = new PIXI.Graphics();
outerBar.beginFill(0xFF3300);
outerBar.drawRect(0, 0, 128, 8);
outerBar.endFill();
healthBar.addChild(outerBar);
// 设置为外层
healthBar.outer = outerBar;
到此 gameScene 场景的创建已经完成了。接下来创建 gameOverScene 场景。
gameOverScene场景相对比较简单,只需要显示一些文字就行,但是 gameOverScene 场景是在 game over 才显示的,visible 为 “false”.
// 创建 `gameOverScene` 组
gameOverScene = new PIXI.Container();
gameOverScene.visible = false;
stage.addChild(gameOverScene);
// 添加 game over 提示语
message = new PIXI.Text(
"The End!",
{fontFamily: "64px Futura", fill: "white"}
);
message.x = 120;
message.y = stage.height / 2 - 32;
gameOverScene.addChild(message);
所有的场景已经完成。这些场景都是在 setup 函数中完成的,也就是加载完素材后创建场景。
让他们动起来
我们创建了不同的 sprite,现在是静态的,如何让他们动起来? 原理很简单,让 sprite 的 x,y 坐标不停的递增,这样看起来就像是在动。在 html5 中有一个方法:requestAnimationFrame
,这个方法接受一个函数作为参数,然后以每秒60次的频率来调用。我们可以用递归的方式来调用 requestAnimationFrame 方法达到一个动态的效果。
创建一个 gameLoop
函数,并在 setup 函数结尾处调用这个函数。
function setup(){
...
gameLoop();
}
function gameLoop() {
requestAnimationFrame(gameLoop);
renderer.render(stage);
}
这个时候可以正常的显示游戏画面,但是还没有动。游戏至少有两种不同的状态,一种是 play,一种是 stop,添加一个 state 变量代表游戏的不同状态,一个 play 函数和 stop 函数,代表不同状态的不同处理逻辑。在 setup 函数中把状态初始化为 play,然后在 gameLoop 函数中调用状态。
function setup(){
...
state = play;
gameLoop();
}
function gameLoop() {
requestAnimationFrame(gameLoop);
// 通过改变 state 的不同值,达到切换状态的目的
state();
renderer.render(stage);
}
function play() {
// 游戏处理逻辑
}
function stop() {
// 游戏结束处理逻辑
}
在 play 函数中添加一些测试代码,看看是否画面可以正常动起来。
function play() {
explorer.x += 1;
}
查看页面,探险者已经可以往右边平移了,删除代码,接下来处理运动范围和碰撞。
限制探险家和怪物的运动范围
在上面的那个测试代码中可以看到,探险家一直在往右边跑,然后跑出了画面,而实际上探险家是有一个运动范围的,限定在地牢之中。
添加辅助函数 contain
. contain 方法接受两个参数,第一个参数是我们要控制的 sprite,另外一个参数是限制的运动范围。
function contain(sprite, container) {
var collision = undefined;
// 如果 sprite 的 x 坐标小于控制范围的 x 坐标,这个时候判定 sprite 已经运动到最左边,x坐标等于控制范围的 x 坐标,并输出这个时候的冲突方向为 left
if (sprite.x < container.x) {
sprite.x = container.x;
collision = "left";
}
//Top
if (sprite.y < container.y) {
sprite.y = container.y;
collision = "top";
}
//Right
if (sprite.x + sprite.width > container.width) {
sprite.x = container.width - sprite.width;
collision = "right";
}
//Bottom
if (sprite.y + sprite.height > container.height) {
sprite.y = container.height - sprite.height;
collision = "bottom";
}
//Return the `collision` value
return collision;
}
让怪物动起来
我们添加了运动限制范围函数之后,就可以先让怪物动起来了。这个游戏定义的是怪物一直沿着 y 轴做往返运动,碰到墙后方向就变为相反。在 play 函数中添加如下代码:
function play() {
blobs.forEach(function(blob){
// 怪物的 y 轴不停地累加,累加值就是速度
blob.y += blob.vy;
// 如果碰到墙后,就变换方向
var blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});
if (blobHitsWall === "top" || blobHitsWall === "bottom") {
blob.vy *= -1;
}
});
}
查看页面,这个时候 6 个怪物已经在上下运动了。
用方向键控制探险家的运动
现在我们的探险家还不能动,这个时候添加一些按键事件监听器。
首先添加辅助函数 keyboard
,把对应的按键 listener 绑定到事件监听器中。
function keyboard(keyCode) {
var key = {};
key.code = keyCode;
key.isDown = false;
key.isUp = true;
key.press = undefined;
key.release = undefined;
//The `downHandler`,把 keydown 事件绑定到 press 方法中
key.downHandler = function (event) {
if (event.keyCode === key.code) {
if (key.isUp && key.press) key.press();
key.isDown = true;
key.isUp = false;
}
event.preventDefault();
};
//The `upHandler`,把 keyup 事件绑定到 release 方法中
key.upHandler = function (event) {
if (event.keyCode === key.code) {
if (key.isDown && key.release) key.release();
key.isDown = false;
key.isUp = true;
}
event.preventDefault();
};
//Attach event listeners
window.addEventListener(
"keydown", key.downHandler.bind(key), false
);
window.addEventListener(
"keyup", key.upHandler.bind(key), false
);
return key;
}
在 setup 函数中添加按键监听.
// 添加按键监听
var left = keyboard(37),
up = keyboard(38),
right = keyboard(39),
down = keyboard(40);
// 当按键按下时,设置速度为 -5 px
left.press = function () {
explorer.vx = -5;
explorer.vy = 0;
};
// 当按键释放时,如果其他按键没有按下,设置速度为 0
left.release = function () {
if (!right.isDown && explorer.vy === 0) {
explorer.vx = 0;
}
};
up.press = function () {
explorer.vy = -5;
explorer.vx = 0;
};
up.release = function () {
if (!down.isDown && explorer.vx === 0) {
explorer.vy = 0;
}
};
right.press = function () {
explorer.vx = 5;
explorer.vy = 0;
};
right.release = function () {
if (!left.isDown && explorer.vy === 0) {
explorer.vx = 0;
}
};
down.press = function () {
explorer.vy = 5;
explorer.vx = 0;
};
down.release = function () {
if (!up.isDown && explorer.vx === 0) {
explorer.vy = 0;
}
};
这个时候按方向键还是没有运动,因为我们没有直接修改 sprite 的x,y 坐标,而是修改x,y 方向的速度 vx,vy 。
在 play 函数中添加如下代码:
// 通过修改 sprite 的vx vy来控制 sprite 是否运动,而 vx,vy 则由按键控制
explorer.x += explorer.vx;
explorer.y += explorer.vy;
// 判断探险家的运动范围
contain(explorer, {x: 28, y: 10, width: 488, height: 480});
查看页面,这个时候我们已经可以用上下左右按键控制探险家的运动了。
判断碰撞
在这个游戏中有两个碰撞要判断:一个是探险家碰撞到怪物的时候,探险家的血条会下降。另外一个碰撞时探险家和宝箱的碰撞以及和门的碰撞。
判断两个 sprite 是否碰撞,首先获取两个 sprite 的中心点的距离,然后获取两个 sprite 的宽度之和的一半,如果小于或等于两个sprite 的中心点距离,则说明发生碰撞了。
添加辅助函数 hitTestRectangle
:
function hitTestRectangle(r1, r2) {
var hit, combinedHalfWidths, combinedHalfHeights, vx, vy;
// 默认没有碰撞
hit = false;
// 获取两个 sprite 的中心点在x,y轴上的值
r1.centerX = r1.x + r1.width / 2;
r1.centerY = r1.y + r1.height / 2;
r2.centerX = r2.x + r2.width / 2;
r2.centerY = r2.y + r2.height / 2;
//获取 sprite 的半宽或半高
r1.halfWidth = r1.width / 2;
r1.halfHeight = r1.height / 2;
r2.halfWidth = r2.width / 2;
r2.halfHeight = r2.height / 2;
// 计算两个 sprite 的 x y 轴的距离
vx = r1.centerX - r2.centerX;
vy = r1.centerY - r2.centerY;
// 计算两个 sprite 的半宽之和及半高之和
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
// 首先判断 x 轴方面,如果 x 轴中心点距离小于两个 sprit 的半宽和,则判断 y 轴方面
if (Math.abs(vx) < combinedHalfWidths) {
// 如果 y 轴方面半宽和也小于 y 轴中心点的距离,则判定为碰撞
hit = Math.abs(vy) < combinedHalfHeights;
} else {
// 否则没有发生碰撞
hit = false;
}
return hit;
}
完善游戏逻辑
在探险家运动的时候,如果探险家和怪物发生碰撞,则血条下降,下降到小于 0 时修改游戏状态为 stop;如果探险家和宝箱发生碰撞并且和门发生碰撞后,游戏状态修改为 stop;
在 play 函数中添加如下代码:
function play() {
explorer.x += explorer.vx;
explorer.y += explorer.vy;
contain(explorer, {x: 28, y: 10, width: 488, height: 480});
blobs.forEach(function (blob) {
blob.y += blob.vy;
var blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});
if (blobHitsWall === "top" || blobHitsWall === "bottom") {
blob.vy *= -1;
}
// 如果探险家和怪物发生碰撞后,explorerHit 为 true
if (hitTestRectangle(explorer, blob)) {
explorerHit = true;
}
});
if (explorerHit) {
// 探险家透明度变为一半
explorer.alpha = 0.5;
// 血条不断下降
healthBar.outer.width -= 1;
} else {
explorer.alpha = 1;
}
// 如果探险家碰撞到宝箱,则把探险家和宝箱绑定到一起
if (hitTestRectangle(explorer, treasure)) {
treasure.x = explorer.x + 8;
treasure.y = explorer.y + 8;
}
// 如果宝箱碰到门后,则停止游戏,显示胜利
if (hitTestRectangle(treasure, door)) {
state = stop;
message.text = "You won!";
}
// 如果血条下降为0后,停止游戏,显示失败.
if (healthBar.outer.width < 0) {
state = stop;
message.text = "You lost!";
}
看看游戏效果吧. 基本上可以正常运行了,还差最后一个步骤,显示停止画面。
停止游戏
当游戏停止时,state 的状态变为 stop,所以在 loopGame 函数中会不停的调用 stop 函数,这个时候在 stop 函数中添加处理逻辑,只需要简单的控制场景的可见性就行。
function stop() {
gameScene.visible = false;
gameOverScene.visible = true;
}
the end
花了 2 个小时学习了一下,结果花了 4 个小时把这篇文章写完。我常常喜欢去尝试一些稀奇古怪的东西,对技术的好奇是驱动我的动力。自己对 pixi 的理解可能也不是特别正确,欢迎指正。
代码
完整代码如下,可以去原文查看更多的技术细节。
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>pixi game demo</title>
<script src="node_modules/pixi.js/dist/pixi.js"></script>
</head>
<body>
<script src="demo.js"></script>
</body>
</html>
demo.js:
var type = "WebGL";
if (!PIXI.utils.isWebGLSupported()) {
type = "canvas";
}
PIXI.utils.sayHello(type);
var renderer = PIXI.autoDetectRenderer(512, 512);
document.body.appendChild(renderer.view);
var stage = new PIXI.Container();
PIXI.loader
.add("images/treasureHunter.json")
.load(setup);
var gameScene, id, dungeon, door, explorer, treasure, blobs, healthBar, gameOverScene, state, explorerHit;
function setup() {
gameScene = new PIXI.Container();
stage.addChild(gameScene);
id = PIXI.loader.resources["images/treasureHunter.json"].textures;
dungeon = new PIXI.Sprite(id["dungeon.png"]);
gameScene.addChild(dungeon);
door = new PIXI.Sprite(id["door.png"]);
door.position.set(32, 0);
gameScene.addChild(door);
explorer = new PIXI.Sprite(id["explorer.png"]);
explorer.x = 68;
explorer.y = gameScene.height / 2 - explorer.height / 2;
explorer.vx = 0;
explorer.vy = 0;
gameScene.addChild(explorer);
treasure = new PIXI.Sprite(id["treasure.png"]);
treasure.x = gameScene.width - treasure.width - 48;
treasure.y = gameScene.height / 2 - treasure.height / 2;
gameScene.addChild(treasure);
var numberOfBlobs = 6,
spacing = 48,
xOffset = 150,
speed = 2,
direction = 1;
blobs = [];
for (var i = 0; i < numberOfBlobs; i++) {
var blob = new PIXI.Sprite(id["blob.png"]);
var x = spacing * i + xOffset;
var y = randomInt(0, stage.height - blob.height);
blob.x = x;
blob.y = y;
blob.vy = speed * direction;
direction *= -1;
blobs.push(blob);
gameScene.addChild(blob);
}
// 添加血条,创建一个容器
healthBar = new PIXI.Container();
healthBar.position.set(stage.width - 170, 6);
gameScene.addChild(healthBar);
// 添加底层黑色矩形,在血条不断降低时显示这个
var innerBar = new PIXI.Graphics();
innerBar.beginFill(0x000000);
innerBar.drawRect(0, 0, 128, 8);
innerBar.endFill();
healthBar.addChild(innerBar);
// 添加外层红色血条
var outerBar = new PIXI.Graphics();
outerBar.beginFill(0xFF3300);
outerBar.drawRect(0, 0, 128, 8);
outerBar.endFill();
healthBar.addChild(outerBar);
// 设置为外层
healthBar.outer = outerBar;
// 创建 `gameOverScene` 组
gameOverScene = new PIXI.Container();
gameOverScene.visible = false;
stage.addChild(gameOverScene);
// 添加 game over 提示语
message = new PIXI.Text(
"The End!",
{fontFamily: "64px Futura", fill: "white"}
);
message.x = 120;
message.y = stage.height / 2 - 32;
gameOverScene.addChild(message);
// 添加按键监听
var left = keyboard(37),
up = keyboard(38),
right = keyboard(39),
down = keyboard(40);
// 当按键按下时,设置速度为 -5 px
left.press = function () {
explorer.vx = -5;
explorer.vy = 0;
};
// 当按键释放时,如果其他按键没有按下,设置速度为 0
left.release = function () {
if (!right.isDown && explorer.vy === 0) {
explorer.vx = 0;
}
};
up.press = function () {
explorer.vy = -5;
explorer.vx = 0;
};
up.release = function () {
if (!down.isDown && explorer.vx === 0) {
explorer.vy = 0;
}
};
right.press = function () {
explorer.vx = 5;
explorer.vy = 0;
};
right.release = function () {
if (!left.isDown && explorer.vy === 0) {
explorer.vx = 0;
}
};
down.press = function () {
explorer.vy = 5;
explorer.vx = 0;
};
down.release = function () {
if (!up.isDown && explorer.vx === 0) {
explorer.vy = 0;
}
};
state = play;
gameLoop();
}
function play() {
explorer.x += explorer.vx;
explorer.y += explorer.vy;
contain(explorer, {x: 28, y: 10, width: 488, height: 480});
blobs.forEach(function (blob) {
blob.y += blob.vy;
var blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});
if (blobHitsWall === "top" || blobHitsWall === "bottom") {
blob.vy *= -1;
}
// 如果探险家和怪物发生碰撞后,explorerHit 为 true
if (hitTestRectangle(explorer, blob)) {
explorerHit = true;
}
});
if (explorerHit) {
// 探险家透明度变为一半
explorer.alpha = 0.5;
// 血条不断下降
healthBar.outer.width -= 1;
} else {
explorer.alpha = 1;
}
// 如果探险家碰撞到宝箱,则把探险家和宝箱绑定到一起
if (hitTestRectangle(explorer, treasure)) {
treasure.x = explorer.x + 8;
treasure.y = explorer.y + 8;
}
// 如果宝箱碰到门后,则停止游戏,显示胜利
if (hitTestRectangle(treasure, door)) {
state = stop;
message.text = "You won!";
}
// 如果血条下降为0后,停止游戏,显示失败.
if (healthBar.outer.width < 0) {
state = stop;
message.text = "You lost!";
}
}
function stop() {
gameScene.visible = false;
gameOverScene.visible = true;
}
function gameLoop() {
requestAnimationFrame(gameLoop);
state();
renderer.render(stage);
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function contain(sprite, container) {
var collision = undefined;
//Left
if (sprite.x < container.x) {
sprite.x = container.x;
collision = "left";
}
//Top
if (sprite.y < container.y) {
sprite.y = container.y;
collision = "top";
}
//Right
if (sprite.x + sprite.width > container.width) {
sprite.x = container.width - sprite.width;
collision = "right";
}
//Bottom
if (sprite.y + sprite.height > container.height) {
sprite.y = container.height - sprite.height;
collision = "bottom";
}
//Return the `collision` value
return collision;
}
function keyboard(keyCode) {
var key = {};
key.code = keyCode;
key.isDown = false;
key.isUp = true;
key.press = undefined;
key.release = undefined;
//The `downHandler`
key.downHandler = function (event) {
if (event.keyCode === key.code) {
if (key.isUp && key.press) key.press();
key.isDown = true;
key.isUp = false;
}
event.preventDefault();
};
//The `upHandler`
key.upHandler = function (event) {
if (event.keyCode === key.code) {
if (key.isDown && key.release) key.release();
key.isDown = false;
key.isUp = true;
}
event.preventDefault();
};
//Attach event listeners
window.addEventListener(
"keydown", key.downHandler.bind(key), false
);
window.addEventListener(
"keyup", key.upHandler.bind(key), false
);
return key;
}
function hitTestRectangle(r1, r2) {
//Define the variables we'll need to calculate
var hit, combinedHalfWidths, combinedHalfHeights, vx, vy;
//hit will determine whether there's a collision
hit = false;
//Find the center points of each sprite
r1.centerX = r1.x + r1.width / 2;
r1.centerY = r1.y + r1.height / 2;
r2.centerX = r2.x + r2.width / 2;
r2.centerY = r2.y + r2.height / 2;
//Find the half-widths and half-heights of each sprite
r1.halfWidth = r1.width / 2;
r1.halfHeight = r1.height / 2;
r2.halfWidth = r2.width / 2;
r2.halfHeight = r2.height / 2;
//Calculate the distance vector between the sprites
vx = r1.centerX - r2.centerX;
vy = r1.centerY - r2.centerY;
//Figure out the combined half-widths and half-heights
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
//Check for a collision on the x axis
if (Math.abs(vx) < combinedHalfWidths) {
//A collision might be occuring. Check for a collision on the y axis
hit = Math.abs(vy) < combinedHalfHeights;
} else {
//There's no collision on the x axis
hit = false;
}
//`hit` will be either `true` or `false`
return hit;
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
这难道是 LC 最长的文章 :smile:
@Summer 这段时间比较闲,所以就啰嗦了一下 :laughing:
好厉害,用这个做网页小游戏
我也要找个周末的时间去玩一玩
服气:+1:
深得我心
厉害了