18

一.pixi.js简介

pixi.js是一个非常快速的2D精灵渲染引擎。它可以帮助我们显示,动画和管理交互式图形。如此一来,我们可以使用javascript和其它HTML5技术来轻松实现一个应用程序或者完成一款游戏。它有一个语义化的、简洁的API,包含了许多有用的功能。比如说支持纹理地图集,也提供一个通过动画精灵(交互式图像)所构建的精简的系统。它还为我们提供了一个完整的场景图,我们可以创建嵌套精灵的层次结构(也就是精灵当中嵌套精灵)。同样的也允许我们将鼠标以及触摸的事件添加到精灵上。而且,最重要的还是,pixi.js可以根据我们的实际需求来使用,很好的适应个人的编码风格,并且还能够和框架无缝集成。

pixi.js的API事实上是陈旧的Macromedia/Adobe Flash API的一个改进。熟练flash的开发人员将会有一种回到自己家里一样的熟悉。其它使用过类似API的精灵渲染框架有: CreateJS,Starling, Sparrow 和 Apple’s SpriteKit。pixi.js的优点在于它的通用性:它不是一个游戏引擎。这是极好的,因为它可以完全任由我们自由发挥,做自己的事情,甚至还可以用它写一个自己的游戏引擎。在学习pixi.js之前,我们需要先对HTML,CSS,Javascript有一些了解。因为这会对我们学习pixi.js有很大的帮助。

二.pixi.js安装

1.前往github安装

可以前往github上安装,在这里。也可以在这里下载安装。

2.使用npm安装

也可以使用npm来安装。首先需要安装node.js
当安装node.js完成之后,会自动完成npm的安装。然后就可以通过npm将pixi.js安装在全局。命令如下:

npm install pixi.js -g
//也可以简写为
npm i pixi.js -g

或者也可以将pixi.js安装在当前项目的目录之下。命令如下:

npm install pixi.js -D //或者
npm install pixi.js --save -dev
//也可以简写为
npm i pixi.js -D

三.开始使用pixi.js

1.引入pixi.js

安装pixi.js完成之后,我们就可以使用了。首先在你的项目目录下创建一个基础的.html文件。 然后在你html文件中,使用script标签来,加载这个js文件。代码如下:

<script src="/pixi.min.js"></script>

或者,你也可以使用CDN来引入这个js文件。如下:

<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.1.3/pixi.min.js"></script>
<!--或者使用bootstrap的cdn来引入-->
<script src="https://cdn.bootcss.com/pixi.js/5.1.3/pixi.min.js"></script>

如果使用es6的模块化加载pixi.js,那么就需要注意了,因为pixi.js没有默认导出。所以正确的引入方式应该如下:

import * as PIXI from 'pixi.js'

2.一个简单的示例

好了,接下来就是写点js代码,看看pixi.js是否在工作中。代码如下:

const type = "WebGL";
if (!PIXI.utils.isWebGLSupported()) {
    type = "canvas";
}
PIXI.utils.sayHello(type);

如果能在浏览器控制台中,看到如下图所示的标志,那么就证明pixi.js加载成功。

1

我们来分析一下以上的代码的意思,先定义一个变量,值是字符串"webGL",然后有一个if语句判断如果支持webGL,那么就改变这个变量的值为canvas。然后调用pixi.js所封装的在控制台打印这个值的方法,即sayHello方法。

在线示例

3.创建你自己的pixi应用和舞台

现在,我们可以愉快的开始使用pixi.js呢。首先,我们可以创建一个可以显示图片的矩形区域,pixi.js有一个Application对象来帮助我们创建它。它会自动创建一个canvas的HTML标签。并且还能够自动计算出让你的图片能够在canvas元素中显示。然后,你需要创建一个特殊的pixi容器对象,它被叫做舞台。正如你所看到的,这个舞台元素会被当做根容器,然后你可以在这个根容器使用pixi来显示你想要显示的东西。以下是你需要创建一个pixi应用对象以及舞台对象所必要的代码。

//创建一个pixi应用对象
let app = new PIXI.Application({width: 256, height: 256});
//将这个应用对象元素添加到dom文档中
document.body.appendChild(app.view);

以上代码运行在浏览器中,效果如图所示:

很好,以上的代码所代表的意思就是,我们在HTML DOM中创建了背景颜色为黑色(默认颜色)的一个宽为256,高为256的canvas元素(单位默认是像素)。没错,就是一个黑色的矩形。我们可以看到PIXI.Application是一个构造函数,它会根据浏览器是支持canvas还是webGL来决定使用哪一个渲染图像。函数里面可以不传参数,也可以传一个对象当做参数。如果不传参数,那么会使用默认的参数。这个对象,我们可以称之为option对象。比如以上,我们就传了width和height属性。

在线示例

4.属性

当然,我们还可以使用更多的属性,例如以下代码:

let app = new PIXI.Application({ 
    width: 256,         //default:800
    height: 256,        //default:600
    antialias: true,    //default:false
    transparent: false, //default:false
    resolution: 1       //default:1
});

这些属性到底代表什么意思呢?antialias属性使得字体的边界和图形更加平滑(webGL的anti-aliasing在所有平台都不可用,你需要在你的游戏平台中去做测试。)。transparent属性是设置整个canvas元素的透明度。resolution属性让不同分辨率和不同像素的平台运行起来更容易一些。只要将这个属性的值设置为1就可以应付大多数的工程项目呢。想要知道这个属性的所有细节,可以查看这个项目Mat Grove'sexplanation的代码。

pixi.js默认是通过WebGL来渲染的。因为webGL的速度非常块,并且我们还可以使用一些将要学习的壮观的视觉效果。当然,如果需要强制性的使用Canvas来取代webGL渲染。我们可以将forceCanvas的值设置为true,即可。如下:

forceCanvas:true

在你创建了canvas元素之后,如果你想要改变它的背景颜色,你需要设置app.renderer.backgroundColor属性为任意的十六进制颜色值("0X"加上"0"~"f"之间的任意6个字符组成的8位字符。)。例如:

app.renderer.backgroundColor = 0x061639;

如果想要获取canvas的宽和高,可以使用app.renderer.view.width和app.renderer.view.height属性。

当然,我们也可以改变canvas的大小,只需要使用renderer.resize方法并且传入width和height属性的值即可。不过,为了确保大小能够正确的适应平台的分辨率,我们需要将autoResize的值设置为true。如下:

app.renderer.autoResize = true;
app.renderer.resize(512, 512);//第一个512代表宽度,第二个512代表高度

如果想要canvas填满整个浏览器的窗口,可以提供css样式,然后将canvas的大小设置为浏览器的窗口大小。如下:

app.renderer.view.style.position = "absolute";
app.renderer.view.style.display = "block";
app.renderer.autoResize = true;
app.renderer.resize(window.innerWidth, window.innerHeight);

但是,如果这样做了之后,确保使用如下的css代码来让所有HTML元素的margin和padding初始化为0。

 * { 
    margin:0;
    padding:0;
}
        

上面代码中的星号*是CSS“通用选择器”,它的意思是“HTML文档中的所有标记”。

如果希望让canvas按比例缩放到任何浏览器窗口大小,则可以使用自定义的scaleToWindow函数。可以点击这里查看更多应用对象配置属性。

在线示例

四.核心知识

1.pixi精灵

现在,我们已经有了一个渲染器,或者我们可以称之为画布。我们可以开始往画布上面添加图片。我们希望显示的任何内容都必须被添加到一个叫做stage(舞台)的特殊pixi对象中。我们可以像如下那样使用这个舞台:

//app为实例化的应用对象
let stage = app.stage;//现在这个stage变量就是舞台对象

stage是一个特殊的pixi容器对象。我们可以将它看作是一个空盒子,然后和我们添加进去的任何内容组合在一起,并且存储我们添加的任何内容。stage对象是场景中所有可见对象的根容器。无论我们在舞台中放入什么内容都会在画布中呈现。现在我们的这个盒子还是空的,没有什么内容,但是我们很快就会放点内容进去。可以点击这里查看关于pixi容器对象的更多信息。

重要说明: 因为stage是一个pixi容器。所以它拥有有和其它任何容器一样的属性方法。虽然stage拥有width和height属性,但是它们并不指的是渲染窗口的大小。stage的width和height属性仅仅是为了告诉我们放在里面的东西所占据的区域——更多关于它的信息在前面。

那么我们应该在舞台上面放些什么东西呢?就是一些被称作为精灵的特殊图片对象。精灵基本上就是我们能用代码控制的图片。我们可以控制它们的位置,大小,以及其它用于动画和交互的有用的属性。学会如何制作与控制一个精灵真的是一件关于学习如何使用pixi.js的重要的事情。如果我们知道如何制作精灵并且能够把它们添加到舞台中,我们距离制作游戏仅剩一步之遥。

pixi有一个精灵类,它是一种制作游戏精灵的多功能的方式。有三种主要的方式来创建它们:

1.从单个图片文件中创建。
2.用一个 雪碧图来创建。雪碧图是一个放入了你游戏所需的所有图像的大图。
3.从纹理图集(一个JSON文件,在雪碧图中定义图片大小和位置)。

我们将学习这三种方式。但在此之前,我们先了解一下我们需要了解的图片,然后使用pixi显示这些图片。

2.将图像加载到纹理缓存中

由于pixi使用webGL在GPU上渲染图片,所以图片需要采用GPU能够处理的格式。使用webGL渲染的图像就被叫做纹理。在你让精灵显示图片之前,需要将普通的图片转化成WebGL纹理。为了让所有东西在幕后快速有效地工作,Pixi使用纹理缓存来存储和引用精灵所需的所有纹理。纹理的名称是与它们引用的图像的文件位置匹配的字符串。 这意味着如果你有一个从"images/cat.png"加载的纹理,你可以在纹理缓存中找到它,如下所示:

PIXI.utils.TextureCache["images/cat.png"];

纹理以WebGL兼容格式存储,这对于Pixi的渲染器来说非常有效。然后,您可以使用Pixi的Sprite类使用纹理制作新的精灵。

let texture = PIXI.utils.TextureCache["images/anySpriteImage.png"];
let sprite = new PIXI.Sprite(texture);

但是如何加载图像文件并将其转换为纹理?使用Pixi的内置loader对象。Pixi强大的loader对象是加载任何类型图片所需的全部内容。以下是如何使用它来加载图像并在图像加载完成后调用一个名为setup的函数:

PIXI.loader.add("images/anyImage.png").load(setup);
function setup() {
  //此代码将在加载程序加载完图像时运行
}

如果使用loader来加载图像,这是pixi开发团队的建议。我们应该通过引入loader's resources对象中的图片资源来创建精灵,就像如下这样:

let sprite = new PIXI.Sprite(
    //数组内是多张图片资源路径
    PIXI.loader.resources["images/anyImage.png"].texture
);

以下是一个完整的代码的例子,我们可以编写这些代码来加载图像,调用setup函数,然后从加载的图像中创建精灵。

PIXI.loader.add("images/anyImage.png").load(setup);
function setup() {
  let sprite = new PIXI.Sprite(
    PIXI.loader.resources["images/anyImage.png"].texture
  );
}

这是加载图像和创建图片的通用格式。我们还可以使用链式调用的add方法来同时加载多个图像,如下:

