WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

使用C++在虚幻引擎5中创建多人射击游戏 Unreal Engine 5 C++ Multiplayer Shooter[进行中] #215

Open WangShuXian6 opened 2 days ago

WangShuXian6 commented 2 days ago

使用C++在虚幻引擎5中创建多人射击游戏 Unreal Engine 5 C++ Multiplayer Shooter

1. 引言

  1. 引言
  2. 关于本课程

2. 创建多人插件

  1. 多人游戏概念
  2. 测试多人游戏
  3. 局域网连接
  4. 在线子系统
  5. 在线会话
  6. 为Steam配置
  7. 访问在线子系统
  8. 创建会话
  9. 设置加入游戏会话
  10. Steam区域
  11. 加入会话
  12. 创建插件
  13. 创建我们自己的子系统
  14. 会话界面代理
  15. 菜单类
  16. 访问我们的子系统
  17. 创建会话
  18. 回调到我们的子系统功能
  19. 更多子系统代理
  20. 从菜单加入会话
  21. 跟踪来访玩家
  22. 通向大厅的路径
  23. 美化菜单子系统

3. 项目创建

  1. 项目创建
  2. 测试在线会话
  3. 资产
  4. 调整动画
  5. 爆破角色
  6. 相机和弹簧臂
  7. 角色移动
  8. 动画蓝图
  9. 无缝旅行和大厅
  10. 网络角色

4. 武器

  1. 武器类
  2. 拾取小部件
  3. 变量复制
  4. 装备武器
  5. 远程程序调用
  6. 装备动画姿势
  7. 蹲伏
  8. 瞄准
  9. 运行混合空间
  10. 倾斜和侧移
  11. 空闲和跳跃
  12. 蹲行
  13. 瞄准行走
  14. 瞄准偏移
  15. 应用瞄准偏移
  16. 多人游戏中的俯仰Pitch
  17. 使用我们的瞄准偏移
  18. FABRIK IK
  19. 转向
  20. 旋转根骨
  21. 网络更新频率
  22. 未装备时蹲伏
  23. 旋转跑步动画
  24. 脚步和跳跃声音

5. 发射武器

  1. 投射武器类
  2. 发射蒙太奇
  3. 发射武器效果
  4. 多人游戏中的发射效果
  5. 命中目标
  6. 生成投射物
  7. 投射物移动组件
  8. 投射物曳光
  9. 复制命中目标
  10. 投射物命中事件
  11. 子弹壳
  12. 壳体物理

6. 武器瞄准机制

  1. 爆破HUD和玩家控制器
  2. 绘制十字准星
  3. 准星扩散
  4. 纠正武器旋转
  5. 瞄准时放大
  6. 瞄准时缩小准星
  7. 改变准星颜色
  8. 延长痕迹起点
  9. 命中角色
  10. 为代理平滑旋转
  11. 自动射击
  12. 测试游戏

7. 健康和玩家统计

  1. 游戏框架

  2. 健康

  3. 在HUD中更新健康

  4. 伤害

  5. 爆破游戏模式

  6. 消灭动画

  7. 重生

  8. 溶解材料

  9. 溶解角色

  10. 使用曲线溶解

  11. 消灭时禁用移动

  12. 消灭机器人

  13. 占有

  14. 爆破玩家状态

  15. 失败


8. 弹药

  1. 武器弹药
  2. 能否开火
  3. 携带弹药
  4. 显示携带弹药
  5. 重新装填
  6. 重新装填战斗状态
  7. 允许开火
  8. 更新弹药
  9. 重新装填效果
  10. 自动重新装填

9. 比赛状态

  1. 游戏计时器
  2. 同步客户端和服务器时间
  3. 比赛状态
  4. 设置比赛状态
  5. 热身计时器
  6. 更新热身时间
  7. 自定义比赛状态
  8. 冷却公告
  9. 重启游戏
  10. 爆破游戏状态

10. 不同的武器类型

  1. 火箭弹
  2. 火箭轨迹
  3. 生成火箭轨迹
  4. 火箭移动组件
  5. 扫描射击武器
  6. 光束粒子
  7. 冲锋枪
  8. 带子物理
  9. 霰弹枪
  10. 武器散射
  11. 狙击步枪
  12. 狙击瞄准镜
  13. 榴弹发射器
  14. 投射榴弹
  15. 重新装填动画
  16. 霰弹枪重新装填
  17. 武器轮廓效果
  18. 投掷榴弹蒙太奇
  19. 投掷榴弹时的武器附着
  20. 榴弹资产
  21. 显示附着的榴弹
  22. 生成榴弹
  23. 多人游戏中的榴弹
  24. HUD中的榴弹

11. 拾取

  1. 拾取类

  2. 弹药拾取

  3. 增益组件

  4. 健康拾取

  5. 治疗角色

  6. 速度增益

  7. 跳跃增益

  8. 护盾条

  9. 更新护盾

  10. 护盾增益

  11. 拾取生成点

  12. 添加生成点到等级

  13. 生成默认武器

  14. 副武器

  15. 交换武器

  16. 丢弃副武器


12. 延迟补偿

  1. 延迟补偿概念
  2. 高ping警告
  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. 作弊和验证

  1. 更多多人游戏功能

  2. 返回主菜单

  3. 离开游戏

  4. 玩家簿记

  5. 获得领先

  6. 生成皇冠

  7. 消灭公告

  8. 动态消灭公告

  9. 头部射击

  10. 投射物头部射击

  11. 服务器端回放的头部射击


  1. 团队

  2. 团队

  3. 团队游戏模式

  4. 团队颜色

  5. 设置团队颜色

  6. 防止友伤

  7. 团队得分

  8. 更新团队得分

  9. 团队冷却公告


  1. 夺旗

  2. 夺旗

  3. 持有旗帜

  4. 拾取旗帜

  5. 给旗帜持有者增加负担

  6. 丢弃旗帜

  7. 团队旗帜

  8. 团队玩家开始

  9. 夺旗游戏模式

  10. 选择比赛类型

  11. 访问我们的子系统

  12. 团队和夺旗地图


  1. 恭喜

  2. 恭喜

  3. 附加讲座


  1. Introduction

  2. Introduction

  3. About this Course


  1. Creating a Multiplayer Plugin

  2. Multiplayer Concepts

  3. Testing Multiplayer

  4. LAN Connection

  5. Online Subsystem

  6. Online Sessions

  7. Configure For Steam

  8. Accessing the Online Subsystem

  9. Creating a Session

  10. Setup for Joining Game Sessions

  11. Steam Regions

  12. Joining the Session

  13. Creating a Plugin

  14. Creating our Own Subsystem

  15. Session Interface Delegates

  16. The Menu Class

  17. Accessing our Subsystem

  18. Create Session

  19. Callbacks to our Subsystem Functions

  20. More Subsystem Delegates

  21. Join Sessions from the Menu

  22. Tracking Incoming Players

  23. Path to Lobby

  24. Polishing the Menu Subsystem


  1. Project Creation

  2. Project Creation

  3. Testing an Online Session

  4. Assets

  5. Retargeting Animations

  6. Blaster Character

  7. Camera and Spring Arm

  8. Character Movement

  9. Animation Blueprint

  10. Seamless Travel and Lobby

  11. Network Role


  1. The Weapon

  2. Weapon Class

  3. Pickup Widget

  4. Variable Replication

  5. Equipping Weapons

  6. Remote Procedure Calls

  7. Equipped Animation Pose

  8. Crouching

  9. Aiming

  10. Running Blendspace

  11. Leaning and Strafing

  12. Idle and Jumps

  13. Crouch Walking

  14. Aim Walking

  15. Aim Offsets

  16. Applying Aim Offsets

  17. Pitch in Multiplayer

  18. Using our Aim Offsets

  19. FABRIK IK

  20. Turning in Place

  21. Rotate Root Bone

  22. Net Update Frequency

  23. Crouch Unequipped

  24. Rotating Running Animations

  25. Footstep and Jump Sounds


  1. Firing Weapons

  2. Projectile Weapon Class

  3. Fire Montage

  4. Fire Weapon Effects

  5. Fire Effects in Multiplayer

  6. The Hit Target

  7. Spawning the Projectile

  8. Projectile Movement Component

  9. Projectile Tracer

  10. Replicating the Hit Target

  11. Projectile Hit Events

  12. Bullet Shells

  13. Shell Physics


  1. Weapon Aim Mechanics

  2. Blaster HUD and Player Controller

  3. Drawing the Crosshairs

  4. Crosshair Spread

  5. Correcting the Weapon Rotation

  6. Zoom While Aiming

  7. Shrink Crosshairs when Aiming

  8. Change Crosshairs Color

  9. Extending the Trace Start

  10. Hitting the Character

  11. Smooth Rotation for Proxies

  12. Automatic Fire

  13. Testing the Game


  1. Health and Player Stats

  2. Game Framework

  3. Health

  4. Update Health in the HUD

  5. Damage

  6. Blaster Game Mode

  7. Elim Animation

  8. Respawning

  9. Dissolving the Character

  10. Dissolve Material

  11. Dissolving with Curves

  12. Disable Movement when Elimmed

  13. Elim Bot

  14. On Possess

  15. Blaster Player State

  16. Defeats


  1. Ammo

  2. Weapon Ammo

  3. Can Fire

  4. Carried Ammo

  5. Displaying Carried Ammo

  6. Reloading

  7. Reloading Combat State

  8. Allowing Weapon Fire

  9. Updating Ammo

  10. Reload Effects

  11. Auto Reload


  1. Match States

  2. Game Timer

  3. Syncing Client and Server Time

  4. Match State

  5. On Match State Set

  6. Warmup Timer

  7. Updating Warmup Time

  8. Custom Match States

  9. Cooldown Announcement

  10. Restart Game

  11. Blaster Game State


  1. Different Weapon Types

  2. Rocket Projectiles

  3. Rocket Trails

  4. Spawning Rocket Trails

  5. Rocket Movement Component

  6. Hit Scan Weapons

  7. Beam Particles

  8. Submachine Gun

  9. Strap Physics

  10. Shotgun

  11. Weapon Scatter

  12. Sniper Rifle

  13. Sniper Scope

  14. Grenade Launcher

  15. Projectile Grenades

  16. Reload Animations

  17. Shotgun Reload

  18. Weapon Outline Effect

  19. Grenade Throw Montage

  20. Weapon Attachment while Throwing Grenades

  21. Grenade Assets

  22. Showing the Attached Grenade

  23. Spawning Grenades

  24. Grenades in Multiplayer

  25. Grenades in the HUD


  1. Pickups

  2. Pickup Class

  3. Ammo Pickups

  4. Buff Component

  5. Health Pickup

  6. Healing the Character

  7. Speed Buffs

  8. Jump Buffs

  9. Shield Bar

  10. Updating the Shield

  11. Shield Buffs

  12. Pickup Spawn Point

  13. Adding Spawn Points to the Level

  14. Spawn Default Weapon

  15. Secondary Weapon

  16. Swap Weapons

  17. Drop the Secondary Weapon


  1. Lag Compensation

  2. Lag Compensation Concepts

  3. High Ping Warning

  4. Local Fire Effects

  5. Show the Widget Locally

  6. Replicating Scatter

  7. Replicating Shotgun Scatter

  8. Client-Side Prediction

  9. Shotgun Fire RPCs

  10. Client-Side Predicting Ammo

  11. Client-Side Predicting Aiming

  12. Client-Side Predicting Reloading

  13. Server-Side Rewind

  14. Lag Compensation Component

  15. Hit Boxes

  16. Frame Package

  17. Saving a Frame Package

  18. Frame History

  19. Rewinding Time

  20. Interp Between Frames

  21. Confirming the Hit

  22. Score Request

  23. Server-Side Rewind for Shotguns

  24. Confirming Shotgun Hits

  25. Shotgun Score Request

  26. Requesting a Shotgun Hit

  27. Predict Projectile Path

  28. Post Edit Change Property

  29. Spawning Projectiles Locally

  30. Hit Box Collision Type

  31. Projectile Server-Side Rewind

  32. Projectile Score Request

  33. Limiting Server-Side Rewind

  34. Swap Weapon Animation

  35. Wrapping up Lag Compensation

  36. Cheating and Validation


  1. More Multiplayer Features

  2. Return to Main Menu

  3. Leaving the Game

  4. Player Bookkeeping

  5. Gaining The Lead

  6. Spawning the Crown

  7. Elim Announcements

  8. Dynamic Elim Announcements

  9. Head Shots

  10. Projectile Head Shots

  11. Head Shots for Server-Side Rewind


  1. Teams

  2. Teams

  3. Teams Game Mode

  4. Team Colors

  5. Setting Team Colors

  6. Preventing Friendly Fire

  7. Team Scores

  8. Updating Team Scores

  9. Teams Cooldown Announcement


  1. Capture the Flag

  2. Capture the Flag

  3. Holding the Flag

  4. Picking up the Flag

  5. Burdening the Flag Bearer

  6. Dropping the Flag

  7. Team Flags

  8. Team Player Starts

  9. Capture the Flag Game Mode

  10. Select Match Type

  11. Accessing our Subsystem

  12. Teams and Capture the Flag Maps


  1. Congratulations

  2. Congratulations

  3. Bonus Lecture

WangShuXian6 commented 2 days ago

1. 引言

1. 引言

欢迎来到虚幻引擎5 C++ 多人射击游戏课程。在本课程中,我们将从头开始创建这个多人射击游戏。如你所见,在这次测试环节中,我有来自全球各地的19名玩家与我一起游戏。在本课程中,我们将创建一个完全功能的多人游戏,具有完整的网络功能,优化流畅的游戏体验,能够容纳许多不同的玩家通过在线会话连接。

我们将从创建一个可以添加到任何虚幻引擎项目中的多人插件开始,以启用多人功能。这使得创建多人游戏变得简单,因为我们的插件将处理创建会话、查找和加入会话以及销毁会话,提供一个易于使用的界面,可以根据你的特定游戏进行定制。我们将学习虚幻引擎在线子系统,管理与各种服务(如Steam)连接的代码库。我们的插件代码将适用于虚幻引擎代码库支持的任何服务,我们将使用Steam快速让我们的会话运作并与其他玩家连接。

你将加入一个活跃的Discord社区,在这里你可以找到其他学生并在互联网上测试彼此的游戏,确保你的游戏项目能够正常工作,而不必单独进行测试。我们将确保你有测试伙伴。课程中的插件部分独立于实际游戏项目,因此如果你想直接开始多人游戏编程,只需下载插件,我会教你如何将其添加到你的项目中,以立即启用Steam会话。

接下来,我们将创建一个新的虚幻引擎5项目,并启用我们的插件,进行一些快速的多人测试以验证网络功能。我们将创建一个新的角色类,添加一个网格并实现基本的移动功能,包括蹲伏、跳跃、瞄准和瞄准行走,所有这些都将针对多人进行编程。当然,我们随后将创建武器类和装备武器的功能,所有与战斗相关的行为都将通过战斗组件来处理,以保持角色和战斗功能的有序。

我们将学习游戏在服务器和客户端上的工作原理,了解网络角色和权限,以及如何编程以确保游戏逻辑在多人模式下正常运作。在每个步骤中,我们将确保服务器对所有重要的游戏逻辑(如生命值、伤害和弹药)保持控制,以防止玩家作弊。我们将创建多个武器类,实施头部扫描武器和投射物武器。我们将创建突击步枪、手枪、冲锋枪、狙击步枪、火箭发射器、榴弹发射器、霰弹枪和投掷手榴弹。

我们将使用投射物运动组件,利用虚幻引擎内置的复制行为来处理投射物,直接通过击中事件施加伤害,并通过范围伤害处理爆炸武器。我们将实现随机武器弹散,看看这如何适用于所有武器类型,包括霰弹枪。我们还将确保所有机器之间的随机散布是一致的,以便你在应该命中的时候始终准确命中。我们将实现动态十字准星,它会在瞄准另一个玩家时扩散、收缩并变红,我们将使用瞄准偏移和逆向运动学,使我们的手始终正确放置在武器上,确保武器始终朝向子弹发射的正确方向,并让我们的武器以物理效果和声音弹出弹壳。

接下来,我们将讨论虚幻引擎的类框架,以及引擎类如何与多人模式相关,讨论每个类的角色以及数据和功能应在多人游戏中如何编程。我们将讨论游戏模式与游戏模式基类之间的区别,以及游戏模式为匹配设计的更多功能,包括匹配状态的概念。我们将利用匹配状态,甚至创建我们自己的自定义匹配状态,以便在比赛的不同阶段中使用,包括热身阶段,在此阶段玩家可以在等待热身计时器时飞行穿越关卡,以及冷却状态,在此状态下我们将显示获胜玩家的名称和得分。

我们将创建一个由游戏模式管理的工作倒计时器,并学习如何将客户端的运行时间与服务器的时间同步,以便所有玩家在同一时间看到完全相同的数字。我们将实施玩家淘汰和重生机制,并在玩家得分时分配积分,将玩家的得分存储在玩家状态类中。我们将创建一个游戏状态类,用于跟踪得分最高的玩家以及团队得分和得分最高的团队。游戏中的淘汰机制将会非常出色。

我们将创建一个警报板,在玩家被淘汰时生成,并将玩家传送,使用我们从零开始创建并在运行时动态改变的溶解材质来溶解角色。我们将创建拾取物来增强玩家的能力,包括生命值、护盾、跳跃和速度增强,并通过增强组件处理所有这些行为。我们将创建一个拾取生成器类,能够在随机时间段后生成拾取物,并在HUD上显示所有相关的游戏数据,包括生命条、护盾条、得分和淘汰、武器弹药和携带弹药,并将这些值从C++中通过一个高效的事件驱动系统进行更新,仅在变量更改时更新HUD,而不是在每帧中检查。

接下来,我们将讨论延迟,并回顾AAA射击游戏使用的各种延迟补偿技术,然后实现它们。我们将使用客户端预测,使体验在极高延迟的情况下迅速响应。我们将学习如何在引擎和打包构建中模拟延迟,以便在最严酷的延迟条件下测试游戏。然后,我们将创建一个服务器端回滚算法,这是大多数你喜欢的AAA射击游戏使用的一种高级技术,确保玩家在高延迟情况下始终获得命中反馈。通过存储玩家信息的历史记录,并在服务器上回滚时间以验证命中,确保玩家在他们的机器上每次有效命中时都能获得积分。我们将对头部扫描武器和投射物进行此操作。

接下来,我们讨论服务器端回滚的优缺点,以及始终给予射击反馈可能带来的著名的“拐角死亡”问题,即延迟玩家命中时,另一名玩家在到达掩体之前就已经被击中。我们将学习如何根据玩家的延迟限制服务器端回滚,以消除这个问题。我们的延迟补偿算法将封装在自己的延迟补偿组件中,以保持它们的组织性。

然后,我们将讨论作弊,学习玩家如何作弊以及如何防止作弊,并认识到我们在整个过程中构建的游戏架构能够有效防止作弊,同时学习如何验证发送到服务器的数据,甚至踢出试图发送错误数据的玩家。我们将实施团队游戏模式,并最终实现“夺旗”模式。在课程结束前,我们将打包并测试我们的游戏。

在整个课程中,我们将频繁与真实玩家进行测试,以确保每一步都能在多人模式下正常运作。你将拥有一个完整的多人游戏,充分利用虚幻引擎的多人代码库,与其他玩家在互联网上连接和游戏。你将学习到制作快速、竞争激烈的射击游戏的可靠和高效的方法。我们优化低带宽和最大网络性能,课程结束时,你将拥有足够的技能来实现你想要的任何类型的多人游戏。如果你能够制作射击游戏,那么你就能够制作任何类型的游戏,因为射击游戏在性能上是最具挑战性的,快速竞争的游戏至关重要。

