Hexi

hexi

Hexi是一种有趣而简单的方法,可以用纯JavaScript代码制作HTML5游戏或任何其他类型的交互媒体。看看开始的特性列表和示例文件夹。继续滚动,你会找到一个完整的快速入门指南和初学者指南。如果你以前从来没有做过游戏,教程是最好的开始。

Hexi有什么?您可以使用streamlined API获得WebGL呈现的所有功能,该API允许您以极简的声明性的方式编写代码。它使得编写一款游戏像写诗或画一样简单有趣。试一试!如果您需要任何帮助或有任何问题,请在此存储库的问题中发布一些内容。问题页面是Hexi友好的聊天室——不要害怕寻求帮助:)

使用Hexi: hexi.min.js只需要从这个存储库中获取一个文件。这是所有!用<script>标签链接到您的HTML文档,然后开始!Hexi是在最新版本的JavaScript (ES6/7, 2005 /6)中从头开始编写的,但是它被编译成ES5(使用Babel),这样它就可以在任何地方运行。在开始使用河西之前,你需要知道什么?您应该对HTML和JavaScript有合理的理解。你不必是一个专家,只是一个雄心勃勃的初学者和一个渴望学习的人。如果你不知道HTML和JavaScript,最好的开始学习它的地方是这本书:

基于HTML5和JavaScript的基础游戏设计

Pixi教程

好吧,明白了吗?您知道JavaScript变量、函数、数组和对象是什么以及如何使用它们吗?您知道JSON数据文件是什么吗?你使用过Canvas绘图API吗?然后你就可以开始使用Hexi了!

当然,河西是完全免费使用:为任何事,永远!它的作者是加拿大(多伦多,哈密顿),印度(Kullu Valley, Ladakh),尼泊尔(加德满都,Pokhara, Annapurna大本营),泰国(Ko Phangan, Ko Tao)和南非(开普敦),这是15年来对游戏设计的API可用性研究的结果。”Hexi” 这个名字来源于”Hex” + “Pixi”。它绝对没有其他意义

特性

以下是Hexi的核心功能列表:

  • 你需要的所有最重要的精灵:矩形、圆圈、线条、文本、图像精灵和动画“MovieClip”样式精灵。您可以使用一行代码来创建这些精灵中的任何一个。您还可以创建自己的定制精灵类型。

  • 完整的场景图,包含嵌套的子-父层次结构(包括stageaddChild/removeChild方法)、本地和全局坐标、深度层和旋转支点。

  • 将精灵分组在一起制作游戏场景。

  • 具有用户可定义的fps和完全可定制和完全可删除的简单游戏状态管理器的游戏循环。暂停恢复在任何时候游戏循环。

  • Tileset (spritesheet)支持使用framefilmstrip方法来使用Tileset框架制作精灵。

  • 内置纹理图集支持流行的纹理包装格式。

  • 精灵的关键帧动画和状态管理器。使用show显示精灵的图像状态。使用playAnimation来播放一系列的帧(如果您愿意,可以在循环中)。使用show显示特定的帧号。使用fps设置精灵动画的帧速率与游戏的帧速率无关。

  • 交互按钮精灵有up,overdown的状态

  • 可以将任何精灵设置为交互以接收鼠标和触摸操作。为按钮和交互精灵提供直观的press, release, over, outtap方法

  • 易于使用的键盘键绑定。使用keyboard方法轻松定义您自己的

  • 一个内置的通用指针,可以同时使用鼠标和触摸。分配您自己的自定义press, releasetap方法或使用任何指针的内置属性:isUpisDowntapxy。(它也适用于等距地图!)

  • 使用putTop, putRight, putBottom, putLeftputCenter,方便地放置与其他精灵相对的位置。使用flowRightflowLeftflowUpflow将精灵水平或垂直对齐。

  • 用于预加载图像、字体、声音和JSON数据文件的通用资产加载程序。支持所有流行的文件格式。您可以在任何时候将新资产加载到游戏中。

  • 可选的加载状态,允许您在加载资产时运行操作。可以使用load状态添加加载进度条。

  • 基于pixi的快速聚焦渲染引擎。如果Pixi能做到,那么Hexi也能做到! Hexi只是位于Pixi之上的一层薄薄的代码。您可以随时访问全局PIXI对象,如果您愿意,可以直接编写纯PIXI代码。Hexi包括最新稳定版本的Pixi v3.0自动绑定。

  • 一个复杂的游戏循环,使用一个固定的时间步长,具有可变渲染和精灵插值。这意味着你可以在任何帧中获得光滑的精灵动画。

  • 一个紧凑而强大的“俳句”风格的API,其核心是浅显的、可组合的组件。多做点事情,少写点代码。

  • 使用内置的WebAudio API声音管理器导入和播放声音。用playpausestoprestartplayFromfadeInfadeOut方法控制声音。改变声音的volumepan

  • 使用通用的soundEffect方法从纯代码生成您自己的自定义声音效果。

  • 使用shake摇动精灵或场景。

  • 精灵和场景转换的补间功能: slidefadeInfadeOutpulsebreathewobblestrobe和一些有用的低级补间方法,可以帮助您创建自定义补间。

  • 按照walkPathwalkCurve的连接路径点序列创建一个精灵。

  • 一些有用的方便函数:followEase, followConstant, angle, distance, rotateAroundSprite, rotateAroundPoint, wait, randomInt, randomFloat, containsoutsideBounds

  • 一种快速、通用的hit方法来处理所有类型的精灵的碰撞测试和反应(拦截和反弹)。对所有的东西都使用一种碰撞方法:矩形、圆形、点和精灵的数组。简单!

  • 一套轻量级的、低级的2D几何碰撞方法。

  • 游戏资产的加载进度条。

  • 让精灵用shoot拍摄东西

  • 很容易用grid绘制网格中的精灵

  • 使用tilingSprite轻松创建无缝滚动背景

  • createParticles函数,用于为游戏创建各种粒子效果。使用particleEmitter函数来创建一个恒定的粒子流

  • 使用scaleToWindow使游戏自动缩放到它的最大尺寸,并使其与浏览器窗口内的最佳匹配

  • 使用makeTiledWorld对编辑器的支持。在平铺编辑器中设计游戏,并直接访问游戏代码中的所有精灵、层和对象。这是一个非常有趣,快速和容易的方法来制作游戏。

  • 一个通用的hitTestTile方法,用于处理基于瓷砖的游戏所需的所有碰撞检查。如果您愿意,您可以将它与任何二维几何碰撞方法结合使用,以优化大相位/窄相位碰撞检查。

  • 使用updateMap保持基于瓷砖的世界地图数据数组与移动精灵最新

  • 创建一个在滚动游戏世界中跟随精灵的worldCamera

  • 一个lineOfSight函数,告诉您一个精灵对另一个精灵是否可见

  • 与HTML和CSS元素的无缝集成,以创建丰富的用户界面。使用Hexi也适用于Angular, ReactElm!

  • 一套完整的工具,易于创建等距游戏世界,包括:等距鼠标/触摸指针,使用hitTestIsoTile的等距瓷砖碰撞,和使用makeIsoTiledWorld的全平铺编辑器等距地图支持。

  • shortestPath函数,用于通过类似迷宫这样的基于瓷砖的环境进行A-star寻路,tileBasedLineOfSight函数告诉你迷宫游戏环境中的精灵是否可以看到彼此。

  • 是的,由于Pixi渲染器提供的可访问属性(yay Pixi!), Hexi应用程序符合W3C的可访问性指南

Hexi的模块

Hexi包含了一个有用的模块集合,您可以使用这些模块的任何属性或方法在您的高级Hexi代码中。

  • Pixi:世界上最快的2D WebGL和canvas渲染器

  • Bump:一套完整的2D游戏碰撞功能。

  • Tink:拖放、按钮、通用指针和其他有用的交互性工具。

  • Charm: 简单易用的Pixi精灵动画效果

  • Dust:用于制造爆炸、火焰和魔法的粒子效应

  • Sprite Utilities:更简单、更直观的方法来创建和使用Pixi精灵,并添加状态机和动画播放器

  • Game Utilities: 为游戏收集的有用方法.

  • Tile Utilities: 一组有用的方法,用于制作以Tiled Editor为基础的游戏世界。包括一套完整的等距地图实用程序。

  • Sound.js: 用于加载、控制和生成声音和音乐效果的微型库。为游戏添加声音所需要的一切。

  • Smoothie: 超光滑的精灵动画使用真正的三角时间插值。它还允许您指定游戏或应用程序运行的fps(每秒帧数),并将精灵呈现循环与应用程序逻辑循环完全分开。

阅读每个模块的代码库中的文档,了解如何使用它们,以及它们是如何工作的。因为它们都内置在Hexi中,你不需要自己安装它们——它们可以直接使用

Hexi允许您作为顶级对象访问这些模块方法和属性。例如,如果您想从Bump collision module访问hit方法,您可以这样做:

1
g.hit(spriteOne, spriteTwo);

但如果需要的话,也可以直接访问Bump模块,比如:

1
g.bump.hit(spriteOne, spriteTwo);

(假设你的Hexi实例被称为g);

只需使用lowerCamelCase引用模块名。这意味着您可以使用smoothie和Sprite实用程序模块作为spriteUtilities来访问Smoothie模块。

本公约有两个例外。您可以直接访问Pixi全局对象作为Pixi。此外,Sound.js模块中的函数也只能作为顶级全局对象访问。这样做是为了简化这些模块与Hexi集成的方式,并保持尽可能广泛的跨平台兼容性。

如果您是一名开发人员,并且希望为Hexi做贡献,最好的方法是为这些模块提供新的和改进的特性。或者,如果你真的很有野心,向Hexi开发团队提出一个新的模块(在这个repo的问题中,也许我们会把它添加到Hexi的核心中!)

Hexi 快速启动

要快速开始使用Hexi,请查看Hexi示例文件夹中的Quick start项目。这里是HTML容器页面,这里是JavaScript源文件。源代码得到了完整的注释并解释了所有的工作原理,因此,如果您想这样做,您可以直接跳转到该文件并阅读它。(您将在bin文件夹中找到已编译的ES5 JavaScript文件版本。)

快速启动项目是对所有Hexi的主要特性的访问,您可以将它用作创建您自己的新Hexi应用程序的模板。点击这里尝试一个工作例子:

quickstart