PIXI.loader.add("images/imageOne.png").add("images/imageTwo.png").add("images/imageThree.png").load(setup);

当然,更好的方式是,只需在单个add方法内将要加载的所有文件列出在数组中,如下所示:

PIXI.loader.add(["images/imageOne.png","images/imageTwo.png","images/imageThree.png"]).load(setup);

当然加载程序还允许我们加载JSON文件。这在后面,我们会学到。

3.显示精灵

在我们已经加载了图片,并且将图片制作成了精灵,我们需要使用stage.addChild方法来将精灵添加到pixi的stage(舞台)中。就像如下这样:

//cat表示精灵变量
app.stage.addChild(cat);

注: 记住stage(舞台)是所有被添加的精灵的主容器元素。而且我们不会看到任何我们所构建的精灵,除非我们已经把它们添加到了stage(舞台)中。

好的,让我们来看一个示例关于如何使用代码来学会在舞台上显示一张单图图像。我们假定在目录examples/images下,你会发现有一张宽64px高64px的猫图。如下所示:

以下是一个需要加载图片,创建一个精灵,并且显示在pixi的stage上的所有JavaScript代码:

//创建一个pixi应用对象
let app = new PIXI.Application({ 
    width: 256, 
    height: 256,                       
    antialias: true, 
    transparent: false, 
    resolution: 1
  }
);
//将canvas元素添加到body元素中
document.body.appendChild(app.view);
//加载图像,然后使用setup方法运行
//"images/cat.png"这个路径需要根据自己情况所调整
PIXI.loader.add("images/cat.png").load(setup);
//当图片加载完成,setup方法会执行
function setup() {
  //创建cat精灵,"images/cat.png"这个路径需要根据自己情况所调整
  let cat = new PIXI.Sprite(PIXI.loader.resources["images/cat.png"].texture);
  //将cat精灵添加到舞台中
  app.stage.addChild(cat);
}

当代码运行在浏览器上,你会看到如图所示:

在线示例

如果我们需要从舞台上移除精灵,我们可以使用removeChild方法。如下:

//参数为精灵图的路径
app.stage.removeChild(anySprite)

但是通常将精灵的visible属性设置为false将是使精灵消失的更简单,更有效的方法。如下:

 //anySprite为精灵对象,例如前面示例的cat
 anySprite.visible = false;

4.使用别名

当然我们也可以对我们使用频繁的pixi对象和方法创建一些简略的可读性更好的别名。例如,你难道想给所有的pixi对象添加PIXI前缀吗?如果不这样想,那就给它一个简短的别名吧。例如:以下是为TextureCache对象所创建的一个别名。

 let TextureCache = PIXI.utils.TextureCache;
 

然后,使用该别名代替原始别名,如下所示:

//"images/cat.png"这个路径需要根据自己情况所调整
let texture = TextureCache["images/cat.png"];

使用别名给写出简洁的代码提供了额外的好处:他帮助你缓存了Pixi的常用API。如果Pixi的API在将来的版本里改变了-没准他真的会变!你将会需要在一个地方更新这些对象和方法,你只用在工程的开头而不是所有的实例那里!所以Pixi的开发团队想要改变它的时候,你只用一步即可完成这个操作! 来看看怎么将所有的Pixi对象和方法改成别名之后,来重写加载和显示图像的代码。

    //别名
    let Application = PIXI.Application;
    let loader = PIXI.loader;
    let resources = PIXI.loader.resources;
    let Sprite = PIXI.Sprite;
    //创建一个应用对象
    let app = new Application({ 
        width: 256, 
        height: 256,                       
        antialias: true, 
        transparent: false, 
        resolution: 1
      }
    );
    //将Pixi自动为您创建的画布添加到HTML文档中
    document.body.appendChild(app.view);
    //加载图像并完成后运行“setup”函数
    loader.add("images/cat.png").load(setup);
    //该“setup”函数将在图像加载后运行
    function setup() {
      //创建一个cat精灵类
      let cat = new Sprite(resources["images/cat.png"].texture);
      //将cat精灵类添加到舞台中
      app.stage.addChild(cat);
    }

大多数教程中的例子将会使用Pixi的别名来处理。除非另有说明,否则你可以假定下面所有的代码都使用了这些别名。这就是我们所需要知道的所有的关于加载图像和创建精灵的知识。

在线示例

5.有关加载的更多信息

前面所显示的格式是建议用作加载图像和显示图片的标准模板的格式。 因此,我们可以放心地忽略接下来的几段内容,而直接跳到下一部分“定位精灵”。 但是Pixi的加载程序对象非常复杂,即使您不定期使用它们,也要注意一些功能。 让我们看一些最有用的。

(1).从普通的JavaScript Image对象或Canvas生成精灵

为了优化和提高效率,始终最好从预先加载到Pixi的纹理缓存中的纹理制作精灵。 但是,如果由于某种原因需要从常规的JavaScript Image对象制作纹理,则可以使用Pixi的BaseTexture和Texture类来实现:

//参数为任何JavaScriptImage对象
let base = new PIXI.BaseTexture(anyImageObject);
let texture = new PIXI.Texture(base);
let sprite = new PIXI.Sprite(texture);

如果要从任何现有的canvas元素制作纹理,可以使用BaseTexture.fromCanvas:

//参数为任何canvas元素
let base = new PIXI.BaseTexture.fromCanvas(anyCanvasElement);

如果要更改精灵显示的纹理,请使用texture属性。 将其设置为任何texture对象,如下所示:

anySprite.texture = PIXI.utils.TextureCache["anyTexture.png"];

如果游戏中发生重大变化,就可以使用此技术交互式地更改精灵的外观。

(2).为加载文件分配名称

可以为要加载的每个资源分配一个唯一的名称。只需提供名称(字符串)作为add方法中的第一个参数即可。 例如,以下是将cat的图像命名为catImage的方法。

//第一个参数为分配的别名,第二个参数则是图像路径
PIXI.loader.add("catImage", "images/cat.png").load(setup);

这将在loader.resources中创建一个名为catImage的对象。 这意味着可以通过引用catImage对象来创建一个精灵,如下所示:

//catImage对象下的texture属性
let cat = new PIXI.Sprite(PIXI.loader.resources.catImage.texture);

但是,建议不要使用此功能! 这是因为使用它就必须记住为每个已加载文件指定的所有名称,并确保不要意外地多次使用同一名称。正如在前面的示例中所做的那样,使用文件路径名更加简单,并且不容易出错。

(3).监听加载进度

Pixi的加载程序有一个特殊的progress事件,它将调用一个可自定义的函数,该函数将在每次文件加载时运行。进度事件由加载器的on方法调用,如下所示:

//loadProgressHandler为处理进度的函数
PIXI.loader.on("progress", loadProgressHandler);

以下为在加载链中使用包括on方法的方式,并在每次文件加载时调用用户定义的函数loadProgressHandler。

    //使用on方法
    PIXI.loader.add([
      "images/one.png",
      "images/two.png",
      "images/three.png"
    ]).on("progress", loadProgressHandler).load(setup);
    //loadProgressHandler函数                        
    function loadProgressHandler() {
      console.log("loading"); 
    }
    //setup函数                          
    function setup() {
      console.log("setup");
    }

每次加载其中一个文件时,进度事件都会调用loadProgressHandler以在控制台中显示“loading”。当所有三个文件都加载完毕后,setup函数将运行。 以下是上述代码在控制台中的输出:

loading
loading
loading
setup

这很不错了,但是会变得更好。我们还可以准确地找到已加载的文件以及当前已加载的文件总数的百分比。只需要通过向loadProgressHandler添加可选的loader和resource参数来做到这一点,如下所示:

function loadProgressHandler(loader, resource) { 
    //从resouce中取得已加载的文件或者取得已加载文件的百分比
}

然后,可以使用resource.url查找当前加载的文件。(如果要查找可能已分配给文件的可选名称,请使用resource.name作为add方法中的第一个参数。)然后,您可以使用loader.progress查找当前已加载的总资源百分比。以下是一些执行此操作的代码。

    PIXI.loader.add([
      "images/one.png",
      "images/two.png",
      "images/three.png"
    ]).on("progress", loadProgressHandler).load(setup);          
    function loadProgressHandler(loader, resource) {
      //显示当前加载的文件路径
      console.log("loading: " + resource.url); 
      //显示当前文件加载的百分比
      console.log("progress: " + loader.progress + "%"); 
      //如果第一个参数提供的是文件的可选名称
      //那么在add方法里就要像如下这样接收它们
      //console.log("loading:"+resource.name);
    }                   
    function setup() {
      console.log("All files loaded");
    }

以下是此代码在运行时将在控制台中显示的内容:

loading: images/one.png
progress: 33.333333333333336%
loading: images/two.png
progress: 66.66666666666667%
loading: images/three.png
progress: 100%
All files loaded

这确实好棒,因为我们可以以此为基础创建加载进度条。

注意: 我们也可以在资源对象上访问其他属性。resource.error会告诉您尝试加载文件时发生的任何可能的错误。resource.data允许您访问文件的原始二进制数据。

6.有关loader的更多信息

Pixi的loader具有丰富的功能和可配置性。让我们快速了解一下它的用法,好入门。 loader的可链接的add方法包含4个基本参数:

add(name, url, optionObject, callbackFunction);

以下是对这些参数做描述的简单文档:

1.name (string):要加载的资源的名称。如果未被使用,则会自动使用url。
2.url (string):此资源的网址,相对于loader的baseUrl。
3.options (object literal):加载的选项。
4.options.crossOrigin (Boolean):请求是跨域的吗? 默认为自动确定。
5.options.loadType:资源应如何加载? 默认值为Resource.LOAD_TYPE.XHR。
6.options.xhrType:使用XHR时应如何执行正在加载的数据?默认值为Resource.XHR_RESPONSE_TYPE.DEFAULT。
7.callbackFunction:当资源完成加载时所要调用的函数(回调函数)。

这些参数中唯一需要传入的就是url(要加载的文件)。以下是一些可以使用add方法加载文件的方式的示例。 这是文档称为loader的“常规语法”:

//第一个参数为加载资源的名称,第二个参数为资源路径,然后第三个参数可不传,也就是加载的选项,第四个参数就是回调函数
PIXI.loader.add('key', 'http://...', function () {});
PIXI.loader.add('http://...', function () {});
PIXI.loader.add('http://...');

以下这些是loader的“对象语法”的示例:

PIXI.loader.add({
        name: 'key2',
        url: 'http://...'
}, function () {})                     
PIXI.loader.add({
  url: 'http://...'
}, function () {})
PIXI.loader.add({
  name: 'key3',
  url: 'http://...'
  onComplete: function () {}
})
PIXI.loader.add({
  url: 'https://...',
  onComplete: function () {},
  crossOrigin: true
})

您还可以将对象或URL或两者的数组传递给add方法:

PIXI.loader.add([
    {name: 'key4', url: 'http://...', onComplete: function () {} },
    {url: 'http://...', onComplete: function () {} },
    'http://...'
]);

注意: 如果需要重置loader以加载新一批文件,请调用loader的reset方法:PIXI.loader.reset()。

Pixi的loader具有许多更高级的功能,包括使我们可以加载和解析所有类型的二进制文件的选项。这不是我们日常要做的事情,并且超出了目前我们所学习的范围,因此可以从GitHub项目中获取更多信息

7.定位精灵

