Unreal为例的游戏模式学习

读前须知

​ 在学习这篇笔记之前需要先搭好Unreal Engine的环境,最好是源码版的可以跟着源码一起学习。如果没有相关的环境可以看一下我的这一篇笔记:UE5引擎源码版编译和Windows版本打包

附:

​ 本笔记也作为我本人的课程作业的一种提交方式。可以直接点击:“作业部分”,跳转到对应的章节。

什么是游戏模式:

​ 游戏模式是游戏世界里面组织数据和运作规则的方式,例如:

  1. 这些物体的共同点,不同点,怎么抽象?
  2. 世间万物以什么规则运行?
  3. 数据如何组织、描述?(就是想要变得内容以什么数据结构进行描述和组织?)

这些都是游戏模式所要思考设计的内容。

UE的Gameplay框架:

​ UE中的Gameplay框架包括核心系统和用于处理通用Gameplay元素的框架,如Actor、摄像机、组件、控制器、游戏规则、游戏模式、玩家输入、Gameplay定时器和用户界面。

UE的万物之源:UObject

参考:Unreal Engine C++ API Reference

Module CoreUObject
Header /Engine/Source/Runtime/CoreUObject/Public/UObject/Object.h
Include #include “UObject/Object.h”
Source /Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectGlobals.cpp
1
2
3
4
UObject  
(
const FObjectInitializer & ObjectInitializer
)

​ 其中的特性有:

  • 元数据、反射生成、GC垃圾回收、序列化
  • 通⽤属性和接⼝(Equals、Clone、GetHashCode、ToString、GetName、GetMetaData
    等等)
  • 每个物体是由原⼦构成的⸺uobject=原⼦

物体的表达:Actor+ActorComponent

​ 所有可以放入关卡的对象都是 Actor,比如摄像机、静态网格体、玩家起始位置。Actor支持三维变换,例如平移、旋转和缩放。你可以通过游戏逻辑代码(C++或蓝图)创建(生成)或销毁Actor。

在C++中,AActor是所有Actor的基类。

注意:Actor不直接保存变换(位置、旋转和缩放)数据;如Actor的根组件存在,则使用它的变换数据。

​ Components是一种特殊类型的对象,Actor可以将其作为子对象附加到自己身上。Components对于共享公共行为非常有用,例如显示可视表示、播放声音等功能。它们还可以表示特定于项目的概念,例如车辆解释输入并改变自身速度和方向的方式。例如,一个包含用户可控制的汽车、飞机和船只的项目可以通过改变车辆Actor使用的组件来实现车辆控制和移动的差异。

To be precise : Components are a special type of Object that Actors can attach to themselves as sub-objects. Components are useful for sharing common behaviors, such as the ability to display a visual representation, play sounds. They can also represent project-specific concepts, such as the way a vehicle interprets input and changes its own velocity and orientation. For example, a project with user-controllable cars, aircraft, and boats could implement the differences in vehicle control and movement by changing which Component a vehicle Actor uses.

  • EC架构(Entity-Component Framework):⼀个实体和多种能⼒组合的设计模式。
    • 就像是⼀款即插即⽤,有了设备(Actor),插⼊设备(Component)就能⽤。
  • SceneComponent赋予Actor空间变化信息:FTransform: Location, Rotation, Scale
  • 舞台上的演员(Actor),各⾃⾝怀绝技(Component),为玩家上演⼀场精彩的游戏。

世界的表达:UWorld + ULevel

  • 平⾏世界:GameWorld、PIEWorld(编辑器世界)、PreviewWorld(预览)

  • 关卡构成主⼲卡PersistentLevel+⼦关卡

  • 关卡加载LevelStreaming流式异步加载

    1. WorldPartition(UE5)
      1. 分成了许多layer(像是切割了许多⽔平⾯,位于不同的⽔平⾯上就会加载加载不同的范围)
      2. 每个layer会设置不同的加载范围,在范围内的就会加载
    2. WorldComposition
    3. LoadByLogic
  • 关卡⼤⼩和加载距离

    • LevelBounds + StreamingDistance分层
    • 有多少个关卡,最⼤的覆盖的那个盒⼦
  • 关卡蓝图LevelScriptActor

    • 定义关卡规则⸺⽐如进⼊该关卡速度变慢之类的

