英文教程原作者Christopher (opens new window)

关注他@chriscaleb (opens new window)

本系列教程已经针对PixiJS v4 (opens new window)进行了更新。

你是否玩过诸如Canabalt (opens new window)Monster Dash (opens new window)等无尽的奔跑游戏,并想知道如何创建自己的滚动游戏地图。第一部分介绍了pixi.js渲染引擎,并介绍了视差滚动的基本原理。现在,我们将通过添加视口(viewport)的概念来构建第一次滚动尝试。

# 你将会学到...

  • 扩展pixi.js显示对象
  • JavaScript中面向对象的基础知识
  • 如何添加一个视口到你的滚动

# 你应该了解...

  • 了解面向对象的概念
  • pixi.js的一些基础知识

您将使用在第一个教程中生成的代码。或者,您可以从GitHub下载 (opens new window)以前的教程源代码。另外,本教程的全部源代码也可以在GitHub (opens new window)上找到。

作为提醒,点击上面的图片。它将启动并运行当前版本的视差滚动。目前只有两个层,但我们将开始添加第三个,更复杂的层,在接下来的这个教程。我们将通过添加viewport的概念来添加第三层。在此过程中,我们还将执行一些重要的重构,以便将scroller包装在它自己的类中。

虽然本教程主要针对初学者,但希望您至少对面向对象编程概念有基本的了解。不要担心这句话会让您感到不舒服,因为对于不熟悉面向对象JavaScript的人来说,我仍然会在正确的方向上提供足够的提示。

# 开始

如果您还没有完成本系列的第一个教程,那么我建议您先从那里开始。

还需要记住的是,您需要运行本地Web服务器才能测试您的工作。 如果您尚未这样做,请参阅第一个教程的“入门”部分,以获取有关如何设置Web服务器的详细信息。

# PIXI显示对象的扩展

正如我们之前发现的,pixi.js提供了几种可以使用的显示对象类型。 如果您还记得的话,我们在选择PIXI.extras.TilingSprite之前,先简单的体验过PIXI.Sprite

这两个类有很多相同属性。例如,它们都提供了·positionwidthheightalpha属性。另外,可以通过addChild()方法轻松地将它们添加到容器中。事实上,是PIXI.Container本身是一个显示对象,它还为您提供了SpriteTilingSprite类所使用的许多相同属性。

由于继承的奇妙之处,所有这些通用功能都可用,其中一个类可以继承和扩展另一个类的功能。 为了帮助您理解这一点,请查看下图,该图显示了pixi.js提供的大多数显示对象。

从上图可以看出,最基本的类型是PIXI.DisplayObject类,每个其他显示对象都继承自该类。 此类表示将对象呈现到屏幕所需的绝对必要元素。

TIP

当我说显示对象(display objects)时,我不仅指的是PIXI.DisplayObject类。 相反,我指的是PIXI.DisplayObject及继承自其的所有对象。 本质上,当我使用显示对象一词时,是指pixi.js可以在屏幕上呈现的任何对象。

继承链中的下一个是PIXI.Container,它允许一个对象充当其他显示对象的容器。 我们在第一个教程中使用的addChild()方法就是PIXI.Container提供的,也可以通过PIXI.SpritePIXI.TilingSpite的继承使用。

本质上,继承树中的每个类都是它所继承的类的更专注于某一特殊类的版本。好消息是,我们可以利用继承来创建我们自己的专门显示对象。换句话说,我们可以编写专门的类来表示每个视差层,并让pixi.js处理它们,就好像它们只是另一个显示对象一样。这为我们提供了一种非常好的封装代码的方法,并保持所有东西的整洁。

# 制作一个专门的远层(FAR LAYER)显示对象

因此,让我们从远开始。

打开index.html文件,在init()函数中查找创建和设置该层的代码行。如下所示

var farTexture = PIXI.Texture.fromImage("resources/bg-far.png");	
far = new PIXI.extras.TilingSprite(farTexture, 512, 256);
far.position.x = 0;
far.position.y = 0;
far.tilePosition.x = 0;
far.tilePosition.y = 0;
stage.addChild(far);
1
2
3
4
5
6
7

如果我们能够创建一个表示远层的类,并在类中隐藏我们的实现细节,然后就可以使用下面的代码替换上面那些代码。