现在我们知道了如何创建和显示精灵,让我们了解如何放置和调整精灵的大小。在前面的示例中,cat sprite已添加到舞台的左上角。cat的x位置为0,y位置为0。可以通过更改cat的x和y属性的值来更改cat的位置。通过将cat的x和y属性值设置为96的方法,可以使cat在舞台中居中。

cat.x = 96;
cat.y = 96;

创建精灵后,将以上两行代码添加到setup函数内的任何位置。

function setup() {
    //创建cat精灵
    let cat = new Sprite(resources["images/cat.png"].texture);
    //改变精灵的位置
    cat.x = 96;
    cat.y = 96;
    //将cat精灵添加到舞台中如此便可以看到它
    app.stage.addChild(cat);
}

注意: 在这个例子中,Sprite是PIXI的别名。Sprite,TextureCache是PIXI.utils.TextureCache的别名,resources是PIXI.loader.resources的别名。后面都是使用别名,并且从现在开始,示例代码中所有Pixi对象和方法的格式都相同。

这两行代码会将cat右移96像素,向下移96像素。结果如下:

在线示例

cat的左上角(左耳)代表其x和y锚点。要使cat向右移动,请增加其x属性的值。要使cat向下移动,请增加其y属性的值。如果cat的x值为0,则它将位于舞台的最左侧。如果y值为0,则它将位于该阶段的顶部。如下图所示:

其实可以不必单独设置精灵的x和y属性,而是可以在一行代码中将它们一起设置,如下所示:

//也就是调用set方法即可,传入修改的x参数和y参数
sprite.position.set(x, y)

让我们来看看以上的示例代码修改之后的结果:

在线示例

可以看出来,结果都是一样的。

8.大小和比例

我们可以通过设置精灵的width和height属性来更改其大小。以下便是一个示例,将cat设置为80像素的宽度和120像素的高度。

    cat.width = 80;
    cat.height = 120;

将这两行代码添加到setup函数中,就像如下:

    function setup() {
        //创建cat精灵
        let cat = new Sprite(resources["images/cat.png"].texture);
        //改变精灵的位置
        cat.x = 96;
        cat.y = 96;
        //改变精灵的大小
        cat.width = 80;
        cat.height = 120;
        //将cat精灵添加到舞台中如此便可以看到它
        app.stage.addChild(cat);
    }

效果如图所示:

在线示例

我们会看到cat的位置(左上角)没有变化,只是宽度和高度有变化。如下图所示:

精灵还具有scale.x和scale.y属性,可按比例更改精灵的宽度和高度。以下是将cat的scale设置为一半尺寸的方法:

    cat.scale.x = 0.5;
    cat.scale.y = 0.5;

scale是介于0和1之间的数字,代表精灵大小的百分比。1表示100%(原尺寸),而0.5表示50%(半尺寸)。您可以通过将精灵的scale设置为2来使精灵大小增加一倍,如下所示:

    cat.scale.x = 2;
    cat.scale.y = 2;

Pixi提供了另一种简洁的方法,您可以使用scale.set方法在一行代码中设置精灵的缩放比例。

    //注意参数代表的意思
    cat.scale.set(0.5, 0.5);

如果喜欢这样使用,那就这样用吧!我们来看一个完整的示例:

    //别名
    let Application = PIXI.Application;
    let loader = PIXI.loader;
    let resources = PIXI.loader.resources;
    let Sprite = PIXI.Sprite;
    //创建一个应用对象
    let app = new Application({
        width: 256,
        height: 256,
        antialias: true,
        transparent: false,
        resolution: 1
    });
    //将Pixi自动为您创建的画布添加到HTML文档中
    document.body.appendChild(app.view);
    //加载图像并完成后运行“setup”函数
    loader.add("/static/page/PIXIJS/images/cat.png").load(setup);
    //该“setup”函数将在图像加载后运行
    function setup() {
        //创建cat精灵
        let cat = new Sprite(resources["/static/page/PIXIJS/images/cat.png"].texture);
        //改变精灵的位置
        cat.position.set(96, 96);
        //改变精灵的大小
        cat.scale.set(0.5,0.5);
        //或者这样使用
        //cat.scale.x=0.5
        //cat.scale.y=0.5
        //将cat精灵添加到舞台中如此便可以看到它
        app.stage.addChild(cat);
    }

运行效果如图所示:

在线示例

9.旋转

我们也可以通过将精灵的rotation属性设置为以弧度为单位的值来使其旋转。如下所示:

    cat.rotation = 0.5;

但是旋转发生在哪一点附近?我们可以从下图中看到精灵的左上角代表其x和y位置。该点称为锚点。 如果将精灵的rotation属性设置为0.5,则旋转将围绕精灵的锚点进行。我们也会知道这将对我们的cat精灵产生什么影响。


我们会看到锚点,即cat的左耳,是cat围绕其旋转的假想圆的中心。 如果要让精灵围绕其中心旋转怎么办?更改精灵的锚点,使其居中,如下所示:

    //anchor就是锚点
    cat.anchor.x = 0.5;
    cat.anchor.y = 0.5;

anchor.x和anchor.y值代表纹理尺寸的百分比,范围为0到1(0%到100%)。将其设置为0.5可使纹理在该点上居中。点本身的位置不会改变,只是纹理在其上定位的方式一样。下一张图显示了如果将居中的锚点居中,旋转的精灵会发生什么。

我们会看到精灵的纹理向上和向左移动。这是要记住的重要副作用!就像position和scale一样,我们也可以使用以下一行代码来设置锚点的x和y值:

    //注意参数即可
    cat.anchor.set(x, y);

精灵还具有pivot属性,其作用方式类似于anchor。 pivot设置精灵的x / y原点的位置。 如果更改轴心点然后旋转精灵,它将围绕该原点旋转。例如,下面的代码将把精灵的pivot.x指向32,将其pivot.y指向32。

    //注意参数的意义
    cat.pivot.set(32, 32);

假设精灵为64x64像素,则精灵现在将围绕其中心点旋转。但是请记住:如果更改了精灵的pivot,则还更改了它的x / y原点。那么,anchor和pivot点有什么区别?他们真的很相似!anchor使用0到1归一化的值移动精灵图像纹理的原点。pivot使用像素值移动精灵的x和y的原点。我们应该使用哪个?由我们自己决定。喜欢用哪个就用哪个即可。让我们来看看使用这两个属性的完整示例吧!

第一个示例第二个示例第三个示例

10.从精灵雪碧图中制作精灵

现在,我们也知道了如何从单个图像文件制作精灵。但是,作为游戏设计师,通常会使用雪碧图(也称为精灵图)来制作精灵。Pixi具有一些方便的内置方法来帮助我们完成此任务。所谓的雪碧图就是包含子图像的单个图像文件。子图像代表要在游戏中使用的所有图形。以下是图块图像的示例,其中包含游戏角色和游戏对象作为子图像。

整个雪碧图为192 x 192像素。每个图像都位于其自己的32 x 32像素网格单元中。在图块上存储和访问所有游戏图形是一种处理图形的非常高效的处理器和内存方式,Pixi为此进行了优化。 我们可以通过定义与我们要提取的子图像大小和位置相同的矩形区域,来从雪碧图中捕获子图像。以下是从雪碧图中提取的火箭子图像的示例。

让我们看看执行此操作的代码。首先,就像在前面的示例中所做的那样,使用Pixi的loader加载tileset.png图像。

    //注意这里的路径依据实际情况来修改调整
    loader.add("images/tileset.png").load(setup);

接下来,在加载图像后,使用雪碧图的矩形块来创建精灵的图像。以下是提取子图像,创建火箭精灵并将其定位并显示在画布上的代码。

    function setup() {
        //从纹理创建“tileset”精灵
        let texture = TextureCache["images/tileset.png"];
        //创建一个定义位置矩形对象
        //并且要从纹理中提取的子图像的大小
        //`Rectangle`是`PIXI.Rectangle`的别名,注意这里的参数,后续会详解,参数值与实际情况有关
        let rectangle = new Rectangle(192, 128, 64, 64);
        //告诉纹理使用该矩形块
        texture.frame = rectangle;
        //从纹理中创建一个精灵
        let rocket = new Sprite(texture);
        //定位火箭精灵在canvas画布上
        rocket.x = 32;
        rocket.y = 32;
        //将火箭精灵添加到舞台中
        app.stage.addChild(rocket);
        //重新渲染舞台  
        app.renderer.render(app.stage);
    }

这是如何工作的?Pixi具有内置的Rectangle对象(PIXI.Rectangle),它是用于定义矩形形状的通用对象。它有四个参数。前两个参数定义矩形的x和y位置。最后两个定义其宽度和高度。这是定义新Rectangle对象的格式。

    let rectangle = new PIXI.Rectangle(x, y, width, height);

矩形对象只是一个数据对象。由我们自己来决定如何使用它。在我们的示例中,我们使用它来定义要提取的图块上的子图像的位置和区域。Pixi纹理具有一个有用的属性,称为frame,可以将其设置为任何Rectangle对象。frame将纹理裁剪为矩形的尺寸。以下是使用frame将纹理裁剪为火箭的大小和位置的方法。

    let rectangle = new Rectangle(192, 128, 64, 64);
    texture.frame = rectangle;

然后,我们就可以使用该裁剪的纹理来创建精灵:

    let rocket = new Sprite(texture);

然后它就开始运行啦。由于我们会频繁地使用雪碧图制作精灵纹理,因此Pixi提供了一种更方便的方法来帮助我们完成此任务-让我们继续下一步。

在线示例

11.使用纹理图集

如果我们是开发大型复杂的游戏,则需要一种快速有效的方法来从雪碧图创建精灵。这是纹理图集真正有用的地方。纹理图集是JSON数据文件,其中包含匹配的图块PNG图像上子图像的位置和大小。如果使用纹理图集,那么关于要显示的子图像,我们所需要知道的就是它的名称。我们可以按任何顺序排列雪碧图图像,JSON文件将为我们跟踪其大小和位置。这真的很方便,因为这意味雪碧图图片的大小和位置不会硬编码到我们的游戏程序中。如果我们对雪碧图进行更改(例如添加图像,调整图像大小或将其删除),则只需重新发布JSON文件,我们的游戏就会使用该数据显示正确的图像。我们无需对游戏代码进行任何更改。

Pixi与一种流行的名为Texture Packer的软件工具输出的标准JSON纹理图集格式兼容。Texture Packer的“基本”许可证是免费的。让我们了解如何使用它制作纹理图集,并将该图集加载到Pixi中。(我们也可以不必使用Texture Packer。类似的工具(例如Shoeboxspritesheet.js)可以以与Pixi兼容的标准格式输出PNG和JSON文件。)

首先,要收集在游戏中使用的单个图像文件。

注: (本文中的所有图像均由Lanea Zimmerman创建。您可以在此处找到她的更多作品。谢谢Lanea Zimmerman!)

接下来,打开Texture Packer,然后选择JSON Hash作为框架类型。将图像拖到Texture Packer的工作区中。(我们也可以将Texture Packer指向包含图像的任何文件夹。)它将自动将图像排列在单个雪碧图上,并为其提供与原始图像名称匹配的名称。