每个讲座都有自己的GitHub提交,我将向你展示如何查看每个讲座的代码更改,这样你可以在遇到问题时随时查看代码。你将与一个活跃的Discord社区连接,以便与其他学生和我讨论你的问题,我们将确保你的项目正常运作,即使你遇到困难。你不需要是专家才能参加本课程。基本的C++知识或对类似语言(如C#或Java)的理解即可。如果你已经参加了我的《游戏开发的C++学习》课程,你就可以顺利进行。如果你至少参加过我的其他一门虚幻引擎C++课程(无论是《虚幻引擎C++终极游戏开发课程》还是《虚幻引擎C++终极射击课程》),那么你就已经准备好深入学习了。如果你没有参加过这两门课程,只要你了解一些基本的C++变量、函数、指针和类等内容,你也能顺利进行这门课程。

我甚至还没有告诉你我们在本课程中涵盖的所有功能。你所学习的技能将极大提升你的游戏职业能力,如果你试图在游戏行业找工作,这将为你带来巨大的优势。在面试中提到服务器端回滚,大家会对你产生浓厚的兴趣。因此,如果你准备好在虚幻引擎中制作多人游戏,并创建迄今为止最令人印象深刻的游戏项目,准备好投入进来,创建这款虚幻引擎5 C++ 多人射击游戏吧。

2. 关于本课程

在本次讲座中,我们将讨论课程结构,具体来说,这门课程是如何构建的,以及它的两个主要部分以及我们将在这两个部分中创建的内容。我们还将讨论资源以及如何充分利用这些资源,使您的学习旅程更加轻松。在创建这个多人项目的过程中,我们还将讨论如果您遇到困难或有问题,获取帮助的主要途径。最后,我们会讨论在这门课程中您将面临的挑战和任务。

首先,让我们来谈谈课程结构。这门课程由两个主要部分组成。第一部分是多人插件。我们将为虚幻引擎创建一个实际的插件,您将能够将此插件添加到任何项目中。这个插件包含创建在线会话的功能,并允许玩家通过互联网在您的游戏中连接。这将是一个有价值的工具,您可能会在之后创建的多人项目中使用。

课程的第二部分是实际的游戏项目。在这一部分中,我们将创建我们的角色、武器以及与我们的多人射击游戏相关的所有内容。这两个项目部分是独立的。如果您想了解如何创建插件、如何在虚幻引擎中工作以及如何连接到互联网玩家,您可以从头开始创建插件。但如果您想进入游戏编程,开始制作角色、武器和游戏项目中的所有游戏类,您可以首先开始游戏项目部分,然后将插件添加到其中。这样,您的游戏项目就会自动工作并为您创建多人会话。

在游戏项目部分的第一节课中,资源标签中会有一个可下载的压缩文件,您可以下载插件并开始创建游戏,直接实施多人游戏编程。因此,您可以选择先做插件,或先开始制作游戏项目,然后再回到插件部分。如果您首先开始游戏项目,在该部分的第一节课中,您将下载插件的压缩文件,该讲座将向您展示如何设置您的游戏并添加插件以及配置多人设置。

如果您想首先学习如何创建和管理在线会话,您将从头开始课程并创建多人插件,然后您将理解在线会话的工作原理。这一切与游戏项目部分的实际游戏编程是非常独立的。在游戏项目部分,我们将首先添加多人插件,然后开始创建实际的游戏,在其中实施多人游戏玩法,学习诸如复制、游戏优化、游戏中的战斗、比赛游戏状态等所有相关内容。

这里的要点是,如果您想开始制作实际的游戏并编程游戏机制,您将从游戏项目开始,并添加插件,使您的游戏能够创建在线会话,您可以与其他玩家进行测试。但如果您想先制作插件,您可以从课程的开头开始。如果您是虚幻引擎的新手,我建议您最后再做插件。

现在,让我们来谈谈本课程的资源。我们在这门课程中编程的所有源代码都在GitHub上。每一节课中,我们添加代码更改都有源代码,并且有指向GitHub提交的链接。接下来,我将在本视频中向您展示如何查看源代码,以便您可以看到在任何给定讲座中更改的每一行代码。

在您上课期间,您将在右侧看到讲座。虽然我的显示为未发布,但您看到的不是这样。因为到您上课时,这门课程将会发布,大多数讲座都有一个资源标签,您可以展开该标签以获取本讲座的资源。这里是项目创建讲座,它在“项目创建”部分,您会看到它位于创建多人插件之后。如果您决定跳过插件并最后再做,您将开始项目创建和资源。这里是插件,称为“plugins.zip”。这个讲座将向您展示如何使用它。

要查看本讲座中所做的代码更改,您将选择本讲座的GitHub提交,您会看到每一节讲座中我们做代码更改都有GitHub提交链接。如果您点击这个链接,您将被带到GitHub页面,您不需要登录或拥有GitHub账户即可访问该页面。该页面展示了一些信息,如果您向下滚动,您将看到在此讲座中特定的更改。

我们可以看到更改的单个文件,如果您想查看其他讲座,您可以在顶部点击包含所有讲座的库或仓库。如果您点击那个,您会看到该仓库中存在的所有文件。这里的条形图上有一个数字,这是对该仓库进行的提交次数。如果您点击这个数字,您可以查看每一节课所做的代码更改。

假设您想查看某一讲座中所做的代码更改,您只需点击与您的讲座同名的提交,您将看到在该讲座中所做的所有更改,按照文件显示更改,您可以点击小点图标来最小化这些文件。阅读这些内容的方法是,这些称为diff,diff展示了更改的内容。红色的行表示此行在本次提交中被删除,绿色的行表示添加了某些内容。

例如,在本讲座中,我删除了这行空白,并添加了这一行。页面底部您会看到我删除了这一行并添加了这一行。实际上我没有删除这一行并将相同的行重新添加,我只是通过在这一多行注释的末尾添加内容改变了这一行,而GitHub将其视为更改了整行代码。通过查看这个diff,您可以看到我在这里唯一更改的是删除了一行空白并通过开始一个多行注释和结束多行注释来注释掉这段代码。您可以看到该字符的额外高亮显示,以表明这是实际的更改。

在这里,我们不是查看整个projectile bullet.cpp文件,但我们可以查看此讲座的内容。右上角有三个点,您可以点击查看文件,这样我们将看到在课程这个阶段的整个文件。如果您在编译代码时遇到问题,您可以来到此GitHub仓库,查看此讲座中的项目,并将其与您的代码进行比较。

有一种方法可以复制此文件中的所有内容。就是这个图标,它说“复制原始内容”。如果我点击它,它将复制该文件中的所有文本。如果我想回到我的项目中找到那个文件,然后使用Ctrl+A全选,再使用Ctrl+V粘贴,那么它将粘贴所有的代码。但我强烈建议您不要这样做,因为复制和粘贴大块代码很容易导致编译错误。您可能将类命名为不同的名称,可能将某些变量命名为不同的名称,您的项目本身也可能有不同的名称,如果这些名称有任何不同,您的代码将无法编译。因此,我建议您查看代码本身,确保了解哪些内容不同。如果您的代码与我所做的不同,您可以分析它,但我不建议复制整个文件并将其粘贴到您的项目中,因为这几乎总是导致编译错误。如果您确实想这样做,您可以这么做。

如果您返回到整体库,通过点击它,这就是当前版本的库,换句话说,就是完成后的项目。因此,如果您点击“源代码”,您可以查看最终源代码在课程结束时的样子。

有两种方法可以查看代码。您可以查看完成项目的整体代码,或者您可以查看提交记录,点击特定讲座的提交,查看在该讲座中所做的更改,这些更改以绿色表示添加的代码,以红色表示删除的代码。您还可以点击三个点,查看该阶段课程的完整文件。

这是您可以使用GitHub仓库的方法,每一节讲座中做的更改都会有指向该讲座的GitHub提交的链接。GitHub是您在整个课程中可以使用的一个非常有用的资源。

下一个资源是Druid Mechanics Discord社区。这是一个Discord服务器。在这个Discord服务器中,我们有我发布的所有课程的社区,很多人在服务器中积极交流,互相请求帮助。因此,在这个Discord服务器中,您可以结识其他学生,与他们交谈,您可以更快地得到回复,进行更为轻松的互动讨论。您可以展示您的代码并请求他人的查看,包括我自己。我也在其中非常积极地回答问题,讨论课程中的问题、未来课程的想法等。此外,如果您在Discord中,您将可以获得更多课程的优惠券,因此您总是能够以最低价格获取课程。

加入Discord很简单。我在所有的资源标签中都有链接。第一个是“学生Discord”,点击此链接,您将有机会加入Discord服务器。如果您没有Discord账户,注册是免费的,Discord账户非常有用,因为您可以加入多个Discord服务器,与其他人聊天。Discord也有桌面应用程序可供下载,您还可以在移动设备上下载。我使用Discord的移动版,能及时接收学生在上面提问的通知,您也可以通过浏览器打开它。

一旦您加入Druid Mechanics Discord社区,您将访问这些频道。您首先要去规则频道,阅读规则,确保您理解它们。我们有一个用于一般讨论的频道和公告频道,您可以了解我未来课程的进展情况以及我在什么阶段。我还设有一个直播频道,因为我偶尔会进行直播,您可以在这里获取相关信息。这里还有课程优惠券,我每个月都会发布课程优惠券,您可以在这里获取课程的折

扣,查看您想参加的其他课程。此外,还有一个名为“寻找帮助”的频道,您可以在这里请求其他学生的帮助。若您需要帮助,寻找其他学生并与他们联系是一个很好的方式。

因此,这是您在此课程中的主要资源。我们有GitHub作为主要的代码资源,您可以在课程中跟踪每一节课所做的代码更改。同时我们有Druid Mechanics Discord社区,您可以与其他学生和我进行互动,交流问题、想法,随时获取您需要的帮助。

让我们再谈谈您在这门课程中将面临的挑战。您将创建一个完整的多人项目,您将通过第一部分学习插件的开发,而在第二部分将创建完整的游戏。虽然许多课程仅涵盖项目的某个部分,但在本课程中,您将全面了解制作完整多人游戏的过程,了解该过程的所有要素。这可能是一个相当大的挑战,特别是如果您是编程新手或没有游戏开发背景的话。您需要花费时间理解代码的每一部分,确保您知道每个特性的工作原理以及为什么使用它。确保在您开始时,您完全理解每一节课的代码,这对确保您能顺利继续课程是非常重要的。

作为课程的一部分,我建议您定期检查代码,进行复习,确保您了解每个功能。只有这样,您才能确保课程的顺利进行,并获得您想要的知识和能力。随着课程的进行,您可能会遇到困难,这都是正常的。我鼓励您不要害怕请求帮助,这就是为什么我们在Discord上有社区,您可以在这里获得支持。

在课程的每个阶段,我将提供我建议的最佳实践,这些最佳实践将帮助您在课程中顺利完成。请注意,有时在编程中,您可能会在某个部分停滞不前,无法理解某个概念,但如果您仔细阅读每一节课中的所有内容并积极参与与其他学生的交流,您将能够逐步克服这些障碍。

我期待着在接下来的课程中与您一起探讨这些内容,带您进入多人游戏开发的世界。在接下来的课中,我们将深入探讨每个部分,帮助您掌握所需的知识和技能。希望您能在这门课程中获得丰硕的成果!

WangShuXian6 commented 2 days ago

2. 创建多人插件

1. 多人游戏概念

欢迎。在本视频中,我们将讨论多人游戏的一些基本概念。首先,我们将定义什么是多人游戏。接着,介绍在不同计算机上运行的游戏之间通过网络传输信息的各种方法。我们会讨论点对点模型、客户端-服务器模型,最后谈谈虚幻引擎使用哪种模型,以便更好地理解在创建虚幻引擎中的多人游戏时发生的情况。

首先,让我们讨论一下多人游戏。与单人游戏相比,多人游戏本质上更复杂。在单人游戏中,游戏只在一台计算机上运行一个游戏会话。虽然您可以配置一个分屏游戏,通过同一台机器上的多个设备输入来进行游戏,但这种类型的游戏不需要互联网,也不需要在不同机器上运行的游戏之间传输信息。这也被称为本地多人游戏。因此,当我们在本课程中使用“多人”一词时,我们指的是真正的多人游戏,即两个或多个游戏实例在不同的机器上运行。在多人游戏中,至少有两个游戏实例在各自的机器上运行,两个玩家都在控制角色。因此,关于游戏变化的信息需要传送到其他游戏实例。例如,如果玩家一移动了他们的角色,玩家二需要看到这个动作,那么关于玩家一角色移动的信息必须发送到玩家二的计算机。同样,玩家二也会移动他们的角色,所以关于玩家二角色移动的信息必须返回到玩家一的机器。那么,问题是我们如何传输这些信息?

有几种方法可以构建一个系统,以便在游戏会话之间共享信息。我们首先讨论的是点对点连接。这是一种最简单的信息传输方式。当玩家一移动他们的角色时,移动信息会直接发送到玩家二的计算机。同样,当玩家二移动他们的角色时,移动信息会发送回玩家一的机器。对于简单的两人游戏来说,这是一种容易实现的系统,并且效果不错。然而,它也存在一些问题。

想象一下,如果我们有第三个玩家,每当玩家一移动他们的角色时,他们必须将该信息不仅发送给玩家二,还要发送给玩家三。同样,当玩家二移动他们的角色时,他们必须现在将该信息发送给玩家一和三。而当玩家三移动他们的角色时,他们又必须将信息发送给玩家一和二。这导致了大量的数据在网络中传输,而这种模式在玩家数量较多的游戏中无法很好地扩展。

另一个点对点连接的问题是没有权威版本的游戏。每个游戏实例都是不同的。当玩家移动他们的角色时,他们的本地游戏版本已经偏离了其他机器上运行的版本,需要一定时间将这些信息传输到其他机器上的游戏实例进行更新。至于哪个游戏实例是正确的,这个问题是没有答案的。所有的游戏实例都是不同的,没有哪个是所谓的“正确版本”。

另一种制作多人游戏的方法是使用客户端-服务器模型。在客户端-服务器模型中,指定一台机器作为服务器,而所有其他机器则作为客户端。每个客户端只与服务器进行通信,永远不会直接向其他客户端发送信息。这意味着每个客户端只需满足与服务器发送和接收信息的带宽要求,而不是与其他运行游戏的机器通信。因此,当玩家一移动他们的角色时,他们将该移动信息发送到服务器,服务器然后将信息发送到每个其他客户端,使玩家一的角色能够在其他屏幕上更新。

服务器通常是权威的,尽管这并不是唯一的实现方式。这意味着服务器运行一个被认为是正确版本的游戏。当玩家想要移动他们的角色时,他们实际上是向服务器发送请求,服务器会检查该移动是否合适,然后移动角色并将该移动更新信息分发给所有客户端。服务器向客户端分发数据的过程被称为复制。

实现客户端-服务器模型有不同的方式。一种方法是使用监听服务器。在这种情况下,一个玩家的机器充当服务器,因此人类玩家实际上在玩游戏,该机器负责渲染服务器版本的游戏画面。另一种方法是使用专用服务器,在这种情况下,机器被指定为游戏的服务器,但没有人实际上在这台机器上玩游戏,因此没有必要将图形渲染到屏幕上,因为没有人能看到它们。专用服务器允许服务器机器仅处理模拟权威版本的游戏,并将数据复制到客户端。大规模的多人游戏(如MMO)通常使用专用服务器,特别是在高风险的竞技游戏中,涉及资金的比赛。

监听服务器确实为主机玩家提供了一些优势,因为移动角色不需要向服务器发送请求。由于主机玩家就是服务器,他们没有等待数据传输的延迟。然而,每个客户端必须向服务器发送请求,并等待数据通过网络复制回去。对于大多数小型游戏而言,这种方式是可以的,并且在良好的互联网连接下,差异通常是微不足道的。随着玩家数量的增加,对专用服务器的需求变得更加必要。

现在,让我们谈谈虚幻引擎使用的是什么。虚幻引擎使用权威客户端-服务器模型。这意味着一台机器始终充当服务器,其他机器作为客户端连接到它。服务器版本是权威版本,并始终被视为正确的游戏版本。在运行虚幻引擎的单人游戏时,您仍然在使用客户端-服务器模型,只不过客户端和服务器是同一台机器。在整个课程中,我们将深入学习这个系统的工作原理,以及如何处理游戏代码,以保持服务器对正确游戏机制的控制。

总结一下,我们了解了什么是多人游戏,以及当我们提到多人游戏时的确切含义。我们讨论了在机器之间实现信息共享的各种方法,包括点对点模型和客户端-服务器模型。我们提到服务器可以是监听服务器,其中一个玩家实际上在该机器上玩游戏,或者是专用服务器,其中机器专门用于模拟权威版本的游戏并向所有客户端复制信息。我们提到虚幻引擎使用权威客户端-服务器模型。服务器是作为专用服务器还是监听服务器取决于您如何配置项目。

现在我们了解了一些基本概念,准备深入研究虚幻引擎中的多人游戏。期待很快见到您!

2. 测试多人游戏

欢迎。在本讲中,我们将讨论如何在编辑器中测试多人游戏,以及如何通过局域网(LAN)连接到游戏。首先,让我们看看如何在编辑器中测试游戏。

在编辑器中测试游戏

我将首先创建一个新项目,启动 Unreal Engine 5。点击“游戏”,创建一个基于第三人称模板的新项目,这样我们就能拥有一个已经设置好的第三人称角色,可以进行奔跑和跳跃。我将把这个项目设置为 C++ 项目,并选择一个位置,我在 C 盘上有一个名为 Multiplayer Course 的文件夹。选择这个文件夹,并将项目命名为 MP Testing(多人测试),然后点击“创建”。

在这个项目中,你可能已经熟悉第三人称模板。我们有这个角色可以使用,进行基本的运行和跳跃功能。Unreal Engine 具有内置的多人测试功能。在编辑器中,播放按钮旁边有三个点,允许我们更改播放模式。在下拉菜单的底部,我们看到“多人选项”,可以将其更改为1到4之间的值。如果我们将其更改为2,当我们启动游戏时,将会有两个游戏实例运行。

网络模式

在“玩家人数”下方,我们有网络模式。如果我们将鼠标悬停在选项上,工具提示会提供一些信息。选择“以独立模式播放”将启动一个独立游戏,这不会创建专用服务器,也不会自动连接到一个。但选择“以监听服务器播放”时,工具提示说编辑器将同时充当服务器和客户端。选择这个选项并按下播放,我们会看到两个游戏实例在运行。编辑器中的实例是监听服务器。我们可以在不同的窗口中看到我们的角色在移动。

当我按 Shift + F1 以获取鼠标光标并点击第二个窗口时,我们现在在这个游戏的第二个实例中进行游戏,它是连接到监听服务器的客户端。我们可以看到顶部的标签显示为客户端1。让我们停止游戏,再次点击三个点,将玩家数量更改为3。点击播放后,我们会看到两个额外的窗口,编辑器中的游戏实例仍然是监听服务器。

使用专用服务器

接下来,我将更改网络模式为“以客户端模式播放”。工具提示显示编辑器将充当客户端,同时在后台启动一个服务器。这样会创建一个专用服务器,而在编辑器中运行的游戏实例将是连接到该服务器的客户端。点击播放后,我们会看到三个角色,这三个游戏实例都是连接到专用服务器的客户端。我们在场景中看到的第四个角色是因为场景中存在一个角色。当编辑器启动专用服务器时,它不会拥有这个角色,而是会生成三个额外的角色,由连接到专用服务器的客户端控制。

Unreal Engine 允许我们测试专用服务器和监听服务器的游戏,这使得测试多人代码非常方便,因为我们不必打包项目并发送到另一台机器上,只需确保多人代码正常工作。

设置局域网连接

有时,我们希望连接到另一台机器,以确保在两台机器连接在一起时能够正常工作。局域网(LAN)连接是指通过本地网络连接的两台或多台计算机。假设你有一个单一的路由器,多个机器连接到该路由器,每台机器都有自己的本地 IP 地址。这些计算机通过同一网络的本地 IP 地址互相访问。

许多游戏提供了通过局域网连接的选项。我们将看到如何通过局域网连接到 Unreal Engine 游戏。我们将连接逻辑放在角色蓝图中。打开角色蓝图类,在顶部打开完整的蓝图编辑器。删除这三个未使用的节点。

设置蓝图逻辑

我希望能够按下一个键以开始新的游戏会话。因此,我将右键点击并输入“键盘”,选择数字 1 键。这样我们可以将一些蓝图逻辑映射到数字 1 键被按下时的行为。我将同样地为数字 2 和数字 3 键做相同的事情。当我按下数字 1 键时,我希望切换到一个可以等待其他玩家加入的关卡。

为此,我们需要另一个关卡。在第三人称项目的“地图”中创建一个新关卡。点击文件,选择新建关卡,然后选择默认。保存当前关卡为“lobby”,这是其他玩家连接后将前往的关卡。

我将选择这个地面,并在细节面板中将其放大,以便有更多空间供角色移动。保存后,我们有了一个可以前往的关卡。回到第三人称角色中,为数字 1 键映射逻辑。

当我按下数字 1 键时,我希望简单地打开一个关卡。拖动“按下”执行引脚,输入“打开关卡”,选择“按名称打开关卡”。第一个输入是关卡名称,我将输入“lobby”。记住,这是区分大小写的。这样就能打开“lobby”关卡。如果我们在后面添加“?listen”,我们指定应以监听服务器的方式打开这个关卡,以接收来自多个玩家的连接。

当我运行游戏实例并按下数字 1 键时,将前往这个关卡并以监听服务器的方式打开它。接下来,我将在另一台计算机上运行这个游戏实例,以便连接到该关卡。

连接到指定 IP 地址

对于数字 2 键,我将拖动“按下”执行引脚,输入“执行控制台命令”。这个节点允许我们执行控制台命令,有一个命令可以让我们连接到特定的 IP 地址。我将使用当前计算机的 IP 地址。运行另一台连接到同一 Wi-Fi 的计算机,以便连接到这个游戏的 IP 地址。

如果你不知道自己的 IP 地址,可以打开命令提示符,输入命令“ipconfig”。找到 IPv4 地址,这是我的本地 IP。记住,这个地址无法从我的本地网络之外访问,但我将运行另一台连接到同一 Wi-Fi 的机器,能够通过这个地址连接到游戏。

在“执行控制台命令”节点中,输入“open”命令,后面跟上 IP 地址并留一个空格。这样执行控制台命令时,将连接到这个 IP 地址。一旦连接成功,当前游戏实例将加入在这个 IP 地址上运行的实例,并加入该开放的关卡。

打包和测试

我将编译并保存,然后回到“lobby”关卡,打包这个游戏。点击平台并选择要打包的项目平台。我在 Windows 上,因此选择 Windows,点击“打包项目”。选择打包到的文件夹,创建一个名为 build 的新文件夹,并将新打包的项目放在这个文件夹中。

打包完成后,我可以打开包含我项目的文件夹,找到 build 文件夹。在 Windows 文件夹中,我看到 MP testing.exe,这是独立游戏的可执行文件。要将其发送到另一台机器,我必须将整个 Windows 文件夹发送过去,因为可执行文件依赖于其他文件和文件夹。

我将这个文件夹放在 Google Drive 上,然后在另一台机器上下载。完成下载后,打开下载文件夹,找到这个 Windows 文件夹(以 zip 格式)。右键单击并选择“全部提取”,提取后将其重命名为“LAN Connection”,然后打开。

现在我可以双击 MP testing.exe 启动游戏,或右键选择打开。如果弹出警告,选择“更多信息”并点击“仍要运行”。启动游戏后,如果按下数字 1 键,将打开“lobby”关卡并设置为监听服务器。等待另一位玩家启动游戏并按下数字 2 键,他们就可以加入游戏,并与我一起玩。

总结

我们学习了如何在 Unreal Engine 编辑器中测试多人游戏,以及如何设置局域网连接,打包游戏并在连接到同一本地网络的两台计算机上进行游戏。在下一节视频中,我们将学习如何使用 C++ 函数设置局域网连接。下次见!

3. 局域网连接

欢迎大家。在本次讲座中,我们将设置一个局域网连接。这次我们将使用C++函数,而不是蓝图节点。我们已经通过蓝图建立了局域网连接,但了解如何通过C++实现这些功能也很有用。接下来,让我们开始。

这是我们在创建C++项目时生成的Visual Studio项目。在解决方案资源管理器中,我将打开源文件夹,接着打开MPTesting,并进入MPTestingCharacter.h。在这里,我想创建几个简单的可调用蓝图函数。所以,我将滚动到公共部分的底部,创建几个函数。第一个是一个返回类型为void的函数,我将其命名为OpenLobby,并给它一个UFunction宏,使其可被蓝图调用。如果你不熟悉如何创建函数并使其可被蓝图调用,我建议你参加我的其他关于虚幻引擎C++基础的课程。

现在我们可以为OpenLobby创建函数定义。顺便说一下,我还将创建另外几个函数。第一个,我将简单地命名为CallOpenLevel。在这个函数中,我们基本上将做和蓝图中相同的事情。现在,我将为这个函数添加一个类型为const FString的输入参数,并将其定义为常量引用,我将其命名为Address,这将用于IP地址。接下来,我将复制函数宏并将其粘贴到CallOpenLevel的上方,以使其成为可调用的蓝图函数。

通常在虚幻引擎中,有多种方法可以实现相同的功能。我将展示两种不同的方法来打开一个关卡。因此,我们将为此创建两个不同的函数。下一个函数将命名为CallClientTravel,我们也将使用const FString引用作为输入参数的类型。与其他函数一样,我们将使其可调用蓝图。接下来,让我们为这两个函数实现定义。我们将为CallOpenLevel和CallClientTravel创建定义。

接下来,让我们打开MPTestingCharacter.cpp,看看这三个函数。第一个是OpenLobby。在这个函数中,我们将调用新世界类中的一个函数。首先,我们需要获取世界的访问权限,我们可以说:UWorld* World,并使用GetWorld函数进行初始化。检查世界是否有效后,我们可以调用世界类中的一个函数ServerTravel。我们将使用World->ServerTravel。这个函数接受三个输入,但只有第一个输入是必需的,其他输入有默认值,我们可以保持不变。我们只需传递新关卡的路径名即可。我们可以使用字符串字面量,因此在双引号中,我将输入我们的大厅关卡的路径。

现在,回到编辑器,我可以通过查找大厅关卡,右键单击并选择“复制文件路径”来确定该路径。在Visual Studio的下一行,我将简单粘贴我复制的内容,你会看到它显示了大厅关卡的文件路径。需要注意的是,当我们传递关卡路径时,我们不需要传递完整路径。到内容文件夹的所有内容可以用“Game”来替代。所以我们输入/Game/,然后输入我们大厅地图的路径,这将是ThirdPersonCPP/Maps/Lobby。我们不需要新地图扩展名,它会自动假定是新地图。但我们想要这个“listen”选项,添加这个路径的选项,我们使用一个问号后跟选项?listen。这样我们就会传送到大厅关卡,并将其指定为监听服务器。因此,我可以删除粘贴在这里的文本。现在当我们调用OpenLobby函数时,将会传送到大厅关卡,并且它将被设置为监听服务器。

接下来,我们需要实现CallOpenLevel。对于CallOpenLevel,我们将使用一个游戏静态函数,称为OpenLevel。这里是关于该函数的虚幻引擎文档,它位于GameplayStatics中,函数签名的描述并不太详细,但我们可以获取GameplayStatics的包含内容。所以我们将复制这个包含内容,然后回到MPTestingCharacter.cpp,粘贴包含GameplayStatics的代码。现在我们可以使用GameplayStatics函数并调用OpenLevel。

我们将使用新的GameplayStatics函数OpenLevel。第一个输入参数是一个世界上下文对象,我们可以传入this。世界上下文对象只是一个存在于世界中的对象,它为这个GameplayStatics函数提供了一个可以追溯到世界的对象。大多数新的GameplayStatics函数需要一个世界上下文对象,而这个指针指向当前对象的实例,所以这将很好地工作。接下来的输入参数类型是FName,它是我们想要打开的关卡名。但我们也可以在这里传递地址,例如一个IP地址,所以我们可以使用我们的输入参数Address。由于Address是FString,而OpenLevel需要FName,但如果我们使用星号运算符,那么返回的是C风格字符串,这可以被该函数接受。因为C风格字符串可以隐式创建一个新的FName对象。所以我们传入的是C风格字符串,它将是我们在蓝图中使用节点CallOpenLevel时传入的任何字符串,作为可被蓝图调用的函数。

接下来,另一种旅行到关卡的方法是使用一个称为ClientTravel的函数。这里是关于ClientTravel的虚幻引擎文档。你可以看到它是一个在PlayerController类中的函数。因此,我们需要PlayerController来调用这个函数,它表示旅行到一个不同的地图或IP地址。为了调用ClientTravel,我们需要PlayerController,可以通过游戏实例获取。如果我们调用GetGameInstance,这个函数返回一个UGameInstance,并且在UGameInstance中,有一个获取第一个本地PlayerController的函数,称为GetFirstLocalPlayerController。这个函数返回一个APlayerController,我们可以将其存储在一个类型为APlayerController指针的局部变量中。我们将其命名为PlayerController,并将GetFirstLocalPlayerController返回的值赋给我们的PlayerController变量。

一旦我们获取了PlayerController,我们可以添加一个效果来确保它不是空值。所以我们可以检查if(PlayerController)。在这个检查中,我们可以调用ClientTravel函数。所以我们将说PlayerController->ClientTravel。这个函数实际上接受一个FString,而不是FName。这是ClientTravel和OpenLevel之间的一个区别。这里可以是一个IP地址,这正是我们在蓝图中使用这个节点时将传入的内容。因此,我们将简单地传递一个地址。

你会看到我们有更多的输入参数,下一个没有默认值,所以我们需要提供一个。它是类型ETravelType,一个特定的枚举,用于指定我们旅行到新关卡的方式。稍后我们会讨论这些旅行类型。现在,我们只需传入ETravelType枚举常量中的N。我们在这里选择TravelAbsolute。再次强调,不用担心,这种旅行类型会在适当的时候解释。但现在我们知道我们可以调用ClientTravel并传送到这个地址。

接下来,让我们编译我们的代码。如果你忘了如何编译,可以去“构建”并选择“构建解决方案”,我简单地使用Control + Shift + B。一旦我们编译完成,我们可以回到编辑器,现在我们可以使用这些可调用的蓝图函数。

我将回到蓝图文件夹,打开第三人称角色蓝图。现在,我希望使用我的可调用蓝图函数。对于按键1,我想调用我们的新可调用蓝图函数OpenLobby。这是我们的C++函数,在其中我们硬编码了大厅关卡的路径并调用了ServerTravel。对于按键2,我希望调用我们创建的可调用蓝图函数CallOpenLevel。这将需要一个IP地址,我将使用我的本地IP地址,所以我将填写这个地址。如果你使用的是本地IP地址,你必须确保传入的是你的IP地址,而不是我的,否则将无法连接。

对于按键3,我希望使用我们创建的第三个可调用蓝图函数CallClientTravel。与上一个函数一样,它也需要地址。我将把我的IP地址粘贴在这里。现在我可以编译并保存,这样我们就准备好测试了。我将返回大厅,并打包这个游戏。

这是我的构建文件夹,其中包含之前打包的游戏。我将删除它。现在,在编辑器中,我将转到“平台”,“Windows”,并选择“打包项目”,然后选择构建文件夹。因此,我会点击选择文件夹。现在打包完成后,我可以将这个打包项目发送到另一台计算机上。这里是我的Google Drive,我之前上传了打包项目。我将右键单击Windows文件夹并删除它,以免造成对我正在使用的项目版本的混淆。现在,这是我新打包的项目在Windows文件夹中。将整个Windows文件夹拖到我的Google Drive上。一旦上传完成,我将在另一台计算机上下载并提取,就像之前那样。

一旦我在另一台机器上下载了我的项目,并准备好测试者来启动游戏并加入我的游戏,我将启动我的打包可执行文件。这里是它,我将双击启动,并按下1键以打开指定为监听服务器的关卡,等待另一台机器上的测试者按下2键。好了,他们来了,现在我们可以玩了。通过局域网连接,就像我们之前所做的那样。只不过现在我们是通过C++函数连接的。

接下来,我将再次启动游戏。这次,我的另一台机器上的测试者将通过3键加入。因此,我将打开关卡并

等待他们加入。好了,他们来了,我们准备好玩游戏了。完美。在本视频中,我们使用C++函数而不是之前视频中的蓝图节点设置了局域网连接。设置局域网连接相对简单。困难的部分是连接不同网络上的机器。接下来的课程将专注于创建可以通过网络连接的游戏,而不仅仅是通过局域网连接。在接下来的视频中,我们将学习如何设置这样的连接。下次见。

4. 在线子系统

欢迎大家。在本视频中,我们将详细讨论IP地址,具体是什么,以及本地IP地址和公共IP地址之间的区别。接着,我们会讨论游戏如何能够在互联网上彼此连接,而无需知道对方的IP地址,这涉及到虚幻引擎的在线子系统。让我们先花点时间了解IP地址。

到目前为止,我们已经能够连接到同一局域网中计算机上的游戏实例。但我们希望与不在我们本地网络上的玩家连接。在继续之前,让我们讨论一下这究竟意味着什么。你的本地IP地址,也称为内部IP地址,是由本地网络路由器分配给你的计算机的地址。它通常以192.168开头,后面跟着更多数字。大多数网络路由器会为不同设备分配内部IP地址,这些地址可能会根据设备连接的顺序而变化。这些地址仅在你的本地网络内可见,外部网络无法通过本地IP地址连接到你的计算机。

你的计算机通过以太网电缆或Wi-Fi信号与路由器相连,而路由器则与你的互联网服务提供商(ISP)相连。为了让你的计算机连接到互联网,互联网服务提供商会将一个外部或公共IP地址分配给你的路由器。这个外部IP地址是互联网上其他人看到的。来自互联网的信息首先会发送到公共IP地址,然后再转发到你的本地IP地址。

我们已经看到在局域网连接中连接游戏是多么简单。因为两台计算机在同一局域网中,所以一台计算机可以使用其本地IP地址连接到另一台。但如果你想与在国家另一端甚至世界另一端的人一起玩游戏呢?这个人很可能连接到不同的局域网,因此连接到他们的本地IP地址是不可行的。如果你知道对方机器的公共IP地址,你可以连接到它。但这并不是理想的选择,因为你永远无法知道别人的IP地址,除非他们告诉你。

你有没有想过,多少次你登录你最喜欢的视频游戏时,需要输入你朋友的IP地址?如果你没有玩这个游戏的朋友呢?难道你只能孤单地玩?当然不是。你通常登录游戏,游戏本身会为你找到其他玩家。无论他们身在何处,你只想玩你的游戏。那么,你的视频游戏是如何知道如何找到其他在线玩家的IP地址的呢?

答案有很多种。有时游戏有自己的专用服务器,当你登录时,你连接到其中一个服务器,该服务器有一份可以连接的IP地址列表。服务器选择一个,并向你显示已登录的朋友的名字,并将你连接到该玩家。但设置专用服务器需要时间和金钱。玩家越多,你的游戏需要的服务器、存储和处理能力就越多。你可以通过使用“监听”服务器来规避这个问题。一名玩家启动游戏并设置为监听服务器,其他玩家作为客户端加入。但问题仍然存在:客户端如何知道要连接哪个IP地址,以便将你的游戏连接到在线的其他人?

这必然需要一个中间环节。你的游戏在登录时会发送信号到某个服务器,该服务器将你连接到其他正在登录的玩家。游戏通常会通过一个服务来托管这些服务器,因为这比创建自己的服务器更省时,而且通常提供附加功能,如安全性和好友系统。要设计一个系统来托管游戏、维护玩家安全并扩展到处理大量用户,需要超越单纯编写游戏逻辑的专业知识。如果你希望将游戏发布到一些流行的服务,如Xbox或PlayStation,你必须重写你的游戏系统,以便与他们的代码库兼容。

如果我们能够只学习一个代码库,然后让我们的游戏连接到任何我们选择的服务,那将多好。而这正是我们要做的,这将涉及虚幻引擎的在线子系统。

我们的目标是创建一个游戏,能够在不需要知道对方IP地址的情况下,加入其他已登录的玩家。为此,虚幻引擎为我们提供了一个在线子系统。当你在虚幻引擎中制作一个多人游戏时,通常会连接到一个管理游戏会话并将来自世界各地的玩家连接在一起的服务。这些服务包括Steam、Xbox Live、Facebook游戏等等。你的游戏连接到其中一个服务,该服务有自己的代码来处理玩家之间的连接。例如,Steam有自己用于连接玩家、设置好友列表等的代码。

那么,这是否意味着你需要学习所有Steam的代码库?如果你想将游戏移植到Xbox,又该怎么办?你需要学习Xbox Live服务的所有代码吗?这将非常不方便。虚幻引擎为我们提供了一种方式,仅使用一个代码库,即虚幻引擎的代码库。虚幻引擎有我们可以用来连接服务的函数,具体取决于我们游戏配置使用的服务。虚幻引擎将在幕后处理所有平台特定的细节,当一个程序或游戏引擎允许我们仅与单一代码库交互时,这被称为抽象层。我们只处理虚幻引擎的代码,其他平台特定的代码被抽象掉,因此我们无需担心它。

虚幻引擎的网络抽象层旨在连接各种在线服务,称为在线子系统。虚幻引擎的在线子系统包含我们可以用来连接服务、托管我们的游戏会话并将我们连接到其他玩家的函数。我们指定使用哪个服务,虚幻引擎在幕后处理该服务的特定内容。这使得创建多人游戏变得非常强大。我们可以使用虚幻引擎的在线子系统编程我们的游戏,而无需更改代码以将游戏发布到不同的服务。我们只需为新服务配置项目。

因此,由于我们只需处理抽象层,所以选择哪个服务实际上并不重要。在本课程中,我们选择Steam,因为它是托管多人会话的最受欢迎的服务之一,而且很容易入门,学习如何使用,而无需申请开发者许可证或支付费用。当然,一旦你有了想要出售的游戏,并希望在Steam商店中上架,可能需要做所有这些事情。但在你学习的过程中,你仍然可以使用Steam来托管你的会话,以确保游戏在你迈出下一步之前正常运行。

我们这一部分的目标是创建一个系统,允许我们启动游戏并简单按下一个按钮,然后我们的游戏将搜索任何在线并已登录的玩家,并与其中一名玩家连接。我们不想输入某人的IP地址,只想点击播放。因此,在这一部分中,我们将设计并创建这个系统,同时将其打包成一个精美的小插件,以便我们可以将其添加到新的虚幻引擎项目中并重复使用。

一旦我们创建了这个系统,就不必在我们计划制作的新多人游戏中再次创建它。稍后,我们将把我们的代码打包成一个紧凑的模块,可以插入任何项目并使用。听起来很棒,对吧?确实如此。一旦我们创建了一种通过互联网连接游戏与其他玩家的方法,我们就可以专注于为我们的多人游戏创建游戏逻辑,然后可以在其他虚幻引擎项目中重用我们的代码,制作全新的多人游戏。这是你游戏开发和虚幻引擎学习的下一个层次。当你与其他玩家一起游戏时,一切都会变得更加有趣。

在下一个视频中,我们将讨论虚幻引擎的在线子系统,它如何工作,并制定一个简单菜单系统的游戏计划,以便与我们将创建的小插件连接。总结一下,我们学习了IP地址及其本地IP地址仅在本地网络中使用,而路由器的公共或外部IP地址则是其他互联网连接到的地址。我们了解到,为了在不知道另一台计算机IP的情况下连接到它,我们需要一个中间步骤,无论是我们的专用服务器还是专业服务如Steam或Xbox Live托管的服务器。我们学习到虚幻引擎有自己管理连接在线服务的代码库,称为虚幻引擎在线子系统。我们了解到这个在线子系统为我们提供了一个抽象层,这样我们就不需要学习每个服务的代码库,只需学习虚幻引擎的代码,我们将学习如何使用这个在线子系统,以便只需编写一次代码连接到其他已登录的玩家,然后将其打包成可以在多个虚幻引擎项目中使用的插件。我们将深入讨论这些概念,同时创建旨在连接到其他互联网玩家的菜单系统。我们很快再见。

5. 在线会话

欢迎。在本讲中,我们将讨论虚幻引擎在线子系统类的作用,以及它包含的与多人游戏会话相关的多个接口。我们将深入探讨会话接口,这是一个旨在处理设置和加入游戏会话的接口,并制定一个使用该接口将我们的游戏连接并与其他玩家一起游戏的计划。

首先,让我们谈谈在线子系统。在线子系统提供了一种访问在线平台服务功能的方法。这里的在线平台指的是像Steam和Xbox Live等服务。每个平台都有其支持的服务集,包括好友、成就、设置匹配会话等功能。在线子系统包含了一组接口,用于处理每个平台的不同服务。通过在线子系统,我们可以处理这些接口,无论我们选择哪个服务,只需在项目的配置文件中进行相应的配置即可。我们将在即将到来的课程中一起学习这个过程。

在线子系统是一个类型为 IOnlineSubsystem 的类,可以通过该类的静态函数 Get 访问。这个函数返回一个指向类型为 IOnlineSubsystem 的在线子系统的指针。一旦我们获得了在线子系统,就可以访问之前提到的不同接口。当前我们最关心的接口是会话接口。会话接口负责创建、管理和销毁游戏会话,并处理搜索会话和其他匹配功能。可以将会话视为在服务器上运行的游戏实例,具有一组属性。会话可以被公开,便于其他玩家找到并加入,或者是私密的,只有被邀请的人才能加入。

典型的游戏会话的基本生命周期如下:

  1. 创建会话,设置所需的属性。
  2. 等待其他玩家加入,并注册每位玩家。
  3. 当足够的玩家加入后,启动会话,所有玩家在同一会话中进行游戏。
  4. 比赛结束后,可以结束会话并注销玩家。
  5. 然后可以更新会话,改变比赛设置,或简单地销毁会话。

目前我们只需关注几个关键的功能,具体为:CreateSession(创建会话)、FindSessions(查找会话)、JoinSession(加入会话)、StartSession(开始会话)和 DestroySession(销毁会话)。通过使用这些功能,我们可以建立一个可工作的游戏。

接下来,我们将讨论如何实现这一点。我们的目标是能够在游戏菜单中点击一个按钮。现在,我们假设有两个菜单按钮:“主持”和“加入”。当玩家点击“主持”时,我们的代码将配置会话设置,然后调用会话接口的 CreateSession 函数。完成后,我们可以打开大厅级别,等待其他玩家加入。当有人开始游戏并点击“加入”时,我们将配置一些搜索设置,以帮助过滤出不想加入的游戏会话。然后我们将调用接口函数 FindSessions。这将返回一些搜索结果,我们将遍历这些结果并选择一个有效的会话,接着调用会话接口的 JoinSession 函数。完成后,我们将能够获取正确的IP地址,并使用 ClientTravel 函数加入其他玩家的监听服务器,与他们一起进入大厅级别。

我们将把所有这些功能放入一个专门处理这些会话相关特性的整洁类中。但我们不想一开始就创建太多新类。现在,我们将从角色类中访问在线子系统,并调用这些函数,看看一切是如何工作的。一旦我们理解了如何使用在线子系统及其会话接口函数,我们将创建自己的类来处理这些功能,并将其设计为可以在我们想要的任何游戏中使用。

在下一个视频中,我们将创建一个新项目,并将其配置为Steam平台。这样,我们在会话接口中调用的任何函数都将自动连接到Steam平台并访问其在线会话服务。随后我们将在角色类中访问在线子系统,并在屏幕上打印一些文本,以确保我们成功连接到Steam。

总结一下,我们了解了更多关于在线子系统的信息,以及它包含的接口,这些接口具有连接到我们选择的在线平台服务所需的所有功能。我们目前最感兴趣的是会话接口,它包含了设置和管理游戏会话所需的所有功能。然后,我们为游戏制定了一个计划,将作为设置会话代码时的指导。我们将开始使用这些会话相关函数,并在角色类中了解它们的工作原理,随后再创建一个专门处理游戏会话相关功能的类。再见,期待与您再次相见。

6. 为Steam配置

欢迎。在本讲中,我们将创建一个新项目,并将其配置为使用Steam平台。之后,我们将访问在线子系统,并在屏幕上打印一些文本,以验证我们是否已成功连接到Steam。让我们开始创建新项目。

我已经打开了Epic Games启动器,现在要启动虚幻引擎5。接下来,我选择“游戏”,并选择第三人称模板,这样我们可以有一些能够四处奔跑的角色。确保这是一个C++项目,我的项目位置设置为C:\multiplayer。现在,我们将为该项目创建一个菜单系统,项目名称定为“菜单系统”,然后点击“创建”。

现在新项目已打开。首先,为了将此项目配置为Steam,我们需要启用Steam插件。我们可以在菜单中选择“编辑”,然后选择“插件”,打开插件菜单。在插件菜单中,我可以使用搜索栏,搜索“在线子系统Steam”,你会看到一个名为“在线子系统Steam”的插件,它提供了对Steam平台的访问。我们需要启用它,所以我会点击“启用”,现在编辑器告诉我必须重启虚幻编辑器才能使插件更改生效。让我们点击“立即重启”。

编辑器已经重启,我可以关闭插件窗口。接下来,我们需要实际启用Steam模块。我将缩小这个窗口,进入我的Visual Studio项目。模块是引擎中的代码包。如果我点击EUI 5旁边的下拉菜单,我们会看到一个名为“插件”的文件夹,展开后可以看到构成引擎各个部分的不同插件。每个插件都是一个模块,包含特定功能的代码。

为了访问特定模块,我们需要将其添加到构建文件中。如果我点击“源”旁边的下拉菜单,再点击“菜单系统”旁边的下拉菜单,我会看到一个名为“menu_system.Build.cs”的文件。点击它,这个文件包含了“PublicDependencyModuleNames”,任何列在这里的模块将在项目中可用。我们将通过添加逗号和模块名称(用双引号括起来)来向此列表添加一个模块,这个模块将是“OnlineSubsystemSteam”。添加完“OnlineSubsystemSteam”后,我们还需要添加“OnlineSubsystem”。这两个模块是不同的,在线子系统是与Steam进行交互的整体子系统,而在线子系统Steam是我们将用于连接Steam的特定子系统。

现在让我们编译一下。编译成功后,我们需要将项目配置为使用Steam作为在线子系统。为此,我将打开包含项目的文件夹。在该文件夹中,有一个名为“Config”的文件夹,打开后我们可以看到一些以.ini为扩展名的配置文件。我们可以使用文本编辑器打开“DefaultEngine.ini”,我将使用Notepad++。

这是“DefaultEngine.ini”文件。我们只需在此文件中添加一些内容,以便我们的项目配置为使用Steam。下面是虚幻引擎在线子系统Steam的文档。如果你想进一步了解,可以阅读这篇文章。向下滚动,我们会看到关于需要添加到“DefaultEngine.ini”的设置的信息。

首先,我们需要在“DefaultEngine.ini”文件中包含一个名为“[/Script/Engine.GameEngine]”的部分,并包含“[OnlineSubsystem]”的定义。网络驱动程序是用于将计算机连接到网络的程序。我们需要将网络驱动程序设置为Steam网络驱动程序,以便使用Steam网络。

我们还将定义一个在线子系统部分,将默认平台服务设置为Steam。这样,我们配置了游戏以使用Steam网络服务。接着,我们需要添加“[OnlineSubsystemSteam]”部分,启用Steam并设置我们的Steam开发应用ID。我们使用开发者ID 480,因为我们没有自己的Steam开发应用ID。你可以去Steam网站申请自己的开发应用ID,但在这之前,你可以使用开发应用ID 480,这是Steam的示例项目“Space War”的ID。许多人使用此开发应用ID,以便在没有自己开发应用ID的情况下学习如何使用Steam服务。

我们还需要这个附加部分“[OnlineSubsystemSteam.SteamNetDriver]”,以便将“NetConnectionClassName”设置为“OnlineSubsystemSteam.SteamNetConnection”。这些是我们需要配置项目以使用Steam的设置。现在,我们将复制这些内容并添加到“DefaultEngine.ini”中,然后保存。

我们已在“DefaultEngine.ini”中添加了一些设置。接下来,我将返回到包含项目的文件夹,并重新生成Visual Studio项目文件,以确保所有内容都已更新。在此之前,我将关闭Visual Studio项目,并关闭虚幻引擎项目。

在重新生成Visual Studio项目文件之前,我将删除一些自动生成的文件夹:Saved、Intermediate和Binaries。然后我可以右键点击“.uproject”文件,选择“生成Visual Studio项目文件”。由于我使用的是虚幻引擎5,所以选择该版本,然后点击“确定”。

你会看到Saved和Intermediate文件夹已恢复。让我们双击新项目以重新打开它,并会提示我们重建缺失的模块。这是因为我们删除了一些文件夹。我将点击“是”,现在你会看到Binaries文件夹又回来了。

项目已再次打开,我将缩小它并再次打开“.sln”文件,这是我的项目。关闭Builder.cs文件。现在我们的项目已配置为将Steam作为其子系统,我们可以在代码中访问在线子系统。在下一个视频中,我们将通过代码访问在线子系统,并添加一些屏幕调试消息,以显示该子系统的名称,以便我们能看到是否成功连接到Steam。

总结一下,我们创建了一个新项目,并将其配置为使用Steam作为在线子系统。我们已将“OnlineSubsystem”和“OnlineSubsystemSteam”模块添加到项目的构建文件中的公共依赖模块名称中。现在我们可以访问在线子系统,并准备在代码中开始使用它。再见,期待下次见面。

7. 访问在线子系统

欢迎大家。在本次讲座中,我们将通过代码访问在线子系统,并将子系统名称打印到屏幕上。这样我们就能看到我们连接的是哪个子系统。首先,我们需要在代码中访问在线子系统。正如我之前提到的,我们将创建一个类来处理所有与会话相关的代码,但在深入学习这些函数如何工作之前,我们将暂时将一些代码添加到我们的角色类中。模板创建了一个菜单系统角色类,所以我们现在打开 MenuSystemCharacter.hMenuSystemCharacter.cpp 文件。

首先,我想滚动到文件底部并创建一个公共部分,以便将与会话相关的代码集中在一起,这样就不会与其他代码混淆。我们添加一个公共部分,并在这里放置所有与在线会话相关的变量和函数。

接下来,我们进入 MenuSystemCharacter.cpp 文件。这里是构造函数,我们向下滚动到构造函数底部,为自己腾出空间。在这里,我们想要访问在线子系统。在线子系统类的类型是 IOnlineSubsystem。我要创建一个指向这个类的指针,称为 OnlineSubsystem。我们看到 IOnlineSubsystem 是未定义的,所以我们需要包含定义它的头文件。

这是 IOnlineSubsystem 类的文档,我们可以看到它的包含语句为 OnlineSubsystem.h。复制这个,然后回到角色类,将该包含语句粘贴到顶部。现在,IntelliSense 应该会跟上,IOnlineSubsystem 现在已定义,所以我们有了一个未初始化的指针。接下来,我们需要实际访问 IOnlineSubsystem,可以使用 IOnlineSubsystem::Get() 这个静态函数。

现在我们可以检查这个指针是否有效。如果指针有效,我们就可以使用 OnlineSubsystem 访问会话接口。我们可以通过指针调用 GetSessionInterface 函数,这个函数返回一个 IOnlineSession 指针,我希望将其存储在一个变量中。因此,我们回到 MenuSystemCharacter.h,添加一个注释,说明这是指向在线会话接口的指针,类型为 IOnlineSessionPtr

这个指针是一个特定类型的智能指针,不能前向声明。我们可以看到其类型,通过悬停可以看到它是 typedef TSharedPtr<IOnlineSession>。这个智能指针设计用于指向 IOnlineSession 对象,并且是线程安全的。我们将此指针命名为 OnlineSessionInterface,并使用 GetSessionInterface() 函数将会话接口存储到此指针中。

我们接下来尝试编译代码,看看是否有错误。出现了一些错误,因为 OnlineSessionInterface 是一个未知类型。通常我们在定义指针变量时会前向声明,但我们不能这样做。因此,我们可以选择在 MenuSystemCharacter.h 文件中包含 IOnlineSession 的头文件,或者使用 TSharedPtr<IOnlineSession> 进行前向声明并将其标记为线程安全。

为了避免在头文件中包含 IOnlineSession,我们选择后者,使用 TSharedPtr<IOnlineSession>。这样做后,我们需要在 MenuSystemCharacter.cpp 中包含 IOnlineSession 的头文件,这是 Interfaces/OnlineSessionInterface.h。现在,我们在角色构造函数中有了在线会话接口。

接下来,我们希望验证是否找到了在线子系统,并查看我们正在使用哪个子系统。我们可以使用引擎提供的 AddOnScreenDebugMessage 函数在屏幕上打印文本。我们先检查 GEngine 是否有效,然后调用 GEngine->AddOnScreenDebugMessage。我将使用 -1 作为键,这样就不会替换之前的消息,打印时间为 15 秒。

参数包括 FColor,我将使用蓝色。然后是我们的字符串,我将使用 FString::Printf 来格式化字符串。第一个参数是字符串本身,我将打印“找到的子系统”,并通过格式化字符串插入子系统名称,这个名称可以通过 OnlineSubsystem 指针获得。

运行游戏时,我们应该能访问在线会话接口,并打印出我们连接的在线子系统的名称,预期是 Steam。现在让我们尝试编译。

如果您看到“无法删除热重载文件”的消息,这通常是延迟问题。处理此问题的方法是关闭 Visual Studio 和 Unreal Engine 编辑器,然后删除 BinariesIntermediateSaved 文件夹,再重新生成项目文件。

确保您有一个 Steam 帐户。如果没有,请创建一个并安装 Steam 应用。启动 Steam 应用并登录后,再运行 Unreal Engine 游戏连接 Steam。

现在返回编辑器,您应该看到“找到子系统:NULL”。这并不是指指针为空,而是 Unreal Engine 实际上有一个名为 NULL 的在线子系统,旨在进行局域网连接。我们在连接 Steam 在线子系统时遇到了一些问题。您可以尝试以下方法:首先,在 PC 上测试游戏,或使用“在编辑器中播放”。然后更改网络模式为专用服务器或监听服务器。查看调试消息,然后打包游戏并测试打包后的构建,看看是否连接到了 Steam。

暂停视频,尝试一下这些步骤。我们发现如果仅在编辑器中播放,会显示“找到子系统:NULL”。当我们更改网络模式为监听服务器并播放时,仍然看到 NULL。最后,当我们更改网络模式为客户端并播放时,仍然看到“找到子系统:NULL”。

下一步是打包构建。我将选择平台,Windows,然后选择“打包项目”。在项目文件夹中创建一个名为 Build 的新文件夹,并将打包后的项目存放在其中。打包完成后,返回项目文件夹并打开 Build 文件夹,双击可执行文件启动它。此时,您应该在左上角看到“找到子系统:Steam”,并且右下角会弹出 Steam 通知,确认我们已连接。

通过这些步骤,我们验证了在打包的项目中,我们能够连接到 Steam 在线子系统。接下来的视频中,我们将使用存储的会话接口指针,调用 CreateSession 函数以创建我们的第一个游戏会话,并通过屏幕上的调试消息验证新会话的创建。

总结一下,我们创建了一个新项目,并配置它使用 Steam 作为在线子系统。我们还在角色类中访问了在线子系统,并创建了指向在线会话接口的指针。在下一节课中,我们将使用这个指针。期待与大家再次见面!

8. 创建会话

在线会话接口与 C++ 中的指针检查

在线会话接口应持有会话接口。但在 C++ 中,正如你所知,使用指针之前检查其有效性是很好的习惯。我们将使用一个 F 检查,并利用我们的在线会话接口指针。作为类型 T 的智能指针共享指针,我们检查其有效性的方法是使用 is valid 函数。这个函数属于 T 共享指针包装类。我们正在检查它是否有效,但我想使用否定运算符来检查 is valid 是否返回 false。因此,如果进入这个 if 检查内部,那么我们的在线会话接口确实无效,在这种情况下将简单返回。

如果我们通过了这个 if 检查,那么会话接口确实有效。一旦我们开始创建会话,我们会迅速发现如果我们已经创建了一个会话,就不能在不先销毁现有会话的情况下创建另一个会话。这意味着我们要做的第一件事是检查会话是否已经存在。我们将使用我们的在线会话接口指针,并调用函数 get named session。这个函数需要一个 F 名称,因为一旦我们开始创建会话,我们将给它们命名。我们可以为会话名称创建一个 F 名称变量,但我们始终会使用相同的会话名称。存在一个全局变量叫做 name_game_session。如果我们总是使用这个名称游戏会话,那么我们将始终检查是否存在一个带有此名称的会话。

获取命名会话返回一个类型为 F 命名在线会话的对象。我们将把它存储在一个叫做 existing_session 的变量中,并简单地使用 auto 来方便地声明这个局部变量。我们将检查这个现有会话是否不是一个空指针。因此我们会说,如果 existing_session 不是空指针,那么在这种情况下,我们将简单地销毁会话。因此,我们将调用在线会话接口的 Destroy Session,并传入会话名称,即 name_game_session。到目前为止,我们已经确保在线会话接口是有效的。如果无效,我们将简单地返回,并获取现有会话。如果存在并且这个现有会话不是空指针,我们将简单地销毁当前会话。

创建新会话与会话设置

现在我们准备创建一个新会话,这将涉及设置一些会话设置。这里是 F 在线会话设置的文档页面。这是创建新会话时会话设置的类型。我们将包括 online session settings.h。我们需要使用 T 共享指针类型作为我们的在线会话设置变量的包装。现在,我们将在这里创建一个本地变量,类型为 T 共享指针 F 在线会话设置,并称之为 session_settings

这是一个非正式化指针。为了创建此类型的新对象,我们需要构造一个。初始化新共享指针的方法是使用 make shareable 函数,并在 make shareable 内使用 new 关键字,后跟我们正在创建的类的构造函数,这将是一个 F 在线会话设置对象。因此,我们调用这个类型的构造函数,make shareable 将其包装在 T 共享指针中。

F 在线会话设置类有几个变量可以用于我们的会话设置。我们不会立即讨论这些变量。我们将简单地使用它来调用 create session,并使用我们的在线会话接口指针调用 Create Session。现在,Create Session 需要几个参数。它需要一个 F 唯一网络 ID,称为 hosting player ID,然后需要一个会话名称,最后是一个名为 New Session Settings 的在线会话设置对象。

首先,我们需要这个 F 唯一网络 ID。获取它的方法是通过获取世界的第一个本地玩家。我们将创建一个联系本地玩家的指针,称为 Local Player。我们可以通过 get world 获取第一个本地玩家的控制器,使用 get first local player from controller。这返回一个 U 本地玩家,U 本地玩家允许我们获取该唯一网络 ID。因此,我们将在这里传入本地玩家,并调用 U 本地玩家类中的函数 get preferred unique net ID。这返回类型 F 唯一网络 ID。

创建会话的参数与委托

我们需要在返回值前使用解除引用运算符。接下来是会话名称,我们将再次使用 name_game_session。第三个参数是我们的会话设置。因此,我们将输入 session_settings。如果再次查看输入参数列表,我们看到需要传入我们的会话设置,但我们传入的是指针。它不期望一个指针。因此,我们也需要解除引用我们的会话设置指针,这样可以消除所有错误。

现在我们调用 Create Session,并有一个委托来响应它,但我们仍然没有将这个委托添加到会话接口的委托列表中。因此,在创建游戏会话之前,我们将添加我们的委托到其委托列表。我们将在线会话接口并调用 add on create session complete delegate handle。我们可以在这里传入我们的委托。因此,在线会话接口现在将我们的委托添加到其委托列表中。一旦我们创建了会话,我们绑定到这个委托的回调函数将被调用。这个回调就在 on Create Session Complete 中。在这里我们将验证我们的会话确实已创建。

验证会话的创建与反馈

现在我们调用 Create Session,是时候验证我们的会话是否已经创建,我们将在回调函数中打印会话名称。在 on Create Session completes 中,广播了 on create session complete 委托后,我们的回调函数将接收广播的信息。首先,我们检查这个布尔值 B was successful。如果我们成功创建了一个会话,这将为 true,因此我们可以进行 if 检查,查看 B was successful 是否为 true。如果为 false,我们将添加一个屏幕调试消息。

我们将使用 F G Engine.add on screen debug message,时间显示使用 -1,显示时间为 15.F,颜色使用 F 颜色,我会使用红色以引起我们的注意。假如 B was successful 为 false,消息将简单地初始化为 failed to create session。这样,如果出现问题且无法成功创建会话,我们将在屏幕上看到它。

测试与配置会话设置

我们将在成功情况下复制这个消息,并将其颜色改为蓝色。对于字符串,我想使用 print F 再次,因此我会说 print F 并使用文本宏初始化字符串,字符串将为 created session 并使用该格式指定我们的会话名称。因为这个会话名称是 F 名称,我们将使用 to string 转换为字符串,并用解引用运算符转换为 C 风格字符串。

接下来,我们将设置一些会话设置。在线会话设置类有几个变量。我们将使用其中一些来配置即将创建的会话。我们将说 session_settings,第一个设置是 B is land match。我们将其设置为 false,因为我们不是在创建局域网比赛。我们实际上希望通过互联网连接。

会话设置还有一个连接数的设置,决定多少玩家可以连接到游戏。它叫做 num public connections,我们将其设置为 4,这样在编程能力加入会话时,我们最多可以有四个玩家。会话设置还有一个设置叫做 B allow join in progress,这意味着如果会话正在运行,其他玩家可以加入。我们将其设置为 true。

下一个会话设置是 B Allow Join via presence。当 Steam 设置游戏会话时,它使用所谓的 presence。它不会将任何玩家从世界各地连接到其他玩家。它有区域,并在你所在区域寻找进行中的会话。我们需要使用 presence 才能使连接工作。因此,我们将这个设置设置为 true。

我们的最后一个设置是 B should advertise,这允许 Steam 广告会话,以便其他玩家可以找到并加入该会话。我们也将其设置为 true。最后,我们将设置 B use his presence,这将允许我们使用 presence 来寻找在我们所在区域进行的会话。因此,我们将其设置为 true。

测试游戏与解决常见问题

现在我们已经设置了传入 create session 的所有会话设置。一旦调用这个,使用这些设置将创建会话。我挑战你测试这个。打包游戏,确保你启动 Steam,因为 Steam 必须在后台运行才能连接到 Steam。然后进行游戏测试,看看我们在屏幕上打印出什么。你是否成功创建了一个游戏会话?

现在我们准备测试我们的游戏。我将首先保存所有内容,然后转到平台,选择 Windows,并打包项目。这里还有上次打包的构建,我将简单删除它。在我的构建文件夹中,我将选择文件夹。现在我们正在打包游戏。

现在打包完成,我可以在我的项目中打开该构建文件夹。在构建文件夹中,这是 Windows 文件夹,其中有我的菜单系统 .xy。我将双击它以启动游戏。现在我看到左上角找到子系统 Steam。因此我们连接到 Steam,并准备尝试创建会话。我将按下数字键 1,看到创建的会话,名称

Game session。这是因为我们使用了全局变量 name_game_session。该字符串值为字面上的 game session。因此,我们成功创建了会话。

接下来的步骤是实现从另一台机器加入会话的功能。在下一个视频中,我们将继续创建一个大堂级别。创建会话后,我们将前往此级别,在那里我们可以等待其他玩家加入。我们还将设置一个加入会话的功能。这样我们可以打包项目,并将其发送到另一台机器,后者可以启动项目并通过互联网加入我们的运行游戏会话。

总结与常见错误解决

总结一下,我们回顾了委托以及它们在管理虚幻引擎中的在线会话中扮演的重要角色。我们创建了一个 F on create session complete 委托以及一个回调函数,然后将其绑定到这个委托,并将这个委托添加到会话接口的委托列表中。最后我们调用了 Create Session,结果创建了一个会话。此操作完成后,我们的回调被触发,因为我们绑定到添加到会话接口委托列表的 on create session complete delegate

如果你在创建会话时遇到任何问题,特别是在使用 Unreal Engine Preview 2 或 Unreal Engine 5.0(最新正式发布)时。我有一些学生在没有以下代码的情况下无法创建会话,Default Engine I and I。如果你从虚幻引擎文档中复制这些行,可能会看到类似的内容。如果无法创建会话,请将这两行替换为 B in it Server on client equals true,位于 online subsystem steam 下。如果你有这个,那么你应该能够创建会话。

另外,有人遇到问题,即创建 C++ 项目时出现崩溃,使用第三人称模板或第三人称角色。这是虚幻引擎 5 中的一个已知错误,仍需修复,与第三人称角色的动画蓝图相关。有几种方法可以解决这个问题。首先,你可以找到第三人称蓝图,打开第三人称角色,选择网格,然后在动画类中将其设置为 none。这意味着角色将处于 T 姿势,但如果你只是想检查连接,这就没问题。

如果你想测试具有复制角色的多人游戏,另一种选择是简单地使用旧的第三人称人偶,因此你可以从其他项目中获取人偶,或者创建一个旧的 Unreal 4 项目,使用第三人称角色。不管你如何获得,你可以将该人偶的角色蓝图迁移到你的项目中。对于此类不稳定性,抱歉。这是 Unreal Engine 5 仍需修复的一个问题。

有些学生遇到此错误消息,表示 Windows SDK 未正确初始化,无法生成数据。如果你在打包时收到此消息,解决方案很简单。你需要确保拥有最新的 SDK 以及 .NET Core 3.1 运行时 ELTs,可以通过更新 Visual Studio 的 Visual Studio 安装程序来确保这一点。因此,打开你的 Visual Studio 安装程序,并在你使用的版本中,修改并转到单个组件,确保安装 .NET Core 3.1 运行时 ELTs。这些是一些并不明显的事情,但使用虚幻引擎的新版本,我们正在使用更新的技术,需要确保拥有所有必要的组件。更新你的 Visual Studio 并确保安装该组件是个好主意。

9. 设置加入游戏会话

创建加入游戏会话的蓝图可调用函数

欢迎回来。在本讲中,我们将创建一个可以从角色类调用的加入游戏会话蓝图可调用函数,并开始设置查找和加入游戏会话的功能。这将涉及为“查找会话完成”创建一个委托,该委托将在调用会话接口函数 find sessions 时触发或广播。这将涉及创建一些搜索设置,接下来我们将看看如何设置这些内容。

创建加入游戏会话函数

首先,我们需要创建我们的蓝图可调用函数 join game session。以下是我的菜单系统项目,在 menu system character.h 文件中。我想创建另一个蓝图可调用函数,这将与我们的键盘按键之一(可能是数字键 2)关联。因此,我将定义一个返回类型为 void 的函数 join game session,可以复制 UFUNCTION 宏并粘贴,以使这个函数可供蓝图调用。现在我们可以快速为这个函数定义一个函数体。

在这个函数中,我们将首先查找会话,因此我会在这里添加一个注释,说明我们将在这里“查找游戏会话”。我们将调用会话接口函数 find sessions

创建委托和回调函数

为了查找会话,我们需要为查找游戏会话创建一个委托和回调函数。因此,这将是我们的下一步。与会话接口函数 create session 一样,接口函数 find sessions 也有一个关联的委托。我们将创建一个委托变量,就像我们为 Create Session 做的那样。这个委托的类型是 FOnFindSessionsCompleteDelegate,我们可以将其命名为 FindSessionsCompleteDelegate。我们还需要一个回调函数,因此我们可以在保护区域的顶部创建它,放在 OnCreateSessionComplete 下面。这个函数将是一个 void 函数,称为 OnFindSessionsComplete,它必须接受一个布尔输入参数,命名为 BWasSuccessful。这个布尔值将简单地反映查找游戏会话的成功或失败。

我也可以为这个回调创建一个定义,这个函数将在会话接口广播查找会话完成的操作时被调用。现在我们有了委托和回调,是时候将回调绑定到我们的委托上了。

我们已经将一个函数绑定到委托上,我们将在构造函数中执行相同的操作,正如我们为 CreateSessionCompleteDelegate 所做的那样。在构造函数体之前,我们已经初始化了我们的第一个委托。我们将在其后添加一个逗号,并在此处初始化第二个委托。我们将说 FindSessionsCompleteDelegate,并在其后加上括号。

FOnFindSessionsCompleteDelegate 类也有 CreateNewObject 函数。因此,我们将说 FOnFindSessionsCompleteDelegate::CreateNewObject。我们需要一个用户类,因此我们可以使用这个类的 typedef,后面跟上我们创建的回调函数的名称,即 OnFindSessionsComplete。通过使用 CreateNewObject 来构造这个类型的新的委托对象,我们在初始化时将用户对象和回调函数传递给构造函数。因此,我们有效地将我们的函数绑定到了新的委托上。

设置会话搜索设置

现在我们已经构造了委托并将回调函数绑定到它,是时候开始设置一些会话搜索设置了,这将在调用会话接口函数 find sessions 时需要。以下是 Unreal Engine 文档中关于 FOnlineSessionSearch 的信息。这个类有许多会话搜索设置,我们可以在调用 find sessions 时设置。我们已经包含了 OnlineSessionSettings.h,所以这一点已经解决。

在我们的 join game session 蓝图可调用函数中,我们可以开始准备调用会话接口函数 find sessions。这将涉及使用 FOnlineSessionSearch 类类型。我们将创建一个共享指针来包装它。因此,我们将说 TSharedPtr<FOnlineSessionSearch> SessionSearch,并使用 MakeShareable 初始化它。在 MakeShareable 内,我们将使用 new 关键字与 FOnlineSessionSearch 的构造函数。现在我们有了一个 T 共享指针来存储我们的在线会话搜索。

我们将设置一些会话搜索的设置。我们将说 SessionSearch,并使用箭头运算符。首先,我想设置的变量是 MaxSearchResults。通常我们只希望有几个搜索结果,但我们使用的开发应用程序 ID 是 480,这在许多人使用时共享,因为他们没有自己的开发应用程序 ID。请记住,480 是“SpaceWar”开发应用程序 ID,它是一个示例 Steam 游戏,旨在让人们了解 Steam 服务的工作原理。因此,我们将 MaxSearchResults 设置为一个较大的数字,因为在搜索时我们很可能会找到很多会话。这些会话是其他开发人员使用开发应用程序 ID 480 的。

我们的会话搜索还有一个变量叫做 bIsLANQuery。我们将其设置为 false,因为我们不使用局域网。现在我们准备调用 find sessions。这意味着我们需要检查在线会话接口指针,我们将在顶部执行此操作。因此,我们将说 if (!OnlineSessionInterface.IsValid()),如果在线会话接口无效,我可以在这一点简单返回。

现在,在函数的底部,我准备调用 find sessions,这将在在线会话接口上进行。因此,我们将说 OnlineSessionInterface->FindSessions。我们可以看到弹出窗口需要这个 FUniqueNetId,这意味着我们需要从控制器中获取第一个本地玩家。我们在创建游戏会话时通过创建类型为 ULocalPlayer 的常量指针并使用 GetWorld()->GetFirstLocalPlayerFromController() 完成了这项工作。所以我会复制这行代码并粘贴到这里。现在我们有了这个本地玩家。要获取唯一网络 ID,我们将使用解引用运算符,后面跟着 LocalPlayer->GetPreferredUniqueNetID()

接下来输入的是类型为 FOnlineSessionSearch 的搜索设置。但请注意,它以 TSharedRef 的形式传入。我们有一个 T 共享指针,但可以通过 SessionSearch.ToSharedRef() 将其转换为共享引用。我们本可以一开始就创建共享引用,但我之所以使用共享指针来包装这些,是因为我们后续计划将这些变量放在这个类中,以便稍后可以访问它们。但我们可以很容易地通过 ToSharedRef 从 T 共享指针获取 T 共享引用智能包装器类型,所以这不是什么大问题。

现在我们在调用 FindSessions 时,我们希望我们的回调函数在完成时被调用,这意味着我们需要将我们的委托添加到会话接口的委托列表。因此,我们可以在创建会话搜索变量之前在这里完成这一操作。我们将说 OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate),这将添加我们的委托。

