基于PixiJs实现的简单小游戏

2,997 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

游戏玩法

开局只有一只手电筒,玩家需要通过鼠标控制手电筒在屏幕内移动,来找到名为captainNpc

游戏素材

游戏素材包括一张背景地图以及一张船长的头像,背景地图来源于网络,船长头像感谢Captaincc的无私奉献

游戏实现所需要准备的前置知识

  • PixiJs的应用和舞台
  • PixiJs的纹理缓存
  • PixiJs的精灵
  • PixiJs的图形绘制类
  • PixiJs的滤镜
  • PixiJs的遮罩
  • PixiJs的容器
  • PixiJs的事件系统
  • 碰撞检测算法

PixiJs的应用和舞台

按照我们写游戏的路子,首先是需要绘制一个游戏区域,然后在游戏区域内填充我们的元素,PixiJs也一样,创建应用方法调用后会在页面上创建一个canvas元素,之后所有的内容,都会渲染在这个画布上,PixiJs对页面上元素的管理是树状的结构,根元素是应用,下一级就是舞台stage, 舞台stage可以理解为是根节点下的根容器,接下来画布上所有的可见元素,都是挂载在舞台下的

// 创建应用
const app = new PIXI.Application();
document.body.appendChild(app.view);
// 舞台
console.log(app.stage)

PixiJs的纹理缓存

因为Pixi使用WebGL在GPU上渲染图像,图像需要转换为GPU可以处理的东西,这个东西被称为texture(纹理)。为保证快速高效,Pixi使用texture cache(纹理缓存)来存储和引用你的精灵需要的所有纹理。texture(纹理)的名称就是引用图像的文件的路径。

tips1 通过app.loader.add加载图像或纹理贴图时,可以指定别名,别名需要全局唯一。接下来使用的时候可以通过指定的别名来找到纹理缓存

tips2 app.loader.add可以链式调用添加多个纹理缓存

app.loader.add('grass', 'https://pic.qy566.com/pixijs/images/bg.jpg')
        .add('captain', 'https://pic.qy566.com/pixijs/images/captain.png');
// app.looader.load 是加载资源的方法,接收的参数是一个回调函数,在资源加载完成时调用
app.loader.load(setup);

PixiJs的精灵

精灵可以理解为是图片的加载类,主要是加载外部的图片资源,精灵的创建方式有五种

  • 通过单个图像文件
  • 通过雪碧图创建(与css里的雪碧图用法一致)
  • 通过纹理贴图实现 (需要特定的应用来生成图片与对应的描述json文件)

我们采用的是单个图像文件的方式来创建的精灵, 这里我们的背景图引入的时候,就是采用的图像别名的方式引入的纹理缓存

const background = new PIXI.Sprite(resources.grass.texture);
app.stage.addChild(background);
background.width = app.screen.width;
background.height = app.screen.height;

image.png

PixiJs的图形绘制类

添加完背景图,接下来就是绘制手电筒光了,PixiJs提供了Graphics类来绘制几何图形

// 圆的大小
const radius = 50;

// 圆的边缘模糊的大小
const blurSize = 16;
// 绘制圆形填充红色
const circle = new PIXI.Graphics()
			.beginFill(0xFF0000)
			.drawCircle(radius + blurSize, radius + blurSize, radius)
			.endFill();

PixiJs的滤镜

我们采用的是高斯模糊效果的滤镜,创建一个矩形区域,然后通过通用的纹理生成方法用添加了滤镜的圆形去填充这个矩形区域的纹理

// 高斯模糊滤镜
circle.filters = [new PIXI.filters.BlurFilter(blurSize)];
// 创建大小是圆形2倍的矩形区域
const bounds = new PIXI.Rectangle(0, 0, (radius + blurSize) * 2, (radius + blurSize) * 2);
// 创建以圆形作为填充的通用纹理
const texture = app.renderer.generateTexture(circle, PIXI.SCALE_MODES.NEAREST, 1, bounds);
// 生成精灵
const focus = new PIXI.Sprite(texture);
app.stage.addChild(focus);

PixiJs的遮罩

针对某个精灵的遮罩要用到mask属性 ,mask 属性指定的是对应精灵的可视区域

background.mask = focus;

image.png

PixiJs的容器

讲容器之前,我们先把要寻找的元素添加进舞台,指定宽高为30,位置是根据应用的容器宽高进行随机

const captain = new PIXI.Sprite(resources.captain.texture);
app.stage.addChild(captain);
captain.width = 30;
captain.height = 30;
captain.position.x = Math.floor(Math.random() * app.screen.width)
captain.position.y = Math.floor(Math.random() * app.screen.height)

这个时候我们发现了一个问题,就是 我们的mask没办法遮住新添加的captain

image.png

这个时候怎么办?请看下节 PixiJs的容器

PixiJs的容器

PixiJs的容器的容器概念类似于Ps里的分组的概念,将某些精灵,聚合为一个视为一种不包含纹理的特殊精灵。那现在我们就需要修改一下我们的代码

  • 创建容器
let containers = new PIXI.Container();
  • 将背景添加到容器内
- app.stage.addChild(xx);
+ containers.addChild(xx);
  • 将要寻找的元素添加到容器
- app.stage.addChild(captain);
+ containers.addChild(captain);
  • 将容器添加到舞台
+ app.stage.addChild(containers);
  • 为容器添加遮罩
- background.mask = focus;
+ containers.mask = focus;

PixiJs的事件系统

添加完元素以后,我们就需要让页面响应我们的mousemove事件,来更新foucs的位置

app.stage.interactive = true;
app.stage.on('mousemove', pointerMove);
function pointerMove(event) {
    focus.position.x = event.data.global.x - focus.width / 2;
    focus.position.y = event.data.global.y - focus.height / 2;     
}

tips1 这里app.stage.interactive = true; 是关键,只有设置了interactive的容器才会响应dom事件

碰撞检测算法

如何判断游戏胜利呢?这里就需要用到碰撞检测算法,其实就是根据坐标以及两个元素的大小判断是否发生了交集,这里因为滤镜的原因,所以我对标准碰撞算法做了修正,修正了blur的量

function hitTestRectangle(r1, r2) {

    //Define the variables we'll need to calculate
    let 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
    // 修正滤镜产生的blur量
    combinedHalfWidths = r1.halfWidth + r2.halfWidth - 20;
    combinedHalfHeights = r1.halfHeight + r2.halfHeight - 20;

    //Check for a collision on the x axis
    if (Math.abs(vx) < combinedHalfWidths) {
        if (Math.abs(vy) < combinedHalfHeights) {
                hit = true;
        } else {
                hit = false;
        }
    } else {

            hit = false;
    }

        return hit;
};

获胜条件

在鼠标移动事件内比较focuscaptain是否发生碰撞即可

if (hitTestRectangle(captain, focus)) {
    clearTimeout(timer)
    document.getElementById('time').innerHTML = '总用时'+ t + '秒'
    containers.mask = null
    document.getElementById('btn').style.display = 'block'
}

总结

相比较于传统的dom实现来说,通过PixiJs 实现的游戏代码量要明显更少,因为PixiJs本身就封装好了大量易用的API,我们可以直接进行调用,除了文档纯英文这一点以外,PixiJs是一款很优秀的现代游戏引擎。