注:(如果使用的是免费版本的Texture Packer,则将Algorithm设置为Basic,将Trim mode模式设置为None,将Extrude设置为0,将Size constraints 设置为Any size,然后将PNG Opt Level一直滑到左边至0。这些是基本设置,可让免费版本的Texture Packer创建文件而没有任何警告或错误。)

完成后,点击Publish按钮。选择文件名和存储位置,然后保存发布的文件。 最终将获得2个文件:一个PNG文件和一个JSON文件。 在此示例中,文件名是treasureHunter.json和treasureHunter.png。为了简便点,只需将两个文件都保存在一个名为images的文件夹中。(可以将JSON文件视为图像文件的额外元数据,因此将两个文件都保留在同一文件夹中是很有意义的。)JSON文件描述了图集中每个子图像的名称,大小和位置。如以下摘录了一个文件内容,描述了Blob Monster(泡泡怪)子图像。

    "blob.png":
    {
        "frame": {"x":55,"y":2,"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}
    },

treasureHunter.json文件还包含“dungeon.png”,“door.png”,“exit.png”和“explorer.png”属性,每个属性都具有相似的数据。这些子图像中的每一个都称为帧。拥有这些数据确实有帮助,因为现在无需知道纹理图集中每个子图像的大小和位置。只需要知道精灵的帧ID。帧ID只是原始图像文件的名称,例如“blob.png”或“explorer.png”。

使用纹理图集的众多优点之一是,可以轻松地在每个图像周围添加2个像素的填充(默认情况下,Texture Packer会这样做。)这对于防止纹理渗漏的可能性很重要。纹理出血(注:出血是排版和图片处理方面的专有名词,指在主要内容周围留空以便印刷或裁切)是当图块上相邻图像的边缘出现在精灵旁边时发生的一种效果。发生这种情况的原因是计算机的GPU(图形处理单元)决定如何舍入小数像素值的方式。它应该向上或向下取整?每个GPU都不同。在GPU上的图像周围留出1或2个像素的间距,可使所有图像始终显示一致。

注:(如果图形周围有两个像素填充,并且在Pixi的显示方式中仍然发现奇怪的“偏离一个像素”故障,请尝试更改纹理的缩放模式算法。方法如下:

    texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;

。由于GPU浮点舍入错误,有时会发生这些故障。)

现在我们已经知道如何创建纹理图集,让我们了解如何将其加载到游戏代码中。

ps:关于以上的示例所涉及到的图片资源可点击此处下载。

下图为本人使用Texture Packer创建的纹理图集的一个展示:

可点击此处(JSON), 此处(png)下载已经创建的纹理图集JSON文件和PNG文件。

12.加载纹理图集

可以使用Pixi的loader来加载纹理贴图集。如果是用Texture Packer生成的JSON,loader会自动读取数据,并对每一个帧创建纹理。下面就是怎么用loader来加载treasureHunter.json。当它成功加载,setup方法将会执行。

    //路径与实际项目有关
    loader.add("images/treasureHunter.json").load(setup);

现在,纹理图集上的每个图像都是Pixi缓存中的单个纹理。我们可以使用与Texture Packer中相同的名称(“ blob.png”,“ dungeon.png”,“ explorer.png”等)访问缓存中的每个纹理。

13.从加载的纹理图集创建精灵。

Pixi提供了三种从纹理图集创建精灵的方式:

1.使用TextureCache:

   let texture = TextureCache["frameId.png"],
   sprite = new Sprite(texture);

2.如果使用的是pixi的loader来加载纹理贴图集,则使用loader的 resources属性。

   let sprite = new Sprite(resources["images/treasureHunter.json"].textures["frameId.png"]);

3.要创建一个精灵需要写太多东西了!所以建议给纹理贴图集的textures对象创建一个叫做id的别名,就像是这样:

   let id = PIXI.loader.resources["images/treasureHunter.json"].textures;

现在就可以像这样实例化一个精灵了:

    let sprite = new Sprite(id["frameId.png"]);

这真的太棒了!

以下为在setup函数中如何使用这三种不同的方法来创建和显示dungeon,explorer,和treasure精灵。

//定义这三个变量,方便之后的使用
let textureId;
//地牢
let dungeon;
//探险者
let explorer;
//宝藏
let treasure;
//setup函数
function setup(){
    //有3种不同的方式来创建和显示精灵
    //第一种,使用纹理别名,TextureCache为PIXI.utils.TextureCache的别名
    let dungeonTexture = TextureCache['dungeon.png'];
    //Sprite为PIXI.Sprite的别名
    dungeon = new Sprite(dungeonTexture);
    //调用addChild方法将精灵添加到舞台中
    app.stage.addChild(dungeon);
    //第二种,使用resources来创建,也要注意参数根据实际情况来写
    explorer = new Sprite(resources["images/treasureHunter.json"].textures['explorer.png']);
    //将探险者的坐标设置一下,也就是设置探险者的位置,探险者在舞台中间,x方向距离随便设置
    explorer.x = 68;
    explorer.y = app.stage.height / 2 - explorer.height / 2;
    app.stage.addChild(explorer);
    //为所有的纹理图集创建一个别名
    textureId = PIXI.loader.resources['images/treasureHunter.json'].textures;
    treasure = new Sprite(textureId["treasure.png"]);
    //将宝藏的坐标设置一下
    treasure.x = app.stage.width - treasure.width - 48;
    treasure.y = app.stage.height / 2 - treasure.height / 2;
    //将宝藏精灵添加到舞台中去
    app.stage.addChild(treasure);
}
                    

下图为以上代码所展现的结果:

舞台尺寸为512 x 512像素,您可以在上面的代码中看到app.stage.height和app.stage.width属性用于对齐精灵。 以下是浏览器的y位置垂直居中的方式:

    explorer.y = app.stage.height / 2 - explorer.height / 2;

学习使用纹理图集创建和显示精灵是一个重要的基本操作。因此,在继续之前,我们再来编写用于添加其余精灵的代码:blobs和exit,这样您便可以生成如下所示的场景:

以下是完成所有这些操作的全部代码。还包括了HTML代码,因此可以在适当的上下文中查看所有内容。(可以在此处下载代码。)请注意,已创建了blobs精灵,并将其添加到循环中的舞台上,并分配了随机位置。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>从纹理图中创建精灵</title>
    </head>
    <body>
        <script src="https://www.eveningwater.com/static/data/web/PixiJS/source/dist/pixi.min.js"></script>
        <script>
            //别名
            let Application = PIXI.Application,
                Container = PIXI.Container,
                loader = PIXI.loader,
                resources = PIXI.loader.resources,
                TextureCache = PIXI.utils.TextureCache,
                Sprite = PIXI.Sprite,
                Rectangle = PIXI.Rectangle;
            //创建pixi应用对象
            let app = new Application({
                width: 512,
                height: 512,
                antialiasing: true,
                transparent: false,
                resolution: 1
            });
            //将应用对象添加到dom中
            document.body.appendChild(app.view);
            //加载json文件,并在加载完成之后执行setup函数,注意这里的json文件路径,后面的也是
            loader.add("./texture.json").load(setup);
            //定义一些需要用到的变量
            let dungeon, explorer, treasure, door, textureId;
            function setup() {
                //以下分别使用三种不同的方式来创建精灵
                //第一种
                let dungeonTexture = TextureCache["dungeon.png"];
                dungeon = new Sprite(dungeonTexture);
                app.stage.addChild(dungeon);
                //第二种
                explorer = new Sprite(
                    resources["./texture.json"].textures["explorer.png"]
                );
                explorer.x = 68;
                //设置探险者的位置
                explorer.y = app.stage.height / 2 - explorer.height / 2;
                app.stage.addChild(explorer);
                //第三种
                textureId = PIXI.loader.resources["./texture.json"].textures;
                treasure = new Sprite(textureId["treasure.png"]);
                //设置宝藏的位置
                treasure.x = app.stage.width - treasure.width - 48;
                treasure.y = app.stage.height / 2 - treasure.height / 2;
                app.stage.addChild(treasure);
                //创建出口的精灵
                door = new Sprite(textureId["door.png"]);
                door.position.set(32, 0);
                app.stage.addChild(door);
                //制作泡泡怪精灵
                let numberOfBlobs = 6,//数量
                    spacing = 48,//位置
                    xOffset = 150;//偏移距离
                //根据泡泡怪精灵的数量来制作精灵
                for (let i = 0; i < numberOfBlobs; i++) {
                    let blob = new Sprite(textureId["blob.png"]);
                    let x = spacing * i + xOffset;
                    //随机生成泡泡怪的位置
                    let y = randomInt(0, app.stage.height - blob.height);
                    // 设置泡泡怪的位置
                    blob.x = x;
                    blob.y = y;
                    //将泡泡怪添加到舞台中
                    app.stage.addChild(blob);
                }
            }
            //随机生成的函数
            function randomInt(min, max) {
                return Math.floor(Math.random() * (max - min + 1)) + min;
            }
        </script>
    </body>
    </html>

在线示例

我们可以在上面的代码中看到所有的blob都是使用for循环创建的。每个blobs沿x轴均匀分布,如下所示:

    let x = spacing * i + xOffset;
    blob.x = x;

spacing的值为48,xOffset的值为150。这意味着第一个Blob的x位置为150。这会将其从舞台的左侧偏移150个像素。每个后续的Blob的x值将比循环的上一次迭代中创建的Blob大48个像素。这样沿着地牢地板从左到右创建了一条均匀分布的怪物线。

每个blob也被赋予一个随机的y位置。以下为执行此操作的代码:

    let y = randomInt(0, stage.height - blob.height);
    blob.y = y;

可以为blob的y位置分配介于0到512之间的任何随机数,512是stage.height的值。这在名为randomInt的自定义函数的帮助下起作用。randomInt返回一个随机数,该随机数在您提供的任何两个数字之间的范围内。

    //注意参数代表的意思
    randomInt(lowestNumber, highestNumber);

这意味着,如果您想要一个介于1到10之间的随机数,则可以这样获得:

    let randomNumber = randomInt(1, 10);

以下是完成所有这些操作的randomInt函数定义:

    function randomInt(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

randomInt是一个很好的用来做游戏的工具函数,在写游戏的时候会经常用到它。

14.移动精灵

我们现在知道了如何显示精灵,但是如何使它们移动呢?这很容易:使用Pixi的代码创建循环功能,这称为游戏循环。放入游戏循环中的任何代码都会每秒更新60次。可以编写以下代码来使cat精灵以每帧1个像素的速率向右移动。

    function setup() {
        //开始游戏循环,创建一个这样的函数
        //Pixi的`ticker`提供了一个delta参数
        app.ticker.add(delta => gameLoop(delta));
    }
    function gameLoop(delta){
        //是cat精灵移动1px
        cat.x += 1;
    }

如果运行这段代码,我们将看到精灵逐渐移到舞台的右侧。

这是因为每次gameLoop运行时,它都会将cat的x位置加1。

    cat.x += 1;

每一个你放进Pixi的ticker的函数都会每秒被执行60次。你可以看见函数里面提供了一个delta的内容,他是什么呢?delta的值代表帧的部分的延迟。可以把它添加到cat的位置,让cat的速度和帧率无关。下面是代码:

    cat.x += 1 + delta;

是否选择添加此增量值在很大程度上是美学选择。而且只有当您的动画努力保持每秒60帧的一致显示速率时,效果才会真正显着(例如,如果它在慢速设备上运行,则可能会发生这种情况)。本文中的其余示例将不使用此增量值,但可以根据需要在自己的工作中随意使用它。 可以不必使用Pixi的代码来创建游戏循环。如果愿意的话,可以使用requestAnimationFrame,如下所示:

function gameLoop() {
    //每60秒执行一次游戏循环函数
    requestAnimationFrame(gameLoop);
    //移动cat精灵
    cat.x += 1;
}
//开始游戏循环
gameLoop();
                    

采用哪种方式随我们自己的喜好。这就是移动精灵所有的操作!只需在循环内以较小的增量更改任何sprite属性,它们就会随着时间推移进行动画处理。如果要让精灵沿相反的方向(向左)设置动画,只需给它指定一个负值即可,例如-1。

以下是以上示例的完整代码:

    //别名
    let Application = PIXI.Application,
        Container = PIXI.Container,
        loader = PIXI.loader,
        resources = PIXI.loader.resources,
        TextureCache = PIXI.utils.TextureCache,
        Sprite = PIXI.Sprite,
        Rectangle = PIXI.Rectangle;
    //创建应用对象
    let app = new Application({
        width: 256,
        height: 256,
        antialias: true,
        transparent: false,
        resolution: 1
    });
    //将应用对象添加到dom中
    document.body.appendChild(app.view);
    // 加载图像资源
    loader.add("images/cat.png").load(setup);
    //定义cat精灵
    let cat;
    function setup() {
        //创建cat精灵
        cat = new Sprite(resources["images/cat.png"].texture);
        cat.y = 96;
        app.stage.addChild(cat);
        //开始游戏循环
        app.ticker.add(delta => gameLoop(delta));
    }
    function gameLoop(delta) {
        // 移动1像素
        cat.x += 1;
        //也可以使用增量
        //cat.x += 1 + delta;
    }

在线示例

注意: (cat变量需要在setup和gameLoop函数之外定义,以便可以在两个函数中进行访问。)

我们还可以为精灵的比例,旋转或大小设置动画-无论如何!我们将看到更多有关如何为精灵设置动画的示例。

15.使用速度属性

为了方便自己,给游戏增加点灵活性,最好使用两个速度属性(vx和vy)控制精灵的移动速度。vx用于设置精灵在x轴上(水平)的速度和方向。vy用于在y轴上(垂直)设置精灵的速度和方向。与其直接更改精灵的x和y值,不如先更新速度变量,然后将这些速度值分配给精灵。这是交互式游戏动画所需的额外的一个模块。

第一步是在精灵上创建vx和vy属性,并为其赋予初始值。

    cat.vx = 0;
    cat.vy = 0;

设置vx和vy的值为0意味着精灵并没有被移动(即静止)。接着,在游戏循环中,更新我们想要移动的vx和vy的速度值,然后把它们赋值给精灵的x和y属性。以下是使用这种方式来使得精灵每帧往右下方移动1像素的示例代码:

    function setup() {
        //创建cat精灵
        cat = new Sprite(resources["images/cat.png"].texture);
        cat.y = 96; 
        cat.vx = 0;
        cat.vy = 0;
        app.stage.addChild(cat);
        //开始游戏循环
        app.ticker.add(delta => gameLoop(delta));
    }
    function gameLoop(delta){
        //更新cat精灵的速度
        cat.vx = 1;
        cat.vy = 1;
        //将速度属性值赋值给cat精灵的位置,即x和y坐标
        cat.x += cat.vx;
        cat.y += cat.vy;
    }

当运行以上的代码,cat精灵就会每帧往右下方移动1像素。如下图所示:

想要让cat精灵往不同方向移动吗?让cat精灵往左移动,那么就将vx 的值设置为负数,例如-1。想要让它往上移动,那么就将vy的值设置为负数,例如-1。想要让cat精灵移动的更快,只需要将vx和vy的值设置大一点,就像3,5,-2,-4。(负号代表方向)。

我们会看到如何通过利用vx和vy的速度值来模块化精灵的速度,它对游戏的键盘和鼠标控制系统很有帮助,而且更容易实现物理模拟。

在线示例

16.游戏状态

考虑到样式,也为了帮助模块化代码,个人建议还是像下面这样构造游戏循环:

    //定义一个变量设置游戏开始状态
    let state = play;
    app.ticker.add((delta) => { gameLoop(delta)});
    //开始游戏循环
    function gameLoop(delta){
        //更改游戏状态
        state(delta);
    }
    function play(delta){
        cat.vx = 1;
        cat.x += cat.vx;
    }

我们会看到gameLoop每秒60次调用了state函数。state函数是什么呢?它被赋值为play。意味着play函数会每秒运行60次。以下的示例展示了如何用一个新模式来重构上一个例子的代码:

    //为了方便其它函数使用变量,将定义全局变量
    let cat;
    let state;
    function setup() {
      //创建cat精灵
      cat = new Sprite(resources["images/cat.png"].texture);
      cat.y = 96; 
      cat.vx = 0;
      cat.vy = 0;
      app.stage.addChild(cat);
      //开始设置游戏状态
      state = play;
      //开始游戏循环
      app.ticker.add(delta => gameLoop(delta));
    }
    function gameLoop(delta){
      //更新当前的游戏状态
      state(delta);
    }
    function play(delta) {
      //每帧让cat移动1像素
      cat.vx = 1;
      cat.x += cat.vx;
    }

是的,也许这有点让人不快(head-swirler)!但是,不要让它吓到我们,而是花一两分钟来思考这些功能是如何连接的。正如我们将要看到的那样,像这样构造游戏循环将使进行切换游戏场景和关卡之类的事情变得非常容易。

在线示例

17.键盘控制运动

只需多做一些工作,我们就可以构建一个简单的系统来使用键盘控制精灵。为了简化我们的代码,建议使用称为keyboard的自定义函数来侦听和捕获键盘事件。如下所示:

    function keyboard(value) {
        let key = {};
        key.value = value;
        key.isDown = false;
        key.isUp = true;
        key.press = undefined;
        key.release = undefined;
        //键盘按下开始操作
        key.downHandler = event => {
          if (event.keyCode === key.value) {
            if (key.isUp && key.press)key.press();
            key.isDown = true;
            key.isUp = false;
            event.preventDefault();
          }
        };
        //键盘按下结束
        key.upHandler = event => {
          if (event.keyCode === key.value) {
            if (key.isDown && key.release)key.release();
            key.isDown = false;
            key.isUp = true;
            event.preventDefault();
          }
        };
        //绑定监听的事件
        const downListener = key.downHandler.bind(key);
        const upListener = key.upHandler.bind(key);
        window.addEventListener("keydown", downListener, false);
        window.addEventListener("keyup", upListener, false);
        //解绑事件的监听
        key.unsubscribe = () => {
          window.removeEventListener("keydown", downListener);
          window.removeEventListener("keyup", upListener);
        };
        return key;
    }

keyboard函数用起来很容易,可以像这样创建一个新的键盘对象:

    let keyObject = keyboard(keyValue);

它的一个参数是您要收听的键值。可以点击此处查看键盘键值列表。然后将press和release方法分配给键盘对象,如下所示:

    keyObject.press = () => {
        //key object pressed
    };
    keyObject.release = () => {
      //key object released
    };

键盘对象也有isDown和isUp的布尔值属性,用它们来检查每个按键的状态。但是 不要忘记使用unsubscribe方法删除事件侦听器:

    keyObject.unsubscribe();

在examples文件夹里看一下keyboardMovement.html文件是怎么用keyboard函数的,利用键盘的方向键去控制精灵图。运行它,然后用上下左右按键去让猫在舞台上移动。

以下是所有的代码:

    let cat;
    let state;
    function setup(){
        //创建cat精灵
        cat = new Sprite(resource["./images/cat.png"].texture);
        //设置cat精灵的坐标
        cat.y = 96;
        cat.vx = 0;
        cat.vy = 0;
        //添加到舞台中
        app.stage.addChild(cat);

        //键盘按键事件注意参数为实际对应的键盘key,是整数数字
        let left = keyboard("arrowLeft");
        let right = keyboard("arrowRight");
        let up = keyboard("arrowUp");
        let down = keyboard("arrowDown");

        //当按下左方向键往左改变速度,即改变vx为负值,在这里是5,vy不变
        left.press = () => {
            cat.vx = -5;
            cat.vy = 0;
        }
        //当释放左方向键时初始化速度
        left.release = () => {
            //如果右键没有被按下,并且cat的vy速度为0
            if(!right.isDown && cat.vy === 0){
                cat.vx = 0;
            }
        }
        //当按下右方向键
        right.press = () => {
            cat.vx = 5;
            cat.vy = 0;
        }
        //当释放右方向键
        right.release = () => {
            if(!left.isDown && cat.vy === 0){
                cat.vx = 0;
            }
        }
        //当按下上方向键
        up.press = () => {
            cat.vy = -5;
            cat.vx = 0;
        }
        //当释放上方向键
        up.release = () => {
            if(!down.isDown && cat.vx === 0){
                cat.vy = 0;
            }
        }
        //当按下下方向键
        down.press = () => {
            cat.vy = 5;
            cat.vx = 0;
        }
        //当释放下方向键
        down.release = () => {
            if(!up.isDown && cat.vx === 0){
                cat.vy = 0;
            }
        }
        state = play;
        //开始游戏循环
        app.ticker.add((delta) => {
            gameLoop(delta);
        })
    }
    function gameLoop(delta){
        //更新游戏状态
        state(delta);
    }
    function play(delta){
        cat.x += cat.vx;
        cat.y += cat.vy;
    }

在线示例

18.精灵分组

(1).精灵分组含义

精灵分组使我们可以创建游戏场景,并将相似的精灵作为一个单元一起管理。Pixi有一个名为Container的对象,我们可以使用它来完成一些操作。让我们看看它是如何工作的。 假设您要显示三种精灵:猫,刺猬和老虎。创建它们并设置它们的位置-但是不要将它们添加到舞台上。

    //The cat
    let cat = new Sprite(id["cat.png"]);
    cat.position.set(16, 16);
    //The hedgehog
    let hedgehog = new Sprite(id["hedgehog.png"]);
    hedgehog.position.set(32, 32);
    //The tiger
    let tiger = new Sprite(id["tiger.png"]);
    tiger.position.set(64, 64);

接下来,创建一个animals容器以将它们全部分组,如下所示:

    let animals = new Container();

然后使用addChild方法将这些精灵添加到分组容器中

    animals.addChild(cat);
    animals.addChild(hedgehog);
    animals.addChild(tiger);

最后,把分组添加到舞台中

    app.stage.addChild(animals);

注: (stage对象也是一个Container。它是所有Pixi精灵的根容器。)

以上的代码效果如下图所示:

我们是看不到这个包含精灵图的animals分组的。它仅仅是个容器而已。

现在,我们可以将这个animals分组视为一个单元。我们也可以将Container视为一种没有纹理的特殊精灵。如果需要获取animals包含的所有子精灵的列表,可以使用children数组来获取。

    console.log(animals.children)
    //Displays: Array [Object, Object, Object]

它告诉我们animals有三个子精灵。因为animals与任何其他Sprite一样,因此可以更改其x和y值,alpha,scale和所有其他sprite属性。我们在父容器上更改的任何属性值都将以相对方式影响子精灵。因此,如果设置了animals的x和y位置,则所有子精灵将相对于animals的左上角重新定位。如果将animals的x和y位置设置为64,会发生什么?

    animals.position.set(64, 64);

整个精灵组将向右下方移动64个像素。如下图所示:

animals也有其自己的尺寸,该尺寸基于包含的精灵所占据的面积。我们可以找到它的宽度和高度值,如下所示:

    console.log(animals.width);
    //Displays: 112
    console.log(animals.height);
    //Displays: 112
                        

如果更改animals的宽度或高度会怎样?

    animals.width = 200;
    animals.height = 200;

所有子精灵将缩放以匹配该更改。如下图所示:

我们可以根据需要在其他容器中嵌套尽可能多的容器,以根据需要创建深层次结构。但是,DisplayObject(如Sprite或另一个Container)一次只能属于一个父级。如果使用addChild将精灵作为另一个对象的子级,则Pixi会自动将其从当前父级中删除。这是我们无需担心的有用的管理。

(2).局部和全局位置

当把精灵添加到一个容器中时,它的x和y是相对于精灵分组的左上角来定位的,这就是精灵的局部位置。例如:你认为cat精灵在以下图中所处的位置是?

让我们来获取它的值:

    console.log(cat.x);
    //Displays:16

16?是的!这是因为cat相对于分组的左上角只仅仅偏移了16像素而已。 16就是cat的局部位置。

精灵当然也有全局位置。全局位置就是舞台左上角到精灵的锚点(通常值得是精灵的左上角的距离)的距离。我们可以通过toGlobal方法来获取精灵的全局位置。如下:

    //父精灵上的方法,传入子精灵位置参数
    parentSprite.toGlobal(childSprite.position)

以上代码的意思就是如果我们要在animals分组中找到cat精灵的全局位置,那么就要像如下这样写代码:

    console.log(animals.toGlobal(cat.position));
    //Displays: Object {x: 80, y: 80...};

然后它就会给我们一个x和y的位置值,即80。更确切的说,cat的全局位置就是相对于舞台左上角的位置。

如果不知道精灵的父容器是什么?我们如何找到精灵的全局位置呢?每个精灵都会有一个parent属性来告诉我们它的父容器(或者说叫父精灵分组)是什么。如果将一个精灵正确的添加到了舞台中,那么舞台就是精灵的父容器。在以上的示例中,cat精灵的父容器就是 animals精灵分组。那也就意味着可以通过编写如下的代码来获取cat的全局位置。

   cat.parent.toGlobal(cat.position);

即使我们不知道cat精灵的父容器是谁,它一样会执行。当然还有一种更好的方式来计算出精灵的全局位置,并且它通常也是一种最佳方式,所以听好啦!如果我们想要知道精灵到canvas左上角的距离,并且不知道或者不关心精灵的父容器是谁,可以使用getGlobalPosition方法。以下展示了如何获取tiger精灵的全局位置:

    tiger.getGlobalPosition().x
    tiger.getGlobalPosition().y

在我们已经写好的示例中,它会返回我们x和y的值是128。更特别的是, getGlobalPosition返回的值非常精确:当精灵的局部位置改变的同时,也会返回给我们准确的全局位置。

如果想要将全局位置转为局部位置应该怎么办?我们可以使用toLocal方法。它的工作方式很类似,通常是以下的格式:

    sprite.toLocal(sprite.position, anyOtherSprite)

使用toLocal方法可以找到一个精灵与另一个任意精灵之间的距离。以下代码展示了如何找到tiger相对于 hedgehog的位置。

    tiger.toLocal(tiger.position, hedgehog).x
    tiger.toLocal(tiger.position, hedgehog).y

上面的代码会返回一个32的x值和一个32的y值。我们可以在示例图中看到tiger的左上角和hedgehog的左上角距离32像素。

(3).使用ParticleContainer分组精灵

Pixi有一个额外的,高性能的方式去分组精灵的方法称作:ParticleContainer(PIXI.particles.ParticleContainer)。任何在ParticleContainer里的精灵都会比在一个普通的Container的渲染速度快2到5倍。这是用于提升游戏性能的一个很棒的方法。 可以像这样创建ParticleContainer:

    let superFastSprites = new PIXI.particles.ParticleContainer();

然后用addChild方法去往里添加精灵,就像往普通的Container添加一样。

如果使用ParticleContainer,我们就不得不做出一些妥协。在一个ParticleContainer里的精灵仅仅只有一些基本的属性: x,y,width,height,scale,pivot,alpha, visible——就这么多。而且,它包含的精灵不能拥有自己的嵌套子级ParticleContainer也无法使用Pixi的高级视觉效果,例如滤镜和混合模式。每个ParticleContainer只能用一个纹理(因此,如果您想要具有不同外观的精灵,则必须使用雪碧图)。但是对于获得的巨大性能提升,这些妥协通常是值得的。而且,还可以在同一项目中同时使用Containers和ParticleContainers,因此可以微调优化。

为什么在Particle Container的精灵会如此快呢?因为精灵的位置是直接在GPU上计算的。Pixi开发团队正在努力让尽可能多的雪碧图在GPU上处理,所以很有可能用的最新版的Pixi的 ParticleContainer的特性一定比现在在这儿描述的特性多得多。查看当前ParticleContainer文档以获取更多信息。

无论在哪里创建一个ParticleContainer,都会有四个属性参数需要提供: size,properties,batchSize,autoResize。

    let superFastSprites = new ParticleContainer(maxSize, properties, batchSize, autoResize);

maxSize的默认值是1500。所以,如果需要包含更多的精灵,那就把这个值设为更大的数字。 properties参数是一个对象,对象包含5个需要设置的布尔值:scale, position,rotation,uvs,alphaAndTint。position的默认值是true,其它4个参数的默认值是false。这意味着在ParticleContainer中,想要改变 scale,rotation,uvs,alphaAndTint,就不得不把这些属性设置为true,就像如下这样:

    let superFastSprites = new ParticleContainer(size, 
        {
          rotation: true,
          alphaAndtint: true,
          scale: true,
          uvs: true
        }
    );

但是,如果认为不需要使用这些属性,请将它们设置为false可以最大限度地发挥性能。什么是uvs属性?仅当具有在动画时更改其纹理的粒子时,才将其设置为true。(所有精灵的纹理也都必须在同一雪碧图上才能起作用。) (注意:UV映射是3D图形显示术语,指的是被映射到3D表面上的纹理(图像)的x和y坐标。U是x轴,V是y轴。WebGL已经使用x,y和z用于3D空间定位,因此选择U和V表示2D图像纹理的x和y。)

在线示例

19.pixi画几何图形

(1).描述

使用图像纹理是制作精灵最有用的方法之一,但是也有其自己的低级绘制工具。可以使用它们制作矩形,形状,线,复杂的多边形和文本。而且,幸运的是,它使用了与Canvas Drawing API几乎相同的API,因此,如果已经熟悉canvas,就没有什么真正的新知识了。但是最大的好处是,与Canvas Drawing API不同,使用Pixi绘制的形状由WebGL在GPU上渲染。Pixi可以利用所有未开发的性能。让我们快速浏览一下如何制作一些基本形状。下面是我们将要使用的代码来创造的图形。

(2).矩形

所有的形状的初始化都是先创造一个Pixi的Graphics的类 (PIXI.Graphics)的实例。

    let rectangle = new Graphics();

然后使用参数为十六进制颜色代码值的beginFill方法来设置矩形的填充颜色。以下是将其设置为浅蓝色的方法。

    rectangle.beginFill(0x66CCFF);

如果想要给形状设置一个轮廓,使用方法。以下为给矩形设置一个4像素宽alpha值为1的红色轮廓的示例:

    //第一个参数为轮廓线宽度,第二个参数为轮廓线颜色值,第三个参数为alpha值
    rectangle.lineStyle(4, 0xFF3300, 1);

使用drawRect方法来画一个矩形,它的四个参数分别是x,y,width,height。

    rectangle.drawRect(x, y, width, height);

使用endFill方法来结束绘制。就像Canvas Drawing API一样!以下是绘制矩形,更改其位置并将其添加到舞台所需的全部代码。

    let rectangle = new Graphics();
    rectangle.lineStyle(4, 0xFF3300, 1);
    rectangle.beginFill(0x66CCFF);
    rectangle.drawRect(0, 0, 64, 64);
    rectangle.endFill();
    rectangle.x = 170;
    rectangle.y = 170;
    app.stage.addChild(rectangle);

以上代码可以在(170,170)这个位置创造一个宽高都为64的蓝色的红框矩形。

(3).圆形

使用drawCircle方法来创造一个圆。它的三个参数是x, y和radius。

    drawCircle(x, y, radius)

与矩形和精灵不同,圆的x和y位置也是其中心点(圆点)。以下是制作半径为32像素的紫色圆圈的代码。

    let circle = new Graphics();
    circle.beginFill(0x9966FF);
    circle.drawCircle(0, 0, 32);
    circle.endFill();
    circle.x = 64;
    circle.y = 130;
    app.stage.addChild(circle);

(4).椭圆形

作为Canvas Drawing API的一个方面,Pixi允许您使用drawEllipse方法绘制椭圆。

    drawEllipse(x, y, width, height);

x / y位置定义了椭圆的左上角(假设椭圆被一个不可见的矩形边界框包围-该框的左上角将代表椭圆的x / y锚点位置)。以下代码绘制了一个黄色的椭圆,宽50像素,高20像素。

    let ellipse = new Graphics();
    ellipse.beginFill(0xFFFF00);
    ellipse.drawEllipse(0, 0, 50, 20);
    ellipse.endFill();
    ellipse.x = 180;
    ellipse.y = 130;
    app.stage.addChild(ellipse);

(5).圆角矩形

Pixi还允许您使用drawRoundedRect方法制作圆角矩形。最后一个参数cornerRadius是一个数字(以像素为单位),该数字确定应将圆角设置为多少。

    drawRoundedRect(x, y, width, height, cornerRadius)

以下是绘制一个圆角为10像素的矩形的代码。

    let roundBox = new Graphics();
    roundBox.lineStyle(4, 0x99CCFF, 1);
    roundBox.beginFill(0xFF9933);
    roundBox.drawRoundedRect(0, 0, 84, 36, 10)
    roundBox.endFill();
    roundBox.x = 48;
    roundBox.y = 190;
    app.stage.addChild(roundBox);

(6).线段

从前面的例子我们已经知道使用lineStyle方法来绘制一条线段了。与Canvas Drawing API一样,我们可以使用moveTo和lineTo方法来画线段的开始和结束点。以下代码画了一条4像素宽,白色的对角线。

    let line = new Graphics();
    line.lineStyle(4, 0xFFFFFF, 1);
    line.moveTo(0, 0);
    line.lineTo(80, 50);
    line.x = 32;
    line.y = 32;
    app.stage.addChild(line);

PIXI.Graphics对象(如线条)具有x和y值,就像sprites一样,因此绘制它们之后,可以将它们放置在舞台上的任何位置。

(7).多边形

我们还可以使用drawPolygon方法将线连接在一起并用颜色填充它们,以制作复杂的形状。drawPolygon的参数是x / y点的路径数组,这些点定义形状上每个点的位置。

    let path = [
        point1X, point1Y,
        point2X, point2Y,
        point3X, point3Y
    ];
    graphicsObject.drawPolygon(path);

drawPolygon将这三个点连接在一起以形成形状。以下是使用drawPolygon将三条线连接在一起以形成带有蓝色边框的红色三角形的方法。在位置(0,0)处绘制三角形,然后使用其x和y属性将其移动到舞台上的位置。

    let triangle = new Graphics();
    triangle.beginFill(0x66FF33);
    triangle.drawPolygon([
        -32, 64,
        32, 64,
        0, 0         
    ]);
    triangle.endFill();
    triangle.x = 180;
    triangle.y = 22;
    app.stage.addChild(triangle);

在线示例

20.显示文本

使用Text对象(PIXI.Text)在舞台上显示文本。在最简单的形式中,可以这样操作:

    let message = new Text("Hello Pixi!");
    app.stage.addChild(message);

这将在画布上显示单词“Hello,Pixi”。Pixi的Text对象继承自Sprite类,因此它们包含所有相同的属性,例如x,y,width,height,alpha和rotation。 就像在其他精灵上一样,在舞台上放置文本并调整其大小。例如,可以使用position.set来设置消息的x和y位置,如下所示:

    message.position.set(54, 96);

这将为我们提供基本的,无样式的文本。但是,如果想变得更时髦,请使用Pixi的TextStyle(PIXI.TextStyle)函数来定义自定义文本样式。以下为示例代码:

    let style = new TextStyle({
        fontFamily: "Arial",
        fontSize: 36,
        fill: "white",
        stroke: '#ff3300',
        strokeThickness: 4,
        dropShadow: true,
        dropShadowColor: "#000000",
        dropShadowBlur: 4,
        dropShadowAngle: Math.PI / 6,
        dropShadowDistance: 6,
    });

这将创建一个新的样式对象,其中包含要使用的所有文本样式。有关可以使用的所有样式属性的完整列表,请参见此处。 要将样式应用于文本,请添加样式对象作为Text函数的第二个参数,如下所示:

    let message = new Text("Hello Pixi!", style);

如果要在创建文本对象后更改其内容,可以使用text属性。

    message.text = "Text changed!";

如果要重新定义样式属性,可以使用style属性。

    message.style = {fill: "black", font: "16px PetMe64"};

Pixi通过使用Canvas Drawing API将文本呈现为不可见的临时画布元素来制作文本对象。然后,它将画布变成WebGL纹理,以便可以将其映射到精灵。这就是需要将文本的颜色包裹在字符串中的原因:这是Canvas Drawing API的颜色值。与任何画布颜色值一样,可以使用用于常见的颜色单词,例如"red"或"green",也可以使用rgba,hsla或hex颜色模式。Pixi还可以包装长行文本。将文本的wordWrap样式属性设置为true,然后将wordWrapWidth设置为文本行应达到的最大长度(以像素为单位)。使用align属性设置多行文本的对齐方式。如下例:

    message.style = {wordWrap: true, wordWrapWidth: 100, align: center};

注: align不会影响单行文字。

如果要使用自定义字体文件,可以使用CSS@font-face规则将字体文件链接到运行Pixi应用程序的HTML页面。

    @font-face {
        font-family: "fontFamilyName";
        src: url("fonts/fontFile.ttf");
    }

将此@font-face规则添加到HTML页面的CSS样式表中。

Pixi还支持位图字体。还可以使用Pixi的loader来加载位图字体XML文件,就像加载JSON或图像文件一样。

在线示例

21.碰撞检测

(1).碰撞检测介绍

我们现在知道了如何制作各种图形对象,但是可以使用它们做什么呢?一个有趣的事情是构建一个简单的碰撞检测系统。可以使用一个名为hitTestRectangle的自定义函数,该函数检查是否有两个矩形Pixi精灵正在接触。

    hitTestRectangle(spriteOne, spriteTwo)

如果它们重叠(即碰撞),则hitTestRectangle将返回true。我们可以将hitTestRectangle与if语句一起使用,以检查两个精灵之间的碰撞,如下所示:

    if (hitTestRectangle(cat, box)) {
        //There's a collision
    } else {
        //There's no collision
    }

如我们所见,hitTestRectangle是游戏设计广阔领域的门槛。 运行examples文件夹中的collisionDetection.html文件以获取有关如何使用hitTestRectangle的工作示例。使用键盘上的方向键移动猫。如果猫碰到盒子,盒子会变成红色,然后"hit!"将由文本对象显示。

我们已经看到了创建所有这些元素的所有代码,以及使猫移动的键盘控制系统。唯一的新东西就是play函数内部使用hitTestRectangle来检查碰撞的函数。

    function play(delta) {
        //使用cat精灵的速度属性来移动
        cat.x += cat.vx;
        cat.y += cat.vy;
        //检查cat精灵与box精灵是否碰撞
        if (hitTestRectangle(cat, box)) {
          //如果碰撞则改变文本
          //盒子颜色变成红色
          message.text = "hit!";
          box.tint = 0xff3300;
        } else {
          //如果没有碰撞重置文本与盒子颜色
          message.text = "No collision...";
          box.tint = 0xccff99;
        }
    }

由于play函数每秒被游戏循环调用60次,因此该if语句会不断检查猫和盒子之间的碰撞。 如果hitTestRectangle返回的是true,则文本消息对象使用文本显示"hit!":

    message.text = "Hit!";

然后,通过将盒子的tint属性设置为十六进制的红色值,将盒子的颜色从绿色更改为红色。

    box.tint = 0xff3300;

如果没有碰撞,则文本和盒子将保持其原始状态:

    message.text = "No collision...";
    box.tint = 0xccff99;

这段代码非常简单,但是突然之间创建了一个似乎完全活跃的交互式世界。几乎就像魔术!而且,也许令人惊讶的是,我们现在拥有开始使用Pixi制作游戏所需的全部技能!

(2).碰撞检测函数

但是hitTestRectangle函数呢?它是做什么的,它是如何工作的?这样的碰撞检测算法如何工作的细节不在本文的讨论范围之内。(如果真的想知道,可以了解这本书的用法。)最重要的是,知道如何使用它。但是,仅供参考,以防万一,也可以参考完整的hitTestRectangle函数定义。我们能从注释中弄清楚它在做什么?

    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
        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 occurring. Check for a collision on the y axis
          if (Math.abs(vy) < combinedHalfHeights) {
            //There's definitely a collision happening
            hit = true;
          } else {
            //There's no collision on the y axis
            hit = false;
          }
        } else {
          //There's no collision on the x axis
          hit = false;
        }
        //`hit` will be either `true` or `false`
        return hit;
    };