处理查找会话完成的回调

最后,我想确保我们实际上找到了某些会话。我们将转到 OnFindSessionsComplete。我提到过,我们希望将这个会话搜索作为一个变量,以便稍后在回调函数中访问它。原因是 FOnlineSessionSearch 类型包含一个名为 SearchResults 的变量。我们将从这个会话搜索中获取搜索结果,这意味着它需要是一个成员变量,而不是局部变量。因此,我将复制它并将其添加为我们私有部分的一个变量。

我们将拥有一个类型为 TSharedPtr<FOnlineSessionSearch> 的成员变量。在 join game session 中,而不是声明一个新的局部变量,我将删除数据类型并直接访问我们的私有 SessionSearch 成员变量,我们在 OnFindSessionsComplete 中也将访问它,因为我们希望获取这些搜索结果。

接下来,我们可以使用 for 循环遍历搜索结果。我们将说 for (auto& Result : SessionSearch->SearchResults),这样我们就可以遍历在线会话搜索结果的 T 数组,并为每个结果做些事情。从这些搜索结果中,我们可以获取一些信息。例如,我可以使用 Result.GetSessionIdStr() 获取会话 ID 字符串,因此我可以将这个 auto 改为 FString。我们还可以获取会话的拥有者用户名,这也是 FString,所以我将其命名为 User,这将来自 Result.Session.OwningUserName

