我已经告诉过你,你现在已经拥有了开始制作游戏所需的所有技能。什么?你不相信我?让我来证明给你看!让我们来看看如何做一个简单的物品收集和躲避敌人的游戏--寻宝猎人。(你可以在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`
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));
}
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
函数创建了两个名为gameScene
和gameOverScene
的容器分组,它们都被添加到舞台上。
gameScene = new Container();
app.stage.addChild(gameScene);
gameOverScene = new Container();
app.stage.addChild(gameOverScene);
2
3
4
5
所有属于主游戏的精灵都被添加到gameScene
分组。游戏结束时显示的文本将被添加到gameOverScene
分组中。
尽管gameOverScene
在setup
中被创建,但是在游戏开始时,不应该被显示出来。所以,我们给它的visible
属性设置为false
。
gameOverScene.visible = false;
当游戏结束时,gameOverScene
的visible
属性将被设置为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);
}
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;
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;
2
# 制作游戏消息文本(Message Text)
当游戏结束时,会显示“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);
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
}
2
3
4
5
6
7
8
9
让我们看看所有这些功能是如何运行的。
# 移动猎人
猎人是用键盘控制的,而实现这一功能的代码与前面学习的键盘控制代码非常相似。keyboard
对象修改猎人的速度,play
函数根据速度更新猎人的位置。
explorer.x += explorer.vx;
explorer.y += explorer.vy;
2
# 移动范围
猎人只能在墙壁内移动,下图的绿色边框展示了猎人的移动范围。
我们已经在自定义函数contain
中处理了这件事。
contain(explorer, {x: 28, y: 10, width: 488, height: 480});
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;
}
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;
}
});
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});
将怪物的vy
(垂直速度)值乘以-1,就能让他向相反方向移动了。
# 碰撞检测
我们用hitTestRectangle
这个方法来检测猎人是否触碰到怪物。
if(hitTestRectangle(explorer, blob)) {
explorerHit = true;
}
2
3
如果hitTestRectangle
返回true
,这意味猎人和怪物发生了碰撞,explorerHit
的变量的值被设为true
。如果explorerHit
为true
,则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;
}
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;
}
2
3
4
# 到达出口并结束游戏
有两种情况结束游戏:一个是,你带着宝藏到达出口;另一个就是,你的生命条为0。
如果你带着宝藏,到达出口,那就赢得了游戏,游戏状态state
的值被设为end
,然后message
的文字被设为You won
。
if (hitTestRectangle(treasure, door)) {
state = end;
message.text = "You won!";
}
2
3
4
如果你的生命条为0,那你输掉了游戏。state
被设为end
,message
的文字被设为You lost
。
if (healthBar.outer.width < 0) {
state = end;
message.text = "You lost!";
}
2
3
4
这是啥意思呢?
如果你还记得前面的例子,gameLoop
每秒执行60次,gameLoop
执行,都会调用state
方法更新游戏状态。如下所示:
function gameLoop(delta){
//Update the current game state:
state(delta);
}
2
3
4
5
你也一定记得,我们初始化的时候,给state
赋的值是play
。现在游戏结束了,我们不需要再执行play
了,要替换为end
。在一个较大的游戏中,可能会有tileScene
这个状态,代表的是游戏等级,例如levelOne
、levelTwo
、levelThree
等。
现在让我们看下end
函数都做了哪些事情:
function end() {
gameScene.visible = false;
gameOverScene.visible = true;
}
2
3
4
它只是翻转了游戏场景的可见性。游戏结束时,让gameScene
隐藏,让gameOverScene
显示。
这是一个非常简单的示例,说明了如何切换游戏状态,但是您可以在游戏中拥有任意数量的游戏状态,并根据需要填充尽可能多的代码。 只需将state
的值更改为要在循环中运行的任何函数。
而这正是宝藏猎人的全部! 只需多做一些工作,您就可以将这个简单的原型变成完整的游戏-试试吧!