在线示例

22.实例学习:寻宝猎人小游戏

到此为止,我们现在已经拥有开始制作游戏所需的所有技能。 什么? 你不相信我吗 让我向你证明! 让我们来看看如何制作一个简单的对象收集和避免敌人的游戏,称为《寻宝猎人》。

《寻宝猎人》是可以使用到目前为止学到的工具制作的最简单的完整游戏之一的一个很好的例子。 使用键盘上的箭头键可帮助探险家找到宝藏并将其带到出口。六个Blob怪物在地牢壁之间上下移动,如果碰到了探索者,他将变成半透明, 并且右上角的血量进度条会缩小。如果所有血量都用光了,舞台上会显示“ You Lost!”; 如果探险家带着宝藏到达出口,则显示“ You Won!”。 尽管它是一个基本的原型,但《寻宝猎人》包含了您在大型游戏中发现的大多数元素:纹理图集图形,交互性,碰撞以及多个游戏场景。 让我们浏览一下游戏的组合方式,以便可以将其用作自己的一款游戏的起点。

(1).代码结构

打开treasureHunter.html文件,你将会看到所有的代码都在一个大的文件里。 下面是一个关于如何组织所有代码的概览:

    //创建pixi应用以及加载所有的纹理图集的函数,就叫setup
    function setup() {
        //游戏精灵的创建,开始游戏状态,开始游戏循环
    }
    function gameLoop(delta) {
      //运行游戏循环
    }
    function play(delta) {
      //所有的游戏魔法都在这里
    }
    function end() {
      //游戏最后所运行的代码
    }
    //游戏需要用到的工具函数:
    //`keyboard`, `hitTestRectangle`, `contain`and `randomInt`