一旦我们找到一些会话,我们可以遍历找到的会话并将其打印到屏幕上。我们将使用 GEngine->AddOnScreenDebugMessage。与之前一样,我们将使用 -1 作为键,15.F 作为显示时间,并将使用 F 颜色青色。我们可以添加一个 FString,因此我将使用 FString::Printf。在文本宏中,我将说 ID: %s, User: %s,并需要传入几个 C 风格字符串来格式化这些字符串,因此我将使用解引用运算符将 ID 和用户转换为 C 风格字符串。

处理搜索会话设置与编译

在找到会话后,我们可以打印

出从每个找到的会话中获取的数据。现在,在测试之前,我们需要在会话搜索上设置另一个选项。因为我们使用了 presence,我们必须在搜索设置中指定这一点。因此,我们将说 SessionSearch->QuerySettings.Set()。有一个 setter 函数,它需要一个键,并且存在一个名为 SEARCH_PRESENCE 的宏。如果我们将鼠标悬停在这个宏上,我们可以看到它扩展为名为 PresenceSearch 的 F 名称。当我们搜索会话时,查询设置会涉及一种比较操作。我们正在指定这个为 true,并且我们使用的比较操作符在一个名为 EOnlineComparisonOp 的枚举中定义。因此,我们将使用双冒号选择常量 Equals

当我们调用 FindSessions 并传入我们的会话搜索时,我们已设置查询设置以确保我们找到的任何会话都在使用 presence。我们就这样完成了,因此让我们编译这个。接下来,我将回到编辑器,打开第三人称角色。我们已经将我们的蓝图可调用函数 CreateGameSession 绑定到数字键 1。我将右键单击,输入键盘并选择数字键 2,并从这里调用 JoinGameSession。我们现在尚未加入任何会话。我们所做的只是找到游戏会话,并且我们的回调应对此做出响应并打印出这些会话的名称。因此,我将编译并保存这个。

处理常见问题与打包项目

如果你使用的是 Unreal Engine Preview 2 或 Unreal Engine 5.0(最新正式版本),我发现一些学生在查找会话时遇到问题,解决这个问题的方法是将会话设置中的布尔值 bUseLobbiesIfAvailable 设置为 true。如果你无法找到会话,请尝试在会话设置上设置此项。你可能会看到红色波浪线,但没关系,只要你能编译,就没问题。因此,如果你无法找到会话,请尝试这个,编译你的代码,并且为了安全起见,你可能还想重新生成插件的模块。

在保存整个项目后,我将打包它,以便可以在另一台机器上进行测试。因此,我将转到平台,选择 Windows,并打包项目。现在,这是我之前的构建,我将删除它并在我的构建文件夹中简单点击选择文件夹。现在我正在打包这个项目。

一旦我完成了这个项目的打包,我可以将其压缩并放在我的 Google Drive 上,以便可以从我的另一台机器下载。现在这是我的项目文件夹。我将打开构建文件夹并简单地将其压缩。因此,我将右键单击,选择发送到压缩文件夹,一旦我完成压缩,就可以将其放在我的 Google Drive 上。

现在我在这里有我之前的构建。我将简单地右键单击并选择移除,以便不会混淆打包项目在哪里,并且我将上传它。现在,我已经上传了压缩的项目,并且我已经到达另一台机器并下载了它,我在该机器上提取了项目并从那里启动了游戏,按下数字键 1。请记住,当我们按下数字键 1 时,我们正在创建一个游戏会话。因此,我在另一台机器上创建了一个游戏会话,现在我将从这台机器加入。

我将打开我的构建文件夹,打开 Windows 文件夹并启动 menu system.xy。我们看到找到子系统 Steam。现在我可以按下数字键 2,我们看到了 ID 和在我的另一台机器上运行的 Steam 账户的用户名。因此,我们确实找到了另一个会话。很好,现在我们找到了我们创建的会话,准备加入该会话。

总结与下一个步骤

在下一个视频中,我们将获取要加入的 IP 地址,并根据找到的搜索结果加入游戏会话,这样我们就可以在两台不同的机器上在同一个关卡中一起玩游戏。在本讲中,我们创建了一个名为 Join Game Session 的蓝图可调用函数,并且还创建了一个类型为 FOnFindSessionsCompleteDelegate 的委托。我们还创建了一个回调并将其绑定到此委托。然后我们创建了一个类型为 FOnlineSessionSearch 的对象,并将其作为成员变量,以便在回调函数中访问。我们指定了一些搜索设置,并调用了 FindSessions。在我们的回调函数中,我们访问了这个在线会话搜索成员变量中的搜索结果,以获取创建该会话的用户的 ID 和用户名。干得好!在下一个视频中,我们将设置加入该游戏会话的功能,以便让两个游戏实例在同一关卡中进行游戏。我们很快再见。

10. Steam区域

Steam 区域概述

欢迎回来!在本视频中,我们将快速回顾 Steam 用于连接玩家的区域设置。你会注意到我的游戏列表中有一个“Space War”。这是因为我正在使用开发应用程序 ID 80,这是 Space War 的开发应用程序 ID。当你连接到 Steam 在线子系统并进行游戏测试时,这会显示你正在玩这个游戏。

一旦你创建了一个会话,并从另一个游戏中加入,Steam 会使用 presence,这意味着它只会将你连接到同一区域的其他玩家。

设置 Steam 区域

要查看或更改区域设置,你可以进入 Steam 的“设置”菜单。在“下载”选项中,有一个“下载区域”。这里显示 Steam 会自动选择最近的下载服务器位置,但可以被覆盖。如果你在进行两台相对接近的机器的测试,这个设置可能会自动设置为相同的区域。但是,如果你与在国家另一端或全球另一端的人进行测试,确保你们的区域设置相同是非常重要的。

例如,我的区域被自动设置为美国凤凰城,这是离我最近的区域。只要两台机器的 Steam 账户设置在相同的区域,就应该能够找到并加入会话。因此,如果你与其他人或另一台机器进行测试,请确保两台机器的 Steam 账户都设置为覆盖该区域,以便你们连接到同一 Steam 区域的服务器。

关键要点

这样做可以确保你们能够顺利连接并进行游戏测试。下次见!

11. 加入会话

创建游戏大厅关卡

欢迎回来!在本讲中,我们将创建一个大厅关卡,以便在创建游戏会话后可以前往该关卡并等待其他玩家加入。我们还将指定会话设置中的匹配类型,以确保在找到会话后能够检查该会话是否配置为正确的匹配类型。

创建大厅关卡

首先,让我们创建大厅关卡。在我们的菜单系统项目中,我将进入 Maps 文件夹,在 Third Person Copy 中创建一个新的大厅关卡。因此,我将文件中选择“新建关卡”,并选择“默认关卡”。接下来,我会选择地板并在 X 和 Y 方向上缩放它,比例设置为 10,以便我们有足够的空间活动。

然后,我将保存当前关卡,命名为 Lobby,现在我们就拥有了这个大厅关卡。接下来,我们希望在创建会话后立即前往该关卡。

连接大厅关卡

在 Visual Studio 项目中,我将找到 OnCreateSessionComplete 的回调函数。到目前为止,我们在这个函数中只是打印了一条消息,但如果 BWasSuccessful 为 true,我还想在这里打开大厅关卡并将其作为一个“监听服务器”。这意味着我们需要获取世界,因此我将创建一个本地 UWorld 指针并用 GetWorld() 初始化它。我们需要确保世界是有效的,然后从世界类中调用 ServerTravel

要指定路径,我可以通过右键单击大厅关卡资产并选择“复制文件路径”来轻松获得路径。在 Visual Studio 中,我将粘贴这个路径。我们不需要完整路径,一切到内容文件夹的部分可以用 Game 替换。接下来是 Third Person Copy/Maps/Lobby。我们将路径添加到字符串中,并用 ?listen 来指定选项,以便调用 ServerTravel 时能够打开大厅关卡。

指定匹配类型

现在我们已经有了大厅关卡,并将在创建会话后打开它。接下来,我们需要在会话设置中指定匹配类型。我们的游戏可能有不同的匹配类型,例如自由对战或团队对战。我们将使用会话设置的 Set 函数来设置键值对,以便在找到会话后进行检查。

我将使用 Set 函数,并首先传入一个 FName 作为键,命名为 MatchType,然后使用 FString 作为值,指定匹配类型为 FreeForAll。这样,随着游戏的发展,我们可以在会话设置中指定键值对,以确保我们只加入具有匹配类型的会话。

检查匹配类型

在我们的 OnFindSessionsComplete 回调中,我们将检查搜索结果并确认它们的匹配类型。我们将访问会话设置并使用 Get 函数来检查 MatchType 键。我们创建一个 FString 变量 MatchType 来接收值,然后检查这个值是否等于 FreeForAll。如果匹配,我们可以打印消息并准备加入会话。

加入游戏会话

为了加入游戏会话,我们需要设置一个新的委托和回调。委托的类型为 FOnJoinSessionCompleteDelegate,我们需要一个名为 OnJoinSessionComplete 的回调函数。这个回调函数将接受两个输入参数:一个 FName 作为会话名称,另一个是 EOnJoinSessionCompleteResult 类型的结果。

在构造函数中,我们将构建并绑定这个委托。接下来,我们将在找到匹配类型后调用 JoinSession。我们需要获取 FUniqueNetId 和会话名称,并传入正确的 FOnlineSessionSearchResult。调用 JoinSession 后,我们将等待完成并在回调中获取 IP 地址。

获取 IP 地址并旅行到大厅

获取 IP 地址时,我们将使用 GetResolvedConnectString 函数。我们创建一个本地 FString 变量 Address,并传递 Name_GameSession 作为会话名称。调用后,我们检查返回值,如果成功,我们将打印连接字符串。最后,使用 ClientTravel 方法从本地玩家控制器前往该地址。

打包和测试

在代码编写完成后,我们将编译代码并在编辑器中保存所有内容,打包项目以进行测试。我们删除之前的构建并重新打包。上传新的构建到 Google Drive 后,在另一台机器上下载并解压。

测试时,我的测试者在其机器上创建了会话并等待我加入。之后,我能够顺利加入他们的游戏会话。我们可以相互主机和加入游戏会话,这证明了我们的系统可以通过 Steam 无缝连接。

小结

在本讲中,我们创建了大厅关卡并为会话指定了匹配类型,同时实现了加入游戏会话的功能。接下来,我们将开始创建插件,使这个系统可以在任何项目中重复使用,无需每次创建新的多人游戏时都重新实现。我们将设置新的类来处理所有会话相关的功能。期待在下一个讲座中再见!

12. 创建插件

创建多玩家会话插件

欢迎回来!在本讲中,我们将创建一个新插件来处理我们的多人会话相关功能,并配置我们的插件以使用在线子系统,从而可以访问管理会话所需的所有在线子系统功能。

插件概述

Unreal Engine 插件是为特定目的设计的代码和数据集合。开发者可以在每个项目的基础上轻松启用或禁用这些插件。它们可以添加运行时游戏功能,甚至在我们创建游戏时添加编辑器功能。插件可以通过引擎中的插件编辑器启用。

在 Unreal Engine 中,插件由一个或多个模块组成。模块可以视为 Unreal Engine 的构建块。引擎本身实现为大量模块的集合,每个模块都是自己独立的 C++ 代码单元,具有自己的构建文件。模块仅包含代码,因此不包含其他资产,如网格或纹理。这有助于将特定任务封装在一起,确保模块只负责其设计的内容,同时也保持引擎的组织结构。

我们的项目实际上也是一个模块。我们可以在项目的构建文件中查看项目依赖的模块。每当我们启用一个插件时,它会被添加到项目的 .uproject 文件中。如果我们打开游戏项目的 .uproject 文件,可以看到一个名为 Plugins 的部分,其中包含我们启用的 Online Subsystem Steam

创建插件

创建我们自己的插件实际上很简单。我们只需打开插件编辑器,通过“编辑”菜单选择“插件”。在插件编辑器中,有一个“新建插件”按钮。点击它后,我们会看到多个插件类型选项。我们将选择“空白插件”。接下来,我们可以为插件命名,由于我们的插件与多人游戏会话相关,因此我们将其命名为 MultiplayerSessions

我们还可以提供一些描述数据,例如插件的作者和简短描述,例如“用于处理在线多人会话的插件”。点击“创建插件”后,插件将成功创建。

检查和配置插件

创建插件后,我们可以在内容浏览器中点击设置,启用“显示插件内容”。如果未勾选,请点击以启用它。接着打开 Visual Studio 项目,查看项目文件已被修改并包含新插件。我们可以看到新插件文件夹中包含 MultiplayerSessions,它拥有自己的 .uplugin 文件、资源和源代码文件夹。

在源代码文件夹中,我们可以看到 MultiplayerSessions 文件夹及其私有和公共文件夹,以及其自己的 Build.cs 文件。这个 MultiplayerSessions 插件作为一个独立的模块存在,独立编译,生成自己的二进制文件和中间文件夹。

配置插件依赖

在新插件的 .uplugin 文件中,我们可以指定插件所需的依赖项。在模块部分下,我们可以添加逗号并在方括号后添加其他插件。例如,我们将使用 Online Subsystem 插件。语法为在大括号中指定插件名称和启用状态。我们还将添加 Online Subsystem Steam 插件,以便我们的项目为 Steam 配置。

构建和验证插件

在设置好依赖关系后,我们的 MultiplayerSessions 插件现在包含了 Online SubsystemOnline Subsystem Steam 插件。接下来,我们可以在 Build.cs 文件中添加依赖模块。我们将向公共依赖模块名称添加 Online SubsystemOnline Subsystem Steam,以便在公共文件中访问这些模块。

完成后,我们可以编译项目,以确保插件正常工作。编译成功后,我们将在项目文件夹中看到新插件的编译文件和中间文件夹。

总结

在本讲中,我们创建了一个新插件,设置了插件依赖关系,使其依赖于在线子系统插件。我们还向插件的公共依赖模块名称中添加了几个模块。接下来,我们将开始添加类,以处理多人在线会话的功能。期待在下一节课中见到你!

13. 创建我们自己的子系统

创建多人会话子系统

欢迎回来!在本讲中,我们将创建一个类来处理所有的多人会话功能,并将实现一个自己的子系统。这将是一个游戏实例子系统,我们将讨论其含义以及如何运作。

选择父类

创建处理多人游戏会话的新类时,首要问题是选择哪个父类。处理游戏会话时,一个不错的选择是游戏实例类。创建游戏实例类后,我们知道游戏实例将在游戏创建时生成,并且在游戏关闭之前不会被销毁。游戏实例在不同关卡之间持久存在,因此在切换关卡时,游戏实例仍然保持不变。这使得游戏实例成为包含多人会话功能的理想父类。

不过,游戏实例类可能会包含很多功能,不仅限于多人会话。因此,我们可以通过创建一种称为游戏实例子系统的类,使其与游戏实例共存,独立处理多人会话功能。

游戏实例子系统

根据 Unreal Engine 的文档,子系统是自动实例化的类,具有管理生命周期的功能。子系统为程序员提供了易于使用的扩展点,可以立即获得蓝图和 Python 的支持,同时避免修改或重写引擎类的复杂性。这意味着我们无需将所有多人功能添加到游戏实例类中,而是可以创建自己的子系统。

游戏实例子系统将在游戏实例创建后自动生成,并在游戏实例关闭时被销毁和垃圾收集。使用子系统的优点包括节省编程时间,避免覆盖引擎类,并避免向忙碌的类添加 API。

创建多人会话子系统

现在,让我们创建自己的游戏实例子系统。我们希望将子系统放在我们的 MultiplayerSessions 插件中,以便将插件添加到其他项目时能够访问该类。首先,我们将在 C++ 类文件夹中右键点击,选择新建 C++ 类。我们将基于 UGameInstanceSubsystem 类进行创建。

在选择父类时,我们会搜索 UGameInstanceSubsystem,然后选择它作为父类。接下来,我们将选择将新类创建在哪个模块中,选择 MultiplayerSessions 模块。

给我们的子系统命名为 MultiplayerSessionsSubsystem,然后点击“创建类”。成功添加类后,我们需要重新编译 MultiplayerSessions 模块,才能在内容浏览器中查看该类。

编译与访问子系统

关闭 Unreal Engine 和 Visual Studio 后,生成 Visual Studio 项目文件。生成后,双击新项目,确认缺少模块并点击“是”以构建这些模块。

在 Visual Studio 中,我们现在可以看到 MultiplayerSessionsSubsystem 类,包含公共和私有文件夹以及对应的头文件和源文件。我们可以开始在子系统中添加在线会话功能。

添加在线子系统接口

我们将在公共部分、保护部分和私有部分中添加基本内容。我们将定义构造函数并包含在线子系统的头文件,以便使用 IOnlineSessionPtr 类型的智能指针。将该指针设置为私有,以便在子系统内部使用。

在构造函数中,我们将访问在线子系统。通过 IOnlineSubsystem::Get() 获取子系统指针,并确保它不是空指针。如果有效,我们将使用 GetSessionInterface() 获取会话接口并存储在 sessionInterface 指针中。

编译并准备下一步

完成后,我们将编译项目,确保没有错误。接下来,在下一个视频中,我们将开始添加会话接口委托,并创建相应的回调函数,为使用会话接口功能做好准备。

小结

在本讲中,我们创建了一个名为 MultiplayerSessionsSubsystem 的游戏实例子系统,存储了会话接口。通过在线子系统访问该接口,将使我们能够管理游戏会话功能。期待在下一节课中再见!

14. 会话界面代理

添加多人会话子系统功能

欢迎回来!在本讲中,我们将向我们的多人会话子系统添加一些功能,包括可以从任何使用此子系统的类(例如菜单)调用的函数。我们还将设置子系统以使用在线会话接口,这将涉及添加一些委托和回调,以及一些委托句柄,以便在不再需要它们时可以从会话接口的委托列表中移除这些委托。

委托和回调概述

管理在线会话涉及使用多个会话接口函数,例如创建会话、查找会话、加入会话、开始会话和销毁会话。这些函数都存在于会话接口中,但我们将在创建的子系统中处理所有这些,以便可以从其他类(如菜单)访问它。因此,我们的子系统需要一些公用函数。