far = new Far();
stage.addChild(far);
1
2

大大减少了代码,对吧?另外,它的可读性也更强了。

让我们通过创建一个名为Far的类来实现它,它代表了我们的视差滚动的Far层。在项目的根文件夹中创建一个新文件,并将其命名为Far.js

现在定义一个名为Far的函数,它代表我们这个类的构造函数。

function Far(texture, width, height) {
  PIXI.extras.TilingSprite.call(this, texture, width, height);
}
1
2
3

在构造函数下面添加以下行,然后保存文件





 

function Far(texture, width, height) {
  PIXI.extras.TilingSprite.call(this, texture, width, height);
}

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);
1
2
3
4
5

上面那行表示这个类继承了PIXI.extras.TilingSprite所有的属性和方法。

TIP

构造函数是创建类实例的特殊函数类型。在JavaScript中,构造函数的名称也用于指定类的名称。

那么为什么我们的Far类要继承PIXI.TilingSprite呢?如果你还记得在第一个教程中,我们使用了TilingSprite的实例来表示我们的每个视差层。因此,在我们自己的表示这些视差层的自定义类中使用这些特性是有意义的。基本上我们要说的是:我们的Far类是特殊的PIXI.extras.TilingSprite类(专门表示视差层的)。

因为我们的Far类继承自PIXI.extras.TilingSprite,所以我们需要记住要初始化TilingSprite。 这是通过在我们自定义类的构造函数中调用TilingSprite类的构造函数来完成的。 我在下面突出显示了执行此操作的代码行:


 




function Far(texture, width, height) {
  PIXI.extras.TilingSprite.call(this, texture, width, height);
}

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);
1
2
3
4
5

这样做是因为我们希望我们的Far类继承TilingSprite的所有功能。因为TilingSprite需要将三个参数传递给它的构造函数,所以我们需要确保我们自己的类接受这些参数并使用它们来进行初始化。下面我会突出显示类中用到这些参数的代码行:

 
 




function Far(texture, width, height) {
  PIXI.extras.TilingSprite.call(this, texture, width, height);
}

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);
1
2
3
4
5

我们还需要向Far类添加一些额外的功能,但实际上我们已经可以开始将其集成到index.html页面中了。

# 实例化您的FAR类

回到index.html页面。

为了使用Far类,您需要引入包含这个类的js文件。






 

<body onload="init();">
  <div align="center">
    <canvas id="game-canvas" width="512" height="384"></canvas>
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.0.0/pixi.min.js"></script>
  <script src="Far.js"></script>
1
2
3
4
5
6

删除下面高亮的代码行


 






  var farTexture = PIXI.Texture.fromImage("resources/bg-far.png");
  far = new PIXI.extras.TilingSprite(farTexture, 512, 256); // 删除此行
  far.position.x = 0;
  far.position.y = 0;
  far.tilePosition.x = 0;
  far.tilePosition.y = 0;
  stage.addChild(far);
1
2
3
4
5
6
7

用这个替换它:


 






  var farTexture = PIXI.Texture.fromImage("resources/bg-far.png");
  far = new Far(farTexture, 512, 256); // 用此行替换
  far.position.x = 0;
  far.position.y = 0;
  far.tilePosition.x = 0;
  far.tilePosition.y = 0;
  stage.addChild(far);
1
2
3
4
5
6
7

好吧,我承认,目前还没有太大的改进。让我们继续封装Far

# 封装位置相关代码

index.html中,我们设置了farpositiontilePosition属性。 让我们删除该功能,将其封装在Far类中。

首先从index.html删除下面高亮的代码行:



 
 
 
 


  var farTexture = PIXI.Texture.fromImage("resources/bg-far.png");
  far = new Far(farTexture, 512, 256);
  far.position.x = 0;
  far.position.y = 0;
  far.tilePosition.x = 0;
  far.tilePosition.y = 0;
  stage.addChild(far);
1
2
3
4
5
6
7

保存所做的更改,然后移至Far.js文件。 现在,直接在类自己的构造函数中设置positiontilePosition属性(添加高亮的代码行):




 
 
 
 


function Far(texture, width, height) {
  PIXI.extras.TilingSprite.call(this, texture, width, height);
	
  this.position.x = 0;
  this.position.y = 0;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
}
1
2
3
4
5
6
7
8