把这个当作你游戏代码的蓝图,让我们看看每一部分是如何工作的。

(2).用setup函数初始化游戏

加载纹理图集图像后,setup函数即会运行。它仅运行一次,并允许您为游戏执行一次性设置任务。 在这里创建和初始化对象,精灵,游戏场景,填充数据数组或解析加载的JSON游戏数据的好地方。 以下是Treasure Hunter中setup函数及其执行任务的简要视图。

    function setup() {
        //创建游戏开始场景分组
        //创建门精灵
        //创建玩家也就是探险者精灵
        //创建宝箱精灵
        //创造敌人
        //创建血量进度条
        //添加一些游戏所需要的文本显示
        //创建游戏结束场景分组
        //分配玩家的键盘控制器
        //设置游戏状态
        state = play;
        //开始游戏循环 
        app.ticker.add(delta => gameLoop(delta));
    }

代码的最后两行,state = play;和gameLoop可能是最重要的。 将gameLoop添加到Pixi的切换开关中可以打开游戏引擎, 并在连续循环中调用play函数。但是,在研究其工作原理之前,让我们先看看设置函数中的特定代码是做什么的。

a.创建游戏场景

setup函数将创建两个容器组,分别称为gameScene和gameOverScene。这些都添加到舞台中。

    gameScene = new Container();
    app.stage.addChild(gameScene);
    gameOverScene = new Container();
    app.stage.addChild(gameOverScene);