您将首先看到一个加载栏,它显示正在加载的文件(声音和图像)的百分比。然后,您将看到一个旋转消息,要求您在屏幕上单击以创建猫。当音乐在背景中播放时,只要你点击鼠标,猫就会出现在屏幕上。(噢,对不起!我忘记警告你音乐了!文本字段会旋转并计算您创建的猫的数量。这些猫自己在屏幕上来回移动和反弹,同时在大小上进行缩放,并摆动它们的透明度。这里有一个你将看到的例子:

quickstart

为什么是猫?因为

如果您知道这个快速启动应用程序是如何开发的,那么您就可以快速地使用Hexi来提高效率了——让我们来看看吧!

(注意: 如果你是游戏编程新手,觉得需要一个更温和、更有条理的Hexi入门,请查看前面的教程部分。你将学习如何从头开始制作3个完整的游戏,并且每一个游戏逐渐建立在你在之前的游戏中学到的技能上。

HTML容器

惟一需要开始使用Hexi的文件是hexi.min.js。它有一个非常简单的“安装”:只需将它链接到带有<script>标记的HTML页面。然后链接包含游戏或应用程序代码的主JavaScript文件。下面是一个典型的Hexi HTML容器页面:

1
2
3
4
5
6
7
<!doctype html>
<meta charset="utf-8">
<title>Hexi</title>
<body>
<script src="hexi.min.js"></script>
<script src="main.js"></script>
</body>

当然,您可以加载游戏所需的所有外部脚本文件。

如果您需要更精细的控制,您可以使用三个独立的文件来加载河西:Pixi渲染器、Hexi模块和Hexi core.js文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!doctype html>
<meta charset="utf-8">
<title>Hexi</title>
<body>

<!-- Pixi renderer, Hexi's modules, and Hexi's core -->
<script src="pixi.js"></script>
<script src="modules.js"></script>
<script src="core.js"></script>

<!-- Main application file -->
<script src="bin/quickStart.js"></script>
</body>

这样做的一个好处是,它允许您使用自己的自定义Pixi构建,或者您希望使用的特定版本的Pixi来交换Hexi的内部版本。或者你对Hexi的模块做了一些疯狂的修改,你想尝试一下。但通常,你可能永远都不需要这样做。

Hexi 架构

所有的乐趣都发生在您的主JavaScript文件中。Hexi应用程序有一个非常简单但灵活的架构,您可以扩展到任何您需要的大小。小游戏用几百行代码或大游戏用几百个文件-河西可以做到!这是典型的Hexi应用的结构:

  1. 启动Hexi
  2. load函数,在文件加载时运行
  3. setup函数,初始化你的游戏对象,变量和精灵。
  4. play函数,它是在循环中运行的游戏或应用程序逻辑。

这是真实代码的样子:

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
//1. Setting up and starting Hexi

//An array of files you want to load
let thingsToLoad = ["anyFiles", "youWant", "toLoad"];

//Initialize and start Hexi
let g = hexi(canvasWidth, canvasHeight, setup, thingsToLoad, load);
g.start();

//2. The `load` function that will run while your files are loading

function load(){

//Display an optional loading bar
g.loadingBar();
}

//3. The `setup` function, which initializes your game objects, variables and sprites

function setup() {

//Create your game objects here

//Set the game state to `play` to start the game loop
g.state = play;
}

//4. The `play` function, which is your game or application logic that runs in a loop

function play(){
//This is your game loop, where you can move sprites and add your
//game logic
}

创建任何类型的游戏或应用程序都需要这个简单的模型。您可以使用它作为您自己的项目的启动模板,同样的基本模型可以扩展到任何大小。

让我们了解如何使用这个体系结构模型来构建快速启动应用程序。

建立和启动Hexi

首先,创建一个数组,列出要加载的所有文件。快速启动项目加载一个图像文件、一个字体文件和一个音乐文件。

1
2
3
4
5
let thingsToLoad = [
"images/cat.png",
"fonts/puzzler.otf",
"sounds/music.wav"
];

如果没有要加载的文件,请跳过这一步。

接下来,用hexi函数初始化Hexi。下面介绍如何初始化屏幕大小为512x512像素的Hexi应用程序。它告诉Hexi在thingsToLoad数组中加载文件,在加载时运行一个名为load的函数,然后在一切准备就绪时运行一个名为setup的函数。

1
let g = hexi(512, 512, setup, thingsToLoad, load);

现在,您可以通过一个名为g的对象访问应用程序中的Hexi实例(不过,您可以给它取任何名称。我喜欢用“g”,因为它代表“game”,而且打字很短。

hexi函数有5个参数,尽管只需要前3个参数。

  1. 画布宽度
  2. 画布高度
  3. setup函数
  4. 你在上面定义的thingsToLoad数组。可选
  5. load函数。可选

如果您跳过最后两个参数,Hexi将跳过加载过程并直接跳转到setup函数。

可选地设置游戏逻辑循环应该运行的每秒帧数。(精灵将被独立地呈现,并进行插值,达到60 fps)如果不设置fps, Hexi将默认为60 fps。

1
g.fps = 30;

设置一个低于60的fps会给你带来更多的性能开销,你的游戏也会看起来很棒。

您还可以选择添加边框并设置背景颜色。

1
2
g.border = "2px red dashed";
g.backgroundColor = 0x000000;

如果你想要缩放和对齐游戏屏幕到最大的浏览器窗口大小,你可以使用scaleToWindow方法。

1
g.scaleToWindow();

最后,调用start方法使Hexi运行

1
g.start();

这是很重要的!如果不调用start方法,Hexi将不会启动!

load函数,在加载时运行

如果您在初始化时提供了一个名为load的函数,您可以显示一个加载栏并加载进度信息。只需创建一个名为load的函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
function load(){

//显示当前正在加载的文件
console.log(`loading: ${g.loadingFile}`);

//显示当前加载的文件的百分比
console.log(`progress: ${g.loadingProgress}`);

//添加可选的加载条
g.loadingBar();
}

setup函数,初始化并创建游戏对象

现在您已经启动了Hexi,并加载了所有的文件,您可以开始制作东西了!这在setup函数中发生。如果您有任何对象或变量要跨多个函数使用,请在setup函数之外定义它们,如下所示:

1
2
3
4
5
6
7
8
//These things will be used in more than one function
let cats, message;

//Use the `setup` function to create things
function setup(){

//... create things here! ...
}

让我们看看安装函数中的代码是如何工作的。我们要做猫——很多的猫!-所以创建一个叫cats分组来把它们放在一起是很有用的。

1
cats = g.group();

在快速启动项目中,你可以用鼠标(或触摸)轻击屏幕来制作一个新的猫。所以,我们需要一个能为我们产生新的猫精灵的函数。(精灵是交互图形,你可以在屏幕上进行动画和移动。)Hexi允许您使用sprite方法创建一个新的精灵。只要为精灵提供您想要使用的文件名。应该使用addChild方法将创建的每个新的cat精灵定位并添加到cats组中。我们还希望猫用呼吸法使它的鳞片动起来,用breathe方法使它的透明度动起来。一个叫做makeCats的函数可以做到这一切。makeCats有两个参数:x和y的位置,相对于屏幕左上角,你希望猫出现的位置。

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
let makeCat = (x, y) => {

//Create the cat sprite. Supply the `sprite` method with
//the name of the loaded image that should be displayed
let cat = g.sprite("images/cat.png");

//Set the cat's position
cat.setPosition(x, y);

//You can alternatively set the position my modifying the sprite's `x` and
//`y` properties directly, like this
//cat.x = x;
//cat.y = y;

//Add some optional tween animation effects from the Hexi's
//built-in tween library (called Charm). `breathe` makes the
//sprite scale in and out. `pulse` oscillates its transparency
g.breathe(cat, 2, 2, 20);
g.pulse(cat, 10, 0.5);

//Set the cat's velocity to a random number between -10 and 10
cat.vx = g.randomInt(-10, 10);
cat.vy = g.randomInt(-10, 10);

//Add the cat to the `cats` group
cats.addChild(cat);
};

(在示例文件夹中的tweening示例中,您可以了解更多关于breathepulse方法如何工作以使cat具有动画效果的信息。)

我们还需要创建一个文本精灵来显示”Tap for cats!”我们可以使用Hexi的text方法。

1
message = g.text("Tap for cats!", "38px puzzler", "red");

text方法的参数是您想要显示的文本、字体大小和家族以及颜色(您可以使用任何HTML/CSS颜色字符串值、RGBA或HSLA值)。

使用Hexi的putCenter方法将文本居中

stage是什么?它是所有Hexi精灵在第一次创建时都属于的根容器。

您还可以使用putLeftputRightputTopputBottom方法来帮助您对齐相对于其他对象的对象。这些方法的可选的第2和第3个参数定义x和y偏移量,这有助于您微调定位。

因为我们希望文本消息围绕中心点旋转,所以我们必须将其pivotXpivotY值设置为0.5。

1
2
message.pivotX = 0.5;
message.pivotY = 0.5;

0.5表示“精灵的中心”

您也可以使用这种替代语法来设置轴心点:

1
message.setPivot(0.5, 0.5);

我们需要一些方法来告诉Hexi,当屏幕被点击或点击时,创建一个新的猫。我们还希望文本信息告诉我们当前屏幕上有多少只猫。Hexi有一个内建的指针对象和一个tap方法,我们可以通过编程来帮助我们做到这一点

1
2
3
4
5
6
7
8
g.pointer.tap = () => {

//Supply `makeCat` with the pointer's `x` and `y` coordinates.
makeCat(g.pointer.x, g.pointer.y);

//Make the `message.content` display the number of cats
message.content = `${cats.children.length}`;
};

我们还想要加载的音乐文件开始播放。我们可以使用Hexi的声音方法访问音乐声音对象。使用声音对象的循环方法使其连续循环,并使用play立即开始播放。

1
2
3
let music = g.sound("sounds/music.wav");
music.loop = true;
music.play();

我们现在已经把一切都安排好了!这意味着我们已经完成了应用程序的设置状态,现在可以切换到play。如何做到这一点:

1
g.state = play;

play状态是一个在循环中运行的函数,它是我们所有应用程序逻辑的所在。下面我们来看看它是如何工作的。

play函数:循环应用程序逻辑

Hexi应用程序中最后需要的就是一个play函数。

1
2
3
4
function play() {

//All this code will run in a loop
}

play函数在一个连续循环中被调用,无论您设置什么fps(每秒帧数)值。这是您的游戏逻辑循环。(渲染循环将由Hexi在后台运行,在您的系统可以处理的最大fps中。)您可以使用pause方法随时暂停Hexi的游戏循环,并使用resume方法重新启动它。(查看Flappy Fairy 项目,了解如何使用pauseresume来管理状态复杂的应用程序。)

Quick Start项目的play功能只做两件事:它使文本旋转,并在屏幕上移动和反弹猫。这是整个play函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function play() {

//Rotate the text
message.rotation += 0.1;

//Loop through all of the cats to make them move and bounce off the
//edges of the stage
cats.children.forEach(cat => {

//让猫从屏幕边缘反弹
let collision = g.contain(cat, g.stage, true);

//Move the cat
g.move(cat);
});
}

这是所有!与我们在设置函数中所做的所有工作相比,play函数实际上什么都不做!但是它是如何工作的呢?

它首先通过将消息文本精灵的rotation(旋转)属性更新0.1弧度,使文本围绕中心旋转。

1
message.rotation += 0.1;

由于这个新的旋转值被应用到一个连续循环内的旧旋转值中,它逐渐增加了值并使文本旋转。

接下来,代码将遍历cat组的子数组中的所有精灵。

1
2
3
cats.children.forEach(cat => {
//Loop through each `cat` sprite in the `chidren` array
});

所有的Hexi组都有一个名为children的数组,它告诉你它们包含哪些精灵。无论何时使用addChild方法向组添加精灵,精灵都会被添加到组的子数组中。Hexi的根容器,叫做stage,它还有一个子数组,它包含了你的Hexi应用程序中的所有精灵和组。即使sprite对象也有一个子数组,这意味着您可以使用addChild与其他精灵分组来创建复杂的游戏对象。

当代码循环遍历每只猫时,它首先检查猫是否触碰了屏幕的边缘,如果触碰了,它会朝相反的方向反弹。Hexi contain方法可以帮助我们做到这一点。

1
let collision = g.contain(cat, g.stage, true);

将第三个参数设置为true是导致猫反弹的原因

猫在move方法的帮助下在屏幕上移动。

1
g.move(cat);

move方法通过其vx和vy速度值更新精灵的位置。(所有的Hexi精灵都有vx和vy值,初始化为0)。通过向move提供一个由逗号分隔的精灵列表,您可以一次移动多个精灵。您甚至可以为它提供包含您想要移动的所有精灵的数组。以下是移动实际上在幕后所做的:

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

这就是一切!这就是关于Quick Start应用程序的所有知识,以及关于Hexi所需的几乎所有知识!

把它进一步

使用这个基本的Hexi架构,您可以创建任何东西。只需将Hexi的状态属性设置为任何其他函数,以切换应用程序的行为。方法如下:

1
g.state = anyStateFunction;

状态只是普通的旧JavaScript函数!很简单!

根据需要编写尽可能多的状态函数。如果是一个小项目,您可以将所有这些功能保存在一个文件中。但是,对于一个大项目,在需要时从外部JS文件加载函数。使用您喜欢的任何模块系统,如ES6模块、CommonJS、AMD或旧的HTML <script>标签。这个简单的架构模型可以扩展到任何大小,并且是您需要知道的唯一的体系结构模型。保持简单,保持快乐!

现在您已经大致了解了Hexi是如何工作的,请阅读教程来深入了解细节。

教程

我们要做的第一个游戏是一个简单的对象收集和敌人躲避游戏叫做寻宝猎人。在web浏览器中打开文件01_treasurehunter.html。(您可以在Hexi的教程文件夹中找到它,您需要在webserver中运行它)。如果您不想费事设置一个webserver,请使用一个文本编辑器,比如方括号,它将自动为您启动一个方括号(请参阅方括号的文档)。

寻宝猎人

游戏入口

(按照上面的链接来玩这个游戏。)使用键盘移动资源管理器(蓝色方块),收集宝藏(黄色方块),避开怪物(红色方块),到达出口(绿色方块)。是的,你现在必须发挥你的想象力。

以下是完整的JavaScript源代码:

!寻宝猎人源码

不要被它表面上的简单所迷惑。寻宝者包含了游戏所需的一切:

  • 交互性
  • 碰撞
  • 精灵
  • 游戏循环
  • 场景
  • 游戏逻辑
  • “Juice” (以声音的形式)

(什么果汁?请观看本视频阅读本文,以了解这一游戏设计要素。

如果你能做一个简单的游戏,如寻宝游戏,你几乎可以做任何其他类型的游戏。是的,真的!从寻宝人到Skyrim或塞尔达只是很多小步骤的事情;添加更多的细节。你想要加多少细节取决于你自己。

在本教程的第一阶段,您将了解如何制作基本的寻宝游戏,然后我们将添加一些有趣的特性,如图像和字符动画,这将使您全面了解Hexi如何工作。

如果你是一个经验丰富的游戏程序员和快速的自我启动者,你可能会发现在Hexi的例子文件夹里的代码是一个更有效率的开始学习的地方——看看它。示例文件夹中的完整注释代码还详细说明了这些教程中没有涉及的特性的具体和高级用法。当您完成这些教程的学习后,这些示例将带您进入下一个阶段。

设置HTML容器页面

在开始使用JavaScript编程之前,需要设置一个最小的HTML容器页面。HTML页面加载hexi.min.js是唯一需要使用Hexi所有特性的文件。它还装载了treasureHunter.js文件,即包含所有游戏代码的JavaScript文件。

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<meta charset="utf-8">
<title>Treasure hunter</title>
<body>
<!-- Hexi -->
<script src="../bin/hexi.min.js"></script>

<!-- Main application file -->
<script src="bin/treasureHunter.js"></script>
</body>

这是有效的HTML5文档所需的HTML代码的最小数量

文件路径在您的系统上可能不同,这取决于您如何设置项目文件结构。

初始化Hexi

下一步是编写一些JavaScript代码,根据指定的一些参数初始化和启动Hexi。下面这段代码初始化了一个屏幕大小为512×512像素的游戏。它还从sounds文件夹预先加载chimes.wav声音文件。

1
2
3
4
5
6
7
8
//Initialize Hexi and load the chimes sound file
let g = hexi(512, 512, setup, ["sounds/chimes.wav"]);

//Scale the game canvas to the maximum size in the browser
g.scaleToWindow();

//Start the Hexi engine.
g.start();

你可以看到,hexi函数的结果被分配给一个叫做g的变量。

1
let g = hexi(//...

现在,无论何时你想在游戏中使用Hexi的任何自定义方法或对象,只要在其前面加上g。(g很好,很短,很容易记住;g = “game”)

在本例中,Hexi创建一个大小为512×512像素的画布元素。由前两个参数指定:

1
512, 512, setup,

第三个参数setup意味着只要Hexi初始化,它就应该在你的游戏代码中查找并运行一个叫做setup的函数。在setup函数中,任何代码都完全取决于您,您将很快看到如何使用它来初始化一个游戏。(你不必调用这个函数setup,你可以使用任何你喜欢的名字)

Hexi允许你用一个可选的第4个参数预加载游戏资产,它是一个文件名数组。在第一个示例中,您只需要预加载一个文件:chimes.wav,您可以看到将chimes.wav的完整文件路径作为一个字符串在初始化数组中列出:

1
["sounds/chimes.wav"]

您可以在这里列出任意数量的游戏资产,包括图像、字体和JSON文件。在运行任何游戏代码之前,Hexi将为您加载所有这些资产。

Hexi实现了Pixi的超级资源加载器。您可以直接通过Hexi的loader属性访问加载器,您可以通过resources属性访问资源。或者,直接使用PIXI.loader,如果你想。您可以在这里了解更多关于Pixi的加载程序是如何工作的

我们希望游戏画布能够扩展到浏览器窗口的最大大小,以便显示尽可能大的内容。我们可以使用一个叫做scaleToWindow的有用方法来为我们做这个。

1
g.scaleToWindow();

scaleToWindow将会在你的游戏中找到最适合的。长、宽的游戏屏幕垂直居中。高或方的屏幕水平居中。如果您想指定您自己的浏览器背景颜色来边框游戏,请提供scaleToWindow的参数,如下所示:

1
g.scaleToWindow("seaGreen");

最后你需要做的是调用河西的start方法

1
g.start();

这是打开Hexi引擎的开关

定义你的全局变量

在Hexi开始后,声明游戏函数需要使用的所有变量

1
2
let dungeon, player, treasure, enemies, chimes, exit,
healthBar, message, gameScene, gameOverScene;

因为它们不包含在函数中,所以这些变量是“全局的”,因为你可以在所有的游戏函数中使用它们。(它们不一定是“全局的”,因为它们驻留在全局JavaScript名称空间中。如果您想确保它们不存在,那么将所有JavaScript代码封装在一个封闭的函数中,以将其与全局空间隔离开来。或者,如果您想用一种奇特的方式,使用JavaScript ES6/2015模块来执行本地范围。

用setup函数初始化游戏

只要Hexi启动,它就会在你的游戏代码中查找并运行一个叫setup的函数(或者你想给这个函数起的其他名字)。setup函数只运行一次,允许您为您的游戏执行一次设置任务。它是创建和初始化对象、创建精灵、游戏场景、填充数据数组或解析加载的JSON游戏数据的好地方。

这是《寻宝猎人》中setup函数的简要鸟瞰视图,以及它执行的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function setup() {
//Create the `chimes` sound object
//Create the `gameScene` group
//Create the `exit` 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`
g.state = play;
}

最后一行代码g.state = play可能是最重要的,因为它启动了play函数。play函数在一个循环中运行所有的游戏逻辑。但是在我们看它如何工作之前,让我们看看setup函数中的特定代码是做什么的。

创建chimes声音对象

从上面的代码中可以看到,我们在游戏中预装了一个名为chimes.wav的声音文件。在你可以在游戏中使用它之前,你必须使用Hexi的sound方法来引用它,比如:

1
chimes = g.sound("sounds/chimes.wav");

创建游戏场景

Hexi有一个有用的方法叫做group,它可以让你把游戏对象分组在一起,这样你就可以把它们作为一个单元来使用。组用于将称为 精灵 的特殊对象分组(您将在下一节中了解这些对象)。但它们也被用来制作游戏场景。

《寻宝猎人》使用了两个游戏场景:游戏主游戏gameScene和游戏结束时显示的gameOverScene。以下是使用group方法制作gameScene的方式:

1
gameScene = g.group();

创建组之后,可以使用addChild方法向gameScene添加精灵(游戏对象)。

1
gameScene.addChild(anySprite);

或者,您可以使用add方法一次添加多个精灵,如下所示:

1
gameScene.add(spriteOne, spriteTwo, spriteThree);

或者,如果你喜欢,你可以在你完成所有的精灵之后创建游戏场景,然后用一行代码将所有的精灵分组,如下所示:

1
gameScene = g.group(spriteOne, spriteTwp, spriteThree);

在前面的示例中,您将看到一些如何向组添加精灵的不同示例。

但是什么是精灵,你是怎么做的呢?

制作精灵

精灵是任何游戏中最重要的元素。精灵只是你可以用特殊属性控制的图形(形状或图像)。你能在游戏中看到的一切,比如游戏角色、对象和背景,都是精灵。Hexi允许你制作5种基本的精灵:矩形、圆形、直线、文本和精灵(基于图像的精灵)。你几乎可以用这些基本的精灵类型制作任何2D动作游戏。(如果还不够,还可以定义自己的自定义精灵类型。)第一个版本的宝藏猎人只使用矩形精灵。你可以做一个这样的矩形精灵:

1
2
3
4
5
6
7
8
9
let box = g.rectangle(
widthInPixels,
heightInPixels,
"fillColor",
"strokeColor",
lineWidth,
xPosition,
yPosition
);

你可以用Hexi circle方法做一个圆形精灵:

1
2
3
4
5
6
7
8
let ball = g.circle(
diameterInPixels,
"fillColor",
"strokeColor",
lineWidth,
xPosition,
yPosition
);

仅仅使用矩形和圆形精灵来设计一个新游戏的原型通常很有用,因为这样可以帮助你以一种纯粹的、基本的方式专注于游戏的机制。这就是《寻宝猎人》的第一个版本。下面是创建出口、玩家和宝物精灵的setup函数的代码。

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
//The exit door
exit = g.rectangle(48, 48, "green");
exit.x = 8;
exit.y = 8;
gameScene.addChild(exit);

//The player sprite
player = g.rectangle(32, 32, "blue");
player.x = 68;
player.y = g.canvas.height / 2 - player.halfHeight;
gameScene.addChild(player);

//Create the treasure
treasure = g.rectangle(16, 16, "gold");

//Position it next to the left edge of the canvas
treasure.x = g.canvas.width - treasure.width - 10;
treasure.y = g.canvas.height / 2 - treasure.halfHeight;

//Alternatively, you could use Ga's built in convience method
//called `putCenter` to postion the sprite like this:
//g.stage.putCenter(treasure, 208, 0);

//在宝藏上创建一个“pickedUp”属性,帮助我们弄清楚这个宝藏是否被玩家捡走了
treasure.pickedUp = false;

//Add the treasure to the `gameScene`
gameScene.addChild(treasure);

注意,在创建每个精灵之后,都使用addChild将其添加到gameScene中。以下是上述代码产生的结果:

v1

让我们进一步了解这些精灵是如何定位在画布上的。

定位精灵

所有精灵都有x和y属性,您可以使用它们来精确地定位画布上的精灵。x和y值指的是相对于画布左上角的精灵像素坐标。左上角的x和y值为0。这意味着你给精灵赋值的任何正的x和y值会将它们与那个角点的位置分别放在左边(x)和下面(y)。例如,这是位置在出口门(绿色方块)的代码。

1
2
exit.x = 8;
exit.y = 8;

您可以看到,这段代码将门设置为右8像素,在画布左上角以下8像素。正x值将精灵定位在画布左边缘的右边。正y值将它们放置在画布的上边缘以下。

精灵也有widthheight属性,以像素为单位告诉你它们的宽度和高度。如果你想知道精灵的一半宽度或一半高度是多少,可以使用halfWidthhalfHeight

Hexi也有一些方便的方法,帮助您快速定位精灵相对于其他精灵的位置:putTopputRightputBottomputLeftputCenter。例如,下面是位于宝藏精灵(黄金盒)上面的代码行。代码将宝藏放置在画布右边缘左边的26像素处,并将其垂直居中。

1
2
treasure.x = g.canvas.width - treasure.width - 10;
treasure.y = g.canvas.height / 2 - treasure.halfHeight;

这是一大堆复杂的定位代码。相反,您可以使用Hexi的内置putCenter方法来实现同样的效果:

1
g.stage.putCenter(treasure, 220, 0);

(stage)舞台 是什么?它是所有精灵的根容器,与画布的尺寸完全相同。你可以把舞台想象成一个巨大的、看不见的精灵,和画布一样大,它包含了游戏中的所有精灵,以及那些精灵可能被分组的任何容器(比如gameScene)。putCenter的工作原理是将宝物放在舞台中心,然后将其x位置偏移220像素。以下是使用putCenter的格式:

1
anySprite.putCenter(anyOtherSprite, xOffset, yOffset);

您可以用同样的方式使用其他put方法。例如,如果您想将一个精灵直接定位到另一个精灵的左边,而没有任何偏移,您可以使用putLeft,如下所示:

1
spriteOne.putLeft(spriteTwo);

这将把spriteTwo直接放在spriteOne的左边,并垂直对齐它。

分配动态属性

在我们继续之前,您需要注意一个小细节。创建精灵的代码还向宝藏精灵添加了一个pickedUp属性:

1
treasure.pickedUp = false;

您将看到我们将如何在游戏逻辑中使用treasure.pickedUp,以帮助我们确定游戏的进度。如果需要,可以动态地将任何自定义属性或方法分配给这样的精灵。

创建敌人精灵

寻宝游戏中有6个敌人精灵(红色方块)。它们水平间隔,但有随机的初始垂直位置。所有的敌人精灵都是在一个for循环中使用这个代码在setup函数中创建的:

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
43
//Make the enemies
let numberOfEnemies = 6,
spacing = 48,
xOffset = 150,
speed = 2,
direction = 1;

//An array to store all the enemies
enemies = [];

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

//Each enemy is a red rectangle
let enemy = g.rectangle(32, 32, "red");

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

//Give the enemy a random y position
let y = g.randomInt(0, g.canvas.height - enemy.height);

//Set the enemy's direction
enemy.x = x;
enemy.y = y;

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

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

//Push the enemy into the `enemies` array
enemies.push(enemy);

//Add the enemy to the `gameScene`
gameScene.addChild(enemy);
}

这段代码产生的结果如下:

.

代码通过Hexi的randomInt方法给每个敌人一个随机的y位置:

1
let y = g.randomInt(0, g.canvas.height - enemy.height);

randomInt将给出参数中提供的任意两个整数之间的随机数。(如果你需要一个随机小数,可以使用randomFloat来代替)。

所有的精灵都有称为vx和vy的属性。它们决定了精灵在水平方向(vx)和垂直方向(vy)移动的速度和方向。《寻宝人》中的敌人只是上下移动,所以他们只需要一个vy值。它们的vy是速度(2)乘以方向(1或-1)

1
enemy.vy = speed * direction;

如果方向是1,敌人的vy就是2。这意味着敌人将以每帧2像素的速度移动屏幕。如果方向是-1,敌人的速度是-2。这意味着敌人将以每帧2像素的速度在屏幕上移动。

当敌人的vy被设置好后,方向就会颠倒,这样下一个敌人就会朝相反的方向移动

1
direction *= -1;

您可以看到,创建的每个敌人都被推进到一个称为enemies的数组中

1
enemies.push(enemy);

在后面的代码中,您将看到我们将如何访问这个数组中的所有敌人,以确定他们是否正在触摸播放器。

健康条

你会注意到当玩家触碰到一个敌人时,屏幕右上角的健康条的宽度会减小。

health bar

这个健康条是怎么做的?它只是两个相同位置的矩形精灵:一个黑色的矩形在后面,一个绿色的矩形在前面。它们被组合在一起组成一个叫做healthBar的单一化合物sprite。然后将健康条添加到gameScene中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Create the health bar
let outerBar = g.rectangle(128, 16, "black"),
innerBar = g.rectangle(128, 16, "yellowGreen");

//Group the inner and outer bars
healthBar = g.group(outerBar, innerBar);

//Set the `innerBar` as a property of the `healthBar`
healthBar.inner = innerBar;

//Position the health bar
healthBar.x = g.canvas.width - 148;
healthBar.y = 16;

//Add the health bar to the `gameScene`
gameScene.addChild(healthBar);

您可以看到一个名为inner的属性被添加到healthBar。它只是引用了innerBar(绿色矩形),以便以后可以方便地访问它。

1
healthBar.inner = innerBar;

你不需要这么做;但是,嘿,为什么不呢!这意味着如果你想控制内栏的宽度,你可以写一些流畅的代码,如下所示:

1
healthBar.inner.width = 30;

这是相当整洁和可读的,所以我们将保持它!

游戏结束的场景

如果玩家的生命值降至零,或者玩家设法将宝物带到出口,游戏结束,游戏在屏幕上显示。现场游戏只是显示“你赢了”或“你输了”的一些文本,这取决于结果。

you won

这使得怎么样?文本由文本精灵构成。

1
2
3
let anyText = g.text(
"Hello!", "CSS font properties", "fillColor", xPosition, yPosition
);

在上面的示例中,第一个参数“Hello!”是要显示的文本内容。使用content属性稍后更改文本精灵的内容。

1
anyText.content = "Some new content";

以下是如何在setup函数中创建消息文本的游戏

1
2
3
4
//Add some text for the game over message
message = g.text("Game Over!", "64px Futura", "black", 20, 20);
message.x = 120;
message.y = g.canvas.height / 2 - 64;

接下来,创建一个名为gameOverScene的新组。消息文本被添加到其中。gameOverScene的可视属性设置为false,以便在游戏开始时不可见。

1
2
3
4
gameOverScene = g.group(message);

//Make the `gameOverScene` invisible for now
gameOverScene.visible = false;

在游戏结束时,我们将设置游戏场景的可见属性为true以显示文本消息。我们还将gameScene的可见属性设置为false,以便所有的游戏精灵都隐藏起来。

键盘交互

你用键盘箭头键控制玩家(蓝色方块)。Hexi有一个内置的箭头控制arrowControl方法,让您快速添加箭头键的交互性游戏。提供要作为第一个参数移动的精灵,以及作为第二个参数移动的每帧像素数。以下是如何使用arrowControl方法来帮助玩家在按下箭头键时每帧移动5个像素。

1
g.arrowControl(player, 5);

使用arrowControl是实现键盘交互性的一种简单而快速的方法,但是通常需要更好地控制按下键时发生的情况。Hexi有一个内置的keyboard方法,你可以定义自定义键。

1
let customKey = g.keyboard(asciiCode);

为要作为第一个参数使用的键提供ascii码号

所有这些键都有可定义的pressrelease方法。以下是如何可选地创建和使用这些键盘对象来帮助移动宝藏猎人中的玩家角色。(您将在setup函数中定义此代码):

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//Create some keyboard objects using Hexi's `keyboard` method.
//You would usually use this code in the `setup` function.
//Supply the ASCII key code value as the single argument
let leftArrow = g.keyboard(37),
upArrow = g.keyboard(38),
rightArrow = g.keyboard(39),
downArrow = g.keyboard(40);

//Left arrow key `press` method
leftArrow.press = () => {
//Change the player's velocity when the key is pressed
player.vx = -5;
player.vy = 0;
};

//Left arrow key `release` method
leftArrow.release = () => {
//If the left arrow has been released, and the right arrow isn't down,
//and the player isn't moving vertically:
//Stop the player
if (!rightArrow.isDown && player.vy === 0) {
player.vx = 0;
}
};

//The up arrow
upArrow.press = () => {
player.vy = -5;
player.vx = 0;
};
upArrow.release = () => {
if (!downArrow.isDown && player.vx === 0) {
player.vy = 0;
}
};

//The right arrow
rightArrow.press = () => {
player.vx = 5;
player.vy = 0;
};
rightArrow.release = () => {
if (!leftArrow.isDown && player.vy === 0) {
player.vx = 0;
}
};

//The down arrow
downArrow.press = () => {
player.vy = 5;
player.vx = 0;
};
downArrow.release = () => {
if (!upArrow.isDown && player.vx === 0) {
player.vy = 0;
}
};

您可以看到玩家的vx和vy属性的值根据按下或释放的键而改变。一个正值的vx值将使玩家向右移动,一个负值将使玩家向左移动。一个正值的vy值会让玩家向下移动,一个负值会让玩家向上移动。

第一个参数是要控制的精灵:player。第二个参数是精灵移动每一帧的像素数:5。最后四个参数是顶键、右键、底键和左键的ascii码。(你可以记住这一点,因为它们的顺序是顺时针的,从顶部开始。)

设置游戏状态

游戏状态 是Hexi当前运行的函数。当Hexi第一次启动时,它会运行setup函数(或者你在Hexi的构造函数参数中指定的其他函数)。如果您想要更改游戏状态,请为Hexi的状态属性分配一个新函数。方法如下:

1
g.state = anyFunction;

在《寻宝游戏》中,当设置功能完成后,游戏状态设置为:

1
g.state = play;

这使得Hexi查找并运行一个叫做play的函数。默认情况下,分配给游戏状态的任何函数都将以每秒60帧的速度连续循环运行。(您可以通过设置Hexi的fps属性随时改变帧速率)。游戏逻辑通常在一个连续循环中运行,这就是游戏循环。Hexi为您处理循环管理,所以您不必担心它是如何工作的。

(如果您好奇,Hexi使用一个requestAnimationFrame循环,它具有一个固定的逻辑时间步长和可变渲染时间。它也做雪碧位置插值,以消除任何不一致的峰值在帧率。它运行的是秘密的冰沙,所以你可以使用它的任何属性来调整你的Hexi应用游戏循环以获得最好的效果。

如果您需要暂停循环,只需使用Hexi的暂停方法,如下所示:

1
g.pause();

您可以使用resume方法再次启动游戏循环,如下所示:

1
g.resume();

现在让我们来看看宝藏猎人的游戏功能是如何工作的。

游戏逻辑与play函数循环

正如您刚刚学到的,play函数中的所有内容都是在一个连续循环中运行的。

1
2
3
function play() {
//This code loops from top to bottom 60 times per second
}

这就是所有游戏逻辑发生的地方。这是有趣的部分,让我们看看play函数中的代码是做什么的。

移动玩家精灵

寻宝者在游戏中使用Hexi的move方法来移动精灵。

1
g.move(player);

这相当于这样写代码:

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

它通过添加vx和vy速度值来更新玩家的x和y位置。使用move可以省去输入和查看这个标准的样板代码的麻烦。

您还可以通过提供数组作为参数来移动一个完整的精灵数组。

1
g.move(arrayOfSprites);

现在你可以很容易地移动玩家,但是当玩家到达屏幕边缘时会发生什么?

包含屏幕边界内的精灵

使用Hexi’s contains方法将精灵保持在屏幕的边界内

1
g.contain(player, g.stage);

第一个参数是要包含的精灵,第二个参数是任何带有x、y、width和height属性的JavaScript对象。

如前所述,stage是所有河西精灵的根容器对象,它的宽度和高度与画布相同

但是,您也可以使用自定义对象来提供包含方法,以执行相同的操作。方法如下:

1
2
3
4
5
6
7
8
9
g.contain(
player,
{
x: 0,
y: 0,
width: 512,
height: 512
}
);

这将包含玩家精灵到由对象的维度定义的区域。如果您想精确地调整对象应该包含的区域,这将非常方便。

contain有一个特别有用的特性。如果精灵到达一个包含边,contains将返回一个JavaScript集,告诉您它到达了哪个边:“top”、“right”、“bottom”或“left”。以下是如何使用这个特性来找出精灵在画布上触碰的边缘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let playerHitsEdges = g.contain(player, g.stage);

if(playerHitsEdges) {

//Find out on which side the collision happened
let collisionSide;
if (playerHitsEdges.has("left")) collisionSide = "left";
if (playerHitsEdges.has("right")) collisionSide = "right";
if (playerHitsEdges.has("top")) collisionSide = "top";
if (playerHitsEdges.has("bottom")) collisionSide = "bottom";

//Display the result in a text sprite
message.content = `The player hit the ${collisionSide} of the canvas`;
}

与敌人碰撞

当玩家攻击被任何一个敌人时,健康栏的宽度会减少,玩家会变成半透明的。

collision

这是如何工作的呢?

Hexi有一整套有用的二维几何和基于瓷砖的碰撞检测方法。Hexi实现了Bump碰撞模块,所以Bump的所有碰撞方法都和Hexi一起工作。

寻宝游戏只使用其中一种碰撞方法: hitTestRectangle。它用两个矩形的精灵告诉你它们是否重叠。如果是,它将返回true;如果不是,则返回false

1
g.hitTestRectangle(spriteOne, spriteTwo);

下面是play函数中的代码如何使用hitTestRectangle检查任何敌人和玩家之间的冲突。

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
//Set `playerHit` to `false` before checking for a collision
let playerHit = false;

//Loop through all the sprites in the `enemies` array
enemies.forEach(enemy => {

//Move the enemy
g.move(enemy);

//Check the enemy's screen boundaries
let enemyHitsEdges = g.contain(enemy, g.stage);

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

//Test for a collision. If any of the enemies are touching
//the player, set `playerHit` to `true`
if (g.hitTestRectangle(player, enemy)) {
playerHit = true;
}
});

//If the player is hit...
if (playerHit) {

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

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

//Make the player fully opaque (non-transparent) if it hasn't been hit
player.alpha = 1;
}

这段代码创建了一个名为playerHit的变量,该变量在forEach循环检查所有敌人是否发生冲突之前被初始化为false

1
let playerHit = false;

(因为play函数每秒运行60次,所以playerHit在每个新帧中将被重新初始化为false)

如果hitTestRectangle返回true, forEach循环将playerHit设置为true

1
2
3
if(g.hitTestRectangle(player, enemy)) {
playerHit = true;
}

如果玩家被击中,代码会将alpha值设置为0.5,使玩家半透明。它还可以将healthBar的内部精灵的宽度减少1像素。

1
2
3
4
5
6
7
8
9
10
11
12
13
if(playerHit) {

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

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

} else {

//Make the player fully opaque (non-transparent) if it hasn't been hit
player.alpha = 1;
}

您可以将精灵的alpha属性设置为0(完全透明)到1(完全不透明)之间的任何值。0.5的值使它成为半透明 (Alpha是一个久经考验的平面设计术语,只意味着透明)

这段代码还使用move方法来移动敌人,并包含在画布中。代码还使用contain的返回值来确定敌人是在攻击画布的顶部还是底部。如果它击中顶部或底部,敌人的方向会在以下代码的帮助下颠倒:

1
2
3
4
5
6
7
8
9
10
//Check the enemy's screen boundaries
let enemyHitsEdges = g.contain(enemy, g.stage);

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

将敌人的vy(垂直速度)值乘以- 1会使它朝相反的方向前进。这是一个非常简单的反弹效应。

碰撞的宝藏

如果玩家触摸到宝藏(黄色方块),钟声就会响起。然后玩家可以把宝物带到出口。宝藏集中在玩家身上,并随之移动。

treasure

这是play函数实现这些效果的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Check for a collision between the player and the treasure
if (g.hitTestRectangle(player, treasure)) {

//If the treasure is touching the player, center it over the player
treasure.x = player.x + 8;
treasure.y = player.y + 8;

if(!treasure.pickedUp) {

//If the treasure hasn't already been picked up,
//play the `chimes` sound
chimes.play();
treasure.pickedUp = true;
};
}

您可以看到代码在if语句中使用hitTestRectangle来测试玩家和宝藏之间的冲突

1
if (g.hitTestRectangle(player, treasure)) {

如果这是真的,宝藏就集中在玩家身上

1
2
treasure.x = player.x + 8;
treasure.y = player.y + 8;

如果treasure.pickedUpfalse,那么你知道宝藏还没有被取走,你可以播放钟声:

1
chimes.play();

除了播放Hexi的声音对象,你还可以使用更多的方法来控制它们:暂停、重新启动和播放。(使用playFrom从声音文件中的特定秒开始播放声音,如:soundObject.playFrom(5)。这将使声音从5秒开始播放。

您还可以通过在0和1之间分配值来设置声音对象的音量。以下是如何将音量设置为中等(50%)。

1
soundObject.volume = 0.5;

您可以通过将-1(左扬声器)值设置为1(右扬声器)来设置声音对象的pan。泛音值为0时,两个扬声器的音量都相等。以下是如何将pan设置为在左扬声器中稍微突出一些。

1
soundObject.pan = -0.2;

如果您想要使一个声音连续重复,请将其loop属性设置为true。

1
soundObject.loop = true;

Hexi实现了Sound.js模块来控制声音,因此您可以在Hexi应用程序中使用Sound.js的任何属性和方法。

因为您不想在拾取宝藏后再播放一次钟声,所以代码在声音播放后将treasure.pickedUp设置为true

1
treasure.pickedUp = true;

现在玩家拿起了宝物,你怎么能检查游戏的结局呢?

结束游戏

游戏有两种结局。玩家的健康状况可能会耗尽,在这种情况下,比赛就会失败。或者,玩家可以成功地把宝物带到出口,这样游戏就赢了。如果满足这两个条件中的任何一个,游戏的状态将被设置为结束,消息文本的内容将显示结果。下面是play函数中的最后一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//Does the player have enough health? If the width of the `innerBar`
//is less than zero, end the game and display "You lost!"
if (healthBar.inner.width < 0) {
g.state = end;
message.content = "You lost!";
}

//If the player has brought the treasure to the exit,
//end the game and display "You won!"
if (g.hitTestRectangle(treasure, exit)) {
g.state = end;
message.content = "You won!";
}

结束函数非常简单。它只是隐藏了gameScene并显示了gameOverScene

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

这就是寻宝猎人!在继续之前,尝试使用这些相同的技术从头开始制作您自己的游戏。当你准备好了,继续读下去!

使用图像

在你的Hexi游戏中有三种主要的使用图像的方法

  • 为每个精灵使用单独的图像文件
  • 使用一个 纹理地图集 。这是一个单独的图像文件,包含游戏中每个精灵的子图像。图像文件附带一个匹配的JSON数据文件,该文件描述每个子图像的名称、大小和位置
  • 使用tileset(也称为spritesheet)。这也是一个单一的图像文件,包括每个精灵的子图像。然而,与纹理图集不同,它没有一个描述精灵数据的JSON文件。相反,您需要使用JavaScript在游戏代码中指定每个精灵的大小和位置。在某些情况下,这可以比纹理图集有一些优势

这三种制作图像精灵的方法都使用了Hexi的sprite方法。这是使用它来制作图像精灵的最简单的方法。

1
let imageSprite = g.sprite("images/theSpriteImage.png");

在下一节中,我们将使用图像精灵更新“宝藏猎人”,您将了解将图像添加到游戏中的三种方法。

这部分的所有图片都是由Lanea Zimmerman创作的。你可以在这里找到更多她的艺术作品。谢谢,Lanea !

个人形象

打开并播放下一个版本的宝藏猎人:02_treasureHunterImages.html(在教程文件夹中可以找到)它和第一个版本完全一样,但是所有的彩色方块都被图像所取代。

2

(点击,点击链接玩游戏。)看看源代码,你会发现游戏的逻辑和结构与游戏的第一个版本完全相同。唯一改变的是精灵的外表。这是如何做的呢?

加载图片文件

游戏中的每个精灵都使用一个PNG图像文件。您将在教程的images子文件夹中找到所有的图像。

images

在使用它们制作精灵之前,您需要预先将它们加载到Hexi的资产中。最简单的方法是在第一次初始化引擎时,在Hexi的资产数组中列出图像名称及其完整的文件路径。创建一个名为thingsToLoad的数组,列出要加载的文件名。然后提供该数组作为hexi方法的第四个参数。方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//An array that contains all the files you want to load
let thingsToLoad = [
"images/explorer.png",
"images/dungeon.png",
"images/blob.png",
"images/treasure.png",
"images/door.png",
"sounds/chimes.wav"
];

//Create a new Hexi instance, and start it
let g = hexi(512, 512, setup, thingsToLoad);

//Start Hexi
g.start();

如果在web浏览器中打开JavaScript控制台,就可以监视这些资产的加载进度

现在你可以在你的游戏代码中访问这些图像:

1
g.image("images/blob.png")

尽管预加载图像和其他资产是使它们进入游戏的最简单的方法,但是您也可以使用loader对象及其方法在任何其他时候加载资产。正如我前面提到的,加载程序只是运行在底层的Pixi加载程序的别名,您可以在这里了解如何使用它

现在您已经将这些图像加载到游戏中,让我们看看如何使用它们来制作精灵。

用图片制作精灵

使用sprite方法创建一个图像精灵,使用您之前了解的格式。下面介绍如何使用dungeon.png映像创建精灵。(dungeon.png是512×512像素的背景图像)

1
dungeon = g.sprite("images/dungeon.png");

这是所有!现在,精灵不再显示为一个简单的彩色矩形,而是显示为一个512×512的图像。没有必要指定宽度或高度,因为Hexi根据图像的大小自动计算。您可以使用所有其他的精灵属性,如x、y、宽度和高度,就像使用普通矩形精灵一样。

下面是设置函数的代码,该函数创建了地牢背景、退出门、玩家和宝藏,并将它们全部添加到gameScene组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//The dungeon background
dungeon = g.sprite("images/dungeon.png");

//The exit door
exit = g.sprite("images/door.png");
exit.x = 32;

//The player sprite
player = g.sprite("images/explorer.png");
player.x = 68;
player.y = g.canvas.height / 2 - player.halfWidth;

//Create the treasure
treasure = g.sprite("images/treasure.png");

//Position it next to the left edge of the canvas
//g.stage.putCenter(treasure, 208, 0);

//Create a `pickedUp` property on the treasure to help us Figure
//out whether or not the treasure has been picked up by the player
treasure.pickedUp = false;

//Create the `gameScene` group and add the sprites
gameScene = g.group(dungeon, exit, player, treasure);

作为对该代码原始版本的稍微更有效的改进,group创建了gameScene并在单个步骤中对精灵进行分组

看起来熟悉吗?没错,唯一改变的代码是创建精灵的行。这种模块化是Hexi的一个特性,它可以让你使用简单的形状创建快速的游戏原型,当你的游戏思想发展的时候,你可以很容易地把它换成详细的图片。游戏中的其余代码可以保持原样。

微调控制区域

这个新版本的宝藏猎人有一个小的改进,那就是精灵被包含在地牢里的新方式。它们以一种自然地与艺术品的2.5D视角相匹配的方式被包含在其中,如这张屏幕截图中的绿色方块所示:

perspective

这是一个很容易修改的地方。您所需要做的就是提供包含一个定义包含矩形的大小和位置的自定义对象的包含方法。方法如下:

1
2
3
4
5
6
7
8
g.contain(
player,
{
x: 32, y: 16,
width: g.canvas.width - 32,
height: g.canvas.height - 32
}
);

只需调整x、y、宽度和高度值,这样包含的区域对于您正在制作的游戏来说就显得自然了。

使用一个纹理地图集

如果您正在开发一个大型、复杂的游戏,您将需要一种快速、高效的图像处理方法。纹理图谱可以帮助你做到这一点。一个纹理图集实际上是两个独立的文件,它们是紧密相关的:

  • 一个PNG tileset图像文件,其中包含您希望在游戏中使用的所有图像。(tileset图像有时被称为spritesheet)
  • 一个JSON文件,描述这些子图像在tileset中的大小和位置

使用纹理图集是一个很大的节省时间。您可以按任何顺序排列tileset的子图像,JSON文件将为您跟踪它们的大小和位置。这非常方便,因为这意味着子图像的大小和位置不会硬编码到游戏程序中。如果您对tileset做了更改,比如添加图像、调整它们的大小或删除它们,那么只需重新发布JSON文件,您的游戏将使用更新后的数据来正确显示图像。如果你要制作比一个小游戏更大的东西,你一定要使用纹理图集。

ileset JSON数据的实际标准是一种格式,这种格式由一种叫做纹理包装器的流行软件工具输出(纹理包装器的“基本”许可证是免费的)。即使您不使用纹理包装器,类似的工具如Shoebox以相同的格式输出JSON文件。让我们来看看如何使用它来制作一个纹理贴图,以及如何将它加载到游戏中。

准备的图片

您首先需要为您的游戏中的每个图像提供单独的PNG图像。您已经为“寻宝者”找到了它们,所以您都已经设置好了。打开Texture Packer并选择{JS}配置选项。将你的游戏图像拖到它的工作区中。您还可以将纹理封隔器指向任何包含图像的文件夹。纹理封隔器将自动将图像排列在一个小块图像上,并为它们提供与原始图像文件名匹配的名称。默认情况下,它会给他们一个2像素的填充。

Texture Packer

地图集中的每一个子图像都被称为帧。虽然只是一个大的图像,纹理图集有5帧。每个帧的名称都是它的原始PNG文件名:”dungeon.png”, “blob.png”, “explorer.png”, “treasure.png” 和 “door.png”。这些帧名用于帮助atlas引用每个子映像。

完成后,请确保数据格式设置为JSON(Hash)并单击Publish按钮。选择文件名和位置,并保存已发布的文件。您将得到一个PNG文件和一个JSON文件。在这个例子中,我的文件名是treasureHunter.jsontreasureHunter.png。为了让你的生活更简单,只需将两个文件保存在项目的images文件夹中。(可以将JSON文件视为图像文件的额外元数据)

Texture Packer使用起来可能会很麻烦,因为您需要使所有这些设置都正确,以便在不告诉您有错误的情况下正确地发布。并且,它将试图通过使用不支持免费版本的默认设置来欺骗您升级到付费版本。因此,您需要显式地关闭这些(正如我上面所描述的),以使其工作没有错误。尽管如此,最后的努力还是值得的——所以如果你遇到不可能的困难,继续尝试并在这个资源库中发布一个问题吧!

加载纹理地图集

要将纹理图集加载到游戏中,只需在初始化游戏时将JSON文件包含在Hexi的资产数组中。

1
2
3
4
5
6
7
let thingsToLoad = [
"images/treasureHunter.json",
"sounds/chimes.wav"
];
let g = hexi(512, 512, setup, thingsToLoad);
g.scaleToWindow();
g.start();

这是所有!你不必加载PNG文件- Hexi在后台自动完成。只需告诉Hexi显示哪个tileset帧(子图像)就可以了。

如果你想用纹理图谱的框架来制作精灵,你可以这样做:

1
anySprite = g.sprite("frameName.png");

Ga将创建精灵,并显示纹理贴图中正确的图像。

以下是如何使用纹理图集框架创建寻宝游戏中的精灵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//The dungeon background
dungeon = g.sprite("dungeon.png");

//The exit door
exit = g.sprite("door.png");
exit.x = 32;

//The player sprite
player = g.sprite("explorer.png");
player.x = 68;
player.y = g.canvas.height / 2 - player.halfWidth;

//Create the treasure
treasure = g.sprite("treasure.png");

Hexi知道这些是纹理地图集的帧名,而不是单独的图像,它直接显示它们来自tileset。

如果你需要在游戏中访问纹理地图集的JSON文件,你可以这样获取:

1
jsonFile = g.json("jsonFileName.json");

查看教程文件夹中的treasureHunterAtlas.js文件,查看如何加载纹理图集并使用它制作精灵的工作示例。

外星人武装

本系列教程中的下一个示例游戏是Alien Armada。你能在60个外星人着陆前摧毁他们并毁灭地球吗?点击下面的图片链接玩游戏:

alien armada

使用箭头键移动并按空格键射击。随着游戏的进行,外星人越来越频繁地从屏幕顶端降落。游戏规则如下:

  • 加载并使用自定义字体。
  • 在加载游戏资产时显示加载进度条。
  • 射击子弹。
  • 创建具有多个图像状态的精灵。
  • 生成随机的敌人。
  • 从游戏中移除精灵。
  • 显示一个游戏分数。
  • 重置并重新启动游戏。

您将在教程文件夹中找到完整注释的外星无敌舰队源代码。请务必查看它,以便您能够在正确的上下文中看到所有这些代码。它的一般结构与《寻宝者》相同,并加入了这些新技术。让我们看看它们是如何实现的。

加载并使用自定义字体

Alien Armada使用自定义字体emulogic.ttf显示屏幕右上角的分数。字体文件预先加载了初始化游戏的资产数组中的其余资产文件(声音和图像)

1
2
3
4
5
6
7
8
9
10
let thingsToLoad = [
"images/alienArmada.json",
"sounds/explosion.mp3",
"sounds/music.mp3",
"sounds/shoot.mp3",
"fonts/emulogic.ttf" //<- The custom font
];
let g = hexi(480, 320, setup, thingsToLoad, load);
g.scaleToWindow();
g.start();

要使用该字体,请在游戏的设置函数中创建一个文本精灵。text方法的第二个参数是一个字符串,描述字体的点大小和名称:“20px emulogic”。

1
scoreDisplay = g.text("0", "20px emulogic", "#00FF00", 400, 10);

您可以加载和使用TTF、OTF、TTC或WOFF格式的任何字体

加载进度条

外星人无敌舰队装载了3个MP3音频文件:一个射击声,一个爆炸声和音乐。音乐声音的大小约为2 MB,所以在网络连接缓慢的情况下,这种声音需要几秒钟才能被加载。当这种情况发生时,玩家只能看到空白的画布,而外星人的无敌舰队则装载。有些玩家可能会认为游戏已经冻结,所以游戏会有帮助地实现一个加载栏来通知玩家资产正在加载。它是一个从左向右展开的蓝色矩形,并显示一个数字,告诉您目前加载的游戏资产的百分比

loading

这是一个内置在Hexi引擎的特性。Hexi有一个可选的加载状态,在加载游戏资产时运行。在加载状态下,您可以决定要发生什么。您所需要做的就是编写一个函数,其中包含在加载资产时应该运行的代码,并告诉Hexi这个函数的名称。Hexi的引擎会自动在循环中运行这个函数,直到资产加载完成。

让我们来看看外星人无敌舰队是如何运作的。游戏代码告诉Hexi在加载状态下使用一个名为load的函数。它通过列出load作为Hexi的初始化构造函数的最后一个参数来实现这一点。(请在下面的代码中查找负载):

1
let g = hexi(480, 320, setup, thingsToLoad, load); //<- It's here!

这告诉Hexi在资产加载时在循环中运行load函数

这是来自外星舰队的load函数。它实现了一个loadingBar对象,它显示了正在扩展的blue bar和加载的文件的百分比。

1
2
3
function load(){
g.loadingBar();
}

资产加载后,setup状态自动运行

您将在Hexi的core.js文件中找到loadingBar代码。这是一个非常简单的例子,如果您愿意,您可以使用它作为编写自定义加载动画的基础。您可以在load函数中运行任何您喜欢的代码,因此完全由您决定什么应该发生,什么在您的游戏加载时显示。

发射子弹

你怎么能让大炮射出子弹?

当你按下空格键时,大炮向敌人发射子弹。子弹从炮塔的末端开始,以每帧7像素的速度在画布上移动。如果他们撞到外星人,外星人就会爆炸。如果一颗子弹没有击中并飞过舞台顶部,游戏代码就会移除它。

Shooting

1
bullets = [];

bullets数组在游戏的setup函数中初始化。

然后,您可以使用Here的自定义shoot方法来制作任意方向的精灵射击子弹。下面是实现shoot方法的一般格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
g.shoot(
cannon, //大炮
4.71, //射击角度 (4.71 是向上)
cannon.halfWidth, //子弹在大炮上的x位置
0, //子弹在大炮上的y位置
g.stage, //应该添加子弹的容器
7, //子弹的速度 (像素每帧)
bullets, //用于存储子弹的数组

//A function that returns the sprite that should
//be used to make each bullet
() => g.sprite("bullet.png")
);

第二个参数决定了子弹应该以弧度表示的角度。4.71弧度,在本例中使用,向上。0在右边,1.57在下面,3.14在左边。

第三和第四个参数是子弹在正典上的起始x和y位置。第5个参数是应该添加子弹的容器,第6个参数是应该放入子弹的数组。

最后一个参数是一个函数,它返回一个应该用作子弹的精灵。在这个例子中,子弹是使用游戏中加载的纹理图集中的”bullet.png”帧创建的。

1
() => g.sprite("bullet.png")

用您自己的函数替换此函数,以创建您可能需要的任何类型的自定义子弹。

你的子弹什么时候发射?您可以随时调用shoot方法,无论何时您想要创建项目符号,在代码中的任何位置。在外星无敌舰队中,当玩家按下空格键时,子弹会被发射。游戏通过在空格键的press方法中调用shoot来实现这一点。方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
g.spaceBar.press = () => {

//Shoot the bullet
g.shoot(
cannon, //The shooter
4.71, //The angle at which to shoot (4.71 is up)
cannon.halfWidth, //Bullet's x position on the cannon
0, //Bullet's y position on the canon
g.stage, //The container to which the bullet should be added
7, //The bullet's speed (pixels per frame)
bullets, //The array used to store the bullets

//A function that returns the sprite that should
//be used to make each bullet
() => g.sprite("bullet.png")
);

//Play the shoot sound.
shootSound.play();
};

您可以看到press方法也使shootSound发挥作用。(上面的代码是在游戏的setup函数中初始化的)

还有一件事你需要做:你必须让子弹move。你可以在游戏循环play函数中使用一些代码。使用Hexi的move方法并提供子弹阵列作为参数:

1
g.move(bullets);

move方法自动循环遍历阵列中的所有精灵,并以其vxvy速度值的值更新它们的x和y位置。

现在你知道子弹是如何创建和动画的了。但是当他们击中一个外星人会发生什么呢?

精灵状态

当一颗子弹击中一个外星人时,会出现一个黄色的爆炸图像。这个简单的效果是通过给每个外星精灵两个状态来创造的:一个正常的状态和一个被破坏的状态。外星人被创造,他们的状态被设定为正常。如果他们被击中,他们的状态就会被摧毁。

staus

这个系统是如何工作的?

首先,让我们来看看外星无敌舰队,这里展示的是:

alien

您可以看到定义这两种状态的两个图像帧:alien.png和explosion.png。在创建sprite之前,首先创建一个数组来列出这两帧:

1
2
3
4
let alienFrames = [
"alien.png",
"explosion.png"
];

接下来使用alienframe数组初始化外星精灵

1
alien = g.sprite(alienFrames);

如果你愿意,你可以把这两个步骤合并成一个,像这样:

1
2
3
4
alien = g.sprite([
"alien.png",
"explosion.png"
]);

这将为精灵加载两个帧。第0帧是alien.png 帧,第一帧是explosion.png帧。在第一次创建精灵时,默认显示第0帧。

您可以使用sprite的show方法在sprite上显示任何其他帧号,如下所示:

1
alien.show(1);

上面的代码将把外星人设置为第一帧,这就是explosion.png帧

为了使您的代码更具可读性,在一个特殊的状态对象中定义sprite的状态是一个好主意。给每个状态一个名称,其值对应于该状态的帧号。以下是如何定义外星人的两种状态: normaldestroyed:

1
2
3
4
alien.states = {
normal: 0,
destroyed: 1
};

现在alien.states.normal的值为0,alien.states.destroyed的值为1。这意味着你可以像这样显示外星人的normal状态:

1
alien.show(alien.states.normal);

展示外星人destroyed的状态:

1
alien.show(alien.states.destroyed);

这使您的代码可读性更强,因为您可以一眼看出哪个sprite状态正在显示

注意:Hexi也有一个低级的gotoAndStop方法,它的功能和show一样。尽管你可以在游戏代码中自由使用gotoAndStop,但按照惯例,只有Hexi的渲染引擎在内部使用它

生成随机的外星人

外星人无敌舰队在14个随机选择的位置中的任何一个产生外星人,就在舞台的顶部边界上。外星人最初出现的频率很低,但逐渐开始以越来越高的速度出现。随着游戏的进行,这使得游戏变得越来越困难。让我们看看这两个特性是如何实现的。

外星人时机

当游戏开始时,第一个新的外星人在100帧后产生。在游戏的setup函数中初始化的一个叫做alienFrequency的变量用来帮助跟踪这个。初始化为100。

1
alienFrequency = 100;

另一个名为alienTimer的变量用于计算前面生成的外星人与下一个外星人之间经过的帧数。

1
alienTimer = 0;

在play函数(游戏循环)中,每个帧都更新了一个alienTimer。当alienTimer达到alienFrequency的值时,会产生一个新的异形精灵。这是play函数的代码。(此代码省略了生成外星精灵的实际代码——我们将在前面看到)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Add one to the alienTimer
alienTimer++;

//Make a new alien if `alienTimer` equals the `alienFrequency`
if(alienTimer === alienFrequency) {

//... Create the alien: see ahead for the missing code that does this...

//Set the `alienTimer` back to zero
alienTimer = 0;

//Reduce `alienFrequency` by one to gradually increase
//the frequency that aliens are created
if(alienFrequency > 2){
alienFrequency--;
}
}

您可以在上面的代码中看到,创建精灵之后,alienFrequency减少了1。这将使下一个外星人出现的时间比前一个外星人早1帧,这也是为什么下降的外星人的速度缓慢增加。您还可以看到,在创建精灵之后,alienTimer被设置为0,以便它可以重新计数,以生成下一个新的外星人。

外星人的随机起始位置

在生成任何外星人之前,我们需要一个数组来存储所有的外星人精灵。为此,在setup函数中初始化一个名为alien的空数组。

1
aliens = [];

然后在play函数中创建每个异形,在相同的if语句中,我们看上面。这段代码有很多工作要做:

  • 它设置外星人的图像帧和状态。
  • 它设定了外星人的速度(vx和vy)。
  • 它将外星人定位在顶端边界上的任意水平位置。
  • 最后,它把外星人推进了aliens的数组。

这里有完整的代码来完成这一切:

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
43
44
45
//Add one to the alienTimer
alienTimer++;

//Make a new alien if `alienTimer` equals the `alienFrequency`
if(alienTimer === alienFrequency) {

//Create the alien.
//Assign two frames from the texture atlas as the
//alien's two states
let alienFrames = [
"alien.png",
"explosion.png"
];

//Initialize the alien sprite with the frames
let alien = g.sprite(alienFrames);

//Define some states on the alien that correspond
//to its two frames.
alien.states = {
normal: 0,
destroyed: 1
};

//Set its y position above the screen boundary
alien.y = 0 - alien.height;

//Assign the alien a random x position
alien.x = g.randomInt(0, 14) * alien.width;

//Set its speed
alien.vy = 1;

//Push the alien into the `aliens` array
aliens.push(alien);

//Set the `alienTimer` back to zero
alienTimer = 0;

//Reduce `alienFrequency` by one to gradually increase
//the frequency that aliens are created
if(alienFrequency > 2){
alienFrequency--;
}
}

你可以在上面的代码中看到外星人的y位置将它放置在舞台的顶部边界上。

1
alien.y = 0 - alien.height;

它的x位置是随机的

1
alien.x = g.randomInt(0, 14) * alien.width;

这段代码将其放置在15个可能的随机位置(0到14)之上。以下是这些立场的说明:

random

最后,也是非常重要的一点,代码将外星人精灵推入aliens数组

1
aliens.push(alien);

所有这些代码开始以稳定增长的速度输出外星人

移动外星人

我们如何让外星人移动?用让子弹移动的同样方式。您将在上面的代码中注意到,每个外星人都用vy(垂直速度)值1初始化。

1
alien.vy = 1;

当这个值应用到外星人的y位置时,它将使外星人以每帧1像素的速度向下移动,向舞台的底部移动。游戏中所有的外星人精灵都在外星人阵中。所以要让它们都移动,你需要遍历异形数组中的每个精灵每个帧并将它们的vy值添加到它们的y位置。在play函数中类似的一些代码可以工作:

1
2
3
aliens.forEach(alien => {
alien.y += alien.vy;
});

不过,使用Hexi便捷的内置移动功能更简单。只需提供你想要移动的精灵数组,如下所示:

1
g.move(aliens);

这将自动使用速度更新外星人的位置

让外星人爆炸

既然你已经知道如何改变外星人的状态,你怎么能用这个技能创造爆炸效果呢?下面是来自外星舰队的简化代码,它向您展示了如何做到这一点。使用hittest矩形来检查外星人和子弹之间的碰撞。如果发现了碰撞,取出子弹,显示外星人被摧毁的状态,然后在一秒钟后移除外星人。

1
2
3
4
5
6
7
8
9
10
11
12
if (g.hitTestRectangle(alien, bullet)) {

//Remove the bullet sprite.
g.remove(bullet);

//Show the alien's `destroyed` state.
alien.show(alien.states.destroyed);

//Wait for 1 second (1000 milliseconds) then
//remove the alien sprite.
g.wait(1000, () => g.remove(alien));
}

你可以使用Hexi的万能remove函数从游戏中移除精灵,就像这样:

1
g.remove(anySprite);

您可以通过列出参数中要删除的精灵来选择一次删除多个精灵,如下所示:

1
g.remove(spriteOne, spriteTwo, spriteThree);

您甚至可以使用它来删除一系列精灵中的精灵。只需提供sprite数组作为移除的唯一参数:

1
g.remove(arrayOfSprites);

这将使精灵从屏幕中消失,并将它们从它们所在的数组中清空。

Hexi还有一个方便的方法叫wait,它可以在你指定的任何延迟(以毫秒为单位)之后运行一个函数。外星人的无敌舰队游戏代码使用等待移除外星人后的一个第二次延迟,像这样:

1
g.wait(1000, () => g.remove(alien));

这允许外星人在它从游戏中消失之前显示它的爆炸图像状态一秒钟。

这些都是使外星人爆炸的基本技术,当他们碰撞时把外星人和子弹从游戏中移除。但是在《异形无敌舰队》中使用的实际代码要复杂一些。这是因为代码使用嵌套的filter循环遍历所有的子弹和异形,以便可以相互检查它们是否发生冲突。当发生碰撞时,代码也会播放爆炸声音,并将分数更新为1。这是游戏的play函数的所有代码。(如果您是JavaScript的filter循环的新手,您可以在这里阅读如何使用它们。)

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//Check for a collision between the aliens and the bullets.
//Filter through each alien in the `aliens` array.
aliens = aliens.filter(alien => {

//A variable to help check if the alien is
//alive or dead.
let alienIsAlive = true;

//Filter though all the bullets.
bullets = bullets.filter(bullet => {

//Check for a collision between an alien and bullet.
if (g.hitTestRectangle(alien, bullet)) {

//Remove the bullet sprite.
g.remove(bullet);

//Show the alien's `destroyed` state.
alien.show(alien.states.destroyed);

//You could alternatively use the frame number,
//like this:
//alien.show(1);

//Play the explosion sound.
explosionSound.play();

//Stop the alien from moving.
alien.vy = 0;

//Set `alienAlive` to false so that it can be
//removed from the array.
alienIsAlive = false;

//Wait for 1 second (1000 milliseconds) then
//remove the alien sprite.
g.wait(1000, () => g.remove(alien));

//Update the score.
score += 1;

//Remove the bullet from the `bullets array.
return false;

} else {

//If there's no collision, keep the bullet in the
//bullets array.
return true;
}
});

//Return the value of `alienIsAlive` back to the
//filter loop. If it's `true`, the alien will be
//kept in the `aliens` array.
//If it's `false` it will be removed from the `aliens` array.
return alienIsAlive;
});

只要filter循环返回true,当前正在检查的sprite将保留在数组中。但是,如果发生碰撞,循环返回false,当前的外星人和子弹将从它们的数组中删除。

这就是游戏的碰撞原理!

显示分数

外星人无敌舰队引入的另一个新特性是动态分数显示。每次外星人被击中,游戏屏幕右上角的分数就会增加1分。这是如何工作的呢?

外星人无敌舰队初始化一个被称为scoreDisplay的文本精灵在游戏的setup功能。

1
scoreDisplay = g.text("0", "20px emulogic", "#00FF00", 400, 10);

你在前一节中看到,当一个外星人被击中时,游戏的得分变量会增加1:

1
score += 1;

要明显地更新分数,您只需将分数值设置为记分显示器的内容,如下所示:

1
scoreDisplay.content = score;

这就是一切!

结束并重新设置游戏

游戏有两种结局。要么玩家击落60个外星人,这样玩家就赢了。或者,其中一个外星人必须穿越舞台的底部边缘,这样外星人就赢了。

一个简单的if语句在播放函数检查这个。如果任一条件变为真,胜者将被设置为“玩家”或“外星人”,游戏的状态将被更改为结束。

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
//The player wins if the score matches the value
//of `scoreNeededToWin`, which is 60
if (score === scoreNeededToWin) {

//Set the player as the winner.
winner = "player";

//Change the game's state to `end`.
g.state = end;
}

//The aliens win if one of them reaches the bottom of
//the stage.
aliens.forEach(alien => {

//Check to see if the `alien`'s `y` position is greater
//than the `stage`'s `height`
if (alien.y > g.stage.height) {

//Set the aliens as the winner.
winner = "aliens";

//Change the game's state to `end`.
g.state = end;
}
});

结束函数暂停游戏,使动画冻结。然后它会显示游戏的信息,它要么是“地球被拯救”,要么是“地球毁灭!”取决于结果。另外,音乐音量也被设置为50%。然后在延迟3秒后,调用名为reset的函数。这是完成这一切的完整的结束函数:

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
function end() {

//Pause the game loop.
g.pause();

//Create the game over message text.
gameOverMessage = g.text("", "20px emulogic", "#00FF00", 90, 120);

//Reduce the music volume by half.
//1 is full volume, 0 is no volume, and 0.5 is half volume.
music.volume = 0.5;

//Display "Earth Saved!" if the player wins.
if (winner === "player") {
gameOverMessage.content = "Earth Saved!";
gameOverMessage.x = 120;
}

//Display "Earth Destroyed!" if the aliens win.
if (winner === "aliens") {
gameOverMessage.content = "Earth Destroyed!";
}

//Wait for 3 seconds then run the `reset` function.
g.wait(3000, () => reset());
}

reset函数将所有的游戏变量重置为它们的初始值。它还将音乐音量调回到1。它使用删除函数从异形和子弹数组中删除所有剩余的精灵,以便当游戏再次开始时可以重新填充这些数组。删除也用于删除gameOverMessage,加农炮精灵被重新集中在舞台的底部。最后,游戏状态被设置回播放,而游戏循环通过调用Hexi的恢复方法而停止。

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
function reset() {

//Reset the game variables.
score = 0;
alienFrequency = 100;
alienTimer = 0;
winner = "";

//Set the music back to full volume.
music.volume = 1;

//Remove any remaining alien and bullet sprites.
//The universal `remove` method will loop through
//all the sprites in an array of sprites, removed them
//from their parent container, and splice them out of the array.
g.remove(aliens);
g.remove(bullets);

//You can also use the universal `remove` function to remove.
//a single sprite.
g.remove(gameOverMessage);

//Re-center the cannon.
g.stage.putBottom(cannon, 0, -40);

//Change the game state back to `play`.
g.state = play;
g.resume();
}

这是重新开始游戏所需的所有代码。你可以像你喜欢的那样玩外星无敌舰队,它会像这样不断地重置和重启自己。

飞扬的仙女

Flappy Fairy是对有史以来最臭名昭著的游戏之一的致敬:Flappy Bird。点击链接玩游戏:

Flappy Fairy

点击“Go”按钮,游戏将以全屏模式启动。点击屏幕上的任何地方,让仙女飞起来,帮助她通过15根柱子的缝隙到达终点。当仙女在迷宫中飞翔时,一串五颜六色的仙女尘埃尾随着她。如果她撞到一个绿色的街区,她就会在一阵尘土中爆炸。但如果她能在这15根柱子之间不断缩小的缝隙中穿行,她就能看到一个巨大的漂浮“终点”标志。

go

如果你能做一个像Flappy Fairy一样的游戏,你几乎可以做任何其他类型的2D动作游戏。除了使用你已经学过的所有技巧外,Flappy Fairy还引入了一些令人兴奋的新技巧:

  • 以全屏模式启动游戏。
  • 单击的按钮。
  • 创建一个动画精灵。
  • 使用tilingSprite来创建滚动背景。
  • 使用粒子效果。

您将在教程文件夹中找到完整注释的Flappy Fairy源代码。请务必查看它,以便您能够在正确的上下文中看到所有这些代码。它的总体结构与本教程中的其他游戏相同,并添加了这些新技术。让我们看看它们是如何实现的。

制作按钮

当你按下“Go”按钮时,游戏就开始了。“Go”按钮是一种叫做“button”的特殊精灵。按钮精灵有3个图像帧状态:上、下、下。您可以创建一个按钮,有以下三种状态:

1
2
3
4
5
goButton = g.button([
"up.png",
"over.png",
"down.png"
]);

up.png 是一个图像,它显示了当按钮不与指针交互时按钮应该是什么样子。over.png显示了指针在上面和向下时的按钮的样子。down.png是指针按下按钮时显示的图像。

button

(down.png图像被稍微向下偏移并向右偏移,因此它看起来像是被压下了。您可以将任何您喜欢的图像分配到这些状态,按钮将根据指针与这些状态的交互方式自动显示它们。
(注意:如果你的游戏是触控的,你可能只有两个按钮状态:向上和向下。在这种情况下,只需分配两个图像帧,Hexi将假设它们是指上下状态)

按钮有可以定义的特殊方法:press, release, over, out, tap。您可以为这些方法指定任何代码。例如,当用户释放playButton时,如何更改游戏的状态:

1
2
3
goButton.release = () => {
g.state = setupGame;
};

按钮还有一个名为enabled的布尔(true/false)属性,如果想禁用按钮,可以将其设置为false。(设置为true以重新启用它。)您还可以使用按钮的state属性来确定按钮状态是否为“up”“over”“down”。(这些状态值是字符串。)

重要!只要将按钮的交互属性设置为true,就可以赋予任何精灵按钮的特性,如下所示:

1
anySprite.interact = true;

这将使sprite press, release, over, out和tap方法以及与普通按钮相同的状态属性。这意味着你可以让所有的精灵都可以点击,这对于各种各样的互动游戏来说是非常有用的。

你也可以让舞台对象互动,把整个游戏画面变成一个互动按钮:

1
g.stage.interact = true;

有关如何使用按钮的详细信息,请参见按钮。示例文件夹中的buttons.html文件。

动画精灵

Flappy Fairy的一个奇妙特征是,当它飞起来的时候,精灵会拍打它的翅膀。这个动画是通过在连续循环中快速显示3个简单的图像而创建的。每个图像显示一个稍微不同的动画帧,如下图所示:

fairy

这三张图片只是游戏纹理地图集中三个普通的帧,叫做0.png, 1.png 和 2.png。但是你怎么能把像这样的一系列帧变成精灵动画呢?

首先,创建一个定义动画帧的数组,如下所示:

1
2
3
4
5
let fairyFrames = [
"0.png",
"1.png",
"2.png"
];

然后使用这些帧创建一个精灵,如下所示:

1
let fairy = g.sprite(fairyFrames);

或者,如果你愿意的话,你可以把它合并成一个步骤:

1
2
3
4
5
let fairy = g.sprite([
"0.png",
"1.png",
"2.png"
]);

任何具有多个图像帧的精灵都会自动成为一个动画精灵。如果你想要动画帧开始播放,只需调用sprite的playAnimation方法:

1
fairy.playAnimation();

帧将自动在一个连续循环中运行。如果不希望它们循环,则将loop设置为false。

1
fairy.loop = false;

使用stopAnimation方法停止动画:

1
fairy.stopAnimation();

如果您想知道精灵的动画是否正在播放,请使用Boolean (true/false)的playing属性。

你希望动画播放的速度是快还是慢?你可以这样设置动画的帧/秒(fps):

1
fairy.fps = 24;

精灵动画的帧速率与游戏的帧速率无关。这给你很大的灵活性来微调精灵动画。

如果您不想在动画中使用所有精灵的图像帧,只使用其中的一些帧,该怎么办?例如,假设您有一个带有30帧的精灵,但您只希望在动画的一部分中播放10到15帧。为playAnimation方法提供一个包含两个数字的数组:要播放的序列的第一帧和最后一帧。

1
animatedSprite.playAnimation([10, 15]);

现在只有10到15之间的帧作为动画的一部分。为了使其更具可读性,您可以将序列定义为一个数组,该数组描述了这些动画帧的实际功能。例如,也许他们定义了一个角色的行走周期。你可以创建一个名为walkCycle的数组来定义这些帧:

1
let walkCycle = [10, 15];

然后使用playAnimation数组,如下所示:

1
animatedSprite.playAnimation(walkCycle);

这需要编写更多的代码,但是可读性更好!

有关Hexi的精灵动画系统的更多细节以及你可以用它做什么,请参阅示例文件夹中的keyframeAnimation.html,textureAtlasAnimation.html和animationStates.html文件。

让仙女飞

既然你已经知道如何让精灵动起来,那么当你点击游戏屏幕时,Flappy Fairy的飞行动画是如何触发的呢?

在play函数的每一帧中,从仙女的y位置中减去表示重力的0.05。这就是把仙女拉到屏幕底部的重力效应。

1
2
fairy.vy += -0.05;
fairy.y -= fairy.vy;

但当你轻击屏幕时,仙女就会飞起来。这要感谢Hexi的内置指针对象。它有一个tap方法,您可以定义它来执行您喜欢的任何操作。在Flappy Fairy中,tap方法增加了精灵的垂直速度vy,每次点击都增加了1.5像素。

1
2
3
g.pointer.tap = () => {
fairy.vy += 1.5;
};

Hexi的内置指针对象也有按下和释放的方法,你可以用同样的方法来定义。它还具有布尔(true/false) isUp、isDown和tap属性,如果需要,可以使用它来找到指针的状态。

但是你会注意到仙女只会在她开始飞起来的时候拍打她的翅膀,当她失去动力开始下降的时候停止拍打翅膀。要完成这项工作,您需要根据仙女垂直速度(vy)值的变化,知道仙女正在上升还是在下降。这个游戏采用了一种老掉牙的老把戏来帮助解决这个问题。play函数以一个名为oldVy的新值捕捉到当前帧中仙女的速度。但只有在仙女的位置发生变化后才会这样。

1
2
3
4
5
6
7
8
9
10
function play(){

//...
//... all of the code that moves the fairy comes first...
//...

//Then, after the fairy's position has been changed, capture
//her velocity for this current frame
fairy.oldVy = fairy.vy;
}

这意味着,当下一个游戏帧在上下摆动时,oldVy仍然会存储前一帧中精灵的速度值。这意味着你可以用这个值计算出从前一帧到当前帧的速度变化量。如果她开始上升(如果vy比oldVy大),播放仙女的动画:

1
2
3
4
5
if (fairy.vy > fairy.oldVy) {
if(!fairy.playing) {
fairy.playAnimation();
}
}

如果她开始下降,停止动画,只显示精灵的第一帧。

1
2
3
4
if (fairy.vy < 0 && fairy.oldVy > 0) {
if (fairy.playing) fairy.stopAnimation();
fairy.show(0);
}

这就是仙女怎么飞的!

做一个滚动的背景

Flappy Fairy的一个有趣的新特性是,它有一个无限滚动的背景云从右向左移动。

scrolling background

背景的移动速度比绿色的柱子要慢,这给人一种云离得更远的错觉。(这是一种浅显的伪3D效果,叫做抛物线滚动。)

背景只是一个图像。

background

这张照片的设计使得云平铺无缝:顶部和左边的云与右下角的云相匹配。这意味着您可以连接同一映像的多个实例,它们将显示为创建单个、连续的映像。(图片来自OpenGameArt)

因为这对游戏非常有用,Hexi有一种精灵类型叫做tilingSprite,专为无限的滚动效果而设计。下面是如何创建tilingSprite:

1
2
3
4
5
sky = g.tilingSprite(
"sky.png" //The image to use
g.canvas.width, //The width
g.canvas.height, //The height
);

第一个参数是您想要使用的图像,最后两个参数是精灵的宽度和高度。

贴砖精灵与普通精灵具有相同的属性,增加了两个新属性:tileX和tileY。这两个属性允许您设置精灵左上角的图像偏移量。如果你想连续不断地制作一个平铺的精灵滚动条,只需在游戏循环的每一帧中增加少量的tileX值,如下所示:

1
sky.tileX -= 1;

这就是你做无限滚动背景所需要做的。

粒子效果

你如何创造像火,烟,魔法和爆炸这样的效果?你制造了很多小精灵;有几十个,成百上千个。然后对这些精灵施加一些物理或重力约束,使它们的行为类似于你要模拟的元素。您还需要给他们一些规则,关于他们应该如何出现和消失,以及他们应该形成什么样的模式。这些小精灵被称为粒子。你可以用它们为游戏制作各种各样的特效。

Hexi有一种多功能的内置方法叫做createParticles,它可以制造游戏所需的大多数粒子效果。下面是使用它的格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
createParticles(
pointer.x, //The particle’s starting x position
pointer.y, //The particle’s starting y position
() => sprite("images/star.png"), //Particle function
g.stage, //The container to add the particles to
20, //Number of particles
0.1, //Gravity
true, //Random spacing
0, 6.28, //Min/max angle
12, 24, //Min/max size
1, 2, //Min/max speed
0.005, 0.01, //Min/max scale speed
0.005, 0.01, //Min/max alpha speed
0.05, 0.1 //Min/max rotation speed
);

您可以看到,大多数参数描述了最小值和最大值之间的范围,这些值应该用于更改精灵的速度、旋转、缩放或alpha。您还可以指定应该创建的粒子数,并添加可选的重力。通过定制第三个参数,您可以使用任何精灵生成粒子。只要提供一个函数,返回你想为每个粒子使用的精灵:

1
() => ("images/star.png"),

如果你提供一个具有多个帧的精灵,createParticles方法会自动为每个粒子选择一个随机的帧。最小和最大角度值对于定义粒子从原点辐射出来时的圆形传播非常重要。对于完全圆形的爆炸效果,使用最小角度为0,最大角度为6.28。

1
0, 6.28,

(这些值弧度;等于0和360。)0从3点钟方向开始,指向右边。3.14是9点的位置,6.28让你再次回到0点。如果你想把粒子范围限制在一个更窄的角度,只要提供描述这个范围的最小值和最大值。这里有一些值,你可以用来将角度限制在比萨斜面上,地壳指向左边。

1
2.4, 3.6,

你可以使用像这样的受限角度范围来创建粒子流,就像那些用来创建喷泉或火箭引擎火焰的粒子流。(你马上就会知道怎么做了。)随机间隔值(第六个参数)决定了粒子在这个范围内的间隔是均匀的(假)还是随机的(真)。通过仔细地为粒子选择精灵并精细地调整每个参数,您可以使用这个通用的createparticle方法来模拟从液体到火焰的一切。在Flappy Fairy中,它被用来创造仙尘。

仙女粉尘爆炸

当Flappy Fairy撞上一块木头时,它就消失在一阵尘土中。

explosions

那效果如何呢?

在创建爆炸效果之前,我们必须定义一个数组,该数组列出了我们想要为每个粒子使用的图像。如前所述,如果精灵包含多个帧,那么createparticle方法将在精灵上随机显示一个帧。要完成这项工作,首先定义一组纹理图集框架,你想用它来应对仙女的尘埃爆炸:

1
2
3
4
5
6
dustFrames = [
"pink.png",
"yellow.png",
"green.png",
"violet.png"
];

当仙女碰到一个绿色的方块时,爆炸就发生了。游戏循环在hitTestRectangle方法的帮助下实现了这一点。代码循环遍历这些块。对每个绿色块和精灵之间的冲突进行数组和测试。如果hitTestRectangle返回true,则循环退出,名为fairyVsBlock的冲突对象变为true

1
2
3
let fairyVsBlock = blocks.children.some(block => {
return g.hitTestRectangle(fairy, block, true);
});

hitTestRectangle的第三个参数需要是true,这样就可以使用sprite的全局坐标(gx和gy)来完成碰撞检测。这是因为仙女是舞台的子元素,但是每个块都是块组的子元素。这意味着它们不共享相同的局部坐标空间。使用块精灵的全局坐标迫使hitTestRectangle使用它们相对于画布的位置。

如果fairyVsBlock是正确的,并且精灵现在是可见的,那么冲突代码就会运行。它使精灵隐形,产生粒子爆炸,并在延迟3秒后调用游戏的复位功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (fairyVsBlock && fairy.visible) {

//Make the fairy invisible
fairy.visible = false;

//Create a fairy dust explosion
g.createParticles(
fairy.centerX, fairy.centerY, //x and y position
() => g.sprite(dustFrames), //Particle sprite
g.stage, //The container to add the particles to
20, //Number of particles
0, //Gravity
false, //Random spacing
0, 6.28, //Min/max angle
16, 32, //Min/max size
1, 3 //Min/max speed
);

//Stop the dust emitter that's trailing the fairy
dust.stop();

//Wait 3 seconds and then reset the game
g.wait(3000, reset);
}

使用粒子发射器

粒子发射器只是一个简单的计时器,以固定的间隔产生粒子。这意味着发送器不再只调用createParticles方法一次,而是周期性地调用它。Hexi有一个内置的粒子发射器方法让我们很容易做到这一点。下面是如何使用它的方法:

1
2
3
4
5
6
let particleStream = g.particleEmitter(
100, //The interval
() => g.createParticles( //The `particleEffect` function
//Assign particle parameters...
)
);

粒子发射器法只是围绕着创造粒子法。它的第一个参数是一个以毫秒为单位的数字,它决定了粒子产生的频率。第二个参数是createparticle方法,您可以随意定制它。粒子发射器方法返回一个带有播放和停止方法的对象,您可以使用它来控制粒子流。你可以使用它们就像你用来控制精灵动画的游戏和停止方法一样。

1
2
particleStream.play();
particleStream.stop();

发射器对象还有一个playing属性,根据发射器的当前状态,该属性可以为真,也可以为假。(参见示例文件夹中的particleEmitter .html文件,了解如何创建和使用粒子发射器的更多细节。

Flappy Fairy中使用了一个粒子发射器,当仙女拍打翅膀时,它就会发出一串五颜六色的粒子。粒子的角度被限制在2.4到3.6弧度之间,所以它们以一个锥形的楔形向仙女的左边发射。

emitter

粒子流随机地发出粉色、黄色、绿色或紫色的粒子,每个粒子都是纹理图谱上的一个单独的框架。

以下是产生这种效果的代码:

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
dustFrames = [
"pink.png",
"yellow.png",
"green.png",
"violet.png"
];

//Create the emitter
dust = g.particleEmitter(
300, //The interval
() => {
g.createParticles( //The function
fairy.x + 8, //x position
fairy.y + fairy.halfHeight + 8, //y position
() => g.sprite(dustFrames), //Particle sprite
g.stage, //The container to add the particles to
3, //Number of particles
0, //Gravity
true, //Random spacing
2.4, 3.6, //Min/max angle
12, 18, //Min/max size
1, 2, //Min/max speed
0.005, 0.01, //Min/max scale speed
0.005, 0.01, //Min/max alpha speed
0.05, 0.1 //Min/max rotation speed
);
}
);

您现在可以使用play和stop方法控制尘埃发射器。

创造和移动柱子

你现在知道了Flappy Fairy如何实现了河西的一些特殊功能,以获得一些有趣和有用的效果。但是,如果你是游戏编程的新手,你可能也想知道,那个飘飘欲仙的世界是如何被创造出来的。让我们快速地看一下创建并移动精灵必须导航的绿色柱子的代码,以达到完成标记。

游戏中有15根绿色柱子。每隔5根柱子,顶部和底部之间的缝隙就会缩小。前五根柱子有四个街区的空隙,后五根柱子有三个街区的空隙,后五根柱子有两个街区的空隙。这使得这个游戏变得越来越困难,因为Flappy仙女飞得更远。每一根柱子上的空隙的确切位置都是随机的,每次游戏都是不同的。每根柱子的间距是384像素,这是它们相邻时的样子。

blocks

你可以看到这个缺口是如何从左到右逐渐缩小的。

构成柱子的所有砖块都属于一个叫做blocks的group。

1
blocks = g.group();

嵌套的for循环创建每个块并将其添加到blocks容器中。外环运行15次;一次创建每个支柱。内循环运行8次;在柱子上每隔一段。这些块只有在它们没有占据被随机选择的距离时才会被添加。每五次外环运行一次,间隙的大小就会缩小一倍。

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
//What should the initial size of the gap be between the pillars?
let gapSize = 4;

//How many pillars?
let numberOfPillars = 15;

//Loop 15 times to make 15 pillars
for (let i = 0; i < numberOfPillars; i++) {

//Randomly place the gap somewhere inside the pillar
let startGapNumber = g.randomInt(0, 8 - gapSize);

//Reduce the `gapSize` by one after every fifth pillar. This is
//what makes gaps gradually become narrower
if (i > 0 && i % 5 === 0) gapSize -= 1;

//Create a block if it's not within the range of numbers
//occupied by the gap
for (let j = 0; j < 8; j++) {
if (j < startGapNumber || j > startGapNumber + gapSize - 1) {
let block = g.sprite("greenBlock.png");
blocks.addChild(block);

//Space each pillar 384 pixels apart. The first pillar will be
//placed at an x position of 512
block.x = (i * 384) + 512;
block.y = j * 64;
}
}

//After the pillars have been created, add the finish image
//right at the end
if (i === numberOfPillars - 1) {
finish = g.sprite("finish.png");
blocks.addChild(finish);
finish.x = (i * 384) + 896;
finish.y = 192;
}
}

代码的最后一部分向世界添加了一个大型的finish精灵,Flappy Fairy将会看到它是否能够一直运行到最后。

游戏循环在每一帧中向右移动2个像素块组,但只在结束精灵不在屏幕上时:

1
2
3
if (finish.gx > 256) {
blocks.x -= 2;
}

当完成的精灵滚动到画布的中心时,块容器将停止移动。注意,代码使用finish sprite的全局x位置(gx)来测试它是否在画布区域内。因为全局坐标是相对于画布的,而不是父容器的,所以对于这些需要在画布上找到嵌套精灵位置的情况,它们是非常有用的。

请确保在示例文件夹中检查完整的Flappy Fairy源代码,以便您能够在正确的上下文中看到所有这些代码。

与HTML和CSS的集成

Hexi可以无缝地使用HTML和CSS。您可以自由地将Hexi精灵和代码与HTML元素混合,并使用Hexi的架构来构建一个基于HTML的应用程序。并且,您可以使用HTML为您的Hexi游戏构建一个丰富的用户界面。

它是如何工作的呢?河西采取了完全不干涉的方法。只需编写普通的旧HTML和CSS,然后在Hexi代码中引用HTML。这是所有!Hexi并没有重新发明轮子,所以你可以编写任何你喜欢的低水平的HTML/CSS代码,并将它混合到你的Hexi应用程序中。

您可以在这个代码存储库中的Hexi示例中找到一个html文件夹中的工作示例。这是一个简单的数字猜谜游戏:

包含按钮和文本输入字段的灰色框是HTML元素。这些HTML元素(包括按钮)完全是使用CSS样式的。动态文本和图像是六次精灵。

还有一个无形的<div>元素,它的大小和Hexi的画布一样,位置也一样。大的<div>元素浮在画布上,包含灰色框、按钮和输入字段。

让我们快速地看看这是如何工作的。主要的。html文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!doctype html>
<meta charset="utf-8">
<title>Html integration</title>
<link rel="stylesheet" href="style.css">
<body>

<!-- UI -->
<div id="ui">
<div id="box">
<button>Guess!</button>
<input id="input" type="text" placeholder="X..." maxlength="10" autofocus>
<div>
</div>

<!-- Hexi -->
<script src="../../src/modules/pixi.js/bin/pixi.js"></script>
<script src="../../bin/modules.js"></script>
<script src="../../bin/core.js"></script>

<!-- Main application file -->
<script src="main.js"></script>
</body>

重要的部分是UI部分,就在<body>标签下面。带有id ui的div用于封装框、按钮和输入。

魔术发生在style.css文件。以下是最重要的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
canvas
{ position : relative
}

#ui
{ position : absolute
; left : 0
; top : 0
; width : 512px
; height : 512px

/*Important: set the z-index to 1 so that it appears above Hexi's canvas*/
; z-index: 1
}

Hexi的画布被设置为相对,ui div被设置为绝对。ui也被设置为与Hexi的画布一样的宽度和高度512px。非常重要的是,ui的z-index为1,以迫使它在画布之上显示。其他HTML元素(框、按钮和输入字段)都是完全相对于ui div定位的——请检查完整的CSS代码以获得详细信息。

要访问你的Hexi代码中的按钮和输入字段,只需在河西的设置函数中创建对它们的引用:

1
2
3
4
5
6
7
8
function setup() {

//Html elements
var button = document.querySelector("button");
button.addEventListener("click", buttonClickHandler, false);
var input = document.querySelector("#input");

//...The rest of the setup code creates Hexi sprites...

然后创建一个普通的函数来处理按钮点击,像这样:

1
2
3
4
5
6
7
function buttonClickHandler(event) {

//Capture the player's input from the HTML text input field
if (input.value) playersGuess = parseInt(input.value);

//...the rest of the code...
}

input.value允许您访问用户在输入字段中输入的任何内容。这只是普通的Web API代码——没什么特别的!您可以使用该值来更改任何Hexi精灵属性。请查看源代码以获得详细信息,但这并不令人感到意外。

但是示例代码有一个诀窍,那就是它的袖子。整个Hexi应用程序在浏览器中进行扩展和对齐。这意味着,Hexi的画布和UI div的扩展和保持一致。如果用户改变浏览器窗口的大小,它们甚至会重新缩放和重新对齐。这是如何工作的呢?下面是实现此目的的JavaScript代码(在main.js文件中,就在Hexi的标准初始化代码之后):

1
2
3
4
5
6
7
8
//Scale Hexi's canvas
g.scaleToWindow();

//Scale the html UI <div> container
scaleToWindow(document.querySelector("#ui"));
window.addEventListener("resize", function(event){
scaleToWindow(document.querySelector("#ui"));
});

Hexi的canvas是由Hexi引擎在内部缩放的,但是UI层是使用全局scaleToWindow函数缩放的。(你可以在这里找到scaleToWindow函数。)

HTML和Hexi之间的松散集成意味着您可以随意定制它。如果你想的话,你可以做一些疯狂的低级HTML/CSS编程,把逻辑和你的Hexi精灵混合在一起,设计任何你需要的自定义布局。它只是HTML !而且,是的,如果你想的话,你可以用Angular, React或Elm来编写你的HTML。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2021 朝着牛逼的道路一路狂奔 All Rights Reserved.

访客数 : | 访问量 :