在每个会话操作中,我们需要创建相应的委托,并为这些委托创建回调函数。完成特定委托后,我们应从会话接口的委托列表中移除它,并可以使用委托句柄来实现这一点。

添加功能到子系统

现在,让我们开始向我们的 MultiplayerSessionsSubsystem 类添加功能。我们将为菜单类设计一些公用函数,以便能够处理会话功能。在这里,我将添加一些注释,说明这些函数的用途。

首先,我们创建一个 CreateSession 函数,允许菜单类调用此函数并传递会话设置的信息,包括最大公共连接数和匹配类型。接下来,我们创建一个 FindSessions 函数,允许菜单类指定要查找的最大搜索结果。还将创建 JoinSession 函数,接受一个会话搜索结果作为参数,以及 DestroySessionStartSession 函数。

创建函数体

在声明这些函数后,我们将使用 Visual Studio 的快捷键为它们生成函数体。我们将添加函数体的基础结构,以便可以在其中实现会话相关功能。

接下来,我们需要添加委托变量,以便将其添加到在线会话接口的委托列表中。我们将为每个会话操作创建相应的委托,包括:

创建回调函数

对于每个委托,我们将创建相应的回调函数,放在受保护的部分。这些回调函数包括:

每个回调将根据功能需求接收不同的参数。

初始化委托和回调

现在,我们将在构造函数中初始化这些委托并绑定回调。我们将使用成员初始化列表来完成这一点,确保每个委托都绑定到正确的回调函数。

每次添加委托到列表时,函数会返回一个 FDelegateHandle,我们将为每个委托创建相应的句柄变量,以便后续可以移除这些委托。

编译与下一步

完成这些步骤后,我们将编译代码,以确保没有错误。我们的子系统现在已设置为使用在线会话接口的功能,并准备开始实现会话管理功能。

小结

在本讲中,我们向 MultiplayerSessionsSubsystem 添加了多个函数、委托和回调,以便能够与在线会话接口进行交互。我们还为每个委托创建了句柄,以便在不再需要时移除它们。期待在下一节课中与您见面,我们将开始创建一个菜单类并调用这些函数来管理在线会话!

15. 菜单类

创建菜单类与用户界面

欢迎回来!在本讲中,我们将创建一个菜单类,并制作一个小部件蓝图,将我们的菜单 C++ 类设置为该蓝图的父类。我们还会在菜单中添加“主机”和“加入”按钮,以便通过点击按钮轻松创建或加入会话。

创建菜单类

首先,在我们的菜单系统项目中,我们将创建一个新类,作为菜单小部件的父类。我们希望将此菜单类添加到我们的插件中,这样一来,当我们将插件插入到另一个项目时,菜单将已构建好,方便配置设置,例如连接数量和匹配类型。

MultiplayerSessions 的 C++ 类文件夹中,右键单击选择新建 C++ 类。我们将基于 UUserWidget 类进行创建,这样就可以随后将 C++ 类作为蓝图的父类。

选择 UserWidget 并点击“下一步”。在模块下拉菜单中,选择 MultiplayerSessions,将新类命名为 Menu。点击“创建类”,然后让 Visual Studio 重新加载。

实现菜单类功能

Menu 类中,我们将设置菜单的功能。我们可以添加一个公共函数,命名为 MenuSetup,使其可在蓝图中调用。这将用于将小部件添加到视口,并设置其属性,例如可见性和鼠标光标的显示状态。

MenuSetup 函数体中,我们将添加小部件到视口,并使用 SetVisibility 方法将其可见性设置为可见。为了聚焦在小部件上,我们将设置输入模式,确保用户可以与 UI 交互而不是控制游戏中的角色。

设置输入模式

在设置输入模式时,我们需要获取世界,并从中获取第一个本地玩家控制器。我们将创建一个 FInputModeUIOnly 的结构体,配置焦点和锁定鼠标行为。通过设置 SetWidgetToFocus 方法,我们可以指定聚焦的小部件。

同时,我们将获取玩家控制器并调用 SetShowMouseCursor(true) 以显示光标。

创建菜单蓝图

接下来,我们将创建一个小部件蓝图,命名为 WB_Menu,并在 MultiplayerSessions 内容文件夹中保存。该蓝图将包含主机和加入按钮。我们将添加两个按钮并设置它们的属性,例如大小和位置。

在蓝图的图形视图中,我们将设置按钮的文本,分别命名为“Host”和“Join”。确保按钮大小一致,并根据屏幕大小调整锚点。

重新生成 Visual Studio 项目文件

创建完蓝图后,我们将重新生成 Visual Studio 项目文件,以确保新的菜单类能在项目中正确显示。完成后,打开蓝图并设置其父类为刚刚创建的菜单类。

测试菜单功能

为了测试我们的蓝图可调用的 MenuSetup 函数,我们将在关卡蓝图中调用该函数。通过在 BeginPlay 事件中创建 WB_Menu 小部件并调用 MenuSetup,我们可以检查菜单是否正常显示。

当我们运行游戏时,应该能看到菜单并能够与按钮交互。虽然目前按钮还未编写功能,但我们已为后续的实现打下了基础。

小结

在本讲中,我们创建了一个名为 Menu 的新用户小部件类,并制作了小部件蓝图,设置了其父类为我们的 C++ 类。我们为菜单添加了主机和加入按钮,下一步将实现这些按钮的功能。干得不错!期待在下一节课中见到你。

16. 访问我们的子系统

添加按钮回调和访问子系统

欢迎回来!在本讲中,我们将为菜单小部件中的按钮添加回调,以便可以通过 C++ 函数响应按钮点击事件。同时,我们会从菜单 C++ 类中访问我们的多人会话子系统,以调用相关的功能。

创建按钮回调

首先,我们需要创建按钮回调函数,这些函数将在点击按钮时被调用。在我们的 WB_Menu 小部件蓝图中,我们已经添加了“主机”和“加入”按钮。每个按钮都是从 UButton 类派生的,因此我们可以在 Menu C++ 类中创建指向这些按钮的变量。

Menu.h 文件中,我们将添加两个私有指针变量,分别命名为 HostButtonJoinButton。为了将这些变量链接到蓝图中的按钮,我们需要为它们添加 BindWidget 的元规范。这要求 C++ 变量的名称与蓝图中的按钮名称完全相同。

接着,我们将定义两个无输入参数的回调函数,分别为 HostButtonClickedJoinButtonClicked。这两个函数将在按钮点击时被触发。为了验证我们的回调是否正常工作,我们将使用 GEngine->AddOnScreenDebugMessage 打印调试消息。

绑定按钮点击事件

接下来,我们需要将这些回调函数绑定到按钮的点击事件。为此,我们将在 Initialize 函数中进行绑定。Initialize 是一个受保护的虚函数,在小部件创建后调用。

Initialize 函数中,我们将检查 HostButtonJoinButton 是否有效,然后使用 AddDynamic 方法将我们的回调绑定到每个按钮的 OnClicked 委托。通过这种方式,点击按钮时就会触发相应的回调。

编译和测试

完成这些步骤后,我们将编译代码,确保没有错误。然后,我们将在 Unreal Editor 中测试菜单,以确保点击按钮时可以显示调试消息并触发对应的回调。

访问多人会话子系统

Menu 类中,我们将访问我们的多人会话子系统。我们需要在 Menu.h 文件中添加一个指向 MultiplayerSessionsSubsystem 的私有指针变量。在 MenuSetup 函数中,我们可以通过获取游戏实例并调用 GetSubsystem 方法来访问子系统。

在回调函数 HostButtonClicked 中,我们将调用 CreateSession 函数。虽然目前这个函数是空的,但我们可以确保正确调用它。

小结

在本讲中,我们为菜单小部件的按钮添加了回调函数,并将这些回调与按钮的点击事件绑定。我们还访问了自定义的多人会话子系统,并确保可以从菜单类调用其函数。期待在下一节课中继续实现子系统的功能!

17. 创建会话

实现创建会话功能

欢迎回来!在本讲中,我们将实现我们多人会话子系统中的 CreateSession 函数,该函数将在创建会话后将我们带到大厅关卡。同时,我们还将向菜单设置函数添加输入,以便能够设置公共连接数等属性。

实现 CreateSession 函数

在我们的 MultiplayerSessionsSubsystem 的 CPP 文件中,我们将开始实现 CreateSession 函数。首先,我们将检查会话接口是否有效。如果无效,则直接返回。

接着,我们要检查是否已经存在同名的会话。如果存在,则调用 DestroySession 来销毁该会话。我们使用 GetNamedSession 来获取现有会话,并在确认它不为空后,调用 DestroySession

设置会话设置

接下来,我们需要设置会话的配置。我们将添加一个 TSharedPtr 类型的成员变量 LastSessionSettings 来存储最后创建的会话设置。在 CreateSession 函数中,我们将创建新的 FOnlineSessionSettings 实例,并设置各项属性。

例如,我们会检查当前是否使用 LAN 匹配,并设置 NumPublicConnectionsAllowJoinInProgressAdvertise 等选项。尤其是匹配类型,我们将设置一个键值对。

调用 CreateSession

调用 CreateSession 方法时,我们将需要 FUniqueNetId。我们通过获取第一个本地玩家控制器来实现这一点,并使用其 GetPreferredUniqueNetId 方法获取唯一 ID。创建会话后,我们会检查返回值,如果返回值为 false,则从委托列表中移除委托。

如果创建成功,我们将立即调用 ServerTravel 方法,传递大厅关卡的路径,进行关卡切换。

测试功能

在实现完 CreateSession 函数后,我们可以快速测试它。在 Unreal 编辑器中,点击主机按钮,验证是否能够成功创建会话并切换到大厅关卡。

同时,我们还添加了一个简单的输入处理来关闭游戏,使得测试过程更加方便。

小结

在本讲中,我们实现了 CreateSession 函数,并在成功创建会话后切换到大厅关卡。我们还向菜单设置函数添加了输入,以便自定义会话的设置。期待在下一节课中讨论如何让菜单响应子系统的函数调用!

18. 回调到我们的子系统功能

创建回调和自定义委托

欢迎回来!在本讲中,我们将为多人会话子系统的函数创建一些回调,并声明新的自定义委托,以便菜单类可以响应这些回调。

自定义委托的声明

首先,我们将在 MultiplayerSessionsSubsystem 类中声明一些自定义委托。这些委托将用于在会话创建完成后向菜单类发送通知。我们将使用 DECLARE_DYNAMIC_MULTICAST_DELEGATE 宏来声明一个新的委托,命名为 FMultiplayerOnCreateSessionComplete,它将接受一个布尔参数 bWasSuccessful,表示会话创建是否成功。

然后,我们将在子系统中创建一个公共变量,以便菜单类可以绑定其回调。

菜单类的回调函数

接下来,我们将在菜单类中添加一个回调函数,命名为 OnCreateSession. 该函数将与自定义委托相绑定,并会在会话创建完成后被调用。我们将在该函数中使用 GEngine->AddOnScreenDebugMessage 来打印会话创建的结果信息。

绑定委托

在菜单的 MenuSetup 函数中,我们将检查 MultiplayerSessionsSubsystem 是否有效,并将 OnCreateSession 回调绑定到自定义的 MultiplayerOnCreateSessionComplete 委托。这样,当子系统调用相关函数并触发委托时,菜单类就能接收到通知。

处理创建会话的结果

CreateSession 函数中,我们将进行会话创建并检查结果。如果创建成功,则广播自定义委托,并传递成功状态。如果失败,将广播失败状态。我们将确保在会话创建完成后再切换到大厅关卡。

测试和总结

最后,我们将测试新的功能,确保菜单能够正确响应会话创建的结果。我们还会在游戏中进行简单的调试,确认一切正常。

小结

在本讲中,我们成功创建了自定义委托,并在菜单类中实现了相应的回调函数。通过这种方式,我们能够有效地将菜单和子系统的功能解耦,提高了代码的灵活性和可维护性。期待在下一节课中继续添加更多的回调和功能!

19. 更多子系统代理

创建更多委托和回调

欢迎回来!在本讲中,我们将为多人会话子系统的其他函数创建更多的委托,并在菜单系统中创建相应的回调。这将使我们能够响应查找和加入会话等操作。

声明新的委托

首先,我们将在 MultiplayerSessionsSubsystem 类中声明四个新的委托,因为我们需要处理创建、查找、加入、销毁和启动会话这五个主要的在线会话函数。我们已经为创建会话创建了一个委托,现在我们将为查找、加入、销毁和启动会话分别创建新的委托。

在菜单类中添加回调

接下来,我们将在菜单类中为每个新的委托添加相应的回调函数。这些回调函数将被用来处理委托广播时的响应。例如,我们可以在查找会话成功时显示一条消息。

绑定回调到委托

MenuSetup 函数中,我们将绑定新创建的回调到相应的委托。对于动态多播委托,我们将使用 AddDynamic 方法,而对于普通多播委托,我们将使用 AddUObject 方法。

实现回调

在子系统中,完成相应操作后,我们将通过调用 Broadcast 方法来广播委托。这使得菜单类能够接收到相关的操作结果并做出响应。

测试

在所有的设置完成后,我们将启动游戏进行测试,以确保每个操作都能够正确地触发回调并显示相应的消息。

小结

在本讲中,我们为其他子系统函数创建了新的委托,并在菜单类中实现了回调。通过这种方式,菜单能够响应不同的会话操作,并与子系统进行交互。我们为接下来的功能实现做好了准备,期待在下一节课中继续构建这个系统!

20. 从菜单加入会话

成功找到会话时的处理

当我们成功找到会话时,该如何处理?在这种情况下,我们知道我们的子系统回调将被调用,因为在线子系统会遍历其委托列表并触发该回调,这样我们就可以进入我们在子系统中称为“查找会话完成”的回调。

清除委托

在这里,我们可以做几件事情。我们首先要从会话接口的委托列表中清除委托。因此,我们会说如果会话接口存在,我们可以调用清除“查找会话完成”委托的句柄,并传递我们之前定义的“查找会话完成”委托句柄。

广播到菜单

接下来,我们可以使用自定义委托广播到菜单,称为“多人查找会话完成”。我们将访问该委托并调用广播。现在我们可以传递有效的搜索结果数组。这些结果存在于我们称为“上一个会话搜索”的会话搜索中。我们的在线会话搜索具有搜索结果,你会注意到搜索结果是一个在线会话搜索结果的数组。因此,我们将这些结果传递给菜单,然后我们可以传递成功与否的标志。

如果我们确实获得了一些搜索结果,但出于某种原因该数组是空的,我希望简单地广播,以便菜单知道我们没有找到任何有效的搜索结果。因此,我将放置另一个检查,查看“上一个会话搜索”的搜索结果数量是否小于或等于零。因为这是一个数组,我们有这个“num”函数,可以查看这个数组是否为空。如果是,那么我希望广播一个“假”的结果给菜单。

查找会话的回调

现在,回到我们的“查找会话”函数,这里的广播就是我所说的。因此在“查找会话完成”的回调中,如果数组为空,我们只需广播一个空数组和一个“假”的成功标志。此时我还想在这里返回,以避免在下方再次广播。

因此,现在我们正在从菜单调用查找会话。在我们的子系统中,我们调用在线会话接口的查找会话函数。在这里,我们做了一些检查,清除了委托句柄,并广播给菜单搜索结果数组,以及这是否成功。如果不成功,我们将广播“假”。如果成功,我们在子系统的回调“查找会话完成”中将被调用,然后我们清除委托并广播搜索结果数组。如果搜索结果的数组为空,我们将广播“假”和一个空数组。

加入会话

现在我们找到会话,是时候选择一个会话并加入它了。当我们点击加入按钮时,我们在菜单中调用查找会话,我们的子系统将处理查找会话,并将搜索结果数组广播回菜单,如果成功。该广播将导致我们的回调被调用,回调在“查找会话”中。

我们希望从这些结果中选择一个并加入。现在我们可以再次参考我们的角色类,因为我们在这里做过类似的事情,当我们将回调传递到会话接口委托时,我们在角色类中调用“查找会话完成”。在这里,我们传递了那些会话搜索结果。

选择会话

在菜单中,我们将传递搜索结果。之所以这样做是因为我认为我们如何选择会话取决于游戏的设计。我将遍历搜索结果,找到第一个匹配类型正确的会话并加入。我们可以稍后对此进行精细化处理,但现在我们只想确保这能正常工作。因此,我们将简单地遍历我们的会话结果。

我们将建立一个循环,对于每个结果,我们检查是否具有正确的匹配类型。如果我们回到角色类,我们会看到我们通过获取结果并访问其会话来完成此操作。在会话中,我们有会话设置,然后我们调用“获取”函数,指定一个键并传递一个F字符串,这个“获取”函数将用该会话的键值对中的值填充该F字符串。因此,我们将这样做。我们将复制这一行并粘贴在这里,唯一不同的是,我们将传入一个F字符串,并且我们不能使用匹配类型,因为菜单的头文件中有一个名为“匹配类型”的私有成员变量。因此,我将创建一个名为“settings value”的F字符串局部变量。

加入会话的逻辑

如果“settings value”与本地变量“match type”相等,那么我们就找到了一个有效的搜索结果,因为这个特定的会话使用了正确的匹配类型。这样我们就可以调用我们的子系统函数“加入会话”。如果我们跳转到子系统并查看“加入会话”,我们会看到这只需一个搜索结果。

在菜单中,我们将调用子系统的“加入会话”。在此之前,我希望确保我们的子系统指针不为空。因此我们将说如果“多人会话子系统”指针为空,则返回,这样就没有必要解析搜索结果。但如果不为空,那么我们将遍历搜索结果,找到一个具有正确匹配类型的会话并调用“加入会话”。

如果我们找到一个有效的搜索结果,就没有必要继续循环并在其他结果上调用“加入会话”了,因此我们将此时返回。

处理加入会话

现在我们正在调用子系统的“加入会话”,但让我们回到子系统并查看“加入会话”。如果我们向上滚动,我们会看到“加入会话”什么也不做。我们需要实际调用会话接口函数“加入会话”。这还涉及到处理所有的委托。

首先,我们要检查会话接口是否有效。如果会话接口无效,那么我们将返回。但我们也将广播给菜单,以便菜单知道出现了问题。我们将获取“多人进行中的会话完成”委托并进行广播。该委托将广播一个类型为“E进行中的会话完成结果类型”的值。因此我们将说“E加入会话完成结果”。在这里,我们选择“未知错误”,以便菜单能够收到此信息以防我们无法继续加入会话。

接下来我们需要做几件事情。首先,我们将调用会话接口上的“加入会话”,这意味着我们需要将我们的委托添加到其委托列表中并将其存储在委托句柄中。

存储委托句柄

我们将说会话接口添加“加入会话完成”委托句柄并传入我们的委托,这不是我们的自定义委托,而是会话接口的委托列表中的委托。然后我们将存储这个句柄,我们称其为“加入会话完成委托句柄”。现在我们已经将委托添加到会话接口委托列表中,我们可以调用“加入会话”。

我们需要获取唯一的网络ID,因此我们需要从控制器获取第一个本地玩家。因此,我们将查看“查找会话”并复制该行。然后我们将在这里获取本地玩家并传入首选的唯一网络ID。

对于会话名称,我们始终使用“游戏会话名称”。对于会话搜索结果,我们传入菜单传入的有效搜索结果,称为“会话结果”。就像我们一直在做的那样,我们将检查“加入会话”是否返回“真”。我们将此放在一个F检查中,使用否定操作符来检查它是否返回“假”,这意味着“加入会话”失败。

清除委托并广播

在这种情况下,我们将清除委托,并广播给菜单,告知菜单加入会话失败。首先,我们将清除委托。我们将说会话接口清除“加入会话完成”委托句柄,并传入我们的委托句柄。然后我们可以向菜单广播,表示加入会话失败的情况,因此我们将使用“EA加入会话完成结果”,再次使用“未知错误”作为错误信息。

在加入会话成功的情况下,我们在子系统中有我们的委托,因为我们将其添加到委托列表中,这意味着我们可以进入该回调,在子系统中称为“加入会话完成”。在这里我们可以做两件事。我们可以清除委托并向菜单广播,通知菜单我们已加入会话。

广播加入成功

首先,我们将检查会话接口并清除“加入会话完成”委托句柄。然后我们可以简单地广播。因此,在if检查之后,我们广播自定义委托“多人加入会话完成”,我们将广播在“加入会话完成”中收到的结果。

此时,我们已经加入会话,菜单可以响应,因为该广播将导致菜单的回调被调用。因此,让我们回到菜单,加入会话的回调就是“进行中的会话”。我们只需加入会话,这意味着我们需要获取正确的地址并进行客户端旅行。

获取连接字符串

为了获取正确的地址,我们回到角色中,并在其“加入会话完成”的回调中获取该地址,使用会话接口并调用“获取解析连接字符串”。我们需要在菜单中访问在线会话接口。如果我们直接在这里访问在线会话接口也是可以的,只要在线会话接口不依赖于菜单,且子系统也不依赖于菜单即可。

因此,让我们获取该会话接口。我们回到“多人会话子系统”,滚动到构造函数的顶部,我们会看到这就是我们获取会

话接口的方式。我们首先获取子系统,然后调用子系统获取会话接口。我们将在菜单中粘贴这些。

创建地址

这意味着我们需要包含在线子系统的头文件。在菜单的顶部粘贴“在线子系统.h”。现在我们已经处理了包含错误。由于我们从子系统中复制了这些,因此我们访问该会话接口,它是我们“多人会话子系统”的成员变量,但在这里我们没有这样的变量。

我们可以简单地在菜单中将其定义为局部变量。因此,我们将声明“在线会话指针”,并在此设置。现在我们有了接口,可以获取地址或解析连接字符串。首先,我们将检查会话接口是否有效。然后调用“获取解析连接字符串”。如果我们回到角色中,我们首先要创建一个本地F字符串“地址”,并将其传入“获取解析连接字符串”。

接下来,我们将调用“客户端旅行”,可以看到我们在角色中是如何做到的,首先从游戏实例获取玩家控制器。现在,这一点非常重要,因为在菜单类中没有获取玩家控制器的函数。我们需要从游戏实例获取。因此,我们将复制这些行并粘贴在这里。

测试和发布项目

我们正在获取玩家控制器,通过调用“获取游戏实例”并获取第一个本地玩家控制器,然后调用“客户端旅行”,传入地址,使用绝对旅行类型。这里有很多内容需要理解,我们的系统非常复杂。我们有由会话接口触发的委托,还有由我们的子系统触发的委托。现在我们有了一个强大的系统,我们可以轻松创建新类,比如新的菜单类,并根据游戏需要创建自己的回调。

所以让我们编译。现在我们已经编译完成,我想关闭这个。随着虚幻引擎编辑器的关闭,我将为这个插件重新生成项目文件。因此我们转到插件,删除二进制文件和中间件,然后返回并生成Visual Studio项目文件。

一旦完成,我们将打开我们的项目。点击“是”以重建模块。现在我们回到了编辑器,我们的项目已编译完成,因此我们准备测试系统。这真的很令人兴奋,因为您可以在多个机器上测试。

我会挑战您打包项目并在多台计算机上测试。现在,两台计算机必须在后台运行不同的Steam帐户。如果您没有两台计算机或两个Steam帐户,可以在我的学生Discord上找到其他学生。您要做的是打包项目,将其上传到Google Drive,并下载彼此的项目。

确保您在相同的Steam区域,并且两者都在后台运行Steam。下载并测试您的游戏后,下载他们的游戏并帮助他们测试。这样,您可以验证彼此通过Steam在互联网上连接,确保菜单系统插件正常工作。

确保您正在测试的对象具有相同的平台。如果您在Windows计算机上,请确保他们也在Windows计算机上。好吧,暂停视频并尝试与另一位学生或另一台计算机测试您的游戏。

打包项目

我准备打包这个项目。我将转到平台,选择Windows并打包项目。这里是我的构建文件夹。我将删除之前的构建并选择该文件夹。现在我正在打包游戏。好的,我已经打包游戏并将其压缩,放在我的Google Drive上。

然后我让几位学生从Discord上下载该项目到他们的计算机,解压并启动游戏。我正在托管游戏,加载游戏并点击“托管”。现在这些学生都在加载游戏并点击“加入”。每当其中一名学生加入时,顶部左侧都会打印出找到的子系统Steam字符串。

因此,我有几个学生在这里,他们都在活动。有人在跑,有人在跳,看起来我们的性能相当不错。每个人的动作都很流畅。接下来我们将探讨原因,并在接下来的部分中创建多人功能,当我们将此插件添加到游戏项目并开始制作游戏时。

这是一个很好的起点,我们现在有这个插件,可以添加到任何项目中,实现玩家加入并能够一起玩游戏的行为。在下节课中,我们将统计加入的玩家,并查看是否可以显示玩家何时加入和离开,甚至获取玩家的名称,以便我们可以看到谁在加入。

在这一节课中,我们找到并加入会话,并且我们从菜单而不是角色类进行此操作。我们也很高兴,因为我们完成了插件的核心功能。我们还有一些委托和函数尚未使用,因为我们需要开始创建游戏,以便知道一旦所有玩家加入大厅后该怎么办。

通常我们统计玩家数量,然后启动计时器,将所有玩家迁移到比赛中,允许每个玩家开始互相对战或合作。所以大部分我们的插件已完成。我们仍然会添加一些功能,之后我们准备开始学习如何创建多人游戏的游戏性。这真令人兴奋。为自己感到骄傲,恭喜您。这是一个巨大的里程碑。我们下节课再见。

21. 跟踪来访玩家

创建游戏模式以跟踪玩家

在本讲中,我们将创建一个游戏模式,以便我们可以跟踪进入游戏的玩家数量。这样,我们可以打印一个实时玩家计数,跟踪已加入的玩家数量。稍后,我们可以利用这个玩家计数来决定是否应从大厅过渡到实际比赛。

游戏模式与游戏状态

我们将通过游戏状态来实现这一点。在多人游戏中,有两个非常重要的类:游戏模式和游戏状态。游戏模式负责保持游戏的所有规则,这可能涉及许多内容,包括何时将玩家移动到新关卡,以及如何选择出生位置。游戏模式有几个继承的函数,用于跟踪玩家进入或离开游戏的时间。PostLogin是一个继承的虚函数,每当玩家加入游戏时都会被调用,并且我们可以访问该玩家控制器。Logout函数在玩家离开游戏时被调用,我们同样可以访问控制器。

游戏状态则更倾向于保存游戏的状态信息。客户端可以访问游戏状态并获取此信息。游戏状态类设计用于持有游戏状态信息,而不是特定玩家的状态信息,比如得分和胜利次数等。游戏状态包含一个玩家状态数组,而玩家状态类更适合保存特定于玩家的信息,比如得分。游戏模式可以访问游戏状态,以获取这个玩家状态数组,从而通过检查数组的大小来查看游戏中有多少玩家。

创建游戏模式

我们首先为我们的项目创建一个游戏模式。我们将为大厅关卡创建一个游戏模式,而不会在这个类中添加太多内容,因为这只是为了演示如何跟踪玩家登录和注销的过程。我们将在开始编写多人游戏功能时将此插件添加到实际游戏项目中。

创建大厅游戏模式

让我们创建这个游戏模式。我将把它放在这个特定项目中,而不是在插件中。因此,我将转到常规的C++类文件夹,在菜单系统中创建一个新的游戏模式。右键点击,选择“新建C++类”,向下滚动到“GameModeBase”,然后点击下一步。将此游戏模式命名为LobbyGameMode,然后点击创建类。

在Visual Studio中,我将点击“重新加载所有”,现在我们看到有了这个LobbyGameMode。现在我们有了一个游戏模式,接下来就要实现真正的玩家进出跟踪。

实现玩家登录和注销

LobbyGameMode.h中,我们可以重写PostLoginLogout函数。将它们放在公共部分。我们将写出虚拟函数PostLogin,这个函数接受一个指向玩家控制器的指针,命名为NewPlayer。这是一个重写函数。我们也将重写Logout,这个函数接受一个名为Exiting的控制器。