属于主游戏的所有精灵都添加到gameScene组中。游戏结束时应显示在游戏上方的文本将添加到gameOverScene组。

尽管它是在setup函数中创建的,但当游戏首次启动时gameOverScene不应该可见,因此其visible属性被初始化为false。

    gameOverScene.visible = false;

我们将看到,当游戏结束时,gameOverScene的visible属性将设置为true,以显示出现在游戏结束时的文本。

b.制作地牢,门,探险者与宝藏精灵

玩家,出口,宝箱以及地牢都是从纹理图集中制作而来的精灵。十分重要的是,它们都被作为gameScene的子精灵而添加。

    //从纹理图集中创建精灵
    id = resources["images/treasureHunter.json"].textures;
    //Dungeon
    dungeon = new Sprite(id["dungeon.png"]);
    gameScene.addChild(dungeon);
    //Door
    door = new Sprite(id["door.png"]);
    door.position.set(32, 0);
    gameScene.addChild(door);
    //Explorer
    explorer = new 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
    treasure = new Sprite(id["treasure.png"]);
    treasure.x = gameScene.width - treasure.width - 48;
    treasure.y = gameScene.height / 2 - treasure.height / 2;
    gameScene.addChild(treasure);

把它们都放在gameScene分组会使我们在游戏结束的时候去隐藏gameScene和显示gameOverScene操作起来更简单。

c.制作泡泡怪精灵