世界之上⸺UGameInstance + UEngine

  • UGameInstance

    • 信息存在于整个游戏的⽣命周期,不随着地图的切换和销毁
    • ⾮常适合⾮业务逻辑的全局管理操作,如全局UI、设置、预加载
  • UEngine

    • 管理GameInstance
    • 拉起游戏重要流程
    • Browse、LoadMap、SetClientTrave…
  • UE游戏拉起流程

    • init–>start–>loadMap
    • loadMap中加载了许多内容,⽐如world、模型、player之类的

UE游戏模式中的重要对象

AActor:游戏中最重要的实体

  • 根组件提供世界变化信息
  • 作为⽹络同步的基础单位
  • 标志所有权的Owner指针
    • 通过owner层层追溯,发射⼦弹伤害了某个敌⼈,追溯是谁伤害了敌⼈:⼦弹⸺枪⸺⻆⾊
  • 标志本地权限的Role枚举
    • 权威端,你说了算,服务器
    • 主控端,本机
    • 模拟端,看到的别的玩家的游玩
  • ⽣命周期
    • 分类
    • 关卡内摆放的静态Actor
      • 从map⾥资源⾥拜访来的
      • 最终还是会回到initially component⾥
  • SpawnActor创建的动态Actor
    1. 本地Spawn
    2. ⽹络序列化
  • 重要的⽣命周期函数
    • BeginPlay
    • EndPlay
    • Tick
  • 重要的⽣命周期函数
    • BeginPlay◦
    • Tick
    • EndPlay:在某些条件达成之后就误了
      • 处理结束之后怎么做
    • GC完成收尾⼯作
      • 注意有效性的判断
        • 从level的数组⾥丢出去,⽆⼈认领
        • 最后被GC回收了

APawn:例如国际象棋中的小兵

The Pawn class is the base class of all Actors that can be controlled by players or AI. A Pawn is the physical representation of a player or AI entity within the world. This not only means that the Pawn determines what the player or AI entity looks like visually, but also how it interacts with the world in terms of collisions and other physical interactions. This can be confusing in certain circumstances as some types of games may not have a visible player mesh or avatar within the game. Regardless, the Pawn still represents the physical location, rotation, etc. of a player or entity within the game. A Character is a special type of Pawn that has the ability to walk around.

By default, there is a one-to-one relationship between Controllers and Pawns; meaning, each Controller controls only one Pawn at any given time. Also, Pawns spawned during gameplay are not automatically possessed by a Controller.

Pawn类是所有可以由玩家或AI控制的Actor的基类。棋子是玩家或AI实体在世界中的物理表示。这不仅意味着Pawn决定了玩家或AI实体在视觉上的样子,而且还决定了它在碰撞和其他物理交互方面如何与世界互动。这在某些情况下可能会令人困惑,因为某些类型的游戏可能在游戏内没有可见的玩家网格或化身。无论如何,棋子仍然代表游戏中玩家或实体的物理位置,旋转等。角色是一种特殊类型的棋子,有能力四处走动。

默认情况下,控制器和兵之间是一对一的关系;这意味着,每个控制器在任何给定时间只能控制一个兵。此外,游戏过程中产生的棋子不会自动被控制器拥有。

  • 可操控的
  • 多种多样的形式
    • ⻋⼦、机甲(载具)
  • 被controller控制
  • 基础的输⼊、移动框架的⽀持
    • ⽣产者消费者模型框架
  • 常⽤派⽣类
    • ADefaultPawn
      • 简单球形碰撞 USphereComponent
      • 简单外显 UStaticMeshComponent
      • 简单移动组件 UFloatingPawnMovement
      • 基础的键盘、⼿柄映射
  • ASpectatorPawn
    • 去掉外显 UStaticMeshComponent,不应该被别⼈看到
    • 移动组件替换成忽略时间缩放的USpectatorPawnMovement
      • 观战的时候可以把全局暂停