这两个函数在GameModeBase中存在。如果右键点击并选择“转到定义”,我们可以看到GameModeBase.h,搜索PostLogin,可以找到它并查看相关注释,了解PostLogin函数的解释:该函数在新玩家登录时被调用,通过创建一个玩家控制器并由游戏重写。在调用PostLogin时,玩家控制器已为玩家创建。

创建函数主体

我们需要为这些函数创建函数主体。首先,我们将调用这些函数的超级版本,因此将Super::PostLoginNewPlayer传入,对于Logout则调用Super::Logout,并传入Exiting玩家。

接下来,在PostLogin中,我们可以访问游戏状态。游戏状态作为变量GameState存在于游戏模式中。游戏状态有一个玩家数组,我们将访问它。我们可以使用GameState->Get()函数来获取游戏状态基指针,然后使用箭头运算符访问像玩家数组这样的内容。

玩家数组是一个TArray,因此它具有Num函数,可以返回数组中对象的数量。如果需要,我们可以将其存储在一个名为NumberOfPlayers的本地int32变量中。现在,我们知道游戏中有多少玩家。

显示玩家数量

接下来,我希望将此信息打印到屏幕上。每当玩家加入时,我们可以显示这个玩家数量。因此,我们将调用GEngine->AddOnScreenDebugMessage。在显示玩家数量时,我希望使用键值1,这样每次使用相同的键添加新消息时将替换旧消息,以避免多个消息在屏幕上重复出现。我希望这个消息在屏幕上显示60秒,每次玩家加入时,该60秒将重新计时。我们使用FColor::Yellow,消息内容将为FString::Printf,文本字符串设置为“Players in game: %d”,使用%d格式化数量。

我还想显示一条消息,指示某人已经加入。因此,我将添加另一个屏幕调试消息,但这次将使用键-1,这样我们可以看到多个消息在屏幕上滚动。使用青色作为颜色,内容显示谁加入了游戏。格式化字符串时,使用%s,内容为“has joined the game”。

获取玩家名称

要格式化此字符串,我们需要获取玩家的名称,而玩家状态可以通过控制器访问。我们有这个玩家控制器NewPlayer,右键点击并选择“转到定义”,在玩家控制器中,我们可以看到它继承自控制器。我们可以搜索GetPlayerState,并查看其模板参数。

回到游戏模式,我们将访问玩家状态。首先,从NewPlayer获取玩家状态,使用NewPlayer->GetPlayerState<PlayerState>(),并将其存储在一个名为PlayerState的局部变量中。然后,确保PlayerState有效,从中获取名称。如果我们调用PlayerState->GetPlayerName(),就可以获得名称。

现在,我们需要包含玩家状态的头文件,以便使用该类型。玩家状态的文档包含的头文件在GameFramework中。复制该包含语句并粘贴到顶部。

处理玩家注销

现在我们访问了玩家状态并获得了玩家名称。我们可以将此调用AddOnScreenDebugMessage的代码剪切并粘贴到这里,传入PlayerName以格式化此字符串。

接下来,我们可以复制所有此代码,粘贴到Logout中,而不是使用NewPlayer,而是使用ExitingPlayer。修改字符串内容为“此玩家已退出游戏”,更新玩家离开时的消息。在这里,我们还需要更新玩家数量。因此,我们将复制获取玩家数量的行,并将其粘贴到这里,以便在玩家离开时更新消息。

在调用Logout函数时,玩家数量尚未更新,因此我们可以做一个小黑客,将此值减一,以便打印出正确的数量。请注意,这只是为了测试,而不是在打包的项目中使用。

测试游戏模式

现在,我们有一个跟踪玩家进入和离开的游戏模式。请注意,我们需要将FString转换为C风格字符串,因此在需要的地方使用*。接下来,我们想测试一下。在进行此操作之前,还有一个会话设置需要添加。在MultiPlayerSessionSubsystem.cppCreateSession函数中,我将向LastSessionSettings添加另一个设置,即BuildUniqueID,将其设置为1。

配置最大玩家数量

通过设置BuildUniqueID为1,我们可以确保多个用户可以启动各自的构建并进行托管。否则,您将无法看到它们并尝试加入第一个已托管的游戏,如果该游戏没有开放连接,则无法加入。

我们还希望确保可以容纳尽可能多的玩家。这意味着在配置文件中需要添加另一个设置。打开配置文件夹,找到DefaultGame.ini文件并添加一段配置,包括[/Script/Engine.GameSession]部分,并在此部分使用MaxPlayers设置,将其设置为您希望的玩家数量,例如100。

包装项目并测试

在打包项目之前,我们打印了一些调试消息,我觉得不需要一些其他调试消息,特别是连接到Steam的消息。因此,我将进入MenuSystemCharacter,而不是删除它,我想保留它,所以我们将注释掉该调试消息。

现在,我们不再需要将这两个蓝图可调用的函数映射到1和2键,因为我们将仅使用菜单。现在编译、保存并关闭。

创建蓝图

我们创建了游戏模式类。接下来,我们将在此基础上创建蓝图。由于这是演示项目,我将把它放在ThirdPersonCVP/Blueprints中,右键选择“蓝图类”,在所有类中搜索LobbyGameMode并选择它。命名为BP_LobbyGameMode,打开并确保默认的Pawn类设置为ThirdPersonCharacter,以便我们在大厅中能够移动。

配置大厅级别的游戏模式

接下来,需要设置大厅级别的游戏模式。我将进入地图并打开大厅。在世界设置的游戏模式覆盖部分,选择BP_LobbyGameMode。确保选择的是蓝图类,这样我们可以看到默认Pawn类为ThirdPersonCharacter。如果我们选择C++类,则无法更改默认类。

现在我们已为大厅级别配置了

游戏模式。接下来,我们可以回到第三人称示例地图。

挑战与测试

你的挑战是打包项目并压缩,将其上传到Google Drive,并在Discord上找到愿意进行游戏测试的其他学生。记住,这里没有限制,只需在菜单设置中设定公共连接的数量即可。

在测试时,托管一个会话,查看是否可以跟踪玩家数量,并看到他们的名称。尽情享受这个过程,并祝贺自己,因为这是一个巨大的里程碑,你现在有了一个可以添加到其他虚幻引擎项目的系统。

打包项目

现在,我们准备测试一下。我将通过选择平台,Windows,打包项目,删除上一个构建并选择文件夹。在测试时,我曾在YouTube上举办了一个直播,打包游戏并将其放在Google Drive上提供给我的学生,几位学生加入了我的直播。

你正在观看的正是这次直播的片段。我们有很多玩家加入并测试这个系统。此时,我将展示一些来自直播的片段,如果你想观看完整的直播视频,可以在描述中找到链接。

在这里,我们看到一些人加入。让我们看看能否让更多人加入游戏构建。你会在左上角看到他们的名字。这些调试消息是游戏模式打印的,因此你们在自己的构建中看不到这些消息,因为游戏模式实际上只存在于托管机器上,也就是服务器。我是这个游戏的监听服务器。

追踪玩家数量

游戏模式在PostLogin函数中打印调试消息,而该函数在玩家加入游戏时被调用。我们现在有大约八个人在这里,我希望能否超过16人。请注意,您无法使用两台同时登录相同Steam帐户的计算机加入。

到目前为止,我们已经有18名玩家,这真是太棒了。总之,我们现在正在跟踪进入的玩家数量。这是通过游戏模式访问游戏实例,它有一个玩家状态数组,我们可以检查该数组的大小并打印玩家数量。

我们还通过每个玩家状态访问玩家名称,以便能够显示这些信息。干得好!现在,我们有了一个菜单系统,是时候开始创建实际游戏了。我们现在有了通过真实Steam会话进行多人测试的基础。

我会在下一个视频中见到你。

22. 通向大厅的路径

创建可变大厅路径

欢迎来到本讲。在这一讲中,我们将使大厅关卡的路径成为一个变量。目前,我们的路径是硬编码的,而我们希望插件能够灵活,以便在任何项目中添加并指定大厅关卡的位置。因此,我们将将其作为成员变量,并将其添加为菜单设置蓝图可调用函数的输入参数。这样,一旦我们将插件添加到其他项目并在其中创建大厅关卡,就可以指定该大厅关卡的位置。

添加成员变量

我们希望大厅路径成为一个成员变量。因此,我将在此处的MatchType下方添加一个简单的FString,命名为PathToLobby。对于默认值,我将确保它被初始化,但初始值将是一个空字符串。我们将在调用菜单设置时给它赋一个有效值。

修改菜单设置函数

让我们为菜单设置添加一个输入参数。除了公共连接数量和匹配类型之外,我们还将添加另一个FString,命名为LobbyPath,并为其提供一个默认值。为了使其与第三人称模板开箱即用,只要模板中有一个大厅关卡,我将放入大厅关卡的路径。

我们在WorldServerTravel函数调用中硬编码了该路径。因此,我将复制到包括“Lobby”的路径。接下来,在MenuSetup中,实际构建完整的字符串与选项及其他内容。

Menu.h中,我将粘贴大厅路径。由于我们有了另一个输入参数,因此需要在函数定义中添加这个参数。让我们回到菜单设置,并添加这个第三个FString输入参数。

构建路径字符串

现在我们需要构建字符串。我们将设置成员变量PathToLobby。接下来,使用FString::Printf来设置它。这个函数将接收一个字符串值,字符串值将使用%s和“Listen”选项。因此,如果保持默认值,它将是这个字符串与“Listen”选项。

在这里,我发现我没有一个右括号,所以我将添加最后一个右括号。我们现在有了%s和“Listen”,这意味着我们需要格式化这个字符串的值,这将简单地是LobbyPath。当然,我们需要使用D引用运算符将其转换为C风格字符串。因此,我们现在有了一个大厅路径。

使用大厅路径

由于我们有了PathToLobby,现在可以在调用ServerTravel时使用它。因此,我们可以向下滚动到OnCreateSession,现在我们有了一个可以传递的字符串。让我们将PathToLobby传递给此函数。

重新生成项目文件

接下来,我们可以编译这个项目。关闭所有内容,回到Visual Studio,重新生成项目文件。进入插件文件夹,删除二进制文件和中间文件,回到项目并重新生成项目文件。

完成后,返回到虚幻引擎,我们将重新构建模块,并查看关卡蓝图。打开关卡蓝图时,看到LobbyPath,这不仅允许我们选择路径,还包括了关卡名称。这样我们实际上可以旅行到任何我们喜欢的关卡。

创建新的关卡

如果需要,我可以创建一个新的关卡,甚至在新的文件夹中。我可以右键点击内容,创建一个名为“Maps”的新文件夹。在此文件夹中,我可以创建一个新关卡,选择默认级别,进行简单修改,例如复制这个地面,形成一面墙,并稍微调整一下比例,放到地面上。

保存当前级别时,将其命名为“Starting Map”。然后,我可以右键点击“Starting Map”,选择“复制文件路径”。保存一切后,回到第三人称示例地图,打开关卡蓝图并粘贴路径。当然,我不需要整个路径。将内容文件夹中的路径替换为“Game”,并删除“.NewMap”部分。现在我有了“Game/Maps/StartingMap”,可以点击编译并保存。

启动项目

接下来,启动项目,右键点击新项目并从此处启动游戏。点击托管,我现在已经成功转移到名为“Starting Map”的新地图。通过这一过程,我们使插件变得更加用户友好。希望下载并使用插件的用户无需了解其工作原理,只需知道如何设置某些选项并告诉插件执行预期的操作。

总结

总结一下,我们添加了一个大厅路径变量,以便在菜单设置函数中指定大厅的路径。现在,插件的用户可以将大厅关卡放置在项目中的任何位置,无需将其放在“Third Person CBP”文件夹中,因为并非所有项目都会包含该文件夹。这使我们的插件更加灵活和用户友好。

我们现在已经完成了插件的制作,准备将其添加到项目中并创建实际的游戏。这非常令人兴奋。我们下次见!

23. 美化菜单子系统

完善菜单系统

欢迎来到本视频。在这一讲中,我们将通过一些小的改进来完善我们的菜单系统。我们希望确保始终能够进行托管,因此我们将查看菜单的工作原理,以确保这是可行的。我们还将向菜单中添加一个快速游戏按钮,以便我们可以轻松退出游戏,并在点击按钮后禁用菜单按钮,以防止在一个会话尚未创建之前尝试创建多个会话。

处理会话托管

首先,我们来处理在已创建会话时托管新会话的问题。如果你进行了一些测试,可能注意到一个小问题。假设我在另一台机器上有一个正在运行的会话,我从这台机器上点击加入,现在我在这个游戏会话中。接下来,假设主机玩家决定通过离开游戏来踢掉所有玩家。我的测试者在另一台机器上将退出游戏,并将我踢回主菜单。此时,我已被踢回主菜单。

现在我想能够托管自己的游戏会话,但如果我点击托管按钮,就会看到我们无法创建游戏会话。但是有趣的是,如果等几秒钟再点击托管,我们就可以成功托管。那么这是怎么回事呢?让我们看看代码。在MultiplayerSessionSubsystem.cpp中,如果我向下滚动到CreateSession函数,我看到我在检查是否存在现有会话,如果存在,我会调用DestroySession。问题在于,立即调用DestroySession后,我们又调用CreateSession。因此,虽然已经调用了DestroySession,但信息需要时间通过网络传输到服务器以销毁该会话。如果我们立即调用CreateSession,会话可能还没有被销毁,这种情况下我们会失败。因此,我们需要确保销毁会话的操作已完成,然后再创建新的会话。

使用回调处理会话销毁

幸运的是,我们为所有这些会话操作(包括销毁会话)创建了一些函数和回调。我们创建了一个DestroySession函数,目前还没有使用。我们还有一个委托,可以添加到会话接口的委托列表中。DestroySession函数实际上是绑定到我们的委托的。

回到构造函数,我们有一个DestroySessionCompleteDelegate,我们的OnDestroySessionComplete函数绑定到了这个委托上。因此,我们需要实现我们的DestroySession函数,并在CreateSession中使用它,而不是直接访问会话接口并调用DestroySession。我们应该在子系统的DestroySession函数中处理这个调用。

一旦我们这样做,就可以在OnDestroySessionComplete回调函数中进行反应。因此,如果我们正在创建新会话并且会话已经存在,那么我们知道当在子系统的CreateSession函数中调用CreateSession时,将会失败。由于我们知道将会失败,我们可以在OnDestroySessionComplete回调中创建会话。但是,只有在确认我们要托管会话的情况下才可以这样做。

添加私有变量

为此,我们可以轻松地创建几个变量。让我们回到MultiplayerSessionSubsystem.h,在底部添加几个私有变量。首先,我想创建一个布尔值,命名为bCreateSessionOnDestroy,并初始化为false。在会话销毁的回调中,我们将检查这个变量。如果它为true,我们将创建一个新会话。

现在,如果我们知道创建会话会失败,因为已经存在一个会话,我们希望存储一些关于新会话的信息。我们希望创建开放公共连接的数量和匹配类型。我们需要这些信息的原因是,一旦在这里调用CreateSession,我们必须传入开放公共连接的数量和匹配类型。因此,我们将为最后未成功创建的会话创建一个类型为int32的成员变量LastNumPublicConnections,并创建一个名为LastMatchTypeFString

CreateSession中,我们需要存储这些信息,因为在调用DestroySession时我们会失败,创建会话已经存在。在DestroySession的回调中,我们会再次调用CreateSession。因此,如果现有会话不是空指针,我们将首先将布尔值bCreateSessionOnDestroy设置为true,然后存储公共连接的数量到LastNumPublicConnections,并将LastMatchType设置为当前的匹配类型。

实现销毁会话函数

DestroySession函数中,我们将首先确保会话接口有效。如果SessionInterface无效,则返回。并且,我们会广播我们创建的自定义委托MultiPlayerOnDestroySessionComplete,调用广播并传入false,以指示销毁会话是否成功。

然后,我们将添加我们的DestroySessionCompleteDelegate到会话接口的委托列表中。调用SessionInterface->AddOnDestroySessionCompleteDelegateHandle并传入该委托。接下来,我们将获取返回值并将其存储在委托句柄中。

现在我们可以调用SessionInterface->DestroySession。这需要一个会话名称,我们将使用NAME_GameSession。与其他接口函数一样,这个函数返回一个布尔值,我们将检查这个值。如果返回值为false,我们知道销毁会话失败。因此,我们将清除委托并广播给菜单,传入false值。

处理销毁会话的回调

当销毁会话的操作完成后,我们的MultiplayerSessionSubsystem回调函数将被调用,因为我们已将其添加到会话接口的委托列表中。在OnDestroySessionComplete中,我们将首先使用委托句柄清除委托。然后我们需要检查是否应该创建新会话,因此将首先检查布尔值bWasSuccessful。这样,我们只会在成功销毁会话的情况下执行此操作。

接下来,如果布尔值bCreateSessionOnDestroytrue,这意味着我们在菜单中尝试创建新会话,但会话已经存在,因此我们调用了DestroySession。成功销毁会话后,我们将调用CreateSession,并传入最后的开放公共连接数和匹配类型。

编译并测试

现在我们可以编译代码。我将让我另一台机器运行游戏并再次托管会话。启动我的项目后,我的另一台机器正在托管游戏。现在,我将加入该游戏。加入后,我可以让另一台机器的测试者退出游戏并将我踢回菜单。

我被踢回主菜单后,可以点击托管按钮,看到我们未能创建会话,然后销毁会话。在OnDestroySession回调中,我们再次调用CreateSession,传入我们尝试创建会话时使用的相同值。这样一来,如果会话已经存在,点击托管会话就可以成功。

添加快速游戏按钮

接下来,我们希望添加一个快速游戏按钮,以便我们可以轻松退出菜单。为此,我们将进入MultiplayerSessions内容文件夹中的WBP_Menu,打开菜单小部件。在菜单小部件中,我想添加一个退出按钮。

我将从调色板中拖入一个按钮,放到右上角并重新调整位置。接下来,我将点击锚点,将其锚定到顶部右侧,并将此按钮重命名为QuickButton。我将向按钮添加一些文本,拖入一个文本元素并将字体从粗体改为常规,文本更改为“Quit”。

我们可以通过C++处理这一功能,但对于退出游戏这样简单的功能,实在没有必要。我只需在详细面板中选择按钮,点击“On Click”旁边的加号,将一些功能绑定到该按钮的点击委托上,我们只需调用Quit Game即可。因此,我将拖动并选择Quit Game,然后点击编译并保存。

测试退出按钮

现在,如果我通过右键点击我的项目并选择启动游戏,出现一个退出按钮。如果我点击“Quit”,那么游戏就退出了。

禁用菜单按钮

最后,我们要处理的是点击按钮后禁用菜单按钮,这样就无法重复点击,尝试多次调用CreateSessionFindSessions。禁用菜单按钮很简单。我们只需进入Menu.cpp,向下滚动到HostButtonClickedJoinButtonClicked函数。

一旦我们点击这些按钮,我们希望禁用它们,可以通过访问按钮本身来实现。在这里,我访问HostButton并调用SetIsEnabled(false),这将禁用按钮并使其变灰。我们将在点击HostButton后立即调用此操作,同样也会在点击JoinButton时调用。

如果我们未能成功托管或加入,则应该最终重新启用这些按钮。因此,如果我们点击HostButton并未能创建会话,我们可以回到OnCreateSession回调中,检查bWasSuccessful的值。如果创建会话失败,则打印失败消息,并在此处重新启用按钮。

处理查找会话

对于JoinButton,如果我们未能找到任何会话,最明显的原因是没有人正在托管会话。我们可以在OnFindSessions中添加一个检查,查看`b

WasSuccessful是否为false。如果为false`,我们也要检查会话搜索结果数组是否为空。

如果这两者之一为真,我们将重新启用JoinButton。此外,还有一种可能性是找到会话成功但无法加入。在这种情况下,我们将进入OnJoinSession回调,检查结果是否为成功。如果未成功,我们将重新启用JoinButton,以便能够再次尝试加入会话。

编译与验证

现在让我们编译并启动项目。这次没有游戏会话,所以我在没有会话的情况下点击加入按钮。点击后,按钮会在尝试查找会话时灰掉一下,之后在回调中,OnFindSessions会被调用,因为我们未找到会话,因此重新启用加入按钮。

点击托管按钮后,HostButton应该在我们实际转到大厅之前灰掉,因此在点击时会被禁用。虽然过程很快,因为数据迅速通过网络传输,但按钮确实会在创建新会话的过程中变灰。

总结

总结一下,我们为菜单系统添加了一些完善的功能。现在,如果尝试在已有会话的情况下创建新会话,我们调用MultiplayerSessionSubsystemDestroySession函数,它将访问接口并调用其销毁会话函数。然后在子系统回调中,我们检查是否应创建新会话,因为会话创建失败,接着再次调用CreateSession

我们还添加了一个快速退出按钮,允许从菜单退出。最后,我们在点击按钮后禁用菜单按钮,并在未能找到会话或未能创建会话时重新启用它们。我们将在创建本课程的实际游戏时添加更多菜单元素,但现在我们拥有一个稳健的菜单系统,用于处理游戏会话。

干得好!我们下次见。

WangShuXian6 commented 2 days ago

3. 项目创建

1. 项目创建

创建全新的虚幻引擎项目

你好,欢迎来到本讲。在这一讲中,我们将创建一个全新的虚幻引擎项目,并添加我们的多人会话插件,以便我们的项目能够开箱即用地与在线Steam会话一起使用。这将使我们更容易在真实的多人会话中通过互联网测试我们的项目。让我们开始吧。

创建新项目

第一步是创建一个全新的项目。我们将打开Epic Games启动器并转到库。我们使用的是虚幻引擎5,在拍摄时,最新版本是Early Access 2。请使用可用的最新版本,因此我们可以简单地启动虚幻引擎5。

接下来,我们选择“游戏”,并且我们将从头开始创建一切,所以选择“空白”而不是使用提供的模板。现在,我们需要选择项目的目标位置。我将在这里选择,并点击“选择文件夹”。项目名称我将命名为“Blaster”。这将是一个C++项目,所以我选择C++并点击“创建”。我现在有了我的空白项目,虽然并不完全空白,我们有一个地面和其他一些物体,但总体来说,这个项目是非常基础的。

导航内容浏览器

如果你是虚幻引擎5的新手,来自虚幻引擎4,可能会想知道内容浏览器在哪里。你可以通过点击内容抽屉来调出它,也可以使用Ctrl + Space来显示和隐藏它。如果你想将其固定到底部,可以点击“布局中停靠”,现在你就有了内容浏览器。

当我开发时,我喜欢打开输出日志,因此我将转到“窗口”并勾选“输出日志”,以便在这里也有该标签。如果你看不到“放置演员”面板,可以去“窗口”中勾选“放置演员”。这在创建关卡时可以用来引入常用的演员。

测试多人会话

现在我们有了一个空白项目,我们希望能够通过Steam会话在真实的多人环境中测试此项目。如果你跳过了创建多人插件的第一部分,你可以下载插件,链接将在本视频的资源中提供,我们将在本视频中将该插件添加到项目中。

接下来的步骤是将插件添加到项目中。这里是我的项目文件夹,我已在此下载了插件,所以我将提取它。现在我得到了这个插件文件夹,打开后可以看到“Plugins”文件夹,其中包含“MultiplayerSessions”,这就是我们的插件。在其中有更多的文件夹,包括一个.uplugin和一个包含C++代码的“Source”文件夹。

将插件添加到项目

我将这个“Plugins”文件夹拖到我的项目中。我不想将其放入其他文件夹中,不要拖到“Content”文件夹或其他地方,保持在项目级别就可以了。如果你将其带入其他已有插件文件夹的项目中,只需将“MultiplayerSessions”文件夹放入你的“Plugins”文件夹中。现在,我们的多人会话插件已经添加到项目中。

配置项目以支持多人游戏

为了配置该项目以支持多人游戏,我们需要遵循几个步骤。我在一份PDF文档中概述了这些步骤,你可以在本讲的资源中找到,因此如果你打算在其他项目中再次执行此操作,可以参考该文档。

启用Steam在线子系统插件

首先,我们需要启用“OnlineSubsystemSteam”插件。为此,我们可以转到“编辑”>“插件”。在插件编辑器中,我们可以搜索“OnlineSubsystemSteam”,勾选该插件的启用框。虚幻引擎会告诉我们需要重启以使更改生效,因此我们可以重启。现在选择“是”,它会问我是否希望为多人会话构建模块,我点击“是”。现在,虚幻引擎已重启,我们的“OnlineSubsystemSteam”插件已启用。

修改配置文件

接下来,我们需要更改几个配置文件。返回到项目文件夹,进入“Config”文件夹。注意我们有几个.ini文件。我们需要打开DefaultEngine.ini,并在这里添加一些内容。虚幻引擎有文档说明如何在项目中启用Steam,我将在资源中提供该链接。

我们需要滚动到“Finished Settings”部分,看到这里是我们需要添加到DefaultEngine.ini的一些设置。当你回到课程的第一部分,创建这个插件时,我会解释这些设置的含义。我们只需复制这些并添加到DefaultEngine.ini中。将这些高亮并复制。

DefaultEngine.ini的底部粘贴这些内容。保存并关闭该文件。

配置游戏的最大玩家数量

另一个更改是在我们的DefaultGame.ini中,在这个文件夹中我们将添加一个新部分,使用方括号并键入[/Script/Engine.GameSession]。在该部分下,我们输入MaxPlayers=100。这将允许我们配置能够加入游戏会话的玩家数量,但这个设置将限制我们项目的总玩家数量,默认值为16。因此,如果我们不将其设置为100,当我们设置玩家可以加入的公共连接数量时,如果该值超过16,将被限制在16。因此,这是为我们的项目整体设置最大玩家数量的方法。

保存并关闭该文件。

构建解决方案

我们已添加了一些配置设置。接下来,让我们回到Visual Studio项目,使用“构建解决方案”或Ctrl + Shift + B来构建它。关闭Visual Studio并重新生成项目文件。这意味着我们也需要关闭虚幻引擎。因此关闭虚幻引擎,删除“Saved”、“Intermediate”和“Binaries”文件夹。

现在我们希望为我们的插件生成Visual Studio项目文件。进入“Plugins”>“MultiplayerSessions”,也删除此文件夹中的“Binaries”和“Intermediate”文件夹。然后返回,右键点击“Blaster”,生成Visual Studio项目文件。它会询问我虚幻引擎版本,我使用的是5,所以我点击“确定”。

生成完成后,我们可以双击我们的新项目,它会询问我是否要为Blaster重建模块,这是我的整体项目和“MultiplayerSessions”插件。点击“是”。虚幻引擎打开后,我将最小化它,打开Visual Studio项目解决方案。

生成Visual Studio项目文件后,我现在可以看到插件文件夹,并展开“MultiplayerSessions”的下拉菜单。里面有一个“Source”文件夹和“MultiplayerSessions”,再展开,我可以看到“Public”和“Private”文件夹,并在这里有一些C++文件。“MultiplayerSessionSubsystem”包含创建游戏会话的所有代码,而“Menu.cpp”和“Menu.h”是插件中包含的新用户小部件,它允许我们使用一个准备好的菜单小部件,因此我们不必自己创建一个。

在虚幻引擎中查看插件

现在我们关闭插件文件夹,回到虚幻引擎。在虚幻引擎中我们看不见,但我们的插件已在项目中。为了显示它,我们可以点击设置按钮,然后点击“显示插件内容”,现在我们可以看到我们的“MultiplayerSessions”插件文件夹。我们有一个内容文件夹,在这个文件夹中有一个小部件蓝图。双击它,我们会看到它非常基础,包含一个托管和一个加入按钮,这些按钮上方还有一些文本。

你可以点击图表,看到这里完全是空的,所有逻辑都在C++中。如果点击类设置,你会看到父类是“Menu”,即我们刚添加的C++类。