6个blob怪物是循环创建的。每个blob都被赋予一个随机的初始位置和速度。每个blob的垂直速度交替乘以1或-1,这就是导致每个blob沿与其相邻方向相反的方向移动的原因。每个创建的blob怪物都会被推入称为blob的数组。

    //泡泡怪数量
    let numberOfBlobs = 6;
    //泡泡怪水平位置值
    let spacing = 48;
    //泡泡怪偏移量
    let xOffset = 150;
    //泡泡怪速度
    let speed = 2;
    //泡泡怪移动方向
    let direction = 1;
    //一个数组存储所有的泡泡怪
    let blobs = [];
    //开始创建泡泡怪
    for (let i = 0; i < numberOfBlobs; i++) {
        //创建一个泡泡怪
        let blob = new Sprite(id["blob.png"]);
        //根据`spacing`值将每个Blob水平隔开
        //xOffset确定屏幕左侧的点
        //应在其中添加第一个Blob
        let x = spacing * i + xOffset;
        //给泡泡怪一个随机的垂直方向上的位置
        let y = randomInt(0, stage.height - blob.height);
        //设置泡泡怪的位置
        blob.x = x;
        blob.y = y;
        //设置泡泡怪的垂直速度。 方向将为1或
        //`-1。“ 1”表示敌人将向下移动,“-1”表示泡泡怪将
        //提升。将“方向”与“速度”相乘即可确定泡泡怪
        //垂直方向
        blob.vy = speed * direction;
        //下一个泡泡怪方向相反
        direction *= -1;
        //将泡泡怪添加到数组中
        blobs.push(blob);
        //将泡泡怪添加到gameScene分组中
        gameScene.addChild(blob);
    }
d.制作血量进度条

当我们在玩寻宝猎人的时候,我想应该会发现,当我们的探险者触碰到任何一个敌人的时候,屏幕右上角的血量进度条的宽度都会减少。那么这个血量进度条是如何制作的呢?它仅仅只是两个相同位置重叠的矩形:一个黑色的矩形在后面,一个红色的矩形在前面。它们都被分在healthBar分组中。healthBar被添加到gameScene分组中,然后在舞台上被定位。

    //创建血量进度条
    healthBar = new PIXI.Container();
    healthBar.position.set(stage.width - 170, 4)
    gameScene.addChild(healthBar);
    //创建黑色的矩形
    let innerBar = new PIXI.Graphics();
    innerBar.beginFill(0x000000);
    innerBar.drawRect(0, 0, 128, 8);
    innerBar.endFill();
    healthBar.addChild(innerBar);
    //创建红色的矩形
    let outerBar = new PIXI.Graphics();
    outerBar.beginFill(0xFF3300);
    outerBar.drawRect(0, 0, 128, 8);
    outerBar.endFill();
    healthBar.addChild(outerBar);
    healthBar.outer = outerBar;

我们已经看到一个被叫做outer的属性被添加到healthBar中。它仅仅引用outerBar(红色矩形),以便以后访问时很方便。

    healthBar.outer = outerBar;

我们不必这样做;但是,为什么不呢!这意味着,如果我们想控制红色outerBar的宽度,则可以编写一些如下所示的平滑代码:

    healthBar.outer.width = 30;

那很整洁而且可读,所以我们会保留它!

e.制作文本提示

游戏结束时,根据游戏的结果,一些文本显示“You won!”或“You lost!”。这是通过使用文本精灵并将其添加到gameOverScene来实现的。由于游戏开始时gameOverScene的visible属性设置为false,因此我们看不到此文本。这是setup函数的代码,该函数创建消息文本并将其添加到gameOverScene。

    let style = new TextStyle({
        //字体类型
        fontFamily: "Futura",
        //字体大小
        fontSize: 64,
        //字体颜色
        fill: "white"
      });
    message = new Text("The End!", style);
    message.x = 120;
    message.y = app.stage.height / 2 - 32;
    gameOverScene.addChild(message);

(3).开始游戏

所有使精灵移动的游戏逻辑和代码都发生在play函数内部,该函数连续循环运行。这是play函数的概述:

    function play(delta) {
        //移动探险者并将其包含在地牢中
        //移动泡泡怪
        //检测泡泡怪与探险者的碰撞
        //检测探险者与宝箱的碰撞
        //检测宝箱与出口的碰撞
        //决定游戏是赢还是输
        //游戏结束时,改变游戏的状态为end
    }

让我们找出所有这些功能的工作方式。

(4).移动探险者

探险者是使用键盘控制的,执行该操作的代码与先前学习的键盘控制代码非常相似。键盘对象会修改探险者的速度,并将该速度添加到play函数中探险者的位置。

    explorer.x += explorer.vx;
    explorer.y += explorer.vy;
a.运动范围

但是,新功能是探险者的动作被包含在地牢的墙壁内。绿色轮廓线显示了探险者运动的极限。

这是在名为contain的自定义函数的帮助下完成的。

    contain(explorer, {x: 28, y: 10, width: 488, height: 480});

contain包含2个参数。第一个参数是你想要被包含的精灵,第二个参数则是任意的一个对象,包含x,y,width,height属性,为了定义一个矩形区域。在这个例子中,contain对象定义了一个仅比舞台稍微偏移且小于舞台的区域,它与地牢墙的尺寸所匹配。

以下是完成这些工作的contain函数。该函数检查精灵是否已超出contain对象的边界。 如果超出了,则代码将精灵移回该边界。contain函数还会返回碰撞变量,其值取决于"top","right","bottom","left",具体取决于击中边界的哪一侧。(如果精灵没有碰到任何边界,则碰撞将是不确定的。)

    function contain(sprite, container) {
        let collision = undefined;
        //左
        if (sprite.x < container.x) {
          sprite.x = container.x;
          collision = "left";
        }
        //上
        if (sprite.y < container.y) {
          sprite.y = container.y;
          collision = "top";
        }
        //右
        if (sprite.x + sprite.width > container.width) {
          sprite.x = container.width - sprite.width;
          collision = "right";
        }
        //下
        if (sprite.y + sprite.height > container.height) {
          sprite.y = container.height - sprite.height;
          collision = "bottom";
        }
        //返回collision的值
        return collision;
    }

你将看到如何在前面的代码中使用碰撞返回值,以使Blob怪物在上层和下层地下城墙之间来回反弹。

(4).移动怪物

play函数还可以移动Blob怪物,将它们保留在地牢壁中,并检查每个怪物是否与玩家发生碰撞。如果Blob撞到地牢的顶壁或底壁,则其方向会相反。所有这些都是在forEach循环的帮助下完成的,该循环遍历每帧Blobs数组中的每个Blob精灵。

    blobs.forEach(function(blob) {
        //移动泡泡怪
        blob.y += blob.vy;
        //检查泡泡怪的屏幕边界
        let 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;
        }
    });

我们可以在上面的代码中看到contain函数的返回值如何用于使blob从墙反弹。名为blobHitsWall的变量用于捕获返回值:

    let blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});

blobHitsWall通常是undefined(未定义)的。但是,如果blob碰到了顶壁,则blobHitsWall的值将为“top”。 如果blob碰到底壁,则blobHitsWall的值将为“bottom”。如果以上两种情况均成立,则可以通过反转blob的速度来反转blob的方向。以下是执行此操作的代码:

    if (blobHitsWall === "top" || blobHitsWall === "bottom") {
        //通过改变速度为负值来反转方向
        blob.vy *= -1;
    }

将blob的vy(垂直速度)值乘以-1将翻转其运动方向。

(5).检测碰撞

前面循环中的代码使用hitTestRectangle函数来确定是否有任何敌人触摸了探险者。

    if(hitTestRectangle(explorer, blob)) {
        explorerHit = true;
    }

如果hitTestRectangle返回的是true。也就意味着会发生一次碰撞并且explorerHit变量的值会是true。如果explorerHit的值是true,play函数将会使探险者变成半透明,并且血量进度条的宽度减少1像素。(具体减少多少依据每个人自己定义。)。

    if(explorerHit) {
        //使探险者变成半透明
        explorer.alpha = 0.5;
        //减少血量进度条的宽度
        healthBar.outer.width -= 1;
    } else {
        //使探险者完全透明,如果不能再被撞击
        explorer.alpha = 1;
    }

如果explorerHit为false,则将explorer的alpha属性保持为1,这使其完全不透明。play函数还检查宝箱和探险者之间是否发生碰撞。如果有发生碰撞,宝藏将设置到探险者的位置,并稍有偏移。这使其看起来像探险家正在携带宝藏。

以下是完成这个工作的代码:

    if (hitTestRectangle(explorer, treasure)) {
        //8的数字还可以再大一点点
        treasure.x = explorer.x + 8;
        treasure.y = explorer.y + 8;
    }

(6).到达出口并结束游戏

有两种方式会让游戏结束:探险者携带宝箱并到达了出口就表示你赢了,或者就是你的血量进度条没有了那就表示你失败了。为了赢得游戏探险者仅仅只需要触碰到出口,如果发生了这种情况。那么将游戏的状态state设置为结束end,然后message也就是文本消息提示显示"You won!"

    if (hitTestRectangle(treasure, door)) {
        state = end;
        message.text = "You won!";
    }

如果血量进度条没有了,你也就游戏失败了。也将游戏的状态state设置为结束end,然后message也就是文本消息提示显示"You lost!"

    if (healthBar.outer.width < 0) {
        state = end;
        message.text = "You lost!";
    }

那么以下代码到底是什么意思呢?

    state = end;

我们从前面的示例中记住,gameLoop会以每秒60次的速度不断更新称为状态的函数。这是执行此操作的gameLoop:

    function gameLoop(delta){
        //更新当前的游戏状态
        state(delta);
    }

我们还将记住,我们最初将状态值设置为play,这就是为什么play函数循环运行的原因。通过将状态设置为end,我们告诉代码我们想要另一个函数,称为end的循环运行。在更大的游戏中,还可以具有tileScene状态,以及每个游戏级别的状态,例如leveOne,levelTwo和levelThree。

end函数是什么?以下便是:

    function end() {
        //游戏场景不显示,游戏结束场景显示
        gameScene.visible = false;
        gameOverScene.visible = true;
    }

它只是翻转游戏场景的可见性。这是隐藏gameScene并在游戏结束时显示gameOverScene的内容。 这是一个非常简单的示例,说明了如何切换游戏状态,但是可以在游戏中拥有任意数量的游戏状态,并根据需要填充尽可能多的代码。只需将state的值更改为要在循环中运行的任何函数。 而这正是寻宝猎人的全部!只需多做一些工作,就可以将这个简单的原型变成完整的游戏-试试吧!

在线示例

23.更多关于精灵的知识

到目前为止,我们已经学习了如何使用许多有用的精灵属性,例如x,y,visible和rotation,这些属性使我们可以大量控制精灵的位置和外观。但是Pixi Sprites还具有许多更有趣的有用属性。这是完整列表。 Pixi的类继承系统如何运作?(什么是类,什么是继承?单击此链接以查找。)Pixi的sprites建立在遵循此链的继承模型上:

    DisplayObject > Container > Sprite

继承只是意味着链中后面的类使用链中前面的类的属性和方法。这意味着,即使Sprite是链中的最后一个类,它除了具有自己的独特属性外,还具有与DisplayObject和Container相同的所有属性。 最基本的类是DisplayObject。任何DisplayObject都可以在舞台上呈现。容器是继承链中的下一个类。它允许DisplayObject充当其他DisplayObject的容器。排名第三的是Sprite类。精灵既可以显示在舞台上,也可以作为其他精灵的容器。

(PS:本文基于官方教程而翻译并加入了个人的理解以及示例,不喜勿喷。本文总结在本人的个人网站上)。


夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。