ACharacter 人型角色

  • 近似仿真⼈形的胶囊体碰撞盒UCapsuleComponent
    • 在保证⼀定真实性的同时,节约性能
    • 缺点:⽐如脚已经离地但是仍然没掉下去,因为胶囊体还没离开
  • ⻣骼模型USkeletalMeshComponent
    • ⽐如射击的时候要知道打到的是头还是脚
    • 动画蓝图赋予⼈物⽣动表现
  • ⼈物移动组件UCharacterMovementComponent
    1. 配合胶囊体完成Walking\Falling\Swimming\Flying等多种仿真移动计算
    2. 提供Custom⾃定义移动模式供扩展
    3. ⽹络游戏移动同步架构
      1. 主控端预表现
      2. 服务器端校验
      3. 模拟端预测
        1. match进⾏⼀个差值,减少位置的突变,移动不是⼀帧⼀帧的

AController和APawn的双向奔赴

  • 通过Possess和PossessedBy你就是个有主的Pawn了。
  • Controller、PlayerState指针赋值
  • 网络游戏中Role的改变

AplayerController

可以理解为提线木偶的操控者

  • UInputComponent

​ 绑定输入映射

  • APlayerCameraManager

​ 通过ViewTarget上相机臂作⽤后的UCameraComponent计算相机位置

  • AHUD (heads-up display) 头显

​ 注意和UI的区别,逐渐被更灵活的UMG取代

  • ⽹络连接所有权

​ 注意仅在主控端及服务器存在PlayerController

AGameMode

这个是真的游戏模式

  • 仅服务器拥有,掌控整体游戏流程
  • 定义游戏模式⽤的基础类型
  • 纯服务器逻辑的操作,如AI
  • AGameMode和AGameModeBase区别
    • AGameModeBase,这是所有GameMode的基类,是经典的AGameMode简化版本。
    • AGameMode是AGameModeBase的⼦类。AGameMode更适⽤于标准对抗类游戏(如多⼈射击游戏),完善了对局和⽐赛的概念。

AGameState 游戏状态

  • 所有端都共享同步的游戏数据
  • AGameState和AGameStateBase的区别。
  • 类似AGameMode和AGameModeBase的关系。
  • AGameState是AGameStateBase的⼦类。
  • AGameMode更适⽤于标准对抗类游戏(如多⼈射击游戏),完善了对局和⽐赛的概念。

APlayerState玩家状态

  • 所有端都共享同步的游戏数据
  • PlayerState、Character、Controller的职责区别

作业部分:

作业是在第一人称设计模板项目中实现以下功能:

一、物件规则:

  • 设计命中方块,获得积分X分
  • 方块被子弹命中后缩放为Y倍,再次被命中后销毁

二、游戏流程:

  1. 游戏开始时随机N个方块成为“重要目标”,射击命中后获得双倍积分。
  2. 游戏开始后限时T秒,时间到后游戏结算,打印日志输出每个玩家获得的积分和所有玩家获得的总积分

三、附加题

  1. 利用UMG制作结算UI替代日志打印
  2. 支持多人联机

(目前还不太懂UE的C++的代码规范和继承调用结构,就先用蓝图做一做😭)

效果视频

如果您不想看细节那么可以直接看我的效果视频:

一、物件规则:

​ 首先对于“设计命中方块,获得积分X分;方块被子弹命中后缩放为Y倍,再次被命中后销毁”我做以下操作:

  1. ​ 为了实现这个操作,那么我需要先在我的Character中设置一个变量:例如我是在BP_FirstPersonCharacter中进行设置,添加变量Score,类型为Integer。并且将其设置为Public,默认值设置为0。这表示我目前这个角色所得的所有积分。

image-20241129101113865