添加小部件到视口

我们的WBP_Menu小部件可以通过关卡蓝图添加到视口中。点击“蓝图”,打开关卡蓝图。在“Begin Play”中,我们将调用CreateWidget。对于类,我们可以选择WBP_Menu。这个菜单小部件有一个蓝图可调用函数MenuSetup,这个函数配置公共连接的数量,即允许加入游戏会话的玩家数量,以及匹配类型和大厅路径。这个路径将是我们项目中指定为大厅的关卡。

它设置为默认值,但由于这是一个空白项目,我们没有名为“Lobby”的文件夹或地图。因此,我们将创建一个大厅关卡。我将编译,然后返回到当前打开的关卡,首先保存此关卡。创建一个名为“Maps”的文件夹,点击“Content”,选择“新建文件夹”,创建一个名为“Maps”的文件夹。

创建大厅关卡

现在我将文件,保存当前关卡为,选择“Maps”,将其命名为“GameStartupMap”,并保存。这将是我们的游戏打开时所显示的地图。转到编辑,项目设置,确保这是正确的。点击“Maps and Modes”,对于编辑器启动地图,我们可以选择“GameStartupMap”,对于游戏默认地图,我们也选择“GameStartupMap”。

现在我关闭它,需要创建我们的大厅关卡。转到文件,新建关卡,选择默认,然后保存为“Lobby”。在文件中选择“保存当前关卡为”,在“Maps”文件夹中将其命名为“Lobby”,然后点击保存。

配置大厅路径

现在返回到“GameStartupMap”,重新进入关卡蓝图。在菜单设置函数中,我们需要提供大厅的路径。所以我

将使用“Game/Maps/Lobby”。在你完成本课程第一部分创建这个插件时,会看到它是如何工作的。MenuSetup函数接受这个路径并将其设置为在创建或加入会话时将要进入的大厅关卡。

现在,如果我们点击播放,可以看到这个菜单小部件已添加到视口,这也是在MenuSetup函数中处理的。如果我们点击“Host”,将托管一个游戏会话;如果我们点击“Join”,将搜索任何存在的游戏会话并加入一个。

测试Steam会话

需要注意的是,我们无法在编辑器或“播放”模式中使用Steam会话。我们必须实际打包游戏才能创建和加入Steam会话。因此,这个“GameStartupMap”主要在我们打包和测试游戏时使用。频繁地打包游戏很重要,以确保它在打包时没有错误,并在实际互联网环境中测试游戏。

配置插件设置

如果你使用的是虚幻引擎预览版2或虚幻引擎5.0的最新正式版,可能需要对插件进行一些更改。进入解决方案资源管理器,在Blaster下选择插件,打开“MultiplayerSessions”,在“Source”下选择“MultiplayerSessions”,然后打开“MultiplayerSessionSubsystem”,在CreateSession函数中搜索此部分。

对于预览版2或虚幻引擎5.0的最新正式版,你需要添加一行代码,内容为LastSessionSettings.bUseLobbiesIfAvailable = true;。因此,如果你在使用预览版2或虚幻引擎5.0的最新正式版,请确保在MultiplayerSessionSubsystem.cpp中添加此行,并编译代码。

关闭Visual Studio,关闭编辑器,返回项目,进入插件的“MultiplayerSessions”,删除其中的“Binaries”和“Intermediate”文件夹。然后返回,右键点击新项目,生成Visual Studio项目文件。这将使你对插件代码所做的更改生效。

检查配置文件

在打包游戏之前,转到“编辑”和“项目设置”,搜索“包含的地图”,在打包时查看要包括在构建中的地图列表。我们可以点击加号添加内容,输入“/Game/Maps/Lobby”,或者根据你放置大厅关卡的位置添加。我们还可以添加“Game/Maps/GameStartupMap”。这将确保在打包构建时,也将这些地图包括在内。确保将游戏中的所有地图添加到此列表中,并确保将它们的路径正确放置。

打包测试游戏

在视频结束之前,我们将测试打包的游戏。因此,这里是我的项目,我将右键点击,创建一个名为“Build”的新文件夹。这个构建文件夹可用于打包游戏项目,现在我们就来打包。因此,我将转到平台,选择Windows并打包项目。询问我将项目发送到哪里,我将选择新创建的“Build”文件夹,并点击“选择文件夹”。我可以点击输出日志查看构建进度。

一旦打包完成,我可以返回构建文件夹,现在看到一个Windows文件夹,其中有我的打包游戏项目,即可执行文件Blaster.exe。我们需要所有其他与其打包的文件,因此不要删除此处的任何内容。双击运行游戏项目Blaster.exe,我们将打开到游戏启动地图,看到小部件蓝图,显示托管和加入按钮。

启动Steam客户端

这个插件使用Steam,因此为了托管或加入会话,Steam必须在我们的机器上运行。测试此项目时,我需要两台不同的机器上登录两个不同的Steam帐户,以便找到彼此的游戏会话。我们需要确保连接到相同的Steam区域。可以在Steam的设置中点击下载,我的下载区域设置为“美国凤凰城”。

因此,如果我打包此项目并在另一台机器上使用,另一台机器需要有自己唯一的Steam帐户登录,并且下载区域应与我的机器设置相同,即“美国凤凰城”。点击确定并关闭。

课程总结

我们现在在新项目中添加了我们的“MultiplayerSessionsSubsystem”插件,整个课程中我们将频繁使用它来测试我们的多人游戏,并确保它在通过Steam会话进行在线游戏时按预期工作。在本讲中,我们创建了游戏项目并添加了菜单系统插件。现在我们有了一个菜单小部件和我们的游戏启动地图,我们将在整个课程中使用这个插件,通过Steam网络连接正在玩我们游戏的机器。

做得好!我们现在准备开始真正制作这个游戏,以便进行网络测试。我们下次见!

2. 测试在线会话

测试打包游戏

欢迎回来。在本讲中,我们将测试打包的游戏,以确保我们的插件正常工作。然后,我们将连接到互联网中的其他机器。我们的游戏现在相当基础,因此我们将添加一个第三人称角色,以便在测试时可以看到我们正在玩的伙伴移动。

上传项目

首先,我们需要上传我们的项目,以便可以在第二台机器上下载。我将使用Google Drive上传我们在上一个视频中打包的项目文件。这里是我的项目文件夹,我的“Build”文件夹中包含Windows文件夹,而里面有我们的游戏可执行文件。因此,我将压缩Windows文件夹,右键选择“发送到”压缩(zip)文件夹。压缩完成后,我将这个文件上传到Google Drive。

上传完成后,我将在另一台机器上下载这个压缩文件。

下载并启动游戏

现在,我已经在另一台机器上下载了这个zip文件,并解压缩。在那台机器上,Steam正在运行,并且使用的是与我自己的Steam帐户完全不同的帐户。我在两台不同的机器上登录了两个Steam帐户,并在另一台机器上启动了游戏。

现在,我可以在这台机器上启动游戏,所以我将双击我的可执行文件。打开后,这里是启动关卡,当然我有托管或加入的选项。我将在另一台机器上托管,而在这台机器上加入。因此,我在另一台机器上启动这个游戏项目并点击托管,这导致我的另一台机器进入大厅关卡。

加入游戏会话

现在那台机器正在托管游戏,我将点击加入,结果我也进入了大厅关卡。这时我看到的是由我的另一台机器控制的默认Pawn。当前,我们在这里并没有太多可以做的,实际上我们只是操控着默认Pawn,甚至还没有设置任何复制,因此目前这个项目并不是很有趣。但我们知道的是,我们通过Steam会话在互联网上成功连接了。

退出游戏

我将使用Alt + Tab强制关闭游戏项目,因为我们尚未编程实现优雅退出游戏的方式,稍后我们会处理这个问题。但现在我们知道我们在打包的构建中通过互联网连接了。

添加第三人称角色

现在我想回到我的游戏项目,而不是仅仅使用默认Pawn进行操控。我希望我们能够看到彼此的动作,因此我将快速设置这个。我们可以为项目添加一些内容。我会在内容浏览器中点击添加,然后在顶部选择添加功能或内容包,我可以添加第三人称模板材料。这样做的原因是,第三人称角色已经具有移动功能,并且该功能会自动复制。因此,如果我在另一台机器上加入游戏,我们都可以跑动并看到彼此的动作。

我将点击“添加到项目”,现在我们有了第三人称模板材料。点击关闭,选择我的“Maps”文件夹,进入大厅。在大厅中,我们看到这里有一个玩家起始点,这就是我们开始在此关卡中玩时会生成的位置。

重写游戏模式

为了使用第三人称角色,我们需要重写这个关卡的游戏模式。由于我们在“第三人称BP”下添加了第三人称模板材料,所以有一个第三人称游戏模式。如果我双击这个,我可以看到第三人称游戏模式蓝图,它的默认Pawn类是第三人称角色。

如果我重写大厅关卡的游戏模式并选择第三人称游戏模式,可以看到默认Pawn类是第三人称角色。因此,如果我在“Play In Editor(PIE)”中点击播放,就会看到第三人称角色并且移动正常。

打包游戏

现在我可以打包这个游戏。由于我们的大厅关卡使用了第三人称角色,因此在两个机器上都会生成第三人称角色,且移动会被复制。因此,我们应该能够在关卡中都跑动。

我将首先进入我的构建文件夹并删除之前的构建,然后再次打包游戏。转到平台,选择Windows并打包项目,选择我的构建文件夹并进行打包。打包完成后,我在这里得到了打包的项目,并将其发送到压缩文件夹。

上传新打包版本

现在我已经压缩完成,可以将其上传到Google Drive。为了不混淆我的构建,我将删除之前的一个版本,右键选择删除,现在这个文件夹是空的,因此我可以将新打包并压缩的构建拖入并上传。

上传完成后,我将在另一台机器上下载这个文件以进行游戏测试。

测试多人会话

我在另一台机器上下载了这个zip文件并解压缩,然后启动游戏。这次我想在这台机器上进行托管测试,所以我将启动可执行文件并点击托管。现在我在托管一个游戏,而在我的另一台机器上可以点击加入,这样我们就可以看到在另一台机器上加入的玩家。

我们可以看到,他们在跑动,我也在移动,我们能看到彼此的动作,确认我们在同一个游戏中。因此,我们通过互联网连接起来,这个插件使得建立多人会话变得非常简单。

小结

在本讲中,我们测试了在线会话。我们成功地将两台机器连接并一起玩同一个游戏。我们添加了第三人称角色,以便能够看到彼此移动,证明我们在玩同一个游戏。这是一个很好的起点,我们将以此为基础,继续构建我们的游戏项目。做得好,我们下次见!

3. 资产

添加游戏资产以丰富多人项目

欢迎回来。在本讲中,我们将为我们的多人项目添加一些游戏资产,以使其更有趣。目前,我们正在使用默认的人偶,我们希望引入更吸引人的角色。因此,我们将添加一些环境、动画和可以在多人游戏中使用的武器。

上传项目

首先,我们需要上传我们的项目,以便在第二台机器上下载。我将使用Google Drive。我这里有一个Google Drive,我将上传我们在之前的视频中打包的项目。这里是我的项目文件夹,我的构建文件夹包含Windows文件夹,里面有游戏的可执行文件。

我将压缩Windows文件夹,右键选择“发送到”压缩(zip)文件夹。压缩完成后,我将这个文件上传到Google Drive。

下载并启动游戏

上传完成后,我将在另一台机器上下载这个zip文件,并解压缩。在那台机器上,Steam正在运行,并且使用的是与我自己的Steam帐户完全不同的帐户。我在两台不同的机器上登录了两个Steam帐户,并在另一台机器上启动了游戏。

现在,我可以在这台机器上启动游戏,所以我将双击我的可执行文件。打开后,这里是启动关卡,当然我有托管或加入的选项。我将在另一台机器上托管,而在这台机器上加入。因此,我在另一台机器上启动这个游戏项目并点击托管,这导致我的另一台机器进入大厅关卡。

加入游戏会话

现在那台机器正在托管游戏,我将点击加入,结果我也进入了大厅关卡。这时我看到的是由我的另一台机器控制的默认Pawn。当前,我们在这里并没有太多可以做的,实际上我们只是操控着默认Pawn,甚至还没有设置任何复制,因此目前这个项目并不是很有趣。但我们知道的是,我们通过Steam会话在互联网上成功连接了。

退出游戏

我将使用Alt + Tab强制关闭游戏项目,因为我们尚未编程实现优雅退出游戏的方式,稍后我们会处理这个问题。但现在我们知道我们在打包的构建中通过互联网连接了。

添加武器资产

现在我想为我们的项目添加一些内容。我们将开始寻找一些武器。打开Epic Games启动器,我将搜索“Military Weapons Silver”。我找到了一个包含大量武器的免费资产包,名为“Military Weapons Silver”。这个包在免费收藏中,并且没有计划将其从免费收藏中移除。这是一个很棒的起始包,包含多种武器资产,还包括每种武器的粒子系统和声音。

导入武器资产

现在,虽然这个资产包的支持引擎版本只列出了4.4到4.21,而我们使用的是虚幻引擎5,但我想展示如何将这些资产导入到虚幻引擎5项目中。这是一个非常好的技能,能为你在虚幻引擎5项目中添加资产提供更多选择。

我不能直接给你这些资产,因为我没有许可,因此我将向你展示如何将其添加到你的项目中。首先,我们需要安装虚幻引擎版本4.21。可以在我的库中找到。我已经安装了它。

如果你的硬盘空间有限,你可以选择市场上的其他武器资产或其他来源的资产,照样可以继续进行。但我将展示如何将仅与4.21兼容的资产导入到我们的虚幻引擎5项目中。

创建4.21项目

我将创建一个4.21项目,启动虚幻引擎4.21,选择“新建项目”,保持为蓝图项目,选择“空白”,因为我们不需要任何起始内容。选择一个文件夹,我将其放在Blaster项目旁边,命名为“YUI_For_Assets”,并创建项目。

现在这是我的虚幻引擎4.21项目,它是完全空的。我将把“Military Weapons Silver”资产添加到这个项目中。在市场中找到“Military Weapons Silver”资产包,点击“添加到项目”,选择“YUI_For_Assets 4.21项目”,点击“添加到项目”。

完成后,我在项目中有了“Military Weapons Silver”,并且有一个展示关卡,展示所有资产,包括突击步枪、手枪、狙击步枪、火箭发射器、霰弹枪和榴弹发射器。我们可以在项目中使用这些武器。

迁移资产到虚幻引擎5项目

我希望将这些资产迁移到我的虚幻引擎5项目中。我可以通过右键点击“Military Weapons Silver”下的内容文件夹,选择“迁移”来轻松做到这一点。它会显示所有将要迁移的资产,我点击“确定”,然后回到Blaster项目,选择内容文件夹。

迁移资产时,必须迁移到内容文件夹。选择该文件夹,我看到内容迁移成功完成。

添加环境和动画

现在我已经拥有了这些资产,我准备添加一些环境。我要关闭虚幻引擎4.21项目,返回市场,搜索“Unreal Learning Kit Games”。这是Epic Games发布的一个资产包,包含一个角色和一些环境。

我选择这个资产包的原因是,它有大量资产,并且风格一致。因此,这些资产将会在我们组合成一个游戏时显得非常出色。

创建4.26项目

但这个资产包只与4.26兼容。因此,我需要在库中确保我已安装4.26。创建项目时,我选择4.26,并将其放在与Blaster项目相邻的文件夹中,命名为“UnrealLearningKitGames”,点击“创建”。

现在我创建了这个项目,将关闭虚幻引擎4.21项目,打开“Unreal Learning Kit Games”项目。我们有这个学习套件项目,里面有很多可以使用的资产,同时也是4.26项目。因此,我们可以从市场中为该项目添加更多资产,然后一旦准备好,我们可以将所有内容迁移到虚幻引擎5项目中。

获取动画资产

我还想获取更多资产,打开市场,寻找“Animation Starter Pack”。这里我们看到由Epic Games提供的资产包,免费可用,因此我们可以将其添加到项目中。

确保选择正确的资产,将其添加到学习套件游戏项目中。

编译和准备工作

现在它出现在我的学习套件游戏项目中,我们有动画启动包,这些动画已绑定到虚幻引擎4的人偶。在这里,我们可以使用它来重新定位这些动画到我们想使用的角色上。

接下来,我们将在准备好时进行这项工作。虽然动画启动包提供了很多很好的射击动画,但它并不完全具备我们所需的所有动画。例如,它没有站立和蹲下的转身动画。

结论

在本讲中,我们为我们的项目添加了一些资产。我们将继续寻找一些免费的动画,以确保项目更完整。期待在下节课见到你!

4. 调整动画

添加游戏资产以丰富多人项目

欢迎回来。在本讲中,我们将为我们的多人项目添加一些游戏资产,以使其更有趣。我们将引入一些环境、动画和武器,使游戏体验更加生动。

下载角色模型

首先,我们需要选择一个角色使用。我将选择一个与Epic Games的人偶相似的角色。我们注意到它是T姿势而不是A姿势,这是Epic骨架的姿势,但这没关系。一旦选择了要使用的角色,我们可以点击下载。确保下载为FBX格式,可以保留为T姿势。

导入角色到项目

下载完成后,我将在我的Unreal Learning Kit项目中添加这个角色。首先,在内容浏览器中创建一个名为“Maximo”的新文件夹,然后在其中再创建一个名为“Character”的文件夹。在这里,我可以导入刚才下载的角色,将其重命名为“Maximo Character”。

接下来,点击“添加导入”,选择Maximo Character的FBX文件。导入时,选择导入骨骼网格,而不选择骨骼,因为FBX文件中已有其骨骼。点击导入所有,并确认可能出现的无平滑组信息错误。这是常见的,只需关闭该提示。

组织文件夹

现在,我有了Maximo角色和其骨骼。如果查看骨骼,会发现它与Epic Games的骨骼相似,但层次结构和名称略有不同。在“Maximo”文件夹中右键点击,创建一个名为“Animations”的新文件夹,以便下载Maximo的动画。

下载动画

接下来,我将查找一些Maximo动画。很重要的是,我使用下载的角色,因为所有动画都将与这个角色绑定。我们需要一些转身动画。我将搜索“Turn Right”。选择适合的动画后,确保下载时选择带皮肤的ABC格式,以确保它绑定到正在使用的角色。

同样的步骤,我会下载左转、蹲下转身和其他所需的动画。我们还需要跳跃动画,因此会搜索“Jump”,下载跳跃上升、跳跃下降和跳跃循环动画。下载完成后,我们将导入这些动画到我们的项目中。

导入动画到项目

在“Animations”文件夹中,点击导入,将所有动画导入到项目中。选择Maximo Character Skeleton作为骨骼,不需要导入网格。导入后,我发现每个动画都被导入了两次,带“Take 1”的版本可以删除。

重新绑定动画

我将保留导入的动画,并准备将它们重新绑定到Epic Games的人偶骨骼。打开UE4人偶的骨骼,并在重定向管理器中选择人形骨架。对于Maximo骨骼,我们需要将其设置为人形骨架,并映射骨骼。详细步骤已在附带的PDF中列出。

完成后,我将保存并准备重定向Maximo的动画到Epic骨骼。为了匹配姿势,我会将人偶的手臂抬起,确保它们的姿势一致。调整完成后,我可以重定向动画,确保所有动画都适配到Epic骨骼。

清理和组织文件夹

导入和重定向后,我将在项目中创建一个新的“Assets”文件夹,并将所有动画移动到此文件夹中。检查每个动画,确保它们看起来不错,然后清理不需要的文件夹。

小结

现在我们已经添加了一些资产,并成功将动画重新绑定到Epic骨骼。随着这些新资产的引入,我们可以开始开发游戏项目。干得好!我们下次见!

5. 爆破角色

创建角色类

欢迎回来。我们现在准备开始创建我们的角色类。在本视频中,我们将确保为C++类设置文件夹结构,以保持项目的组织性。让我们开始吧。

设置项目结构

这是我们的项目。目前,C++类文件夹下有Blaster游戏模式基类。我们将创建一个新的角色类,可以通过右键点击C++类中的Blaster,选择“新建C++类”来实现。我们将基于“Character”类进行创建,因此选择“Character”并点击“下一步”。

命名角色类

对于这个项目,我将角色命名为“BlasterCharacter”。因为我们会为这个项目创建许多类,所以我希望将它们组织到自己的文件夹中。我将Blaster类放在名为“Character”的文件夹中。你可以选择是否使用公共和私有的文件夹结构,我个人不使用私有文件夹,因此我将保持现状,只添加BlasterCharacter到它自己的角色文件夹中。点击“创建类”。

处理编译错误

由于我们选择将角色类放在自己的角色文件夹中,我们会收到一条消息,提示我们需要重新编译Blaster模块,这是因为出现了自动编译错误。点击“否”,Visual Studio会询问我是否想重新加载项目,点击“重新加载所有”。

现在在项目中,我可以看到Blaster的源文件夹下有一个角色文件夹,里面有BlasterCharacter.h和BlasterCharacter.cpp。在BlasterCharacter.cpp中,我们有一个包含错误,因为它试图从角色文件夹中包含BlasterCharacter.h,但我们的cpp文件已经在角色文件夹中。因此,我们只需在包含语句中去掉“Character”,就能消除错误。现在我们应该能够成功编译。

创建角色蓝图

现在我们成功编译后,可以返回编辑器,并基于新的BlasterCharacter类创建角色蓝图。右键点击内容,创建一个新文件夹,用于我们为这个项目创建的所有蓝图。我将这个文件夹命名为“Blueprints”,然后在蓝图文件夹中创建一个名为“Character”的文件夹。在这里,我将放置角色蓝图。

创建新蓝图时,我将选择基于BlasterCharacter的C++类,命名为“BP_BlasterCharacter”,这样可以明确区分蓝图类和C++类。点击“创建蓝图类”。

配置角色蓝图

现在这是我们的角色蓝图。首先,我可以选择网格,因此我将选择“Mesh”。在骨骼网格中,我将选择来自学习套件游戏的角色。找到相应的角色后,我发现它面朝Y方向,而我希望它面朝X方向。因此,我将其旋转90度。现在它的脚位于胶囊的中心,而我的胶囊半高度为88,所以我可以将网格向下移动88个单位。

设置其Z轴位置为-88,使角色正确放置在地面上。编译并保存。现在我们有了角色类和基于该类的蓝图,并且将该类放在角色文件夹中。我们的C++解决方案已组织好。

下一步

接下来的视频中,我们将添加一个相机和弹簧臂,并添加一些输入以编程实现移动功能。期待在下节课见到你!

6. 相机和弹簧臂

添加相机组件和弹簧臂

欢迎回来。现在我们已经有了角色类和角色蓝图,是时候开始添加组件,包括相机组件和弹簧臂组件。让我们开始吧。

设置角色蓝图

如果我们进入大厅地图并拖入BP_BlasterCharacter,首先我们没有设置游戏模式,因此不会自动占有角色。但是,如果我们选中角色并在详细面板中搜索“Auto Possessed Player”,将其设置为Player Zero,那么当我们点击播放时,就会自动占有这个角色。但目前我们会发现相机几乎位于角色内部,所以我们需要添加相机和弹簧臂来控制视角。

声明变量

现在我们回到C++代码。在BlasterCharacter.h和BlasterCharacter.cpp中,我们将声明几个新变量。在公共部分的构造函数上方,我们有一个第二个公共部分,包含TTIC和设置玩家输入组件的内容。我想将这些公共函数移动到顶部的公共部分,并且我认为这些注释现在可以去掉,因为我们应该都知道这些函数的作用。

我将保留下面的公共部分,以便为各种成员变量保留简单的getter和setter。现在,在私有部分,我想添加弹簧臂和相机。我们将提前声明弹簧臂组件,命名为“CameraBoom”。这需要一个新的属性宏,我将设置为“VisibleAnywhere”,并将其放在“Camera”类别中。

当然,我们还需要实际的相机组件,也将提前声明,命名为“FollowCamera”。同样,将其设置为“VisibleAnywhere”,并放入“Camera”类别中。现在我们已经声明了变量,但我们还需要在构造函数中构建它们。

构造函数实现

在BlasterCharacter的构造函数中,我将构建相机弹簧臂。使用CreateDefaultSubobject来创建一个新的弹簧臂组件,并将其命名为“CameraBoom”。我们使用的是“USpringArmComponent”,因此需要包含相应的头文件。

通常,我们会将相机弹簧臂附加到根组件,但我想将其附加到网格组件,因为稍后我们会使用蹲下功能来改变胶囊的大小。如果我们将弹簧臂附加到胶囊上,当胶囊大小改变时,弹簧臂的位置也会移动。因此,我将使用SetupAttachment将弹簧臂附加到网格上。

接下来,我将设置相机弹簧臂的目标臂长为600,虽然我们可以稍后调整。相机弹簧臂还需要使用控制旋转,因此我将这个选项设置为true,以便在添加鼠标输入时能够跟随控制器旋转。

创建相机组件

现在我们创建相机组件,使用CreateDefaultSubobject来生成“FollowCamera”,同样使用SetupAttachment将其附加到弹簧臂上。使用USpringArmComponent::SocketName来将相机附加到弹簧臂的插座。需要包含相机组件的头文件。

跟随相机不需要使用控制旋转,因为它是附加在弹簧臂上的,所以我将其设置为false。现在编译代码,我们应该能够成功。

调整组件位置

在蓝图中,我们可以看到相机弹簧臂附加到了网格的底部,并且目标臂长是600单位。由于相机可能与地面发生碰撞,我们需要将弹簧臂的位置上移,确保它不是从地面开始附加。我将相机弹簧臂的Z轴位置设置为88单位,这样它将处于角色的中间。

编译并点击播放,现在我可以看到角色与相机之间的正确距离。

准备编程移动功能

当然,目前我们还不能移动,这因为我们尚未编程实现这一功能。这将是我们的下一个步骤。在本讲中,我们添加了相机组件和弹簧臂,并设置了它们的附加关系。我们将弹簧臂附加到网格,而不是根组件(胶囊),这样在蹲下和改变胶囊大小时,不会影响弹簧臂的高度。

我们现在准备编程移动功能,下一次我们将开始这部分工作。期待下次见面!

7. 角色移动

配置角色输入和移动功能

欢迎回来。现在我们有了角色、弹簧臂和相机组件,是时候为项目配置输入并编程实现角色的移动功能了。让我们开始吧。

设置输入映射

我们需要为项目设置一些输入。为此,转到“编辑”>“项目设置”。在这里,我们可以选择“输入”,然后展开我们的“Action Mappings”和“Axis Mappings”。在这里我看到一些输入映射,这是在从学习套件游戏迁移资产时创建的。我将删除项目设置中的所有轴映射,从头开始设置。

创建动作映射

首先,我们想要能够跳跃。因此,我将创建一个跳跃的动作映射,并将其映射到空格键。我可以点击图标,然后按空格键将其链接到跳跃动作映射。

创建轴映射

接下来,我们将添加一些轴映射。我要添加一个名为“Move Forward”的映射,将其映射到W键,同时还想将其映射到S键,缩放设置为-1,以便向后移动。接下来,我们还需要一个“Move Right”映射,将其映射到D键,同时将A键映射到-1,这样我们就可以左右移动。

此外,我们还想使用鼠标来旋转相机。因此,我将添加两个轴映射,一个叫“Turn”,另一个叫“Look Up”,将“Turn”映射到鼠标X轴,将“Look Up”映射到鼠标Y轴。对于“Look Up”,我会将缩放设置为-1,这样当我向前移动鼠标时,视角会向上看,而不是向下。

创建角色移动函数

现在,我们已经设置了这些动作和轴映射,我们可以在角色类中创建一些函数来绑定这些映射。在BlasterCharacter.h中,我们将创建处理移动的函数。我将这些函数放在protected部分,以便以后可能会从子类中访问。

