双人在线 3D《毁灭战士》(难度:6)
-
简介
在本教程中,您将学习如何构建一个类似于经典游戏“毁灭战士”的3D多人射击游戏。两个玩家(在两台不同的计算机上)可以相互对战。
由于这个项目相当复杂,下面分享了已完成的项目:play.creaticode.com/projects/66e823b5a634f8ee7d92cc88
本教程将逐步介绍此项目的所有关键组件,以便您彻底理解它。
请注意,我们的多人游戏服务器只知道2D游戏世界,但我们可以使用它来支持我们的3D游戏,只要所有对象都在同一地面上。一个简单的思考方式是:如果我们从3D世界的上方俯视,那么游戏世界看起来像2D,而3D世界中的3D盒子在我们从上方看时将看起来像2D正方形。
1 - 角色概览
这个项目包含六个角色:
- Start:此角色包含显示游戏开始界面的代码,以便主机玩家可以创建新游戏供访客玩家加入。它还初始化3D世界。
- Player:此角色允许玩家(用户)控制3D世界中的两个化身,包括处理关键事件和管理玩家的生命值/火力。这是此项目中最复杂的角色。
- Bullet:此角色代表玩家发射的子弹。
- Wall:此角色用于在3D世界中创建墙壁。玩家或子弹不能穿过这些墙壁。
- Powerup:此角色用于生成“生命值”或“火力”的新道具。它们会定期随机出现在游戏世界中,如果没有任何玩家收集,它们会在一段时间后消失。
- Winner:当任一玩家获胜时,此角色将显示游戏的结束画面。
2 - “Start”角色:初始界面
当用户点击绿旗时,我们将显示一个初始界面,其中包含一个标签、一个输入文本框和两个按钮:
以下是在“Start”角色中创建此界面的代码:
请注意,每个游戏只能在2个用户(主机和访客)之间进行,并且由其名称唯一标识。默认名称为“3D Shooter”。如果名称已被占用,则主机需要在创建游戏时选择不同的游戏名称,访客需要使用相同的名称才能加入该游戏。
3 - “Start”角色:创建新游戏
在主机用户指定游戏名称后,他/她将点击“创建新游戏”按钮。我们将读取游戏名称,并将此用户的ID设置为“A”,这代表主机玩家。如果游戏名称为空,我们将显示错误消息,否则我们将尝试创建游戏。
4 - “Start”角色:创建游戏
要创建游戏,我们需要执行以下操作:
- 在此计算机上初始化3D场景;
- 要求游戏服务器使用给定的游戏名称创建一个新游戏。
- 为简单起见,我们将使用默认密码“123”,并假定主机用户为“玩家A”。
- 游戏容量为2,这意味着在一个主机和一个访客加入后,它将不再接受任何加入请求。
- 游戏世界设置为2000 x 2000。请注意,这是一个2D游戏世界,因此它只有宽度和高度。我们假设每个对象都处于地面水平,因此我们不需要在Z维度上指定世界大小。
- 如果游戏世界创建成功,则此计算机将连接到游戏。然后我们可以将主机玩家添加到游戏中(稍后讨论),然后等待其他玩家(访客)加入。
- 如果在短暂等待后我们仍未连接到游戏,则表示我们失败了,最可能的原因是游戏名称已被占用,或者游戏服务器太忙。无论哪种情况,我们都可以要求用户更改游戏名称并重试。
5 - “Start”角色:初始化3D场景
为了设置3D世界,我们创建了一个简单的“草地”场景,然后添加一个没有顶盖的“矩形立方体”,使其成为世界4个边界上的墙壁。世界的大小为2000 x 2000。我们将边界墙的高度设置为100,因此其中一半(50)在地面以上。化身将高100,因此墙壁不会阻挡我们跟随化身的摄像机。创建世界后,我们还使用“添加条形”块在顶部添加生命条和火力条。
6 - “Start”角色:添加生命和火力条
每个玩家开始时有3条生命,因此他/她被子弹击中3次就会失败,但可以通过收集生命奖励来获得生命。每个玩家的起始火力为1,可以通过收集火力奖励将其增加到5。火力控制每发射2颗子弹之间的最小间隔:间隔 = 2秒/火力。因此,当火力为1时,玩家可以每2秒发射一次,不能更快;但是当火力为5时,玩家可以每0.4秒发射一次。
两个玩家当前的生命值和火力值显示在舞台顶部,如下所示:
添加玩家名称和生命条的代码如下。生命条由3个绿色的方形标签表示,它们的名称以“health”开头,然后是玩家的ID“A”或“B”,最后以序列号1/2/3结尾。
添加火力标签的代码非常相似:
7 - “Start”角色:等待访客玩家加入
在主机玩家创建游戏后,他/她需要等待访客玩家成功加入游戏。我们可以重复将玩家信息获取到一个表格中,然后检查玩家数量是否为2。如果是,则我们显示一个“开始游戏”按钮,该按钮允许主机玩家开始游戏。
8 - “Start”角色:加入现有游戏
现在,让我们看看访客玩家方面。访客玩家需要输入与主机玩家相同的游戏名称,然后单击“加入现有游戏”按钮。
单击该按钮时,我们将执行以下操作:
- 将本地玩家保存为“玩家B”,这代表访客玩家。
- 确保用户指定的游戏名称不为空
- 加入游戏
9 - “Start”角色:加入游戏
要加入游戏,我们首先在访客玩家的计算机上创建3D场景,然后向游戏服务器发送一个请求,以使用给定的名称加入游戏。此玩家将是“玩家B”,这代表访客玩家。
等待一段时间后,我们检查是否已连接到游戏。如果是,我们将创建访客玩家(稍后讨论);否则,我们将显示错误消息。
10 - “Start”角色:开始游戏
在访客玩家加入游戏后,“开始游戏”按钮将出现在主机计算机上。当主机用户单击它时,我们将发送一条新消息“开始游戏”:
请注意,此消息将发送到两台计算机,但只有“原始”角色才能接收到它。例如,主机计算机上的玩家A角色将收到它,但访客计算机上的它的克隆将不会收到。这样,“开始游戏”消息在每台计算机上只被接收和处理一次。
11 - “Player”角色:添加玩家
现在,让我们切换到“Player”角色。此角色代表游戏世界中玩家的化身。
要添加主机或访客玩家,我们只需创建“Player”角色的克隆,并使用不同的克隆ID:“A”代表主机,“B”代表访客。这样,我们重用了大部分代码来添加玩家。
克隆创建后,我们将执行以下操作:
- 我们初始化一个变量“上次开火时间”来跟踪上次开火的时间,这有助于我们确定玩家何时可以再次射击
- 我们使用两个变量“生命值”和“火力”来跟踪每个玩家的生命数量和火力等级,起始值分别为3和1。请注意,这些变量对于Player角色的每个克隆都是“私有的”,因此两个玩家可以拥有自己的“生命值”和“火力”值。
- 我们将两个玩家放置在游戏世界的两个相对侧:x为0,y为800或-800。请注意,我们使用它们的克隆ID来区分这是主机玩家还是访客玩家。
- 我们将玩家添加到游戏中,这意味着我们在游戏服务器上注册此玩家。玩家是“动态的”,这意味着它的移动将被跟踪并在两台计算机上复制。
- 我们使用“圆形”形状来表示每个玩家。尽管每个玩家在3D世界中都是3D化身,但在我们的游戏服务器看来,它们都由直径为30的2D圆形表示。
- 我们将阻止玩家化身穿过墙壁对象
- 我们将允许玩家收集道具,这将触发“收集道具”消息。
幕后
当我们将玩家注册到游戏服务器时,幕后发生了什么?游戏服务器模拟了我们在玩游戏时游戏世界中发生的事情。在游戏开始时,当我们注册一个新玩家时,游戏服务器会在其模拟的游戏世界中添加一个新的圆形形状。
此外,该玩家的副本将在另一台用户的计算机上创建。例如,当主机用户创建游戏时,他/她的代码将创建主机玩家“A”,然后在访客用户的计算机上创建玩家“A”的克隆。类似地,当访客用户加入游戏并创建玩家“B”时,它将在主机用户的计算机上复制。因此,在每台计算机上,都有两个“Player”角色的克隆在运行。
12 - “Player”角色:当玩家添加到游戏时
在我们运行“将此角色添加到游戏”块之后,如果成功,将触发另一个块:“添加到游戏时”。这是一个重要的“隐藏”事件,因为它让我们有机会完全初始化玩家。
此块在每台计算机上被调用两次。在主机用户的计算机上,创建主机玩家后,将为此玩家(玩家A)触发此块。稍后,当访客用户加入游戏时,将在主机用户的计算机上创建玩家B的克隆,并且再次触发此块。
触发此块时,我们将执行以下操作:
- 创建一个3D化身来表示此玩家。当前主机是超人,访客是蝙蝠侠。化身将被移动到我们之前指定的初始位置:x为0,y为800或-800。
- 尽管此块在每台计算机上被触发两次,但我们只会添加一次“跟随摄像机”,仅针对代表本地玩家的化身。例如,对于主机用户,他的玩家ID为“A”,因此只有当克隆ID也为“A”时,我们才会添加跟随摄像机。请注意,跟随摄像机的方向锁定为“自由”,这意味着用户可以使用鼠标将摄像机转向任何方向。如果您正在调试游戏,则可以跳过添加跟随摄像机,这样您可以使用默认摄像机轻松查看整个游戏世界。
- 最后,我们将向3D化身添加“奔跑”动画,以便它知道以后如何播放该动画。
13 - “Player”角色:处理游戏开始
如上所述,每台计算机上的“原始”玩家角色将收到“开始游戏”消息。以下是我们收到该消息时执行的操作:
- 删除“开始游戏”按钮
- 如果这是主机玩家(玩家“A”),那么我们还需要触发2个事件:添加墙壁和添加道具。墙壁和道具对象将在访客计算机上复制,因此我们不需要在访客计算机上单独创建它们。此外,“添加道具”消息由一个永远循环处理,该循环定期添加一个新道具,因此我们没有对该消息使用“广播并等待”。
- 我们将化身的当前动画设置为“空闲”,因此当它稍后更改时,我们需要通知另一台计算机。
- 最后,我们启动“处理按键”块,这是一个处理按键输入的永远循环。请注意,只有在此步骤,用户才能开始使用键盘玩游戏。
14 - “Player”角色:处理按键
这是游戏逻辑的主循环,我们不断检查用户按下的按键,并相应地更新玩家化身。具体来说,我们在一个永远循环中执行以下操作:
- 检查我们是否已与游戏服务器断开连接,如果是,则显示错误消息。如果在过去5分钟内未收到任何玩家的更新,则玩家将自动与服务器断开连接。这将帮助游戏服务器回收分配给游戏的资源,以防玩家离开游戏。
- 如果我们仍在游戏中,我们检查空格键并在经过足够的时间后开火。
- 我们还检查移动键以确定移动速度、方向和动画。
- 最后,我们在进入循环之前等待很短的时间。这有助于减少我们在网络上发送的消息数量,并避免过多的“网络流量”减慢游戏服务器的速度。
15 - “Player”角色:处理开火
当我们检测到空格键被按下时,我们检查当前时间是否大于我们上次开火的时间加上等待时间。此等待时间计算为(2 / 火力)。例如,如果火力为2,则最小等待时间为2/2 = 1秒。
如果我们被允许开火,我们将发送“开火”消息,该消息将由“Bullet”角色处理(稍后讨论)。在此消息中,我们需要指定此角色的克隆ID(“A”或“B”)、x/y位置和此玩家的朝向,以便Bullet角色可以在相同的位置和方向创建子弹。
16 - “Player”角色:获取预期速度和动画
为了创造一个有趣的体验,让用户完全控制化身的移动,我们将允许用户同时使用鼠标和键盘。鼠标将用于控制摄像机,所有移动都将相对于摄像机的方向。例如,如果我们将摄像机拖动到右侧,化身将立即转向相同的方向。因此,当我们按下“W”向前移动时,化身将沿着与摄像机相同的方向移动:
为了实现此效果并保持我们的代码简短而整洁,我们将把它分为3种情况:
- 当按下“a”键时,表示用户想要向左移动。
- 当按下“d”键时,表示用户想要向右移动
- 当既没有按下“a”键也没有按下“d”键时,用户想要向前/向后移动或保持静止。
默认假设是预期速度为300,预期动画为“奔跑”,我们可以根据按键情况更改它们。
17 - “Player”角色:处理向左或向右移动
当仅按下“a”键时,用户想要水平向左移动。如果同时按下“w”或“s”键,则用户打算向左前方或左后方对角移动。请注意,我们现在只是设置“预期方向”和“预期速度”,然后我们将使用它们稍后更新化身。
此外,所有方向都相对于摄像机的“水平角度”,即跟随摄像机朝向的方向。此外,当用户同时按下“a”和“s”时,我们将方向设置为向右前方,并将速度设置为负数。
对于“d”键,逻辑完全相同,只是角度相反:
18 - “Player”角色:处理向前或向后移动
如果既没有按下“a”键也没有按下“d”键,化身将在“w”键下向前奔跑,在“s”键下向后奔跑,并且在两个键都没有按下时保持静止。
19 - “Player”角色:更新玩家速度/动画/方向
至此,我们已经计算了预期的移动速度、移动方向和动画,我们需要为3个实体更新它:他/她自己计算机上玩家的化身、游戏服务器上的它的2D表示以及另一台用户计算机上的它的副本。
这分3个步骤完成:
-
我们首先比较本地摄像机的朝向和化身的朝向。如果它们不够(至少相差0.1度),那么我们需要更新化身的朝向。例如,当用户将摄像机旋转到不同的方向时,可能会发生这种情况。我们正在使用“同步设置方向”块,它不仅更新他/她自己计算机上此玩家化身的方向,还更新游戏服务器和另一台用户计算机上的方向。
-
如果预期的移动速度或方向已更改,我们将使用“同步设置速度和方向”块来更新所有3个表示的这些值。请注意,“预期方向”是化身移动的位置,它可能与摄像机的方向不同。例如,当用户按下“a”时,摄像机可能仍然朝前,但预期的移动方向是向左。
-
最后,如果预期的动画已更改,我们需要更新此玩家化身在此计算机和另一台用户计算机上的动画。这可以通过向所有角色广播“更新动画”消息,并将克隆ID和预期动画附加到该消息来完成。我们将接下来讨论如何处理该消息。
20 - “Player”角色:处理“更新动画”消息
当任何角色收到此消息时,我们将拆分附加的信息并提取克隆ID。如果该ID与此克隆的ID匹配,那么我们将更新此角色的动画。回想一下第19步,附加到消息的“animation_info”有两个部分,由“_”连接:“克隆ID”和“预期动画”,例如“A_Run”。
21 - “Wall”角色:添加4个克隆
“Wall”角色相当简单。您可以修改它以设计边界内的任何地图。例如,我们可以在世界的四个边上有四堵墙,如下所示:
为了保持代码简单,所有墙壁对象都是相同的形状,只是其中两个旋转了90度。它们都是“Wall”角色的克隆,克隆ID分别为1、2、3和4:
创建每个克隆时,我们需要根据其克隆ID将其放置在所需的位置和方向。然后,我们需要将其添加到游戏中,以便游戏服务器可以创建一个400 x 50的矩形来表示服务器上模拟的2D世界中的它。相同的克隆也将在访客用户的计算机上创建。
22 - “Wall”角色:创建3D墙壁对象
当我们将Wall角色添加到游戏时,它尚未添加到3D场景中。如前所述,添加角色后,游戏服务器将触发一个新事件“添加到游戏时”,这是我们应该添加表示3D场景中墙壁的3D盒子的位置。这样做的原因是此事件在主机计算机和访客计算机上都会触发,这确保两个玩家都可以在其3D场景中看到墙壁。
3D盒子的x和y大小应与我们之前指定的矩形形状相匹配:400 x 50。墙壁的高度无关紧要,因为游戏服务器只将墙壁视为2D矩形。但是,为了确保玩家看不到墙壁上方,我们应该使墙壁高于玩家100的高度。因此,我们可以将z大小设置为300,因此上半部分为150。根据克隆ID,我们将每个盒子放置在所需的位置和方向。
这就是我们需要对墙壁做的所有事情。我们已经指定,当玩家触摸任何墙壁角色时,它将被停止。您还可以添加3D圆柱体作为墙壁,您只需要确保它们像这样添加为圆形形状:
23 - “Bullet”角色:处理“开火”消息
接下来,让我们看看Bullet角色。它比Wall角色稍微复杂一些。当收到“开火”消息时,会创建一颗子弹。请注意,墙壁仅在主机计算机上添加,但子弹可以在两台计算机上发射。
收到“开火”消息时,我们首先需要确保只有原始的Bullet角色才能处理它。我们检查克隆ID以确保它是“originalsprite”。否则,Bullet角色的克隆也可能会收到“开火”消息,这可能会生成一个新的重复克隆。
附加到“开火”消息的“射击信息”包含有关已开火的玩家的四条信息:玩家ID(“A”或“B”)、x位置、y位置和方向。我们将此信息存储在四个变量中,然后将角色移动到给定的位置/方向。然后,我们制作此角色的克隆。每个克隆都被赋予一个唯一的ID,该ID由两部分组成:玩家ID(“A”或“B”)和一个不断增加1的子弹ID。
24 - “Bullet”角色:将克隆添加到游戏
创建Bullet角色的克隆时,我们首先需要将其添加到游戏中。它将是动态的,因为它将沿直线向前移动。它将被添加为直径为40的圆形,因此游戏服务器将使用2D圆形在模拟的游戏世界中表示它。
之后,我们需要使子弹只击中对手玩家。例如,如果克隆ID以“A”开头,那么我们将子弹设置为仅与克隆ID为“B”的玩家角色碰撞。当发生这种情况时,子弹将摧毁自身,并发送一条新消息“子弹击中玩家”。此消息将由玩家角色处理以减少其生命值(将在下面讨论)。
最后,当子弹击中墙壁或世界边缘时,它将简单地摧毁自身。
25 - “Bullet”角色:添加到游戏时
当我们从游戏服务器收到已将新的Bullet克隆添加到游戏的通知时,我们添加表示3D场景中子弹的3D球体。我们需要采取一些步骤:
- 我们将播放爆炸声。
- 根据克隆ID以“A”还是“B”开头,我们将红色或黑色的球体添加到场景中。它的直径应为40,这与我们在游戏服务器上用来表示它的大小相匹配。同样,您可以这样想:如果我们从顶部俯视,球体看起来像地面上直径为40的圆形。
- 我们将球体移动到起始位置,这是开火的玩家的位置。
- 我们为此子弹设置600的速度以沿起始方向移动。
26 - “Player”角色:处理“子弹击中玩家”
当对手的子弹击中玩家时,该玩家将收到“子弹击中玩家”消息。只有用户自己计算机上的主Player角色会收到此消息,而不是另一台计算机上的它的副本。例如,当玩家A被击中时,只有主机计算机上的Player会收到此消息。这避免了处理两次消息。
如果玩家的生命值仍然大于0,我们将该玩家的生命值减少1。然后我们广播“更新生命值”消息,并附加该玩家的克隆ID和生命值。
27 - “Player”角色:处理“更新生命值”
当Player收到“更新生命值”消息时,我们将确保只有原始角色会处理它,而不是克隆。我们执行以下步骤:
- 提取玩家ID和更新后的生命值
- 使用重复循环遍历表示该玩家生命值的3个矩形标签,并根据生命值将它们设置为绿色或黑色
- 如果生命值已经为0,那么我们发送“游戏结束”消息,参数为对手玩家的ID。请注意,这是结束游戏的唯一方式。
28 - “Winner”角色:处理“游戏结束”
收到“游戏结束”消息时,我们在“Winner”角色中执行以下操作:
- 隐藏3D场景,因为我们将使用2D服装来显示结束画面;
- 播放“Success 7”的声音
- 切换到显示获胜者姓名的服装:A或B
- 3秒后,我们停止整个程序。
29 - “Powerup”角色:处理“添加道具”消息
当Powerup角色收到“添加道具”消息时,它将首先确保只有克隆ID为“originalsprite”的原始角色才会处理此消息。请注意,这仅在主机计算机上完成,并且每当添加新的道具角色时,它都将在访客计算机上复制。
道具可以是获得一个生命值或将火力提高1。您可以根据自己的喜好微调控制新道具类型和频率的参数。显然,如果有太多的“生命值”道具,那么游戏可能永远不会结束,因为两个玩家都可以从命中中快速恢复。
以下是Powerup当前的工作方式:
- 在添加每个新道具之前,我们等待30到60秒之间的随机时间段。您可以延长此时间以减少可用道具的数量。
- 我们在世界范围内选择一个随机位置
- 我们在“生命值”或“火力”之间随机选择一个类型,每个类型的概率为50%。您可以更改概率以生成更多“生命值”或“火力”奖励。
- 我们将角色移动到给定位置,然后创建一个克隆。
- 每个克隆都有一个唯一的ID,由两部分组成:道具类型和序列号。
30 - “Powerup”角色:在主机上创建克隆时
创建新克隆时,我们将其作为静态圆圈(直径为40)添加到游戏服务器上的游戏中。在10到20秒之间的随机超时时间段后,我们从游戏中删除此项目。这样,如果两个玩家不尝试快速收集道具,它将很快消失,使游戏更加精彩。当然,如果您将此超时时间段更改得更长,则两个玩家都将更容易获得能量。
31 - “Powerup”角色:添加到游戏时
与其他角色类似,我们在“添加到游戏时”块下添加表示道具的3D对象。当主机计算机将新道具添加到游戏时,此块在两台计算机上都会触发。
当道具添加到游戏时,我们执行以下操作来添加道具:
- 我们播放声音以通知用户已添加新项目;
- 我们将项目的位置存储在2个变量“我的x”和“我的y”中。我们需要这样做,因为当我们运行“添加模型”块时,它会将角色的x和y位置重置为0。
- 我们根据道具类型添加医疗箱或雷电图标的模型。请注意,它被添加为隐藏的,因为否则模型将在我们移动它之前显示在原点;
- 我们将设置z旋转以使其永远连续旋转。
- 我们将模型移动到保存的位置;
- 最后,我们显示对象。
32 - “Player”角色:处理“收集道具”
当Player角色触摸Powerup对象时,它将触发“收集道具”消息。只有由用户控制的Player克隆才会收到此消息。为了处理它,我们首先播放声音以确认收集了道具。如果是“生命值”类型,并且此玩家的生命值仍然小于3,我们将生命值增加1,并广播“更新生命值”消息,该消息更新顶部的生命值显示。类似地,如果此玩家的火力小于5,我们将其增加1,并发出“更新火力”消息。
33 - “Player”角色:处理“更新火力”
我们处理“更新火力”的方式与处理“更新生命值”的方式非常相似。我们从消息的参数中提取更新后的火力值,然后遍历该玩家的所有5个标签,并根据火力值将每个标签设置为红色或黑色。
总结
我们已经解释了此项目中的所有块。随时尝试重新混合和修改游戏。以下是一些想法:
-
调整游戏参数:您可以对游戏进行许多简单的更改,例如玩家化身、使用的3D模型/纹理、最大生命值/火力值、生成每个新道具之前的时间间隔及其持续时间、玩家和子弹的移动速度、玩家移动的键盘控制、墙壁的大小/数量/位置、世界的大小等。请根据您的喜好随意更改它们。
-
新的道具类型:您可以通过添加新的道具类型来扩展游戏,例如玩家或子弹加速、新型子弹(双发或三发子弹)、允许玩家穿过墙壁或透视墙壁的道具等。
-
2对2游戏:您可以尝试允许四个用户一起玩。您需要在创建游戏时增加游戏容量,并添加新的玩家ID,如“C”和“D”等。
-
AI玩家:您可以创建一个AI机器人来控制一个玩家,以便一个用户可以与AI对战,而不是寻找另一个玩家,或者您可以让2个人类玩家组队与2个AI玩家对战。
-