image-20241129101310590

  1. ​ 其次我们需要对于我们要射击的方块针对性的进行一下小小的修改。我们可以先创建一个继承于Actor的蓝图IBox。对于该类我们,首先我们先改一下材质,来和地图中的其他的方块从外形上来进行分割。这个材质的外形如下面所示:

image-20241129102444784

  1. ​ 然后给该蓝图设置:可以被命中,并且产生碰撞。并且设置以下的变量:
  • 缩放倍数Y,float型。默认设置为0.5

0.5表示第一次被击中后该方块会进行缩放,变成原来的0.5倍

  • hitcount,Integer型,默认值设置为0

该变量可以进行记录我们的该方块的被击打次数。默认被击打次数为0

  • 方块得分X,Integer型,默认值为10

表示我们如果命中该方块,那么命中该方块的角色的得分Score会进行增加X分,此时若要实现多人联机模式,那么就需要对于产生EventHit的“子弹”进行溯源找到对应的Charactor。

  • IsImportant,Boolean型,默认值为False,设置为Public。

表示该目标实例是否是重要目标,若击中重要目标那么就会产生双倍得分并且这一变量可以在外部调用和编辑,以便我们可以在游戏开始的时候对于地图中的重要目标进行初始化,来随机选取特定个方块设置为重要目标

IBox中变量的全家福如下:

image-20241129105252722

  1. ​ 首先在IBox的蓝图中,我们先生成一个EventHit事件检查碰撞,并且检查碰撞的Other对象是否为子弹实例,如果是那么就找到对应的射击角色然后进入流程并且接入判断本实例是否是important实例的Branch:

image-20241129141320002

  1. ​ 然后根据本目标是否是重要目标进行加分操作,如果是重要目标那么就对对应的Charactor的Score变量加上双倍的对应得分,如果不是那么就加上单倍得分。

image-20241129141624694

  1. ​ 之后再对本实例中的hitcount变量进行加一操作并且每次加一结束后要进行判断,判断该物体的受打击次数并且转跳到对应的分支。

image-20241129141911259

  1. ​ 最后根据被击中的次数来对箱子产生销毁或者缩放操作。

image-20241129142027582

  1. ​ 同时我也对于BP_FirstPersonProjectile类进行了一些修改。关闭了子弹的“抛射物反弹”功能和“重力功能”。还有就是碰撞检测到后即可销毁等一些操作。

至此物件规则这一部分就已经完成了。

二、游戏流程

  1. 游戏开始时随机N个方块成为“重要目标”,射击命中后获得双倍积分。
  2. 游戏开始后限时T秒,时间到后游戏结算,打印日志输出每个玩家获得的积分和所有玩家获得的总积分

​ 对于该目标我需要对于游戏的GameMode蓝图进行操作,并且在其中添加一些规则。

为了实现以上的功能我在BP_FirstPersonGameMode中设置了以下变量:

image-20241129143528584

其中GameTimeLimit指定了游戏的限时规则。在蓝图中这样子显示

image-20241129143733448

其中OnGameTimerTick函数我做出的设计如下:

image-20241129143847788

其中的末尾函数OnGameEnd函数我的设计如下,就是简单的打印输入总积分。

这里没有设置UI和跳出是因为我个人学校课业压力和科研压力和时间紧迫的原因,(更多是因为我自己菜😭)

image-20241129144158763

之后在主逻辑蓝图中取出所有的实例并且对于其中的实例循环N次找出其中的N个随机的重要目标并进行相关操作:

image-20241129145226565

image-20241129145401201

至此所有的主逻辑设计都完成了。

三、附加:简单UI设计

设计结果如下图所示:

image-20241129150902463

其中两个TextBlock函数分别绑定:Score(位于Charactor)、CurrentGameTime(位于GameMode)两个变量。

总结下来还是比较简陋的完成了作业。

演示图片:

以下为场景摆放

image-20241129151556625

开始游戏后材质变成草皮的方块就是被系统选定的重要目标。

image-20241129151647094

当第一次击中目标后方块会产生缩放效果

image-20241129151928706

第二次击中后会销毁方块(这里没抓住timing就没截到图)建议还是查看录屏。