如何制作 Pong 游戏 - hyperPong

**注意:**本教程的完整项目文件可在本文末尾找到。
简介
在本教程中,我们将从头开始制作一款类似 Pong 的游戏。我们会介绍 hyperPad 中一些较高级的功能和行为,因此本教程假设你已经对 hyperPad 编辑器有基本了解。如果你觉得自己需要先熟悉从零开始制作游戏,可以先查看完整指南 - 创建平台游戏教程。
Pong 是一款简单的双人游戏,只由两个球拍和一个球组成,可以看作是乒乓球的简易数字版。每位玩家控制球桌一侧的一个球拍。球拍只能前后移动,将球击向对手。如果一名玩家没接到球,另一名玩家就得一分。率先得到 11 分的玩家获胜!
在我们的 Pong 游戏中,玩家可以选择 1 人模式(玩家对 AI)、2 人模式(玩家对玩家),或 0 人“观战”模式(AI 对 AI)。
第 1 部分:设置项目和主菜单
让我们从创建一个新项目开始。
打开 hyperPad 中的“项目”菜单,然后轻点“新建项目”。为项目命名并轻点“下一步”。方向可以保持默认的横向模式。本教程将“设备支持”设为 iPad 4:3。在下一个屏幕中,选择“鸟瞰视图”,其余选项保持默认。
现在我们已经准备好一个新项目,先快速设置主菜单,然后再直接开始制作游戏。首先,将场景重命名为“主菜单”。
轻点“项目/场景菜单”按钮,然后轻点“场景 1”旁边的齿轮按钮。
轻点“设置”选项,将场景重命名为“主菜单”。轻点“保存”以保存并关闭设置窗口。
接下来,为了延续早期复古街机游戏(如 Pong)的传统,我们可以把背景色设为黑色。
轻点“场景设置”按钮并选择“背景”。轻点“更改颜色”按钮,然后将颜色设为黑色(#000000FF)。
现在为主菜单添加标题标签。轻点“特殊对象”按钮并选择“标签”。
在文本字段中输入游戏名称(本教程中输入的是“hyperPong”),将字体大小设得足够大,但不要太大。“hyperPong”标题使用 72 磅大小比较合适。将颜色改为白色(#FFFFFFFF),并将其放在菜单上方靠近顶部的中央位置。
现在,我们已经有了主菜单场景,可以添加按钮来列出所有游戏模式。暂时先保持菜单这样,等游戏可以玩之后再完成它。
第 2 部分:设置游戏场景和球拍
轻点“项目菜单”按钮,然后轻点“添加场景”。将场景命名为“游戏”并轻点“创建”。现在进入“游戏”场景后,你应该会看到一块空白画布。和“主菜单”场景一样,再次轻点“场景设置”按钮,将背景改为黑色(#000000FF)。
开始制作玩家球拍。轻点“特殊对象”按钮并选择“空对象”,创建一个空对象。将它的颜色改为白色(#FFFFFFFF),并确保不透明度滑块调到了最满。
将对象重命名为“Player-1”,把它的物理模式设为“物理”,并保持“可穿透”处于启用状态。
保持“可穿透”启用的原因是,这样我们就能更好地控制球击中球拍时的行为。我们希望能够手动控制球的速度和方向。
在 Player-1 对象属性中,打开“变换”选项卡,将 X 缩放设为 25%,并将它移到游戏场地左侧。
接下来,为球拍添加控制方式,让它能够移动。选中球拍后,回到“物理”选项卡,打开它的行为编辑器。
在“交互”部分中放置一个“摇杆模拟”行为(如果找不到,可能需要轻点“高级”选项卡)。这会自动在场景编辑器中为你创建一个摇杆。
不过,我们不希望球拍只能通过摇杆移动。我们还希望 AI 也能控制球拍移动。
在“自定义”部分中放置一个“方框容器”。它会存储球拍的移动方向。这样一来,无论是玩家操作摇杆,还是 AI 进行计算,都可以通过更改这一个值来控制球拍移动。默认值应设为 0。最好给它取个易于识别的名称,方便后续使用,例如“移动方向”。
选中“方框容器”后,按下“更改输入字段”按钮并轻点“是”。这会为方框容器创建一个“设置输入字段”行为,以便在需要时更改它的值。将“设置输入字段”连接到“摇杆模拟”。
选中“设置输入字段”行为,将“摇杆模拟”的输出值拖到“新值”字段中。选择“y”属性。现在,“移动方向”方框容器会跟踪玩家上下推动摇杆的幅度(y 轴)。
现在我们已经在跟踪摇杆上下移动的幅度,还需要在玩家松开摇杆时将它重置为零。否则,球拍会一直沿着最后记住的方向移动。
在“交互”部分中添加一个“停止触摸”行为,然后轻点它以打开属性。默认情况下,它会跟踪玩家何时停止触摸球拍,但我们想要跟踪的是玩家何时停止触摸摇杆。
轻点对象属性进行更改,然后选择摇杆。你可能需要先选择“全局 UI”层才能选中它。选中后,轻点勾选按钮。现在,这个行为应该会显示摇杆是被跟踪的对象。
轻点之前创建的“设置输入字段”并将其复制。将复制出的“设置输入字段”连接到“停止触摸”行为,并将它的“新值”属性设为 0。
现在,“移动方向”方框容器变量会跟踪摇杆的上下移动,并在松开摇杆时将其重置为 0。这为控制球拍移动提供了一种非常有效的方法,接下来让我们赋予球拍移动能力。
在“自定义”部分中放置一个“计时器”行为。在它的属性中,将间隔设为 0。这个计时器会尽可能频繁地执行(每帧执行一次),这样我们就能非常频繁地更新球拍的移动。
在设置球拍的速度之前,我们要确保“移动方向”方框容器的值限制在合理范围内,以防数值过大或过小。否则,有时球拍会移动得太快。
在“逻辑”部分中放置一个“最小值”行为和一个“最大值”行为。将“最小值”连接到计时器,再将“最大值”连接到“最小值”。你应该会得到类似这样的效果:
在“最小值”行为的属性中,将第一个值设为“移动方向”行为的输出值,第二个值设为 1。
“最小值”行为会输出输入值中最小的数字。因此,如果“移动方向”的值大于 1,它就会输出 1;否则,它会直接输出“移动方向”的值。
在“最大值”行为的属性中,将第一个值设为“最小值”的结果,第二个值设为 -1。
“最大值”行为会输出输入值中最大的数字。因此,如果“移动方向”的值小于 -1,它就会输出 -1;否则,它会输出“最小值”的值(即 1 或“移动方向”中较小的那个值)。
这意味着“最大值”最终输出的值,实际上是限制在 -1 到 +1 之间的“移动方向”值。这样我们就能完全控制球拍的最大移动速度!
-1 到 +1 的范围容易使用,但如果直接把它当作球拍速度,速度会太慢。
再次在“逻辑”部分中放置一个“乘以数值”行为,并将它连接到“最大值”行为。第一个值设为“最大值”行为的输出结果。第二个值可以设为合适的“最大速度”。
现在先设为 20。如果之后觉得球拍移动得太慢或太快,只需将这个数字调高或调低即可。
现在,“乘以数值”行为为我们提供了一个合适的速度范围,最低为 -20,最高为 +20。
现在可以设置球拍的速度了。在“物理”部分中添加一个“设置速度”行为,并将它连接到“乘以数值”行为。将“x”字段设为 0,将“y”字段设为“乘以数值”行为的输出结果。
试试看!运行游戏时,球拍应该会随着摇杆上下移动。
测试时你可能已经注意到,没有任何东西能阻止球拍移出屏幕。在下一部分中,我们会添加边界,让所有东西都保持在可见范围内。
第 3 部分:游戏边界
继续之前,先考虑一下良好的游戏设计。随着我们逐步完善游戏,玩家球拍会靠近屏幕边缘,而不是位于屏幕中央。这会造成一个问题:UI 元素(例如摇杆)会与游戏元素(例如球拍)重叠。
我们不应将整个屏幕作为可玩区域,而应该将边界向内缩进,这样球拍就不会移动到手指所在的摇杆下方,避免它难以看清。这样,在可玩区域底部附近时,我们就不会看不见自己的球拍。我们还可以将这个缩进边界用作硬性边界,防止对象离开可玩区域。
轻点“特殊对象”按钮并创建一个新的“空对象”。轻点这个空对象以打开它的属性。
将它的物理模式保持为“墙”,并禁用“可穿透”。将它的颜色设为方便观察的颜色;不透明的深灰色(#1F1F1FFF)就很合适。将摩擦力设为 0%,弹性设为 100%。
现在,在墙的“变换”选项卡中,将 Y 缩放设为 200%,并把它移到屏幕区域顶部。接着拉伸它,使其覆盖屏幕区域的整个宽度。你还可以启用网格吸附模式,帮助你准确放置。
现在复制这面墙,并将副本移到屏幕底部。
再复制一次,并将它的 X 缩放改为 200%。然后拉伸它的高度,使其覆盖屏幕,并将它放到屏幕左侧。
再复制一次这面竖墙,将它移到屏幕右侧。现在,你应该已经用墙完整围住了游戏场地:
现在运行游戏,你可能会马上发现自己可以像墙不存在一样穿过它。这有两个原因:我们将球拍设为了“可穿透”,并且每帧都在强制设置球拍的速度。这没关系,我们只需要实现一种机制来阻止球拍移动到墙里。
要实现这个行为,我们会检测玩家的位置与墙的位置。如果玩家越过边界,就把玩家移回可玩区域。
为了方便实现,轻点顶部的墙,将 Y 锚点设为 0%。这样,墙的位置就会锚定到它自身的最底部。你可能需要轻点锁形图标解锁锚点字段。
同样,将底部墙的 Y 锚点设为 100%。这样,底部墙的位置就会锚定到它自身的最顶部。
现在可以实现边界检查行为了。轻点球拍并打开它的行为编辑器。
首先,在“逻辑”部分中放置一个“获取边界框”。它默认应该会使用球拍。这个行为会提供球拍的底部和左侧坐标,以及球拍的尺寸。
接下来,在“逻辑”部分中放置一个“相加数值”,并将它连接到“获取边界框”行为。在“相加数值”的属性中,将第一个值设为“获取边界框”结果中的“y”,第二个值设为结果中的“height”。
相加后的结果就是球拍顶部的位置。为了便于阅读,你可以将此行为重命名为更有意义的名称,例如“相加数值 - 球拍顶部”,这样我们就能清楚地看出它的用途。
现在检查球拍是否正在移入顶部或底部的墙。在“变换”部分中放置两个“获取位置”行为,并将它们并行连接到“相加数值”行为。
将第一个“获取位置”行为设为跟踪底部墙,将第二个设为跟踪顶部墙。你也可以重命名这些行为,方便阅读。现在应该会得到类似这样的结果:
为每个“获取位置”行为放置并连接一个“相减数值”行为,位于“逻辑”部分中。
对于连接到跟踪底部墙的“获取位置”行为的“相减数值”,将第一个值设为“获取位置 - 底部墙”结果中的“position_y”,再将第二个值设为“获取边界框”中的“y”。
对于连接到跟踪顶部墙的“获取位置”行为的另一个“相减数值”,将第一个值设为“获取位置 - 顶部墙”结果中的“position_y”,再将第二个值设为“相加数值 - 球拍顶部”结果。
这些相减运算会告诉我们需要将球拍“推回”多远才能让它留在可玩区域中。现在只需判断是否需要推回即可。
在“逻辑”部分中,为两个行为分支分别放置并连接一个“If”行为。
对于连接到“底部墙”分支的“If”行为,将第一个值设为所连接“相减数值”行为的结果。条件设为 >(“大于”),第二个值设为 0。
对于连接到“顶部墙”分支的“If”行为,将第一个值设为所连接“相减数值”行为的结果。条件设为 <(“小于”),第二个值设为 0。
这两个“If”语句会分别在球拍越过对应的墙时继续执行。
如果球拍越过墙,我们要强制停止球拍继续移动,将它的速度重置为 0,然后把它推回游戏场地。
在“物理”部分中,为每个分支放置并连接一个“设置速度”行为。将两个行为的速度 X/Y 值都设为 0。
现在可以把球拍推回可玩区域了。在“变换”部分中,为每个分支放置一个“按偏移量移动”行为。
在每个“按偏移量移动”行为中,保持“按偏移量移动 X”的值为 0,并将“按偏移量移动 Y”的值设为对应分支中“相减数值”的结果。然后将持续时间设为 0 秒(立即执行)。
执行这一整套行为时,它会检查球拍是否进入墙内、进入了多远,并在需要时将球拍推出墙外。现在,我们只需要在球拍移动时执行这套行为链。
为简单起见,在这套行为链的最顶部,从“自定义”部分添加一个“行为组”,并将它连接到“获取边界框”上方。你可以将它重命名为更有意义的名称,例如“检查边界”。
注意:“行为组”行为本身不会执行任何操作;在这里,它只是用来标记检查球拍是否越界的“起点”。
接下来,在“自定义”部分中放置一个“执行行为”。在它的属性中,轻点“选择行为”字段,然后选择刚刚创建的“检查边界”行为。现在,将“执行行为”移到原先设置球拍速度的行为下方。将“执行行为”连接到原本球拍移动行为链中的“设置速度”行为。
现在,每当球拍更新移动速度时,“执行行为”都会将执行流程“跳转”到新的“检查边界”行为链,检查球拍是否移出了边界!
试试看,看看效果!
继续之前还有最后一件事:把球拍移到场地左侧。
现在有了足够的空间,让我们来制作球!
第 4 部分:制作球
返回场景编辑器并创建一个空对象。将它的物理模式设为“物理”,并禁用“可穿透”,这样球就能移动并与边界墙发生物理碰撞。将摩擦力设为 0%,弹性设为 100%。将颜色设为白色(#FFFFFFFF)。为简单起见,也可以把这个对象重命名为“球”。
在“变换”选项卡中,将缩放设为 X=25%、Y=25%,位置设为 X=50%、Y=50%。
接下来,打开球的行为编辑器,为它添加移动行为。在“自定义”部分中放置一个“等待”行为,并将等待时间设为 3 秒。
然后,从“逻辑”部分放置一个“随机数”行为并将它连接到“等待”行为。第一个值设为 -1.0,第二个值设为 1.0。务必包含“.0”,这样它才会随机选择 -1.0 到 1.0 之间(包括小数)的数值;否则它只会选择整数。这个值将作为球首次发球时随机生成的“击球角度方向”。
接下来,从“物理”选项卡中放置并连接一个“施加力”行为。将力的 X 设为 -1,将力的 Y 设为“随机数”行为的结果。这个值会决定球移动的方向,但 1 牛顿的力并不大,所以我们还应该调整乘数。将乘数设为 150,然后测试游戏,看看速度是太快还是太慢。如果不合适,可以按喜好调整这个乘数。运行游戏后,球应该会在 3 秒后开始移动,就像刚刚被发球一样。
测试时,球应该会在墙之间弹跳,但会穿过球拍。这很正常,因为我们还没有为球实现与球拍碰撞时的行为。你可能还会注意到球有时会开始旋转;我们不希望它这样,所以来修正一下。
回到球的行为编辑器,在“变换”部分的任意位置放置一个“锁定旋转”行为,然后再次测试。现在,球就不会再失控旋转了。
开始添加球拍碰撞行为之前,先回到场景编辑器,轻点球拍,然后在球拍属性中打开“标签”选项卡。在文本字段中输入“Paddle”,然后按下 + 按钮为球拍添加这个标签。
现在返回球的行为编辑器。在“对象”部分的任意位置放置一个“碰撞”行为。在这个行为的属性中,轻点“标签”选项卡,然后选择我们创建的 Paddle 标签。现在,每当球撞到任何带有“Paddle”标签的对象时,这个事件都会执行。
我们需要获取球当前的速度,并反转它的 X 速度。这样,如果球正在向左移动,就会开始向右移动,反之亦然。我们还希望球比之前快一点点,这样每打几回合,球就会逐渐加速。
将“获取速度”行为(位于“物理”部分)连接到“碰撞”行为。然后从“逻辑”选项卡连接一个“乘以数值”行为。将第一个值设为“获取速度”的“velocity_x”结果,第二个值设为 -1.03。这个乘积会得到球的负 X 速度,同时将速度提高 3%。接下来,将这个变化应用到球上。
将“设置速度”行为(位于“物理”部分)连接到“乘以数值”行为。将它的 X 速度设为“乘以数值”的结果,Y 速度保持为当前速度。
现在运行游戏,球应该会来回弹跳,击中球拍后反转方向,并略微加速(几乎察觉不到)。
游戏逐渐成形了!球和球拍移动得很顺畅,但球的击球角度不会改变。球击中球拍时,击球角度也应该改变:如果球击中球拍靠上的位置,就应该向上弹;如果击中靠下的位置,就应该向下弹。这会增加游戏的复杂度,也增加技巧性,因为玩家可以在一定程度上控制球的去向。
要确定球的角度应该改变多少,我们需要获取球和球拍之间的位置差。在“变换”部分放置两个“获取位置”行为,并将它们依次连接到“设置速度”行为之后。第一个“获取位置”行为默认继续跟踪球。将第二个“获取位置”行为的对象改为“碰撞”事件行为的输出“ObjectB_ID”。
**注意:**这里的“ObjectB”是发生碰撞的球拍,因为它是碰撞事件相关的第二个对象;第一个对象(“ObjectA”)则是球。
从“逻辑”部分放置并连接一个“相减数值”行为。将第一个值设为跟踪球的第一个“获取位置”行为的“position_y”结果。将第二个值设为跟踪发生碰撞的球拍的第二个“获取位置”行为的“position_y”结果。
然后,从“物理”部分放置并连接一个“施加力”行为。将 X 方向的力设为 0,将 Y 方向的力设为“相减数值”的结果。仅靠位置差产生的力还不够强,因此还需要调整乘数。试试将乘数设为 50,看看游戏中的感觉如何。
测试时,球的击球角度现在应该会根据你击中球拍的位置而改变。如果球击中球拍靠上的位置,就应该更向上弹;如果击中靠下的位置,就应该向下弹。如果球的弹射方向反了,试着交换“相减数值”行为中的两个值,就能解决问题。
球的移动基本上就要完成了!还剩一个小问题:球的速度没有上限!事实上,如果玩一段时间而不重置,球会快到甚至穿过其他对象。我们应该限制球的速度,避免它达到这种极高的速度。
在“施加力”行为之后,从“物理”部分放置并连接一个“获取速度”行为,以获取球的新速度。在它下方,从“逻辑”部分依次放置并连接一个“最大值”行为和一个“最小值”行为。
在“最大值”的属性中,将第一个值设为“获取速度”的“velocity_x”结果,第二个值设为 -25。它会输出两个值中较大的那个。
在“最小值”的属性中,将第一个值设为“最大值”的结果,第二个值设为 25。它会输出较小的值,从而将球的 X 速度限制在 -25 到 25 之间,不能更高,也不能更低!
现在对球的 Y 速度做同样的处理。再依次放置并连接一组“最大值”和“最小值”。在新的“最大值”属性中,将第一个值设为“获取速度”的“velocity_y”结果,第二个值设为 -10。
在新的“最小值”属性中,将第一个值设为新的“最大值”的结果,第二个值设为 10。这样,Y 速度就会被限制在 -10 到 10 之间。你也可以之后按喜好更改这些数值!
现在球的速度值已经被限制,接下来把它们应用到球上。从“物理”选项卡放置并连接一个“设置速度”行为。在它的属性中,将 X 速度设为限制 X 速度的第一个“最小值”行为的结果,再将 Y 速度设为限制 Y 速度的第二个“最小值”行为的结果。
现在,无论一局比赛持续多久,球的速度都不会变得过快。Y 速度也受到了限制,所以球的击球角度不会过大。
第 5 部分:球拍人工智能
虽然复制球拍并制作第二个可玩的球拍也可以,但如果能选择与 AI 对战,而不只是进行双人游戏,岂不是更有趣?当然了!在这一部分中,我们会让球拍拥有 AI,并让玩家可以选择双人模式或与 AI 对战。
我们已经为玩家移动搭建好了系统——也就是更改“移动方向”方框容器的值。用它实现 AI 非常直接。球拍不再使用摇杆,而是根据球在它的上方还是下方来改变“移动方向”。
我们还会给“移动方向”添加一些熵,换句话说,就是一些随机性。这样可以让 AI 显得有点像人类,不会那么机械,也不会总是完美地玩游戏。
下面是我们对 AI 的定义:
- 只有球正在朝 AI 移动时,AI 才会移动。
- “移动方向”就是球和球拍的 Y 轴(垂直)位置差,再加上一些熵。
- 每当球撞到任何东西(球拍、墙或其他对象)时,球拍 AI 的熵都会改变。
- 熵取决于球的速度:球移动得越快,熵值就越高,因此球拍就越不准确。
听起来很简单,开始动手吧。
轻点球拍并进入它的行为编辑器。找到一块空白区域,从“自定义”部分放置一个新的“计时器”行为。将计时器设为 0 秒。为了便于阅读,可以将它重命名为“AI 思考”。
**注意:**如果这里或之后出现提示,说 0 秒计时器过多,可以放心忽略。在我们的项目中不会有很多个。
首先,我们要把“移动方向”设为 0,这样球拍在不该移动时就会停止。轻点第 2 部分中创建的“移动方向”方框行为,然后轻点“更改输入字段”按钮,创建一个新的“设置输入字段”行为。将“设置输入字段”行为连接到“AI 思考”计时器,并将它的“新值”属性设为 0。
在“变换”部分中放置两个“获取位置”行为,并将它们依次连接到“AI 思考”计时器的第二个执行连接点。将其中一个设为跟踪球,另一个保持跟踪球拍。为了更易阅读,也可以重命名这些行为。你应该会得到类似这样的结果:
这样,“移动方向”会先被设为 0,然后才会继续执行其他 AI 行为。
接下来,从“物理”部分放置并连接一个“获取速度”行为,并将跟踪对象设为球。有了这三个行为,我们就拥有了判断球是否正朝球拍移动所需的所有数据。
从“逻辑”部分放置并连接一个“If”行为。将第一个值设为跟踪球拍的“获取位置”行为中的“position_x”结果。将条件设为“小于”(<),第二个值设为跟踪球的“获取位置”行为中的“position_x”结果。
如果这个条件为真,就表示球拍位于球的左侧。换句话说,球在球拍右侧。
我们还需要处理球位于球拍右侧的情况。再放置一个“If”行为,并将它吸附到上一个“If”行为的右侧。这样它会变成“否则如果”行为,在第一个“If”行为为假时执行。它的属性保持原样即可。
现在已经有了判断球在球拍左侧还是右侧的两个分支,接下来需要检查球是否正朝球拍移动。
再放置一个“If”行为并将它连接到第一个“If”行为。将第一个值设为球的“获取速度”行为中的“velocity_x”结果。将条件设为“小于”(<),第二个值设为 0。如果球正向左移动,这个“If”行为就会为真。我们正需要这样的结果,因为前一个“If”行为在球位于球拍右侧时为真!因此,球一定是朝球拍移动。
现在对“否则如果”分支做同样的处理。轻点新的“If”行为并复制它,然后将它连接到“否则如果”行为。将它的条件设为“大于”(>)。这样,如果球位于球拍左侧,这个“If”行为就会执行并检查球是否向右移动。如果是,那么球也一定正朝球拍移动。
如果执行流程通过了这一连串的“If”语句,就表示 AI 可以移动了!现在,我们需要根据球在垂直方向的位置更新“移动方向”。
在“逻辑”部分中放置一个“相减数值”行为。将两个“If”行为都连接到同一个“相减数值”行为。第一个值设为球的“获取位置”行为中的“position_y”结果,第二个值设为球拍的“获取位置”行为中的“position_y”结果。
你应该会得到类似这样的结果:
这样我们就得到了球拍与球在垂直轴上的距离。如果使用这个值,那么球在球拍上方或下方越远,球拍就会尝试移动得越快。现在只需要把这个值设到“移动方向”方框容器中即可!
轻点“移动方向”方框容器,然后按下“更改输入字段”按钮,再创建一个“设置输入字段”行为。将它移动到“相减数值”行为旁并连接起来,将它的“新值”属性设为“相减数值”行为的结果。
测试一下,球拍现在应该会自己动起来!
观看 AI 游戏时,你可能会注意到它玩得非常好。我们现在的 AI 基本上已经可以尽可能完美地玩游戏了。如果想让人类玩家也有合理的获胜机会,这就会成为一个问题。这正是引入熵的原因:给 AI 添加一些随机性,可以让它变得不那么准确、不那么机械,也不会让人类玩家觉得太难对付。
返回球拍的行为编辑器,从“自定义”部分再放置一个“方框容器”,为了便于阅读,将它命名为“熵”。将它的默认值设为 0。然后,从“逻辑”部分放置一个“相加数值”行为。
断开刚刚创建的“设置输入字段”和“相减数值”之间的连接,改为在两者之间连接“相加数值”。在“相加数值”的属性中,将第一个值设为“相减数值”的结果,第二个值设为“熵”方框容器的结果。
在“设置输入字段”的属性中,将“新值”从“相减数值”结果改为“相加数值”结果。
现在,球拍的“移动方向”会加上“熵”方框容器中的值。因此,我们只需在“熵”中存储一个随机值,它就会被加到球拍的“移动方向”中,让球拍变得不那么准确。继续吧。
正如本部分前面所述,我们希望球撞到任何东西(例如球拍或墙)时,熵都会改变。这是为了模拟人类每当球的路线变化时,都必须重新判断球的去向,而且判断可能不准确。
找一块空白区域,从“对象”部分放置一个“碰撞”事件行为。第一个对象选择球作为跟踪对象。第二个对象保持原样。其他设置也都可以保持不变。每当球与任何对象发生碰撞时,这个事件行为都会执行。
本部分前面还提到,我们希望熵也根据球的速度来变化。这是为了模拟球移动得越快,人类判断它的路线就越不准确。
我们会把基础熵设为球的负 Y 轴速度与正 Y 轴速度之间的一个值。从“物理”部分放置并连接一个“获取速度”行为,将它设为跟踪球对象。
从“逻辑”部分放置并连接一个“相减数值”行为。将第一个值设为 0,第二个值设为“获取速度”的“velocity_y”结果。这样就能得到球的负 Y 轴速度。
接下来,从“逻辑”部分放置并连接一个“随机数”行为。将第一个值设为“获取速度”的“velocity_y”结果,第二个值设为“相减数值”的结果。这样就会得到一个介于负“velocity_y”和正“velocity_y”之间的数值。
我们可以直接将随机数的结果作为熵值使用,但这样球拍会变得极其不准确。因此,我们需要稍微缩小这个值。
从“逻辑”部分放置并连接一个“除以数值”行为。将第一个值设为“随机数”的结果,第二个值设为 5。第二个值越大,球拍就越准确;反之,第二个值越小,球拍就越不准确。
**注意:**你可以按喜好更改这个值,让 AI 变得更容易或更难对付。也可以根据所选难度来更改这个值。
现在更新“熵”方框容器。轻点“熵”方框容器,然后轻点“更改输入字段”按钮,为“熵”创建一个“设置输入字段”。将“设置输入字段”连接到“除以数值”,并将它的新值设为“除以数值”的结果。
现在运行游戏,AI 就不会那么机械了。它会犯更多错误,也不会只想着把球打到正中间。
这样一来,我们就完成了创建 AI 所需的所有内容。下一部分中,我们会复制球拍并设置不同的游戏模式。
第 6 部分:第二个球拍和游戏模式
在这一部分中,我们将设置选择不同游戏模式的功能(例如玩家对玩家、玩家对 AI、AI 对 AI),并设置第二个球拍。
在场景编辑器中,选择“全局 UI”层。轻点“创建特殊对象”按钮,创建一个文本为“游戏设置”的标签。将这个标签移到屏幕外,避免玩家看到它。
我们会用这个对象来存储一些游戏设置。由于这个标签位于“全局 UI”层,它会在场景切换时自动保留。存储在此标签上的属性不会在场景切换时重置,因此非常适合用来保存需要保留的数据。
**注意:**某些场景加载行为中的“刷新”设置会导致所有对象(包括全局 UI 对象及其属性)重新加载。在自己的项目中创建全局 UI 对象时,别忘了这一点!
选中“游戏设置”对象,打开“属性”选项卡并创建两个属性。一个叫“Player 1 AI”,另一个叫“Player 2 AI”。将“Player 1 AI”属性设为 0,将“Player 2 AI”属性设为 1。
我们会用这些设置来决定球拍是处于“玩家模式”还是“AI 模式”。例如,如果“Player 1 AI”设为 0,就表示玩家 1 是人类玩家,而不是 AI。如果“Player 2 AI”设为 1,就表示玩家 2 是 AI。我们设置这些默认值,是为了能直接在编辑器中测试游戏。
现在回到“主”层,再次打开球拍的行为编辑器。我们将添加一些行为,根据球拍是玩家还是 AI,禁用相应的行为。
找一块空白区域,从“对象”部分放置一个“获取属性”行为。在它的属性中,将对象字段设为跟踪“游戏设置”标签。你需要在对象选择界面中选择“全局 UI”层才能选中它。
将“属性键类型”设为“预定义”,并将键设为“Player 1 AI”。这个行为会读取游戏设置中玩家 1 的值。
接下来,从“逻辑”部分放置并连接一个“If”行为。将第一个值设为“获取属性”的“attributeValue”结果。如果条件还不是“等于”(=),就将它改为“等于”,并将第二个值设为 0。如果这个球拍应该由玩家控制而不是 AI 控制,这个“If”分支就会执行。
在“自定义”部分中放置并连接一个“关闭行为”。将所选行为设为“AI 思考”计时器。这个行为执行时会禁用“AI 思考”计时器,这意味着只有摇杆可以更改球拍的“移动方向”。
现在需要为 AI 模式做类似的设置。为此,我们需要一个“否则如果”行为。再放置一个“If”行为,并将它吸附到上一个“If”行为的右侧,让它变成“否则如果”。无需更改任何属性。
再次在“自定义”部分中放置两个“关闭行为”,并将它们连接到“否则如果”。其中一个设为“摇杆模拟”行为,另一个设为“停止触摸”行为。
现在,如果球拍设为 AI,它就会禁用摇杆移动事件,只允许“AI 思考”计时器控制移动。这已经很接近我们的目标了,但摇杆既然不起作用,也应该把它隐藏起来。
在“变换”部分中放置并连接一个“隐藏图形”行为。将对象设为摇杆。你可能需要先选择“全局 UI”层才能选中它。
既然我们在这里隐藏了图形,就应该在球拍切换回玩家模式时提供重新显示摇杆的方法。从“变换”部分放置一个“显示图形”行为,并将它连接到关闭“AI 思考”计时器的“关闭行为”。对象设为同一个摇杆。
现在终于可以创建第二个球拍了。轻点 Player 1 球拍,按下复制按钮,然后将它移到可玩区域右侧。打开网格,确保两个球拍对齐。将它的名称设为“Player-2”。
现在需要复制摇杆,让它也有自己独立的控制方式。选择“全局 UI”层并轻点摇杆。复制它并将它也移到右侧。
现在需要更新 Player-2 球拍中的几个行为。回到“主”层,轻点 Player-2 球拍并打开它的行为编辑器。
轻点刚刚创建的“获取属性”。在它的属性中,将键从“Player 1 AI”改为“Player 2 AI”。
然后,将刚刚创建的“显示图形”和“隐藏图形”都改为跟踪第二个摇杆。
最后,将第 2 部分中创建的“摇杆模拟”和“停止触摸”事件行为都改为跟踪第二个摇杆。
现在运行游戏,你应该可以和另一侧的 AI 来回击球了!(记住,我们在游戏设置中将“Player 2 AI”设为了 1,因此会启用 AI 模式并禁用玩家控制。)
现在球拍可以由玩家或 AI 控制了,我们可以开始更新主菜单按钮。进入场景菜单并选择“主菜单”,打开第 1 部分中创建的菜单场景。
轻点“创建特殊对象”按钮并创建一个新标签。将它的颜色改为白色(#FFFFFFFF),以符合主题,并将它放在屏幕中央。把文本改为“单人游戏”。将文本对齐方式也设为居中。
我们需要添加一些行为,让它加载正确的游戏设置。这个按钮应启用玩家对 AI 模式:Player-1 是玩家,Player-2 是 AI。选中新标签并进入它的行为编辑器。
首先,从“交互”部分添加一个“开始触摸”事件行为。它应该已经在跟踪我们的标签。
然后,从“对象”部分放置并连接一个“设置属性”行为。将被跟踪对象设为“游戏设置”标签,你可能需要选择“全局 UI”层才能选中它。将属性键类型设为“预定义”,并选择“Player 1 AI”属性。由于这个按钮会以玩家对 AI 模式启动游戏,将属性值设为 0,这样 Player-1 球拍就不会变成 AI。
接下来,复制“设置属性”行为并将它连接起来。将它的键设为“Player 2 AI”,属性值改为 1。这样 Player-2 球拍就会由 AI 控制。
最后,在“场景”部分中放置并连接一个“加载场景”行为。将所选场景设为“游戏”场景。保持“刷新”选项关闭,否则会重置“游戏设置”属性。如果愿意,也可以选择转场动画。
退出这个标签的行为编辑器,返回“主菜单”场景。选中“单人游戏”按钮,将它复制并移动到另一个标签下方。将文本改为“双人游戏”。
进入新标签的行为编辑器,将属性值改为“Player 1 AI”和“Player 2 AI”都设为 0。这个按钮会启动双人游戏,两边的球拍都由玩家控制。
退出这个标签的行为编辑器,返回“主菜单”场景。选中“双人游戏”按钮,将它复制并移动到另一个标签下方。将文本改为“观看 AI”。
进入新标签的行为编辑器,将属性值改为“Player 1 AI”和“Player 2 AI”都设为 1。这个按钮会启动 AI 对 AI 模式,你可以观看两个 AI 互相对战。
退出这个标签的行为编辑器,返回“主菜单”场景。测试游戏,确保所有游戏模式都正常运行。
“单人游戏”应将左侧球拍设为玩家控制,右侧球拍设为 AI。
“双人游戏”应将左右两侧的球拍都设为玩家控制。
“观看 AI”应将左右两侧的球拍都设为 AI。
第 7 部分:计分和获胜条件
我们的 Pong 克隆游戏目前看起来已经很不错了。在这一部分中,我们会添加计分和获胜功能,让玩家(或 AI)可以互相得分并赢得比赛。打开场景菜单,然后打开“游戏”场景。
轻点球并打开它的行为编辑器。我们需要跟踪球撞到左侧或右侧墙的情况,然后给得分的玩家加分。
在“对象”部分中放置两个“碰撞”行为。其中一个行为的第一个对象默认应为球,第二个对象设为左侧的墙。另一个“碰撞”事件行为的第一个对象也应默认为球,第二个对象设为右侧的墙。
接下来,从“自定义”部分放置两个“广播消息”行为。分别将它们连接到对应的“碰撞”行为。对于连接到左侧墙“碰撞”事件的“广播消息”行为,将它属性中的事件键改为“player 2 scored”。另一个则设为“player 1 scored”。
之后我们会用这些消息给玩家加分。
现在需要让球停止移动并返回原来的位置。在“物理”部分中放置一个“设置速度”行为。将它连接到两条广播消息,并将速度属性 X=0、Y=0。你应该会得到类似这样的结果:
从“变换”部分放置并连接一个“移动到点”行为。将位置类型设为“相对位置(%)”,并将坐标设为 X=50%、Y=50%,同时启用屏幕坐标。然后将持续时间设为 0 秒,使它立即移动。
我们还需要广播一条消息,告知游戏需要重置,这样玩家得分时就能重置发球计时器。
放置并连接一个“广播消息”,将事件键设为“reset”。
在球的行为编辑器中,找到我们在第 4 部分中创建的“等待 3 秒”行为,它会在游戏开始时发球。在它上方,从“自定义”部分放置并连接一个“接收消息”行为,将事件键设为“reset”。
现在,每当游戏重置时,球都会重新发球。不过,比赛刚开始时球不会发球了。我们还需要在创建球时执行发球行为。我们可以通过同时将一个非事件行为连接到“等待”行为来实现。从“自定义”部分放置一个“行为组”,并将它与“接收消息”行为一起连接到“等待”行为。你应该会得到类似这样的结果:
测试游戏时,球每次撞到左侧或右侧墙后都应该会重置,并在 3 秒后发球。
现在我们会在玩家得分时广播消息,接下来创建一个记分牌。关闭行为编辑器,返回场景编辑器。
选择“场景 UI”层,轻点“创建特殊对象”按钮并创建一个新标签。将它的颜色改为白色(#FFFFFFFF),然后移到场地左上方。文本设为“0”,字号设为 72 磅,对齐方式设为居中。
这个标签会记录 Player-1 的分数。进入它的行为编辑器,添加一些计分行为。
在“自定义”部分中放置一个“接收消息”,将事件键设为“player 1 scored”。接下来,在“UI”部分中放置并连接一个“增加分数”行为。将“增加值”设为 1,并将“分数达到时的事件”属性改为 11。
在“场景”部分中放置并连接一个“加载叠加层”行为。将叠加层设为预制的“游戏结束”叠加层。分数达到 11 分时,“增加分数”行为会继续执行并加载“游戏结束”屏幕,标志着比赛结束。
退出行为编辑器并复制记分标签。将它移到右侧,作为 Player-2 的记分标签。
在新标签的行为编辑器中,将“接收消息”的事件改为“player 2 scored”。
测试游戏时,玩家得分后记分牌应该会增加 1 分。如果球撞到左侧墙,Player-2 得分;如果球撞到右侧墙,Player-1 得分。任一玩家达到 11 分后,就会显示“游戏结束”屏幕。
接下来,为屏幕添加一个倒计时,用来显示球发球前的等待时间。
再创建一个标签并将它放在屏幕中央。将颜色设为白色(#FFFFFFFF),并清空文本字段。
打开这个标签的行为编辑器。在“自定义”部分中放置一个“接收消息”,将事件键设为“reset”。接下来,在“UI”部分中放置并连接一个“倒计时”行为,将时间设为 3 秒。
最后,同样在“UI”部分中放置并连接一个“设置标签”,文本字段留空,这样倒计时结束后就不会显示任何文本。
球重置时,我们会看到 3-2-1 的发球倒计时。比赛刚开始时也应该显示倒计时。就像之前设置球的发球行为时那样,放置一个“行为组”,并将它与“接收消息”行为一起连接到“倒计时”行为。你应该会得到类似这样的结果:
测试游戏后,球发球时应该会显示 3-2-1 倒计时。
接下来,把“游戏结束”菜单改为更能体现获胜结果的文字,并进行其他调整。在场景编辑器中打开场景菜单,展开“叠加层”部分。你应该会看到预制的“游戏结束”叠加层。轻点齿轮按钮并进入“设置”,将它的名称改为“玩家 1 获胜”。
接下来,在场景菜单中轻点叠加层的名称以打开它。
首先,轻点“重新开始”标签并删除它。接下来,轻点“主菜单”标签,将文本改为“结束比赛”。这样我们就只能选择返回主菜单,因为重新启动场景也会重置游戏设置,而我们不希望这样。
然后,轻点较大的“游戏结束”标签,将文本改为“玩家 1 获胜!”。最后,将这些标签的颜色改为白色(#FFFFFFFF),以符合游戏的整体主题。现在叠加层应该类似这样:
现在为 Player 2 创建一个叠加层。打开场景菜单,轻点“玩家 1 获胜”叠加层旁边的齿轮按钮。选择“复制”来创建副本。再次打开场景菜单,轻点复制出的叠加层旁边的齿轮按钮,并将它重命名为“玩家 2 获胜”。
在“玩家 2 获胜”叠加层中,将大标签从“玩家 1 获胜!”改为“玩家 2 获胜!”。
现在需要更新计分行为,让它们打开正确的叠加层。在场景菜单中展开“场景”部分并打开“游戏”场景。
轻点 Player-1 记分标签并打开它的行为编辑器。确保“加载叠加层”设为打开“玩家 1 获胜”叠加层。
返回场景编辑器并打开 Player-2 记分标签的行为编辑器。轻点“加载叠加层”行为,将所选叠加层改为“玩家 2 获胜”。
运行游戏后,Player-1 得到 11 分时应该会显示“玩家 1 获胜!”,Player-2 得到 11 分时则会显示“玩家 2 获胜!”。
本教程的这一部分快要完成了!游戏中还应该增加一个细节:改变球的发球方向。目前,球总是朝 Player-1 发球。比赛开始时,球应该随机向左或向右发球;玩家得分时,球应该朝得分的玩家发球。
返回场景编辑器,轻点球并打开它的行为编辑器。从“自定义”部分放置一个“方框容器”,为了方便阅读,将它重命名为“发球方向”。
接下来,从“逻辑”部分放置一个“执行序列”行为。在它的属性中,将“序列类型”改为“随机”。
这个行为会用来决定游戏开始时球的发球方向。轻点“发球方向”方框容器,并按两次“更改输入字段”按钮,创建两个“设置输入字段”行为。将两者并行连接到“执行序列”。其中一个“设置输入字段”行为的“新值”改为 -1。
另一个设为 1。
游戏开始时,“发球方向”会随机设为 -1(向左发球)或 1(向右发球)。在球的发球行为链中找到“施加力”行为,将它的 X 方向力设为“发球方向”方框容器的结果。
多测试几次游戏,比赛开始时,球应该会随机向左或向右发球。
现在只需要根据得分者改变发球方向。从“发球方向”方框容器再创建一个“设置输入字段”行为,并找到玩家撞到墙的碰撞行为。腾出一些空间,将它连接到“player 2 scored”的“广播消息”行为。在“设置输入字段”的属性中,将“新值”设为 1。这样球就会朝 Player-2 发球。
复制这个“设置输入字段”行为,将它连接到另一个“player 1 scored”的“广播消息”行为,并将它的“新值”改为 -1。这样球就会朝 Player-1 发球。
本教程的这一部分就到此为止!Player-1 得分时,球应该向左朝他们发球;Player-2 得分时,球应该向右朝他们发球。此外,玩家现在可以互相得分,最终赢得或输掉比赛。
第 8 部分:暂停菜单、退出和最后的完善
现在,我们已经拥有了完成游戏所需的大部分内容。只差几个小功能,例如游戏中的“暂停”按钮,以及从主菜单优雅退出游戏的方式。最后,我们还应该在主菜单中隐藏摇杆,因为那里用不到它们。完成后,这个项目就大功告成了。
在“游戏”场景的场景编辑器中,选择“场景 UI”层并轻点“创建特殊对象”按钮,创建一个新标签。将它的颜色设为白色(#FFFFFFFF),并放在屏幕顶部。将文本改为“暂停”,并将对齐方式设为居中。
打开它的行为编辑器,从“交互”部分放置一个“开始触摸”行为。在它下方放置一个“加载叠加层”行为,并将所选叠加层设为预制的“暂停菜单”。确保启用“暂停”选项。你也可以按喜好选择转场样式。
接下来稍微调整一下“暂停菜单”。返回场景编辑器并打开场景菜单。展开“叠加层”部分并打开“暂停菜单”叠加层。
和玩家获胜叠加层一样,轻点“重新开始”标签并将它删除。将所有标签的颜色改为白色(#FFFFFFFF),并将“主菜单”标签的文本改为“结束比赛”。
测试游戏时,游戏内的“暂停”按钮现在应该会打开“暂停菜单”叠加层,同时暂停背景中的游戏。轻点“继续”后,游戏应该会从暂停处继续;轻点“结束比赛”则应该会返回主菜单场景。
接下来,为“主菜单”场景做一些最后的完善。返回场景编辑器,打开场景菜单并展开“场景”部分。选择“主菜单”以打开它。
现在创建一个“退出”按钮。为另一个按钮腾出空间,然后复制一个游戏模式按钮。将它移到列表底部,并把它的文本改为“退出游戏”。完成后,打开它的行为编辑器。
在行为编辑器中,删除除“开始触摸”行为以外的所有行为,只留下它。
在“场景”部分中放置并连接一个“退出项目”行为。测试主菜单时,“退出游戏”按钮现在应该会结束游戏。
最后,在主菜单中隐藏摇杆。轻点标题标签并打开它的行为编辑器。我们将在这里添加一些行为来隐藏摇杆。
在行为编辑器中,从“变换”部分放置两个“隐藏图形”行为。一个对象设为其中一个摇杆,另一个“隐藏图形”行为的对象设为另一个摇杆。选择摇杆时,你可能需要先选择“全局 UI”层才能选中它们。
现在测试菜单时,摇杆应该都不会再显示了!运行游戏后,如果球拍是 AI,游戏中的球拍行为会继续隐藏摇杆;如果球拍由玩家控制,则会重新显示摇杆。
结语
恭喜!你已经制作出一款相当不错的经典 Pong 克隆游戏。
需要学习的内容很多,但如果你已经做到这里,就应该已经了解了许多游戏设计主题,例如创建人工智能、根据设置动态更改场景等。这些知识可以应用于各种类型的游戏,让你的作品更出色。
当然,我们还可以继续改进目前的游戏……
你可以自由添加自己的图形和音效,并继续练习新技能。作为一项练习,试着利用我们为 AI 打下的基础,在选择与 AI 球拍对战时实现不同难度(简单、普通、困难等)。(提示:请参阅本教程第 4 部分的末尾!)
本教程的完整项目文件可在此处获取:下载教程项目

