Choose your operating system:
Windows
macOS
Linux
本文档介绍了名为 射击游戏(Shooter Game) 的游戏项目示例。你可以按照如下操作找到该示例:
点击Epic启动程序中的 学习(Learn) 选项卡,然后向下翻到 游戏(Games) 分段。
点击 射击游戏(Shooter Game) 缩略图打开项目主页。点击黄色的 免费(Free) 按钮。
稍作等待后,按钮名称会变成 创建工程(Create Project)。点击按钮后,启动程序会提示你输入项目名称并选择安装路径。
点击 创建(Create) 后,你就可以将该项目下载到指定的目录。
第一人称射击游戏示例展示了PC平台上的多人第一人称射击游戏。该示例展示了基本的武器和游戏模式,以及简单的前端菜单系统。
完整的特性列表如下:
即时伤害武器(ShooterWeapon_Instant)
射弹武器(ShooterWeapon_Projectile + ShooterProjectile)
非团队游戏模式(ShooterGame_FreeForAll)
团队游戏模式(ShooterGame_TeamDeathMatch)
可拾取物品(ShooterPickup)
主菜单(ShooterHUD_Menu)
武器射击系统
基本的武器射击功能 - 例如弹药管理、装弹及复制 - 都在 AShooterWeapon
类中应用。
武器在本地客户端和服务器上切换到射击状态(通过RPC调用)。 DetermineWeaponState()
在 StartFire()
/StopFire()
中进行调用,它会执行一些逻辑来决定武器应处于哪个状态,随后调用 SetWeaponState()
将武器调整到合适的状态。处于开火状态时,本地客户端将会重复调用 HandleFiring()
,而该逻辑会调用 FireWeapon()
。然后它会更新弹药并调用 ServerHandleFiring()
,从而在服务器上执行同样的逻辑。进行每一轮射击时,服务器版本同时会通过 BurstCounter
变量告知远程客户端。
仅从表面上看,我们在远程客户端上执行操作。实际上,武器射击通过 BurstCounter
属性进行复制,使远程客户端可以播放动画并生成效果 - 实现射击时的所有视觉效果。
即时伤害武器射击
即时伤害判定用于射击速度较快的武器,例如步枪或激光枪。基本的原理是:玩家开火瞬间对武器瞄准方向进行线性检测,以便判定是否击中了任意物体。
这种方式的准确度较高,而且对于不在服务器上的Actor也有效果(例如,起修饰作用或已经脱离的Actor)。本地客户端执行计算并告知服务器击中了什么物体。随后,服务器确认此射击并在需要的时候进行复制。
在 FireWeapon()
中,本地客户端采用自摄像机位置开始的轨迹,找到在准星之下的首次阻挡碰撞并将其传递到 ProcessInstantHit()
之后,将会出现以下情况之一:
此次碰撞被传递到服务器以便确认(
ServerNotifyHit()
-->ProcessInstantHit_Confirmed()
)。如果碰撞Actor不存在于服务器上,则此射击在本地进行处理 (
ProcessInstantHit_Confirmed()
)。如果未击中任何物体,则告知服务器(
ServerNotifyMiss()
)。
已确认的碰撞会将伤害应用到碰撞Actor,生成尾迹和冲击效果,并通过在 HitNotify
变量中设置碰撞数据来告知远程客户端。未击中的射击仅仅对远程客户端生成尾迹并设置 HitNotify
,远程客户端会查找 HitNotify
的变更并执行和本地客户端相同的轨迹,按需生成尾迹和冲击效果。
应用即时伤害还包括武器散布(weapon spread)这一特点。为了保持轨迹与验证的一致性,本地客户端会在每次执行 FireWeapon()
时选取随机种子,并在每个RPC和 HitNotify
包中传递它。
射弹武器射击
射弹射击用于模拟一类武器,它们发射的子弹移动速度缓慢,会在冲击时产生爆炸并且受到重力的影响。有时武器发射的结果位置无法在发射瞬间决定,比如投掷手雷。对这类武器来说,实际的物理对象,或 射弹 ,将按照武器瞄准的方向生成并移动。伤害由射弹与世界中另一个对象的碰撞所决定。
对射弹射击来说,和即时伤害射击类似,本地客户端采用来自摄像机的轨迹来查看在 FireWeapon()
内的准星下存在何种Actor。如果玩家正在瞄准某处,它会调整射击方向,对该点造成伤害,并调用服务器上的 ServerFireProjectile()
,从而在武器瞄准的方向上生成射弹Actor。
当射弹的移动组件检测到服务器上产生的伤害,便会爆炸造成伤害,同时生成特效并从复制处脱离,以告知客户端该事件。随后,射弹停止碰撞、移动和可见度,并在一秒后销毁自身,给客户端足够的复制更新时间。
在客户端上,爆炸效果通过 OnRep_Exploded()
进行复制。
玩家武器库
玩家武器库是存储在玩家Pawn(AShooterCharacter
)的 Inventory
(武器库)属性中的 AShooterWeapon
数组引用。当前装备的武器从服务器上复制而来,并且, AShooterCharacter
在 CurrentWeapon
属性中在本地存储当前武器,这样在装备新武器时,会取消之前的武器装备。
当玩家装备武器时,恰当的武器网格体——本地为第一人称,其它为第三人称将被附加到Pawn上,同时在武器上播放动画。在动画持续期间,武器会切换到装备状态。
玩家摄像机
在第一人称模式中,Pawn的网格体被固定至摄像机上,这样手臂就会一直处于玩家的视野之中。这种处理方法的缺点是整个网格体会旋转以匹配摄像机的偏转和倾斜,因此从玩家的视角无法看到人物的腿部。
摄像机更新的基础工作流程为:
AShooterCamera::UpdateCamera()
在每个tick执行。APlayerCamera::UpdateCamera()
被调用,基于玩家输入更新摄像机旋转。AShooterCharacter::OnCameraUpdate()
被调用,执行旋转第一人称网格体所需的计算,以便匹配摄像机。
玩家死亡时,会切换到 死亡 摄像机,它在 AShooterPlayerController::PawnDied()
处理器中有固定的位置和旋转设置。该函数调用 AShooterPlayerController::FindDeathCameraSpot()
,它会在几个不同的位置之间循环,并使用第一个不会被关卡几何体阻挡的位置。
在线多人游戏
在线多人游戏比赛分为3个阶段:
热身
对抗赛
游戏结束
当第一个玩家加入游戏时,开始 热身 阶段。这个阶段很短,计时器进行会倒计时,让其它玩家有机会加入游戏。在此期间,玩家处于 观察 模式,可以在地图四处走动。倒计时结束时,会调用 StartMatch()
,所有玩家会重新开始并生成Pawn。
比赛有时间限制,游戏时间在服务器中的 AShooterGameMode::DefaultTimer()
函数中进行计算,这是通过循环计时器来执行的。这个计时器的速度类似于当前时间膨胀的速度,每秒也就相当于每个 游戏 秒。它被存储在游戏复制信息类( AShooterGRI
)的 RemainingTime
属性中,该属性会随后被复制到客户端。当剩余时间到达0时,调用 FinishMatch()
以终止游戏环节。这样可以告知所有玩家比赛已经结束,无法移动或使用生命值。
菜单系统
我们使用Slate界面框架来创建菜单系统。其中包含 菜单(menus), 菜单控件(menu widgets), 以及 菜单项目(menu items) 。每个菜单都包含单个菜单控件( SSHooterMenuWidget
),负责所有菜单项目的布局、内部事件处理以及动画。菜单项目( SSHooterMenuItem
)是可以执行操作并包含任意数量的其它菜单项目的复合对象。它们可以是标签、按钮或包含由组成其它菜单项目的完整子菜单的"选项卡"。这个菜单可以用键盘或控制器来操作,但示例中仅限于鼠标操作。
每个菜单都是通过 Construct()
函数来 构建 的,这个函数添加了所有必要的菜单项目(包括子项目),并在需要时在函数上附加委托。这是通过辅助方法来完成的—— AddMenuItem()
, AddMenuItemSP()
等,它们在 SShooterMenuWidget.h
文件的 MenuHelper
命名空间中进行定义。
通过菜单的共享指针数组,可以浏览到上一菜单,并且存储到菜单控件的 MenuHistory
变量中。 MenuHistory
类似于存储先前输入菜单的堆栈,并且可以让返回操作更为方便。通过这种方式,菜单间不会创建直接联系,并且可以按需把同一菜单重复用于不同的位置。
动画通过在 SShooterMenuWidget::SetupAnimations()
中定义的插值曲线执行。每条曲线都有起始时间、持续时间以及插值方式。动画可以进行正向或反向播放,并且可以使用 GetLerp()
函数在特定时间为属性设置动画,而这个函数会返回从0.0f到1.0f的值。 在 SlateAnimation.h
的 ECurveEaseFunction::Type
中定义了几个不同的可用插值方式。
主菜单
通过指定 ShooterEntry 贴图为默认值,主菜单会在游戏开始时自动打开。这时会载入特殊的游戏模式 AShooterGameMode
,这个模式使用 APlatformerPlayerController_Menu
类,它会通过创建 PostInitializeComponents()
函数中的 FShooterMainMenu
类的新实例来打开主菜单。
游戏中菜单
游戏中菜单在 AShooterPlayerController
类的 PostInitializeComponents()
函数中进行创建,并且通过 OnToggleInGameMenu()
函数来打开或关闭。
选项菜单
选项菜单可以作为主菜单和游戏中菜单的子菜单来使用。唯一的区别是应用变更的方式:
对于主菜单的变更,在玩家开始游戏时进行应用。
对于游戏中菜单的变更,在菜单关闭时立即进行应用。
选项菜单中的设置被保存到 GameUserSettings.ini
,并且在启动时自动载入。