如果您一般不熟悉面向对象的JavaScript或面向对象的编程,那么您可能想知道this关键字的目的在上面的代码中是什么。 基本上,this可以引用类的已创建实例。 通过this,我们可以引用该实例的所有属性和方法。

因为我们的Far类继承自PIXI.extras.TilingSprite,所以它还具有TilingSprite的所有属性和方法,包括positiontilePosition。 要访问这些属性,我们只需使用this关键字。 这是设置图层x位置的代码行:

this.position.x = 0;
1

还应注意,this关键字也可以引用添加到类中的新属性或方法。

现在保存您的更改并在浏览器中测试您的代码。一切都应按预期运行。在Chrome的JavaScript控制台检查没有抛出错误。

# 封闭纹理

好了,我们开始有进展了。如果你回头看看index.html页面,你会发现代码开始变得更简洁了

 
 
 

var farTexture = PIXI.Texture.fromImage("resources/bg-far.png");	
far = new Far(farTexture, 512, 256);
stage.addChild(far);
1
2
3

但是仍有改进的空间。 毕竟,如果我们可以将位置想着代码直接隐藏在Far类中,那么我们也可以把纹理相关的代码封装到那里。

移至Far.js文件,并在构造函数的开头添加一行创建图层的纹理代码:


 


function Far(texture, width, height) {
  var texture = PIXI.Texture.fromImage("resources/bg-far.png");
  PIXI.extras.TilingSprite.call(this, texture, width, height);
1
2
3

现在显式地将纹理的宽度和高度传递到TilingSprite的构造函数中



 

function Far(texture, width, height) {
  var texture = PIXI.Texture.fromImage("resources/bg-far.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);
1
2
3

由于我们现在直接在类中处理纹理,所以实际上不需要将texturewidthheight参数传递给我们的构造函数。删除所有三个参数并保存代码

function Far(texture, width, height) { //删除texture, width, height
1

您的构造函数现在应如下所示:

function Far() {
  var texture = PIXI.Texture.fromImage("resources/bg-far.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);

  this.position.x = 0;
  this.position.y = 0;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
}
1
2
3
4
5
6
7
8
9

现在要做的就是回到index.html文件,删除我们之前创建的纹理,并将其传递给Far的构造函数的参数:

 
 


var farTexture = PIXI.Texture.fromImage("resources/bg-far.png"); // 删除此行
far = new Far(farTexture, 512, 256); // 删除参数 farTexture, 512, 256
stage.addChild(far);
1
2
3

您的代码现在应该是这样的:

far = new Far();
stage.addChild(far);
1
2

比以前简单明了多了,对吧?所有的实现细节现在都安全地隐藏在我们的Far类中。

# 封装中间层(MID LAYER)代码

我花了一些时间引导您完成创建代表我们的视差滚动的Far类所需的步骤。 该类继承自PIXI.extras.TilingSprite,其行为类似于任何其他pixi.js显示对象。现在利用我们所学的知识来创建一个代表视差滚动器中间层的类Mid

创建一个名为Mid.js的新文件,并向其添加以下代码:

function Mid() {
}

Mid.prototype = Object.create(PIXI.extras.TilingSprite.prototype);
1
2
3
4

现在在构造函数中,创建中间层的纹理并设置它的position属性


 
 
 
 
 
 
 
 



function Mid() {
  var texture = PIXI.Texture.fromImage("resources/bg-mid.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);

  this.position.x = 0;
  this.position.y = 128;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
}

Mid.prototype = Object.create(PIXI.extras.TilingSprite.prototype);
1
2
3
4
5
6
7
8
9
10
11

保存你的Mid.js文件,然后转到index.html,并引入这个js







 

<body onload="init();">
  <div align="center">
    <canvas id="game-canvas" width="512" height="384"></canvas>
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.0.0/pixi.min.js"></script>
  <script src="Far.js"></script>
  <script src="Mid.js"></script>
1
2
3
4
5
6
7

完成后,向下滚动到您的init()函数并删除以下行:




 
 
 
 
 
 


far = new Far();
stage.addChild(far);

var midTexture = PIXI.Texture.fromImage("resources/bg-mid.png");
mid = new PIXI.extras.TilingSprite(midTexture, 512, 256);
mid.position.x = 0;
mid.position.y = 128;
mid.tilePosition.x = 0;
mid.tilePosition.y = 0;
stage.addChild(mid);
1
2
3
4
5
6
7
8
9
10

用高亮的这一行代码替换它们:




 


far = new Far();
stage.addChild(far);

mid = new Mid();
stage.addChild(mid);
1
2
3
4
5

保存你的Mid.js文件并在浏览器中测试你的最新代码。

# 编写一个UPDATE()方法

我们已经对代码进行了大量的重构,但是仍然可以做一些事情。 返回到index.html文件并向下滚动到其主更新循环:

function update() {
  far.tilePosition.x -= 0.128;
  mid.tilePosition.x -= 0.64;

  renderer.render(stage);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7
8

方法中的前两行通过更新其tilePosition属性来滚动图层。 但是,我们的代码目前存在一个小问题:即暴露了MidFar类的内部实现,可以直接更改tilePosition属性。 这违反了封装的面向对象原则。

理想情况下,我们要在类中隐藏实现。 如果两个类都具有一个update()方法,为我们执行滚动,那么我们的代码将易读。 换句话说,对于我们的主循环,下面这种方式更为可取:


 
 






function update() {
  far.update();
  mid.update();

  renderer.render(stage);

  requestAnimFrame(update);
}
1
2
3
4
5
6
7
8

幸运的是,我们将向Far和Mid类都添加一个update()方法。 从远层开始,打开Far.js并添加以下方法:



 
 
 

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

Far.prototype.update = function() {
  this.tilePosition.x -= 0.128;
};
1
2
3
4
5

方法的内容你应该很熟悉。它只是简单地将纹理的平铺位置移动0.128像素,这正是我们目前在index.html主循环中所做的。

好的,保存您的更改并向Mid.js添加一个类似的方法



 
 
 

Mid.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

Mid.prototype.update = function() {
  this.tilePosition.x -= 0.64;
};
1
2
3
4
5

两种方法之间的唯一区别是中间层的update()方法将其滚动更大的数量。

保存所做的更改,然后移回index.html。 现在,我们要做的就是从主循环中调用每一层的update()方法。 删除以下高亮的两行代码:


 
 






function update() {
  far.tilePosition.x -= 0.128;  // 删除
  mid.tilePosition.x -= 0.64;   // 删除

  renderer.render(stage);

  requestAnimFrame(update);
}
1
2
3
4
5
6
7
8

并替换为下面高亮的两行:


 
 






function update() {
  far.update();
  mid.update();

  renderer.render(stage);

  requestAnimFrame(update);
}
1
2
3
4
5
6
7
8

保存所做的更改,并测试所有内容是否都能在Chrome中按预期运行。

# 停下来思考一下

虽然你的视差滚动仍然表现相同,我们实际上做了一些重要的改变,以整体架构的代码。我们采用了更面向对象的设计,利用继承来创建两个表示视差层的特殊显示对象。

能够编写专门的显示对象,在大部分情况下都非常有用。我们的FarMid类本质上与pixi.js支持的任何其他显示对象一样。下图说明了我们两个专用类在Pixi显示对象类的继承结构中的位置。

# 创建一个Scroller类

本教程开始时概述的目标之一是将视差滚动条封装到它自定义类中。现在我们已经写了我们的FarMid类。

这样,我们就可以从index.html中删除我们的MidFar实例,并将它们封装在一个可以满足所有滚动需求的对象中。

让我们编写一个这样的类。创建一个名为Scroller.js的新JavaScript文件,并通过向其添加以下代码来定义一个名为Scroller的类:

function Scroller(stage) {
}
1
2

关于该类,有两点值得注意。 首先,其构造函数希望引用我们的stage舞台(Pixi.Container)。 其次,它不继承任何东西。

FarMid类不同,我们的Scroller类不是一个专门的显示对象。相反,它将使用构造函数的stage参数添加我们的中间层和中间层。

让我们从在类中设置远端层far开始:


 
 


function Scroller(stage) {
  this.far = new Far();
  stage.addChild(this.far);
}
1
2
3
4

第一行代码创建了Far类的一个实例。注意,我们将实例存储在名为far的成员变量中。

TIP

通过使用this关键字将属性直接添加到您的类中来创建成员变量。 成员变量的优点是在类实例的整个生命周期中都可以持久保存,这意味着类的任何其他方法也可以访问它。

第二行将远层(Far的实例)添加到舞台上。

现在,对中间层进行相同的操作。 在构造函数中添加以下两行:





 
 


function Scroller(stage) {
  this.far = new Far();
  stage.addChild(this.far);

  this.mid = new Mid();
  stage.addChild(this.mid);
}
1
2
3
4
5
6
7

我们的类现在有两个成员变量:farmid,这很有用,因为它允许我们从其他方法中访问视差层,我们可以添加到类中。这是非常方便的,因为我们确实需要添加一个额外的方法。它将用于更新两个层的位置。现在让我们继续添加这个方法:









 
 
 
 

function Scroller(stage) {
  this.far = new Far();
  stage.addChild(this.far);

  this.mid = new Mid();
  stage.addChild(this.mid);
}

Scroller.prototype.update = function() {
  this.far.update();
  this.mid.update();
};
1
2
3
4
5
6
7
8
9
10
11
12

还记得我们为中类和远类都编写了update()方法吗?在我们的Scroller类的update()方法中需要做的就是调用这些方法。

# 插入Scroller

现在我们有了一个代表视差滚动条的Scroller类,我们可以回到index.html页面并将其插入。

打开index.html并引入Scroller.js




 

<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.0.0/pixi.min.js"></script>
<script src="Far.js"></script>
<script src="Mid.js"></script>
<script src="Scroller.js"></script>
1
2
3
4

现在转到init()函数并删除以下高亮代码行:









 
 
 
 
 




function init() {
  stage = new PIXI.Stage(0x66FF99);
  renderer = PIXI.autoDetectRenderer(
    512,
    384,
    {view:document.getElementById("game-canvas")}
  );

  far = new Far();
  stage.addChild(far);

  mid = new Mid();
  stage.addChild(mid);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

记住,远层和中间层现在都将由你的Scroller类来处理。因此,让我们创建一个Scroller实例来替换我们刚刚删除的行:









 




function init() {
  stage = new PIXI.Stage(0x66FF99);
  renderer = PIXI.autoDetectRenderer(
    512,
    384,
    {view:document.getElementById("game-canvas")}
  );

  scroller = new Scroller(stage);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7
8
9
10
11
12

我们向Scroller类构造函数传递了一个场景stage的引用。我们这样做很重要,因为Scroller类需要这个引用,以便将它的远视差层和中视差层添加到显示列表中。

现在我们要做的就是在主循环中调用scrollerupdate()方法。 首先,从主循环中删除以下两行:


 
 






function update() {
  far.update(); //删除
  mid.update(); //删除

  renderer.render(stage);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7
8

现在添加以下行以更新滚动条:


 






function update() {
  scroller.update(); //添加此行

  renderer.render(stage);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7

保存更改并使用Chrome测试所有内容。

我们已经成功地重新构造了视差滚动器,所有的东西都包含在一个类中。如果你查看index.html,你会发现我们已经隐藏了上次在第一个教程中编写的所有实现代码。

# 添加视口(VIEWPORT)

我们已经取得了巨大的进步,但是我们还要增加一件事。 为了使滚动条完整,我们需要添加视口的概念。 将视口视为在游戏地图上查看的窗口。

你可能会问,我们不是已经有了视口吗?毕竟,当您在浏览器中运行代码时,我们只会看到在舞台范围内可见的内容。这是完全正确的,但目前还没有办法知道我们在游戏世界中滚动了多远。另外,如果我们可以简单地跳到一个特定的位置,看看我们的图层应该是什么样子,不是很好吗?一旦我们添加了视口(viewport)的概念并提供了一种设置其当前位置的方法,所有这些都将成为可能。

#Scroller类添加setViewportX()方法

我们目前有一个update()方法,可以用来连续滚动视差层。让我们用一个名为setViewportX()的新方法来代替它,我们可以使用它来设置视口(viewport)的水平位置。调用这个方法可以让我们任意定位游戏地图。

让我们从Scroller类开始。

打开Scroller.js并删除现有的update()方法:









 
 
 
 

function Scroller(stage) {
  this.far = new Far();
  stage.addChild(this.far);

  this.mid = new Mid();
  stage.addChild(this.mid);
}

Scroller.prototype.update = function() {
  this.far.update();
  this.mid.update();
};
1
2
3
4
5
6
7
8
9
10
11
12

现在用setViewportX()方法替换它并保存您的更改:









 
 
 
 

function Scroller(stage) {
  this.far = new Far();
  stage.addChild(this.far);

  this.mid = new Mid();
  stage.addChild(this.mid);
}

Scroller.prototype.setViewportX = function(viewportX) {
  this.far.setViewportX(viewportX);
  this.mid.setViewportX(viewportX);
};
1
2
3
4
5
6
7
8
9
10
11
12

我们的setViewportX()方法相当简单。它期望将一个数字作为方法的viewportX参数传递,然后将该值传递给每个层。正如您所看到的,我们的两个层都需要实现它们自己的setViewportX()方法。我们现在就开始吧。

#Far类添加setViewportX()方法

我们将从删除现有的update()方法类开始。打开Far.js并删除以下行:













 
 
 

function Far() {
  var texture = PIXI.Texture.fromImage("resources/bg-far.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);

  this.position.x = 0;
  this.position.y = 0;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
}

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

Far.prototype.update = function() {
  this.tilePosition.x -= 0.128;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

注意

如无特殊说明,删除/添加/替换行,指的是对高亮行的操作。如果无高亮行,则指全部代码。

我们需要能够跟踪视口的水平位置。为此,在类构造函数中定义一个成员变量:










 


function Far() {
  var texture = PIXI.Texture.fromImage("resources/bg-far.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);

  this.position.x = 0;
  this.position.y = 0;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;

  this.viewportX = 0;
}
1
2
3
4
5
6
7
8
9
10
11

现在,将以下常量添加到类中:



 

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

Far.DELTA_X = 0.128;
1
2
3

您对DELTA_X常量的值应该很熟悉。这是我们先前在每次update()调用时将图层的平铺位置偏移的像素数。 当然,使用常量会使我们的代码更具可读性和可维护性,这就是为什么我们选择使用常量。 基本上,每次视口移动一个单位时,我们都会使用该常数将远端图层移动0.128像素。 现在,让我们编写一个setViewportX()方法来为我们做这件事。 添加以下内容:





 
 
 
 
 

Far.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

Far.DELTA_X = 0.128;

Far.prototype.setViewportX = function(newViewportX) {
  var distanceTravelled = newViewportX - this.viewportX;
  this.viewportX = newViewportX;
  this.tilePosition.x -= (distanceTravelled * Far.DELTA_X);
};
1
2
3
4
5
6
7
8
9

上面的代码不难理解。首先,我们计算自上次调用setViewportX()以来走过的距离。然后将视口的新水平位置存储在我们的viewportX成员变量中。 最后,我们将行进的距离乘以DELTA_X常数,以确定将图层的图块位置移动多远。

TIP

请注意,我们的x位置代表视口窗口的左侧。在其他实现中,x位置表示视口的中心也很常见。

保存Far.js

现在,我们需要对Mid类进行相同的更改。

#Mid类添加setViewportX()方法

Mid类的代码与Far类的代码几乎相同,所以我们改起来会很快。

打开Mid.js并删除它的update()方法













 
 
 

function Mid() {
  var texture = PIXI.Texture.fromImage("resources/bg-mid.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);

  this.position.x = 0;
  this.position.y = 128;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;
}

Mid.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

Mid.prototype.update = function() {
  this.tilePosition.x -= 0.64;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

现在将以下行添加到您的类中:










 




 

 
 
 
 


function Mid() {
  var texture = PIXI.Texture.fromImage("resources/bg-mid.png");
  PIXI.extras.TilingSprite.call(this, texture, 512, 256);

  this.position.x = 0;
  this.position.y = 128;
  this.tilePosition.x = 0;
  this.tilePosition.y = 0;

  this.viewportX = 0;
}

Mid.prototype = Object.create(PIXI.extras.TilingSprite.prototype);

Mid.DELTA_X = 0.64;

Mid.prototype.setViewportX = function(newViewportX) {
  var distanceTravelled = newViewportX - this.viewportX;
  this.viewportX = newViewportX;
  this.tilePosition.x -= (distanceTravelled * Mid.DELTA_X);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这两个类之间的唯一区别是,Mid类的DELTA_X常数的值为0.64,这是为了确保其层的滚动速度比远层的滚动速度快。 保存您的更改。

# 测试视口(VIEWPORT)

我们应该测试视口,并确保设置它的位置反映在视差层中。首先,我们需要打开index.html并删除它对scrollerupdate()方法的调用。只需从主循环中删除以下行即可:


 






function update() {
  scroller.update();

  renderer.render(stage);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7

保存index.html文件并在浏览器中测试更改。你应该注意到你看到了你的视差层,但也没有滚动。那是因为我们没有添加任何代码来实际改变视口的水平位置。目前它的默认x位置为0。

在添加代码之前,让我们测试一下scrollersetViewportX()是否工作。

只需按F12键(在Mac上为Cmd + Opt + i)以打开“开发工具”窗口,然后单击“console”选项卡。

打开Chrome的JavaScript控制台,然后尝试输入以下内容,将视口向右移动50像素:

scroller.setViewportX(50);
1

TIP

JavaScript控制台可以访问程序中的任何全局变量。因此,我们可以通过全局scroller变量访问scroller,并调用它的setViewportX()方法。

你应该看到你的视差层移动到左边,这表明我们已经成功地重新定位了视口。

尝试将视口移动到7000的x位置

scroller.setViewportX(7000);
1

# 滚动视口

很明显,我们可以通过不断更新滚动条的视口位置来模拟游戏世界中的运动。我们可以在主循环中这样做,但要做到这一点,我们需要能够获得viewport的当前水平位置。让我们继续,并添加一个新方法到我们的Scroller类,以允许我们这样做。

# 获取视口的位置

目前,我们的Scroller类并不存储当前的视口位置,因此我们需要一个成员变量来实现这个目的。

打开Scroller.js并在构造函数中定义以下成员变量








 


function Scroller(stage) {
  this.far = new Far();
  stage.addChild(this.far);

  this.mid = new Mid();
  stage.addChild(this.mid);

  this.viewportX = 0;
}
1
2
3
4
5
6
7
8
9

调用setViewportX()方法更新viewportX的值:


 




Scroller.prototype.setViewportX = function(viewportX) {
  this.viewportX = viewportX;
  this.far.setViewportX(viewportX);
  this.mid.setViewportX(viewportX);
};
1
2
3
4
5

完成之后,我们可以编写一个getViewportX()方法,它将返回viewport的当前位置。







 
 
 

Scroller.prototype.setViewportX = function(viewportX) {
  this.viewportX = viewportX;
  this.far.setViewportX(viewportX);
  this.mid.setViewportX(viewportX);
};

Scroller.prototype.getViewportX = function() {
  return this.viewportX;
};
1
2
3
4
5
6
7
8
9

保存上述修改。

# 更新主循环

现在剩下的就是不断地更新你的滚动条的视口位置。我们将在你的项目主循环中做这件事。

打开index.html,只需添加以下两行代码:


 
 






function update() {
  var newViewportX = scroller.getViewportX() + 5;
  scroller.setViewportX(newViewportX);
            
  renderer.render(stage);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7
8

第一行代码表示,获取视口的x位置,并将其增加5个单位,赋值给newViewportX。第二行代码表示获设置viewportnewViewportX。实际上,它迫使viewport在每次调用主循环时滚动5个单位。

保存您的工作,然后尝试在Chrome中运行它。 您应该再次看到视差图层滚动离开。 尝试尝试不同的滚动速度。 例如,尝试将视口增加15个单位而不是5个单位。

# 移动视口

让我们在Scroller类中再添加一个方法,它将允许您将viewport从当前位置移动指定的距离。这将帮助我们使我们的主循环更加清晰。

打开Scroller.js并添加以下方法:





 
 
 
 

Scroller.prototype.getViewportX = function() {
  return this.viewportX;
};

Scroller.prototype.moveViewportXBy = function(units) {
  var newViewportX = this.viewportX + units;
  this.setViewportX(newViewportX);
};
1
2
3
4
5
6
7
8

就像我们之前所做的一切一样,这种新方法并不难理解。它只是计算出viewport的新位置,然后调用类setViewportX()方法来设置viewport的位置。

回到index.html并删除以下行:


 
 






function update() {
  var newViewportX = scroller.getViewportX() + 5;
  scroller.setViewportX(newViewportX);

  renderer.render(stage);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7
8

将它们替换为使用moveViewportXBy()方法的这一行


 






function update() {
  scroller.moveViewportXBy(5);

  renderer.render(stage);

  requestAnimationFrame(update);
}
1
2
3
4
5
6
7

保存更改并测试。

# 修改主入口点

本教程的第二部分即将结束。在我们结束之前,让我们打开index.html并做最后一个重构。

虽然我们在减少对全局变量的依赖方面做了令人钦佩的工作,但是我们的index.html文件仍然有一些全局变量。实际上,在大型应用程序中,最好将尽可能多的JavaScript从HTML页面中分离出来。虽然我们的HTML页面中已经没有多少JavaScript了,但是我们可以做得更好。让我们把代码封装在它自己的类Main中。这样,我们当前依赖的全局变量将成为新类的成员变量。

创建一个新文件并命名为Main.js

为这个类创建一个构造函数,把HTML页面的init()函数的代码放在里面:

function Main() {
  this.stage = new PIXI.Container();
  this.renderer = PIXI.autoDetectRenderer(
    512,
    384,
    {view:document.getElementById("game-canvas")}
  );

  this.scroller = new Scroller(this.stage);

  requestAnimationFrame(this.update.bind(this));
}
1
2
3
4
5
6
7
8
9
10
11
12

注意上面使用this关键字。 我们使用它来定义stagerendererscroller作为成员变量。

在我们调用JavaScript函数requestAnimationFrame()时也使用了这个关键字。

requestAnimationFrame(this.update.bind(this));
1

让我们来编写这个类的update方法,它在重新绘制场景时被调用。这个方法的代码来自HTML页面上的update函数:

Main.prototype.update = function() {
  this.scroller.moveViewportXBy(Main.SCROLL_SPEED);
  this.renderer.render(this.stage);
  requestAnimationFrame(this.update.bind(this));
};
1
2
3
4
5

我们再次在需要的地方使用了this关键字,并且利用了JavaScript的bind()函数来确保更新循环的作用域始终是正确的。

另外,请注意,上面的代码在调用scrollermoveViewportXBy()方法时使用了一个名为SCROLL_SPEED的常量。在此之前,我们只是传递了一个硬编码的值。我们把这个常数加到Main类中。在构造函数后直接添加以下行:




 



  requestAnimationFrame(this.update.bind(this));
}

Main.SCROLL_SPEED = 5;

Main.prototype.update = function() {
1
2
3
4
5
6

好了,保存您的修改。

现在让我们打开index.html并删除我们刚刚在Main类中移动完的旧代码。


 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 


<script>
  function init() {
    stage = new PIXI.Container();
    renderer = PIXI.autoDetectRenderer(
      512,
      384,
      {view:document.getElementById("game-canvas")}
    );

    scroller = new Scroller(stage);

    requestAnimationFrame(update);
  }

  function update() {
    scroller.moveViewportXBy(5);

    renderer.render(stage);

    requestAnimationFrame(update);
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

用一个新的init()函数替换您的代码,该函数只实例化Main类:


 
 
 


<script>
  function init() {
    main = new Main();
  }
</script>
1
2
3
4
5

最后,引入Main.js





 

<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.0.0/pixi.min.js"></script>
<script src="Far.js"></script>
<script src="Mid.js"></script>
<script src="Scroller.js"></script>
<script src="Main.js"></script>
1
2
3
4
5

# 总结

现在一切都变得更干净了,我们只有一个Scroller类,用于管理视差图层。 尽管我们这次的重点不是放在pixi.js上,但您至少现在应该了解扩展Pixi的显示对象类的优点。

# 下节预告

所有这些更改都处于理想的位置,现在我们可以处理更复杂的第三个视差层。 该图层将充当您游戏世界的地图,并由一系列精灵组成,而不是简单的重复纹理。 我们还将介绍pixi.js,涵盖各种小东西,包括精灵表单(sprite sheets,此处翻译可能有误),纹理帧和对象池。

请记住,本系列教程和本系列前一篇教程的源代码都可以在GitHub (opens new window)上找到。

下节教程再见。

lastUpdate: 11/16/2021, 6:11:55 AM