我已经告诉过你,你现在已经拥有了开始制作游戏所需的所有技能。什么?你不相信我?让我来证明给你看!让我们来看看如何做一个简单的物品收集和躲避敌人的游戏--寻宝猎人。(你可以在examples文件夹中找到它。)

使用键盘上的箭头键可帮助探险家找到宝藏并将其带到出口。 六个怪物在地牢壁之间上下移动,如果探险家触碰到怪物,他将变成半透明,并且右上角的生命值会减少。 如果所有的生命值都用光了,会显示“You Lost!”; 如果探险家带着宝藏到达出口,会显示“You Won!”。 尽管它是一个基本的原型,但《寻宝猎人》包含了大型游戏中的大多数元素:纹理图集图形,交互,碰撞以及多个游戏场景。

# 代码结构

打开treasureHunter.html文件,你会发现所有的游戏代码都在一个大文件中。代码结构如下:

//Setup Pixi and load the texture atlas files - call the `setup`
//function when they've loaded

function setup() {
 //Initialize the game sprites, set the game `state` to `play`
 //and start the 'gameLoop'
}

function gameLoop(delta) {
 //Runs the current game `state` in a loop and renders the sprites
}

function play(delta) {
 //All the game logic goes here
}

function end() {
 //All the code that should run at the end of the game
}

//The game's helper functions:
//`keyboard`, `hitTestRectangle`, `contain` and `randomInt`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 在setup函数中初始化游戏

加载纹理图集图像后,setup即会运行。 它仅运行一次,并允许您为游戏执行一次设置任务。 在这里创建和初始化对象,精灵,游戏场景,填充数据数组或解析加载的JSON游戏数据。

这是Treasure Hunter中setup方法的概要:

function setup() {
  //Create the `gameScene` group
  //Create the `door` sprite
  //Create the `player` sprite
  //Create the `treasure` sprite
  //Make the enemies
  //Create the health bar
  //Add some text for the game over message
  //Create a `gameOverScene` group
  //Assign the player's keyboard controllers

  //set the game state to `play`
  state = play;

  //Start the game loop 
  app.ticker.add(delta => gameLoop(delta));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

最后两行代码,state = play;gameLoop()很重要。将gameLoop添加到Pixi的ticker,并在一个连续的循环中调用play函数。

先让我们看看setup函数里做了哪些事情。

# 创建游戏场景

setup函数创建了两个名为gameScenegameOverScene的容器分组,它们都被添加到舞台上。

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

gameOverScene = new Container();
app.stage.addChild(gameOverScene);
1
2
3
4
5

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

尽管gameOverScenesetup中被创建,但是在游戏开始时,不应该被显示出来。所以,我们给它的visible属性设置为false

gameOverScene.visible = false;
1

当游戏结束时,gameOverScenevisible属性将被设置为true,以显示在游戏结束时出现的文本。这件事我们稍后再处理。

# 生成怪物

我们通过循环语句创建了六个怪物。 每个怪物都被赋予一个随机的初始位置和速度。 每个怪物的垂直速度交替乘以1或-1,这就是每个怪物上下来回移动的原因。 我们把怪物都会放进一个叫blobs的数组中。

let numberOfBlobs = 6,
    spacing = 48,
    xOffset = 150,
    speed = 2,
    direction = 1;

//An array to store all the blob monsters
blobs = [];

//Make as many blobs as there are `numberOfBlobs`
for (let i = 0; i < numberOfBlobs; i++) {

  //Make a blob
  let blob = new Sprite(id["blob.png"]);

  //Space each blob horizontally according to the `spacing` value.
  //`xOffset` determines the point from the left of the screen
  //at which the first blob should be added
  let x = spacing * i + xOffset;

  //Give the blob a random `y` position
  let y = randomInt(0, stage.height - blob.height);

  //Set the blob's position
  blob.x = x;
  blob.y = y;

  //Set the blob's vertical velocity. `direction` will be either `1` or
  //`-1`. `1` means the enemy will move down and `-1` means the blob will
  //move up. Multiplying `direction` by `speed` determines the blob's
  //vertical direction
  blob.vy = speed * direction;

  //Reverse the direction for the next blob
  direction *= -1;

  //Push the blob into the `blobs` array
  blobs.push(blob);

  //Add the blob to the `gameScene`
  gameScene.addChild(blob);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

##3 制作生命条

当你玩这个游戏的时候,猎人接触到一个敌人的时候,屏幕右上角的生命条的宽度会减小。这个生命条是怎么做的?它只是两个位置完全相同的重叠矩形:一个黑色矩形在后面,一个红色矩形在前面。我们把它俩放到healthBar分组里。然后将healthBar添加到gameScene并放置在舞台上。

//Create the health bar
healthBar = new PIXI.Container();
healthBar.position.set(stage.width - 170, 4)
gameScene.addChild(healthBar);

//Create the black background rectangle
let innerBar = new PIXI.Graphics();
innerBar.beginFill(0x000000);
innerBar.drawRect(0, 0, 128, 8);
innerBar.endFill();
healthBar.addChild(innerBar);

//Create the front red rectangle
let outerBar = new PIXI.Graphics();
outerBar.beginFill(0xFF3300);
outerBar.drawRect(0, 0, 128, 8);
outerBar.endFill();
healthBar.addChild(outerBar);

healthBar.outer = outerBar;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

您可以看到一个名为outer的属性已添加到healthBar。 它仅引用outerBar(红色矩形),以便以后方便使用。比如,修改它的width,这样做简洁易读。

healthBar.outer = outerBar;
healthBar.outer.width = 30;
1
2

# 制作游戏消息文本(Message Text)

当游戏结束时,会显示“You won!” 或 “You lost!”这样的文字!这是使用一个文本精灵,并将其添加到gameOverScene。因为gameOverScenevisible属性在游戏开始时被设置为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);
1
2
3
4
5
6
7
8
9

# 开始游戏

所有的游戏逻辑和精灵移动代码,都放在了play函数中。我们看下,play函数包含哪些东西:

function play(delta) {
  //Move the explorer and contain it inside the dungeon
  //Move the blob monsters
  //Check for a collision between the blobs and the explorer
  //Check for a collision between the explorer and the treasure
  //Check for a collision between the treasure and the door
  //Decide whether the game has been won or lost
  //Change the game `state` to `end` when the game is finished
}
1
2
3
4
5
6
7
8
9

让我们看看所有这些功能是如何运行的。

# 移动猎人

猎人是用键盘控制的,而实现这一功能的代码与前面学习的键盘控制代码非常相似。keyboard对象修改猎人的速度,play函数根据速度更新猎人的位置。

explorer.x += explorer.vx;
explorer.y += explorer.vy;
1
2

# 移动范围

猎人只能在墙壁内移动,下图的绿色边框展示了猎人的移动范围。

我们已经在自定义函数contain中处理了这件事。

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

contain有两个参数。第一个是你想限制移动范围的精灵。第二个是由x, y, width, height属性的一个对象,代表的是一个矩形区域,精灵只能在这个区域内移动。在本例中,这个对象定义了一个区域,该区域与舞台的偏移量很小。它与地牢墙壁的尺寸相匹配。

这是完成所有这些工作的contain函数。该函数检查精灵是否越过了边界。如果有,则代码将精灵移回该边界。contain函数还返回一个collision变量,该变量的值为“top”、“right”、“bottom”或“left”,这取决于精灵击中边界的哪一侧。(如果精灵没有触碰任何边界,collision的值是undefined。)

function contain(sprite, container) {

  let 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 移动怪物

play函数里,也对怪物做了碰撞检测,使它们在地牢范围内移动。如果碰到了墙壁,那它就会回头,向相反相反方向移动。

blobs.forEach(function(blob) {

  //Move the blob
  blob.y += blob.vy;

  //Check the blob's screen boundaries
  let blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});

  //If the blob hits the top or bottom of the stage, reverse
  //its direction
  if (blobHitsWall === "top" || blobHitsWall === "bottom") {
    blob.vy *= -1;
  }

  //Test for a collision. If any of the enemies are touching
  //the explorer, set `explorerHit` to `true`
  if(hitTestRectangle(explorer, blob)) {
    explorerHit = true;
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在上面的代码中,您可以看到我们是如何使用contain函数的返回值,使怪物在墙壁间往返移动的。blobHitsWall这个变量用于捕获它返回值。

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

将怪物的vy(垂直速度)值乘以-1,就能让他向相反方向移动了。

# 碰撞检测

我们用hitTestRectangle这个方法来检测猎人是否触碰到怪物。

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

如果hitTestRectangle返回true,这意味猎人和怪物发生了碰撞,explorerHit的变量的值被设为true。如果explorerHittrue,则play函数使猎人半透明,并将生命条的宽度减少1像素。

if(explorerHit) {

  //Make the explorer semi-transparent
  explorer.alpha = 0.5;

  //Reduce the width of the health bar's inner rectangle by 1 pixel
  healthBar.outer.width -= 1;

} else {

  //Make the explorer fully opaque (non-transparent) if it hasn't been hit
  explorer.alpha = 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

如果explorerHit的值为false,那么我们把猎人的alpha属性设为1,让他不透明。

play函数中还会检查宝藏和猎人之间的碰撞。如果命中,宝藏treasure的位置将被设置为距猎人有稍微偏移的位置。这使它看起来像是猎人在带着宝藏移动。

让我们看下相关的代码。

if (hitTestRectangle(explorer, treasure)) {
  treasure.x = explorer.x + 8;
  treasure.y = explorer.y + 8;
}
1
2
3
4

# 到达出口并结束游戏

有两种情况结束游戏:一个是,你带着宝藏到达出口;另一个就是,你的生命条为0。

如果你带着宝藏,到达出口,那就赢得了游戏,游戏状态state的值被设为end,然后message的文字被设为You won

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

如果你的生命条为0,那你输掉了游戏。state被设为endmessage的文字被设为You lost

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

这是啥意思呢?

如果你还记得前面的例子,gameLoop每秒执行60次,gameLoop执行,都会调用state方法更新游戏状态。如下所示:

function gameLoop(delta){

  //Update the current game state:
  state(delta);
}
1
2
3
4
5

你也一定记得,我们初始化的时候,给state赋的值是play。现在游戏结束了,我们不需要再执行play了,要替换为end。在一个较大的游戏中,可能会有tileScene这个状态,代表的是游戏等级,例如levelOnelevelTwolevelThree等。

现在让我们看下end函数都做了哪些事情:

function end() {
  gameScene.visible = false;
  gameOverScene.visible = true;
}
1
2
3
4

它只是翻转了游戏场景的可见性。游戏结束时,让gameScene隐藏,让gameOverScene显示。

这是一个非常简单的示例,说明了如何切换游戏状态,但是您可以在游戏中拥有任意数量的游戏状态,并根据需要填充尽可能多的代码。 只需将state的值更改为要在循环中运行的任何函数。

而这正是宝藏猎人的全部! 只需多做一些工作,您就可以将这个简单的原型变成完整的游戏-试试吧!

lastUpdate: 2/3/2023, 2:34:54 AM