我将创建一个名为MoveForward的函数,接收一个float类型的值,这个值代表轴映射的值。同时,我还会创建一个名为MoveRight的void函数,接收一个float类型的值。对于TurnLookUp,我也会创建类似的函数。

实现移动逻辑

现在,我们有了这些函数定义。在构造函数中,我将调用SetupPlayerInputComponent,将我们的函数绑定到输入映射。对于MoveForward函数,首先检查控制器是否为null以及值是否不为0。

我们将获取控制器的前方方向,而不是使用角色的前向向量。我们将使用控制器的旋转值来确定移动方向。通过创建FRotator和FVector,我们可以获取相应的前方方向向量。最后,我们将调用AddMovementInput函数来实际移动角色。

对于MoveRight函数,我们将重复相同的步骤,但从旋转矩阵中获取右向量。TurnLookUp函数则更简单,直接调用AddControllerYawInputAddControllerPitchInput

绑定输入

现在我们需要将这些移动函数绑定到输入映射中。在SetupPlayerInputComponent中,我们使用BindAxis将每个轴映射与相应的函数连接起来。

此外,我们也会处理跳跃的动作映射,通过BindAction将其与角色的跳跃函数绑定,稍后可能会重写此函数以添加额外功能。

编译并测试

现在编译代码,并回到编辑器中。确保在详细面板中设置Auto Possessed PlayerPlayer Zero,这样当我们点击播放时,就会自动占有角色。我们应该能够控制角色移动,但目前角色是静止的,因为我们还没有使用动画蓝图,因此没有动画姿势。

下一步

在接下来的步骤中,我们将为角色添加动画蓝图,并继续设置角色的基本功能。很高兴我们已经完成了角色的输入和移动配置,期待在下节课中深入了解多人编程的概念。下次见!

8. 动画蓝图

配置角色动画和运动功能

欢迎回来。现在我们有了角色的运动功能,但角色尚未动画化,因此我们需要设置动画实例,这是我们的动画蓝图所基于的C++类。我们需要这个C++类,以便能够从C++访问并设置其属性。

创建动画实例

在我们的项目中,角色的动作很僵硬,因此我们需要设置动画实例。进入C++类,在角色文件夹中右键单击,选择“新建C++类”,选择所有类并搜索“AnimInstance”。选择后,点击“下一步”,并将其命名为“BlasterAnimInstance”。这将放置在角色文件夹中。

我们会收到与创建角色类时相同的警告消息,再次是因为我们将其放在角色文件夹中,因此点击“否”。在Visual Studio中,我找到BlasterAnimInstance和BlasterAnimInstance.cpp。删除路径中的“Character”文件夹,应该能够成功编译。

设置基本变量

我们的动画实例将非常简单,首先设置几个关键变量来驱动基本的未装备动画。我将添加一个公共部分,并重写几个继承的函数。第一个是NativeInitializeAnimation,这类似于BeginPlay,将在游戏开始时调用。接着重写NativeUpdateAnimation,这个函数每帧调用,接收一个float类型的参数DeltaTime。

我们需要确保在调用这些函数时调用超类的版本。接下来,在私有部分添加一些变量。第一个变量将存储Blaster角色的指针,并将其声明为BlasterCharacter,并设置为蓝图只读,以便可以从动画蓝图中访问。

设置运动状态

接下来,我还会添加几个变量来跟踪角色的运动状态,包括速度、是否在空中和是否正在加速。我们将在NativeInitializeAnimation中设置BlasterCharacter的指针,使用TryGetPawnOwner获取当前角色,并将其转换为BlasterCharacter类型。

NativeUpdateAnimation中,每帧更新这些变量,确保在访问BlasterCharacter之前已正确设置它。我们将获取角色的速度,并只关注其水平速度(将Z轴设为0),然后获取加速度并判断角色是否在空中。

创建动画蓝图

现在,我们可以基于这个动画实例创建动画蓝图。编译代码后,进入蓝图文件夹的角色文件夹,创建一个新的动画蓝图,选择BlasterAnimInstance作为父类,并设置Skeleton为Epic Character Skeleton。命名为“BlasterAnimBP”。

在动画蓝图的类设置中,确保将父类设置为BlasterAnimInstance。这样,我们可以在蓝图中访问在C++中声明的变量,例如速度、是否在空中和加速状态。

创建状态机

我们将创建一个状态机来处理未装备状态。添加新的状态机,并命名为“Unequipped”。在状态机中,我们将创建几个状态,包括“Idle/Walk/Run”和“Jump”。在这些状态之间设置转换规则,根据角色是否在空中来决定状态切换。

我们使用的跳跃动画来自学习套件游戏包,命名为“Jump Start”、“Falling”和“Jump Stop”。在“Jump Stop”中,我们将使用时间剩余节点,以确保在动画播放到一定程度后再返回到“Idle/Walk/Run”。

测试动画

在设置好状态机后,回到BP_BlasterCharacter蓝图,选择网格,使用新的动画蓝图。编译并测试游戏,查看角色是否能够正确运动。注意,角色仍然会随控制器旋转,但我们可以通过设置C++中的一些变量来调整这一点。

设置bUseControllerRotationYaw为false,bOrientRotationToMovement为true,以便角色朝向其运动方向而非控制器方向。

结论

在本讲中,我们设置了动画蓝图,并使用BlasterAnimInstance C++类作为父类,这样我们可以从C++中设置变量并在蓝图中访问它们来驱动动画。角色现在能够运行,跳跃,接下来我们将添加更复杂的功能。做得好!期待在下节课见到你!

9. 无缝旅行和大厅

配置无缝旅行

欢迎回来。现在我们已经为项目设置了角色、输入和动画,是时候讨论如何在多人游戏中实现无缝旅行。无缝旅行是虚幻引擎中首选的旅行方式,它可以提供更流畅的体验,因为客户端不需要从服务器断开连接。它还帮助避免重新连接时可能出现的问题,例如无法找到服务器或服务器突然满员导致玩家无法重新加入。

启用无缝旅行

我们可以在游戏模式中启用无缝旅行。游戏模式有一个布尔变量叫做bUseSeamlessTravel,我们可以将其设置为true。为了使用无缝旅行,我们需要有一个过渡地图或过渡关卡。这是一个简单的小关卡,用于在一个地图和另一个地图之间进行过渡。需要过渡地图的原因是,任何时候都必须加载一张地图。为了从一张地图旅行到另一张地图而不使用过渡地图,我们需要在仍然加载第一张地图的情况下加载第二张地图,这会消耗大量资源。因此,我们使用过渡地图,并在拆除原始地图之前加载它。

旅行方式

我们讨论的旅行方式之一是调用ServerTravel。这是一个仅适用于服务器的函数。当服务器调用ServerTravel时,所有连接的客户端将随服务器一起跳转到新的地图。服务器通过获取所有连接的玩家控制器并调用存在于玩家控制器类中的ClientTravel函数来实现。

创建大厅游戏模式

我们希望在创建的大厅游戏模式中使用ServerTravel。当有足够的玩家加入时,我们可以调用ServerTravel,让所有连接的客户端跟随服务器进入游戏地图。我们已经有两个地图:游戏启动地图和大厅地图。当玩家点击托管并创建Steam会话时,最终结果是玩家会旅行到大厅地图等待其他玩家加入。

现在我们需要为大厅创建一个新的游戏模式类。进入C++类,右键点击Blaster,选择“新建C++类”,搜索“GameMode”。我们将使用GameMode,因为它具有GameModeBase所没有的额外功能。选择GameMode并点击“下一步”。

创建游戏模式文件夹

我希望将多个游戏模式保持在自己的文件夹中,因此在Blaster中创建一个名为“GameMode”的新文件夹,并将这个游戏模式命名为“LobbyGameMode”。点击“创建类”。我们将收到与之前相同的消息,点击“否”。现在我们已经有了大厅游戏模式。

计数连接的玩家

大厅游戏模式需要实现的功能是查看有多少玩家连接到了大厅地图。一旦达到一定数量,我们就可以旅行到实际的游戏地图。我们将使用PostLogin函数来跟踪玩家的连接。我们将在大厅游戏模式的头文件中重写这个虚拟函数。

LobbyGameMode.h中创建一个公共部分,重写PostLogin函数。这个函数接收一个指向新玩家的控制器的指针。我们将创建函数定义,调用Super::PostLogin并传入新玩家。

访问游戏状态

游戏模式中存在一个变量叫做GameState,它持有一个GameStateBase类型的对象。GameState中有一个玩家状态的数组。我们将检查这个数组的长度,来判断有多少玩家在大厅中。

使用TObjectPtr来访问游戏状态,通过Get()函数获取GameStateBase指针,随后可以获取玩家数组的长度,存储在一个名为NumberOfPlayers的变量中。

判断玩家数量

我们需要检查NumberOfPlayers的值,如果达到某个数量,就调用ServerTravel函数。现在我们可以回到编辑器,创建一个新的关卡,作为我们要旅行的目标关卡。

创建Blaster地图

在Maps文件夹中,创建一个新的关卡并命名为“BlasterMap”。接着,我们将使用这个关卡作为目标地图,并在大厅游戏模式中调用ServerTravel。当玩家数量达到2时,调用服务器旅行到BlasterMap。

设置过渡地图

我们还需要创建一个过渡地图。创建一个新的空关卡,命名为“TransitionMap”,并保存到Maps文件夹。接下来,确保在项目设置中将这个过渡地图设置为过渡关卡。

测试无缝旅行

现在一切都设置好了。当有足够的玩家加入游戏时,服务器将调用ServerTravel,并使用过渡地图进行旅行。我们可以在测试时检查这一切是否正常工作。

挑战任务

现在我们有多个地图,但它们看起来都很简单。我想给你一个挑战,去美化这些地图。首先,给游戏启动地图添加一些资产,制作一个视觉上吸引人的背景。然后,给大厅地图增加一些细节,使其看起来更有趣。最后,制作BlasterMap,这将是玩家实际进行游戏的地方。加入障碍物、道具、武器等元素。

完成这些后,我们将一起测试游戏。希望你在创建这些关卡时玩得开心!我将在下节课中与大家见面。

10. 网络角色

理解网络角色

欢迎回来。在本讲中,我们将探讨网络角色的概念,以及如何在多人游戏中区分角色的不同实例。每个角色在网络中的表示取决于它在服务器和客户端之间的角色。

网络角色的分类

当你在服务器上运行游戏时,服务器会为每个连接的玩家创建角色的一个实例。如果有多个玩家连接,那么每个客户端机器上也会有该角色的一个版本。例如,在三人游戏中,每台机器上都会有角色的三个副本,因此在代码中区分哪个角色至关重要。

为了处理这一点,虚幻引擎有一个称为角色的概念,存在一个枚举ENetRole,可以用于识别任何给定角色或Pawn的角色。此角色有权威角色(Authority),这是分配给在服务器上存在的Pawn的角色。由于虚幻引擎使用权威服务器模型,我们认为任何存在于服务器上的Pawn具有权威角色。

在此角色中,还有一种称为模拟代理(Simulated Proxy)的角色,这些是存在于不控制该Pawn的机器上的角色版本。当你在自己的机器上控制角色时,该角色的角色为自主代理(Autonomous Proxy)。需要注意的是,在你的机器上和每台其他机器上都存在角色的多个版本。

创建HUD小部件

现在我们回到编辑器,创建一个新的C++类,用于显示在角色头顶上的小部件。进入C++类的Blaster文件夹,右键点击,选择“新建C++类”,选择“UserWidget”作为基类,这样我们就可以使用C++变量驱动用户小部件。将其放入名为“HUD”的文件夹中,命名为“OverheadWidget”。

我们会收到熟悉的消息,点击“否”后,我们就有了新的用户小部件。在C++类中,我们将删除包含中的HUD部分,以消除编译错误。接下来,我们将这个新用户小部件用作显示角色头顶上角色状态的蓝图的父类。

设置小部件

这个小部件将包含一个文本块。我们在公共部分声明一个新的文本块指针,命名为“DisplayText”,用于设置显示的文本。接下来,我们将在蓝图文件夹中创建一个新的HUD文件夹,并在其中右键选择“用户界面”>“小部件蓝图”,命名为“WBP_OverheadWidget”。

在小部件蓝图中,选择文本块,并确保其变量名称与C++中的变量名称相同,这样C++代码才能正确地控制其显示文本。设置完文本块的字体和对齐方式后,我们可以开始为这个小部件添加功能。

处理网络角色

我们将在C++类中重写一个虚拟函数,称为OnLevelRemovedFromWorld,该函数将在过渡到不同关卡时调用,允许我们从视口中移除该小部件。接下来,我们还将创建一个设置文本的函数,名为SetDisplayText,该函数接受一个FString并更新文本块。

此外,我们将创建一个显示玩家网络角色的函数,名为ShowPlayerNetRole。这个函数将获取传入Pawn的本地角色,并使用switch语句设置相应的字符串。最终,我们将调用SetDisplayText函数以更新显示文本。

将小部件添加到角色

接下来,回到BlasterCharacter类,在私有部分添加一个新的WidgetComponent用于存储OverheadWidget。我们将在构造函数中初始化它,并将其附加到根组件。通过将这个小部件设置为可编辑,我们可以在蓝图中进行调整。

测试网络角色

编译并运行游戏,检查小部件是否在角色头顶上正确显示网络角色信息。在编辑器中,可以通过调整小部件的位置来确保它与角色头部对齐。

小结

在本讲中,我们讨论了网络角色的概念,包括权威、自治代理和模拟代理。我们还创建了一个HUD小部件,能够显示角色的网络状态信息。接下来,我们将使用这些知识进一步开发游戏的多人功能。期待在下节课见到你!

WangShuXian6 commented 2 days ago

4. 武器

1. 武器类

创建武器类

欢迎回来。在本讲中,我们将创建武器类,并添加所有武器应具备的基本组件,以及一个武器状态的枚举,以便根据状态正确处理武器。让我们开始吧。

创建武器类

我们需要一个武器类,所以我将右键单击并添加一个新的C++类。我们的武器类可以是一个Actor,因此选择“Actor”,然后点击“下一步”。我希望将武器类放在名为“Weapon”的文件夹中,这样将来派生出更多类时,可以将所有武器类放在这个文件夹里。将类命名为“Weapon”,然后点击“创建类”。

在弹出消息中点击“否”。现在在Visual Studio中,我可以看到我的武器类。我暂时不需要其余的标签,因此右键点击Weapon.h,选择“关闭所有其他标签”,并使用Ctrl + K, O打开Weapon.cpp。由于武器在“Weapon”文件夹中,因此我们不需要包含路径中的“Weapon”部分,将包含更改为仅包含Weapon.h,这样可以修复所有编译错误。

设置武器组件

进入Weapon.h文件,我们有构造函数、BeginPlayTick函数。和角色类一样,我将删除这些注释,因为现在不需要它们。我想添加所有武器应具备的基本组件,因此在私有部分声明这些组件变量。

我将将Tick函数移动到公共部分,放在武器构造函数下方。在私有部分,我需要一个用于武器的网格。我们在资产文件夹中检查“Military Weapons Silver”,发现这些都是骨骼网格,因此我的武器类需要一个骨骼网格组件。

添加骨骼网格组件

我们将添加一个新的骨骼网格组件,命名为WeaponMesh。将此组件设置为VisibleAnywhere,以便可以从蓝图中设置骨骼网格。此外,我们还需要一个重叠体积,以便在角色靠近时能够装备这个武器。我们将提前声明一个USphereComponent,命名为AreaSphere,并设置相同的可见性。

我们将为武器添加更多组件,但目前这些组件已足够。接下来,我们将实现这些组件,并在Weapon.cpp中进行设置。

构造函数实现

在构造函数中,我将首先将bCanEverTick设置为false。如果我们稍后决定需要武器执行Tick,可以随时更改。然后我会去掉不必要的注释。

我们将使用CreateDefaultSubobject构造WeaponMesh,并设置其附加到根组件。为了让武器在掉落时能碰撞地面和墙壁,我将设置其碰撞响应为阻挡。

设置碰撞响应

设置完碰撞响应后,我希望忽略角色的碰撞,以便角色可以穿过武器而不与其发生碰撞。起初,我希望我的武器网格的碰撞被禁用,这样角色就可以走近并拾取它。

在构造函数中将WeaponMesh的碰撞设置为ECollisionEnabled::NoCollision,并在BeginPlay中为服务器启用碰撞。我们还需要添加区域球体的碰撞设置,只在服务器上处理重叠事件。

添加武器状态枚举

在结束之前,我想为武器的状态添加一个枚举。在Weapon.h中,枚举类型称为EWeaponState,并使用uint8作为底层类型。我们将定义几种状态,包括“Initial”、“Equipped”和“Dropped”,以及“Max”的状态,方便我们在代码中跟踪。

创建蓝图

在完成武器类的设置后,我们可以创建一个蓝图,基于新创建的武器类。进入蓝图文件夹,创建一个名为“Weapon”的新文件夹,然后右键创建蓝图类,基于我们的武器C++类,命名为“BP_Weapon”。

在蓝图中,我们可以设置武器的网格,选择相应的资产,比如突击步枪,并调整其位置和碰撞体积。完成后,编译并保存,然后将武器拖入关卡中。

小结

在本讲中,我们创建了武器类并添加了一些组件,以及一个武器状态的枚举,以便根据当前状态处理武器。下一步,我们将显示一个拾取小部件,提示玩家可以拾取武器。期待在下节课中见到你!

2. 拾取小部件

创建武器拾取小部件

欢迎回来。在本讲中,我们将为我们的武器类制作一个拾取小部件。这将使我们能够知道何时可以拾取武器,然后根据武器状态决定何时设置此拾取小部件的可见性。

设置拾取小部件

首先,我将进入我的HUD文件夹,右键点击,选择“用户界面”,然后选择“小部件蓝图”。我将这个小部件命名为WBP_PickUpWidget。这个小部件很简单,只需添加一个文本块,告知玩家可以拾取武器。因此,我会删除画布面板,直接添加一个文本块,将其命名为“PickUpText”。文本内容设置为“E - Pick Up”,这将让玩家知道可以使用E键拾取武器。可以将字体大小增大到48,并调整文本小部件的大小,同时将字体样式从粗体改为常规,并将文本对齐方式设置为居中。

编译并保存后,我接下来要在武器类中添加一个小部件组件。回到Weapon.h文件中,我将为拾取小部件添加一个私有变量,提前声明一个新的小部件组件指针,命名为PickupWidget。我将使其可以在任何地方编辑,并将其放入“Weapon Properties”类别中。由于其他变量的类别也是“Weapon Properties”,我也将WeaponState变量的类别设置为“Weapon Properties”。

构造小部件组件

接下来,我们需要在Weapon.cpp中构造这个小部件组件。滚动到构造函数的底部,使用CreateDefaultSubobject创建一个新的小部件组件,命名为PickupWidget,并将其附加到根组件。确保包含WidgetComponent.h的头文件。

编译后,回到编辑器,打开BP_Weapon蓝图。在这里,我将设置拾取小部件的空间为“Screen Space”,并选择小部件类为BP_PickUpWidget,并勾选“Draw at Desired Size”。在视口中,将拾取小部件向上移动,以确保它位于武器网格的上方。

显示和隐藏小部件

现在我们需要决定何时显示和隐藏这个小部件。我希望当角色与区域球体重叠时,显示这个小部件。同时,我不再需要在角色上显示“Overhead Widget”。不过,我会保留“Overhead Widget”,因为我们稍后可能会用它来显示角色的名称。

接下来,我们需要为区域球体设置重叠事件,以便能够显示和隐藏小部件。我将在protected部分添加一个虚拟函数,命名为OnSphereOverlap。这是一个重载函数,输入参数包括重叠的组件、其他角色、其他组件、其他身体索引、是否从扫掠中产生的碰撞,以及一个接触结果。

绑定重叠事件

我们将绑定此重叠函数到区域球体的“OnComponentBeginOverlap”事件。在Weapon.cpp中,我们将在HasAuthority检查内部绑定此重叠函数。使用AreaSphere->OnComponentBeginOverlap.AddDynamic来绑定重叠事件。

OnSphereOverlap函数中,我们将检查OtherActor是否为BlasterCharacter,如果是,则设置拾取小部件的可见性为true。确保在设置可见性之前检查PickupWidget是否为有效指针。

初始化小部件

BeginPlay中,我们将确保拾取小部件的初始可见性为false。现在编译代码并测试。运行游戏,使用角色靠近武器,查看拾取小部件是否在服务器和客户端上正确显示。

调试可见性

在测试中,我们发现服务器上可见,但客户端上不可见。这是因为我们仅在服务器上处理重叠事件。我们需要确保在客户端也能看到这个小部件。为此,我们需要引入复制(Replication),以便将可见性状态从服务器同步到客户端。

小结

在本讲中,我们添加了一个拾取小部件到武器类,并创建了一个重叠函数,将其绑定到区域球体的重叠事件。现在服务器控制着小部件的可见性,下一步将是实现变量的复制,从而让客户端知道何时需要显示拾取小部件。干得好!期待下次见到你!

3. 变量复制

理解变量复制和网络交互

欢迎回来。在本讲中,我们将深入探讨变量的复制,以及如何通过网络处理武器的交互。我们将实现复制功能,以便正确处理玩家与武器之间的交互。

创建复制函数

首先,我们需要在BlasterCharacter类中设置复制。在函数定义中创建GetLifetimeReplicatedProps,这是用于注册变量以供复制的函数。将此函数定义移动到构造函数下方,并在函数内部调用Super::GetLifetimeReplicatedProps,将OutLifetimeProps作为输入参数传递。

注册重叠武器变量

在这个函数内部,我们需要注册我们的OverlappingWeapon变量以供复制。使用宏DOREPLIFETIME,并指定BlasterCharacter类以及我们要复制的变量OverlappingWeapon。需要注意的是,我们将使用#include "Net/UnrealNetwork.h"来定义该宏。

这样,OverlappingWeapon变量将初始为未初始化,直到我们在武器类中设置它。

创建Setter函数

为了在武器类中设置重叠武器,我们需要在BlasterCharacter中添加一个公共的Setter函数。这个函数命名为SetOverlappingWeapon,接受一个指向Weapon的指针。我们将在该函数中将OverlappingWeapon设置为传入的指针。

只要OverlappingWeapon的值发生变化,它就会进行复制,这样客户端的BlasterCharacter上也会更新这个变量。

处理重叠事件

在武器类的OnSphereOverlap函数中,我们将获取BlasterCharacter的实例,并使用我们的Setter函数来设置重叠武器。这意味着我们将不再检查PickupWidget是否有效,而是直接设置OverlappingWeapon

在BlasterCharacter类中,我们需要决定何时显示这个小部件。为此,我们将在武器类中创建一个显示小部件的函数,命名为ShowPickupWidget,接受一个布尔参数bShowWidget,用于决定是否显示或隐藏小部件。

调整Tick函数

在BlasterCharacter的Tick函数中,我们将检查OverlappingWeapon是否有效。如果有效,我们调用ShowPickupWidget函数来显示小部件。需要注意的是,这种方法并不高效,因为我们在每帧都进行检查。

编译与测试

编译代码,并在编辑器中测试这个实现。通过控制客户端角色接近武器,验证小部件的显示与隐藏是否正常工作。注意,由于复制是从服务器到客户端的,因此我们需要确保小部件仅在拥有角色的客户端上显示。

引入Rep Notify

为了优化我们的实现,我们将使用Rep Notify机制。当OverlappingWeapon变量发生复制时,Rep Notify函数会被调用。我们可以创建一个命名为OnRep_OverlappingWeapon的函数,以便在变量复制时执行。

在BlasterCharacter中,我们将设置OnRep_OverlappingWeapon来处理何时显示或隐藏小部件。注意,Rep Notify不会在服务器上调用,只会在客户端上调用,这样可以确保小部件仅在需要时显示。

完成复制设置

完成上述设置后,我们可以对代码进行整理,确保逻辑清晰。在测试中,观察到在服务器上显示的Pickup Widget只在客户端上可见。这样,只有拥有角色的客户端会看到武器的拾取提示。

小结

在本讲中,我们学习了变量复制的机制,如何通过Rep Notify函数来管理变量的复制,以及如何优化小部件的显示和隐藏。我们还实现了一个系统,使得在角色与武器交互时,只有特定的客户端会看到相应的小部件提示。期待在下一节课中继续深入!

4. 装备武器

5. 远程程序调用

6. 装备动画姿势

7. 蹲伏

8. 瞄准

9. 运行混合空间

10. 倾斜和侧移

11. 空闲和跳跃

12. 蹲行

13. 瞄准行走

14. 瞄准偏移

15. 应用瞄准偏移

16. 多人游戏中的俯仰

17. 使用我们的瞄准偏移

18. FABRIK IK

19. 转向

20. 旋转根骨

21. 网络更新频率

22. 未装备时蹲伏

23. 旋转跑步动画

24. 脚步和跳跃声音

WangShuXian6 commented 2 days ago

5. 发射武器

1. 投射武器类

2. 发射蒙太奇

3. 发射武器效果

4. 多人游戏中的发射效果

5. 命中目标

6. 生成投射物

7. 投射物移动组件

8. 投射物曳光

9. 复制命中目标

10. 投射物命中事件

11. 子弹壳

12. 壳体物理

WangShuXian6 commented 2 days ago

6. 武器瞄准机制

1. 爆破HUD和玩家控制器

2. 绘制十字准星

3. 准星扩散

4. 纠正武器旋转

5. 瞄准时放大

6. 瞄准时缩小准星

7. 改变准星颜色

8. 延长痕迹起点

9. 命中角色

10. 为代理平滑旋转

11. 自动射击

12. 测试游戏

WangShuXian6 commented 2 days ago

7. 健康和玩家统计

1. 游戏框架

2. 健康

3. 在HUD中更新健康

4. 伤害

5. 爆破游戏模式

6. 消灭动画

7. 重生

8. 溶解材料

9. 溶解角色

10. 使用曲线溶解

11. 消灭时禁用移动

12. 消灭机器人

13. 占有

14. 爆破玩家状态

15. 失败

WangShuXian6 commented 2 days ago

8. 弹药

1. 武器弹药

2. 能否开火

3. 携带弹药

4. 显示携带弹药

5. 重新装填

6. 重新装填战斗状态

7. 允许开火

8. 更新弹药

9. 重新装填效果

10. 自动重新装填

WangShuXian6 commented 2 days ago

9. 比赛状态

1. 游戏计时器

2. 同步客户端和服务器时间

3. 比赛状态

4. 设置比赛状态

5. 热身计时器

6. 更新热身时间

7. 自定义比赛状态

8. 冷却公告

9. 重启游戏

10. 爆破游戏状态

WangShuXian6 commented 2 days ago

10. 不同的武器类型

1. 火箭弹

2. 火箭轨迹

3. 生成火箭轨迹

4. 火箭移动组件

5. 扫描射击武器

6. 光束粒子

7. 冲锋枪

8. 带子物理

9. 霰弹枪

10. 武器散射

11. 狙击步枪

12. 狙击瞄准镜

13. 榴弹发射器

14. 投射榴弹

15. 重新装填动画

16. 霰弹枪重新装填

17. 武器轮廓效果

18. 投掷榴弹蒙太奇

19. 投掷榴弹时的武器附着

20. 榴弹资产

21. 显示附着的榴弹

22. 生成榴弹

23. 多人游戏中的榴弹

24. HUD中的榴弹

WangShuXian6 commented 2 days ago

11. 拾取

1. 拾取类

2. 弹药拾取

3. 增益组件

4. 健康拾取

5. 治疗角色

6. 速度增益

7. 跳跃增益

8. 护盾条

9. 更新护盾

10. 护盾增益

11. 拾取生成点

12. 添加生成点到等级

13. 生成默认武器

14. 副武器

15. 交换武器

16. 丢弃副武器

WangShuXian6 commented 2 days ago

12. 延迟补偿

1. 延迟补偿概念

2. 高ping警告

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. 作弊