我的世界,我做主

引言

上文我们说到在Actor层次,UE用Controller来充当APawn的逻辑控制者,也有了可以接受玩家输入的PlayerController,和能自行行动的AIController。Actor的逻辑编写介绍完了,那么本篇,我们继续爬升,对于由Actors组成的Level这一层次,UE又是怎么控制的呢?

对Level记不太清楚的朋友,可以翻回去查看“GamePlay架构(二)Level和World”的讲述,简单概括就是World是由一个PersisitentLevel和一些subLevels组成的,PersisitentLevel切换了,相应的World也会切换。所以本文的关注点是在这么一个对象层次结构下,UE是怎么设计的,我们又能做些什么。

GameMode

Level,在游戏里的概念里,就是关卡的意思。同时作为游戏的玩家和开发者,我们总是会非常经常的提起关卡,但是关卡具体又是个什么定义呢?游戏里的哪些部分可以算是一个关卡?简单的我们都知道有《愤怒的小鸟》或《植物大战僵尸》的关卡,复杂的有大型FPS游戏里的关卡,而对于更大型的《暗黑3》或者大型无缝地图RPG游戏《巫师3》,甚至是号称超级广阔宇宙《无人深空》,我们能直接了当的说出哪部分是关卡吗?游戏行业发展如今,为了更好的组织游戏逻辑和内容资源,也发展出了一些概念来更好的理解和阐述,虽然叫法不同,不过含义理念都是相通的。比如,Cocos2dx会认为游戏就是由不同的Scene切换组成的,每个Scene又由Layer组成;Unity也认为游戏就是一个个Scene;而UE的视角的是,游戏是由一个个World组成的,World又是由Level组成的。这些概念有什么不同?

让我们从游戏本身的机制上分析:

  • 游戏或玩家的节奏,游戏可以分成一个个阶段,马里奥里的关卡就是一个阶段,而RPG游戏的一个大地图也是一个阶段。一个游戏也可能只有一个阶段,比如一直在宇宙里漫游的游戏。通常一个阶段结束后,会有一个结算,阶段之间,玩家也能明显感觉到切换感。
  • 游戏的机制,有时候即使是同样的场景,玩家却也能感觉就像在玩两个不同的游戏,比如MOBA里的同一张地图上的各种不同挑战模式。
  • 游戏的资源划分,有时候也能遇见同一个玩法应用在不同的场景上,比如赛车游戏的不同跑道。有时候也会在游戏的大地图里从酷热的沙漠到寒冷的极地。游戏开发中也总是倾向于给游戏用到的资源划分成组的进行载入和释放。

通过以上的分析,也和以前的一贯思路一样,我们发现在思考“关卡”这件事情上,也是要保持头脑清晰的分清“表示”和“逻辑”。玩法就是“逻辑”,场景就是“表示”。所以我们如果以逻辑来划分游戏,得到的就是一个个World的概念;如果以表示来划分,得到就是一个个Level。一场游戏中,玩法再复杂但也只有一个,场景却可以无限大,所以可以有很多个表示拼接组装,因此是World包含Level,而不是反过来。现在回过头来回想一下Cocos2dx和Unity的世界观,它们的概念还只是在表示层,在游戏实例和关卡之间少了一个更高级的逻辑概念。

因此UE的世界观是,World更多是逻辑的概念,而Level是资源场景表示。以《巫师3》为例,有好几个国家之间通过传送切换,国家内大地图无缝漫游,显然我们知道不可能把一个国家的所有资源都加载进内存,因此在UE里,一个国家就是许多个Level拼接的,而一个国家就是一个World,它们可以有不同的模式玩法。但毕竟AAA游戏很少,通常的,我们的游戏比较简单的用一个Level就够了,否则这个场景表示的概念就应该叫Area更合适了,也因此通常的这里的Level也常常对应游戏里玩家面对的"关卡",也因此UE里Level的Settings叫做WorldSettings了。

厘清了这些概念了之后,我们就知道,当我们在谈Level的业务逻辑控制的时候,我们实际上谈的是World的业务逻辑。按照UE的设计理念和经过Controller的经历,我想我也不用多解释了从Actor再派生出一个WorldController的方式了,可以直接的享受Actor已经提供的一切福利。一个World的Controller想不出有什么需要展示渲染的,因此可以直接从AInfo派生吧。哦,WorldController是我瞎编的,在UE3里它叫做GameInfo,到了UE4它改名为了GameMode。笼统的讲,一个World就是一个Game,把玩法叫做Mode,我们应该也能接受吧。那我们来看看它:



既然勇敢的承担了游戏逻辑的职责,说他是AInfo家族里的扛把子也不为过,因此GameMode身为一场游戏的唯一逻辑操纵者身兼重任,在功能实现上有许多的接口,但主要可以分为以下几大块:

  1. Class登记,GameMode里登记了游戏里基本需要的类型信息,在需要的时候通过UClass的反射可以自动Spawn出相应的对象来添加进关卡中。前文说过的Controller的类型登记也是在此,GameMode就是比Controller更高一级的领导。

  2. 游戏内实体的Spawn,不光登记,GameMode既然作为一场游戏的主要负责人,那么游戏的加载释放过程中涉及到的实体的产生,包括玩家Pawn和PlayerController,AIController也都是由GameMode负责。最主要的SpawnDefaultPawnFor、SpawnPlayerController、ShouldSpawnAtStartSpot这一系列函数都是在接管玩家实体的生成和释放,玩家进入该游戏的过程叫做Login(和服务器统一),也控制进来后在什么位置,等等这些实体管理的工作。GameMode也控制着本场游戏支持的玩家、旁观者和AI实体的数目。

  3. 游戏的进度,一个游戏支不支持暂停,怎么重启等这些涉及到游戏内状态的操作也都是GameMode的工作之一,SetPause、ResartPlayer等函数可以控制相应逻辑。

  4. Level的切换,或者说World的切换更加合适,GameMode也决定了刚进入一场游戏的时候是否应该开始播放开场动画(cinematic),也决定了当要切换到下一个关卡时是否要bUseSeamlessTravel,一旦开启后,你可以重载GameMode和PlayerController的GetSeamlessTravelActorList方法和GetSeamlessTravelActorList来指定哪些Actors不被释放而进入下一个World的Level。

  5. 多人游戏的步调同步,在多人游戏的时候,我们常常需要等所有加入的玩家连上之后,载入地图完毕后才能一起开始逻辑。因此UE提供了一个MatchState来指定一场游戏运行的状态,意义看名称也是不言自明的,就是用了一个状态机来标记开始和结束的状态,并触发各种回调。

    /** Possible state of the current match, where a match is all the gameplay that happens on a single map */
    namespace MatchState
    {
    extern ENGINE_API const FName EnteringMap; // We are entering this map, actors are not yet ticking
    extern ENGINE_API const FName WaitingToStart; // Actors are ticking, but the match has not yet started
    extern ENGINE_API const FName InProgress; // Normal gameplay is occurring. Specific games will have their own state machine inside this state
    extern ENGINE_API const FName WaitingPostMatch; // Match has ended so we aren't accepting new players, but actors are still ticking
    extern ENGINE_API const FName LeavingMap; // We are transitioning out of the map to another location
    extern ENGINE_API const FName Aborted; // Match has failed due to network issues or other problems, cannot continue
    }

思考:多个Level配置不同的GameMode时采用的是哪一个GameMode?

我们知道除了配置全局的GameModeClass之外,我们还能为每个Level单独的配置不同的GameModeClass。但是当一个World由多个Level组成的时候,这样就相当于配置了多个GameModeClass,那么应用的是哪一个?首先第一个原则需要记住的就是,一个World里只会有一个GameMode实例,否则肯定乱套了。因此当有多个Level的时候,一定是PersisitentLevel和多个StreamingLevel,这时就算它们配置了不同的GameModeClass,UE也只会为第一次创建World时加载PersisitentLevel的时候创建GameMode,在后续的LoadStreamingLevel时候,并不会再动态创建出别的GameMode,所以GameMode从始至终只有一个,PersisitentLevel的那个。

思考:Level迁移时GameMode是否保持一致?

在在travelling的时候,如果下一个Level的配置的GameModeClass和当前的不同,那么迁移后是哪个GameMode?

无论travelling采用哪种方式,当前的World都会被释放掉,然后加载创建新的World。但这个过程中,有点区别的是根据bUseSeamlessTravel的不同,UE可以选择哪些Actor迁移到下一个World中去(实现方式是先创建个中间过渡World进行二段迁移(为了避免同时加载进两个大地图撑爆内存),具体见引用3)。分两种情况:

不开启bUseSeamlessTravel,那么在travelling的时候(ServerTravel或ClientTravel),当前的World会被释放,所以当前的GameMode就被释放掉。新的World加载,就会根据新的GameModeClass创建新的GameMode。所以这时是不同的。

开启bUseSeamlessTravel,travelling时,当前World的GameMode会调用GetSeamlessTravelActorList:

void AGameMode::GetSeamlessTravelActorList(bool bToTransition, TArray<AActor*>& ActorList)
{
UWorld* World = GetWorld(); // Get allocations for the elements we're going to add handled in one go
const int32 ActorsToAddCount = World->GameState->PlayerArray.Num() + (bToTransition ? 3 : 0);
ActorList.Reserve(ActorsToAddCount); // always keep PlayerStates, so that after we restart we can keep players on the same team, etc
ActorList.Append(World->GameState->PlayerArray); if (bToTransition)
{
// keep ourselves until we transition to the final destination
ActorList.Add(this);
// keep general game state until we transition to the final destination
ActorList.Add(World->GameState);
// keep the game session state until we transition to the final destination
ActorList.Add(GameSession); // If adding in this section best to increase the literal above for the ActorsToAddCount
}
}

在第一步从CurrentWorld到TransitionWorld的迁移时候,bToTransitiontrue,这个时候GameMode也会迁移进TransitionWorld(TransitionMap可以在ProjectSettings里配置),也包括GameState和GameSession,然后CurrentWorld释放掉。第二步从TransitionWorld到NewWorld的迁移,GameMode(已经在TransitionWorld中了)会再次调用GetSeamlessTravelActorList,这个时候bToTransitionfalse,所以第二次的时候如代码所见当前的GameMode、GameState和GameSession就被排除在外了。这样NewWorld再继续InitWorld的时候,一发现当前没有GameMode,就会根据配置的GameModeClass重新生成一个出来。所以这个时候GameMode也是不同的。

结论是,UE的流程travelling,GameMode在新的World里是会新生成一个的,即使Class类型一致,即使bUseSeamlessTravel,因此在travelling的时候要小心GameMode里保存的状态丢失。不过Pawn和Controller默认是一致的。

思考:哪些逻辑应该写在GameMode里?哪些应该写在Level Blueprint里?

我们依旧要问这个老土的问题。根据我们前面的知识,我们知道每个Level其实也是有自己的LevelScriptActor的,那么这两个有什么区别?可以从这几个方面来回答:

  • 概念上,Level是表示,World是逻辑,一个World如果有很多个Level拼在一起,那么也就是有了很多个LevelScriptActor,无法想象在那么多个地方写一个完整的游戏逻辑。所以GameMode应该专注于逻辑的实现,而LevelScriptActor应该专注于本Level的表示逻辑,比如改变Level内某些Actor的运动轨迹,或者某一个区域的重力,或者触发一段特效或动画。而GameMode应该专注于玩法,比如胜利条件,怪物刷新等。
  • 组合上,同Controller应用到Pawn一样道理,因为GameMode是可以应用在不同的Level的,所以通用的玩法应该放在GameMode里。
  • GameMode只在Server存在(单机游戏也是Server),对于已经连接上Server的Client来说,因为游戏的状态都是由Sever决定的,Client只是负责展示,所以Client上是没有GameMode的,但是有LevelScriptActor,所以GameMode里不要写Client特定相关的逻辑,比如操作UI等。但是LevelScriptActor还是有的,而且支持RPC,即使如此,LevelScriptActor还是应该只专注于表现,比如网络中触发一个特效火焰。至于UI,可以通过PlayerController的RPC,然后转发到GameInstance来操作。
  • 跟下层的PlayerController比较,GameMode关心的是构建一个游戏本身的玩法,PlayerController关心的玩家的行为。这两个行为是独立正交可以自由组合的。所以想想哪些逻辑属于游戏,哪些属于玩家,就应该清楚写在哪里了。
  • 跟上层的GameInstance比较,GameInstance关注的是更高层的不同World之间的逻辑,虽然有时候他也把手伸下来做些UI的管理工作,不过严谨来说,在UE里UI是独立于World的一个结构,所以也还算能理解。因此可以把不同GameMode之间协调的工作交给GameInstance,而GameMode只专注自己的玩法世界。

GameState

上回说到了APlayerState用来保存玩家的游戏数据,那么同样的,对于一场游戏,也需要一个State来保存当前游戏的状态数据,比如任务数据等。跟APlayerState一样,GameState也选择从AInfo里继承,这样在网络环境里也可以Replicated到多个Client上面去。



比较简单,第一个MatchState和相关的回调就是为了在网络中传播同步游戏的状态使用的(记得GameMode在Client并不存在,但是GameState是存在的,所以可以通过它来复制),第二部分是玩家状态列表,同样的如果在Client1想看到Client2的游戏状态数据,则Client2的PlayerState就必须广播过来,因此GameState把当前Server的PlayerState都收集了过来,方便访问使用。

关于使用,开发者可以自定义GameState子类来存储本GameMode的运行过程中产生的数据(那些想要replicated的!),如果是GameMode游戏运行的一些数据,又不想要所有的客户端都可以看到,则也可以写在GameMode的成员变量中。重复遍,PlayerState是玩家自己的游戏数据,GameInstance里是程序运行的全局数据。

GameSession

是在网络联机游戏中针对Session使用的一个方便的管理类,并不存储数据,本文重点也不在网络,故不做过多解释,可暂时忽略,留待网络章节再讨论。在单机游戏中,也存在该类对象用来LoginPlayer,不过因为只是作为辅助类,那也可看作GameMode本身的功能,所以不做过多讨论。

总结

现在,我们也算讨论完了Level(World)层次的控制,对于一场游戏而言,我们最关心的是怎么协调好整个场景的表现(LevelBlueprint)和游戏玩法的编写(GameMode)。UE再次用Actor分化派生的思想,用同样套路的AGameMode和AGameState支持了玩法和表现的解耦分离和自由组合,并很好的支持了网络间状态的同步。同时也提供了一个逻辑的实体来负责创建关系内那些关键的Pawn和Controller们,在关卡切换(World)的时候,也有了一个负责对象来处理一些本游戏的特定情况处理。

我们的逻辑之旅还没到终点,让我们继续爬升,下篇将介绍Player。

修订

在笔者书写本篇的同时(UE4.13.2),UE同时也完成了4.14的preview3的工作,roadmap里“GameMode Cleanup”的工作也已经完成了,第二天发现4.14正式发布了。因此为了紧跟UE最新潮流时尚,以后要是文章内容所涉及内容被UE修改完善优化的,也会采用修订的方式进行补充说明,之后不再特意作此声明。

4.14 GameMode,GameState的清理

根据搜索到的最早记录"[Request/Improvment] GameMode cleanup."(09-14-2014),是有人抱怨当前的GameMode实现了太多的默认逻辑(例如多人的Match),虽然方便了一些人使用,但是也确实加大了理解的难度,并且有时候还得去屏蔽删除一些默认逻辑。然后顺便吐槽了一番AActor里的Damage,笔者也表示这确实不是AActor应该管的事情。

言归正传,UE在2016-08-24的时候开始加进roadmap,并终于在4.14里实现完成了。如前所述,就是把GameMode和GameState的一些共同最基础部分抽到基类AGameModeBase和AGameStateBase里,并把现在的GameMode和GameState依然当作多人联机的默认实现。所以以后大家如果想实现一个比较简单的单机GameMode就可以直接从AGameModeBase里继承了。



可以看到,其实就是把MatchState给往下拉了一层,并把一些多玩家控制的逻辑,合起来就是网络联机游戏的默认逻辑给抽离开了。同样的对于GameState也做了处理:



把MatchState也抽离到了下层,并增加了几个方便的字段引用(如AuthorityGameMode)。总体功能职责架构上还是没有什么大变化的,吓死我了。

引用

  1. GameMode
  2. GameState
  3. Travelling in Multiplayer

UE4.13.2


知乎专栏:InsideUE4

UE4深入学习QQ群: 456247757(非新手入门群,请先学习完官方文档和视频教程)

个人原创,未经授权,谢绝转载!

《InsideUE4》-8-GamePlay架构(七)GameMode和GameState的更多相关文章

  1. 《InsideUE4》GamePlay架构(十)总结

    世界那么大,我想去看看 引言 通过对前九篇的介绍,至此我们已经了解了UE里的游戏世界组织方式和游戏业务逻辑的控制.行百里者半九十,前述的篇章里我们的目光往往专注在于特定一个类或者对象,一方面固然可以让 ...

  2. 《InsideUE4》-7-GamePlay架构(六)PlayerController和AIController

    PlayerController:你不懂,伴君如伴虎啊 AIController:上来,我自己动 引言 上文我们谈到了Component-Actor-Pawn-Controller的结构,追溯了ACo ...

  3. 《InsideUE4》-10-GamePlay架构(九)GameInstance

    一人之下,万人之上 引言 上篇我们讲到了UE在World之上,继续抽象出了Player的概念,包含了本地的ULocalPlayer和网络的UNetConnection,并以此创建出了World中的Pl ...

  4. 《InsideUE4》-6-GamePlay架构(五)Controller

    <InsideUE4>-6-GamePlay架构(五)Controller Tags: InsideUE4 GamePlay 那一天 Pawn又回想起了 被Controller所支配的恐惧 ...

  5. 《InsideUE4》-5-GamePlay架构(四)Pawn

    <InsideUE4>-5-GamePlay架构(四)Pawn Tags: InsideUE4 我像是一颗棋 进退任由你决定 我不是你眼中唯一将领 却是不起眼的小兵 引言 欢迎来到Game ...

  6. 《InsideUE4》-9-GamePlay架构(八)Player

    你们对力量一无所知 引言 回顾上文,我们谈完了World和Level级别的逻辑操纵控制,如同分离组合的AController一样,UE在World的层次上也采用了一个分离的AGameMode来抽离了游 ...

  7. 《InsideUE4》-3-GamePlay架构(二)Level和World

    UE4深入学习QQ群: 456247757 引言 上文谈到Actor和Component的关系,UE利用Actor的概念组成一片游戏对象森林,并利用Component组装扩展Actor的能力,让世界里 ...

  8. 《InsideUE4》-4-GamePlay架构(三)WorldContext,GameInstance,Engine

    Tags: InsideUE4 UE4深入学习QQ群: 456247757 引言 前文提到说一个World管理多个Level,并负责它们的加载释放.那么,问题来了,一个游戏里是只有一个World吗? ...

  9. 【UE4】GamePlay架构

    新标签打开或者下载看大图 更新: 增加 编程子系统 Subsystem 思维导图 Character pipeline

随机推荐

  1. sqlserver2008存储过程(比较两个日期大小和获取当前月最大天数的存储过程)

    下面简单介绍sqlserver2008两个常用的存储过程 1.比较两个日期大小的存储过程 2.获取当前月份的最大天数的存储过程 1.创建比较两个日期大小的存储过程 1)创建比较两个日期大小的存储过程 ...

  2. What does "size" in int(size) of MySQL mean?

    What does "size" in int(size) of MySQL mean? https://alexander.kirk.at/2007/08/24/what-doe ...

  3. Java--设计模式心得体会

    1.策略模式: 策略模式就是将能够通用的算法,封装成不同的组件,实现同一个接口,使之可以互换. 例子:SpringMVC的9大组件,都采用策略模式.比如HandlerMethodArgumentRes ...

  4. JS效果集锦

    秒后消失 $('.success_message').text( '反馈成功' );    setTimeout(function(){         $( ".success_messa ...

  5. 利用SCORE法则来总结一次偷懒的单元测试过程

    最近遇到一个单元测试的问题,本周正好学个了一个SCORE法则,这里正好练练手应用此法则将问题的前因后果分享给大家. S:背景  代码要有单元测试,检测的标准就是统计代码的单元测试覆盖率,程序员需要达到 ...

  6. discuz 3.x 核心文件class_core.php解析

    class_core.php是discuz 3.x的核心文件,几乎所有PHP脚本都有引用此文件初始化论坛运行环境.以下解析引用3.2版discuz. line 12-15:常量定义IN_DISCUZ: ...

  7. Gradle's dependency cache may be corrupt解决方法

    问题描述: Error:Unable to find method 'com.google.common.cache.CacheBuilder.build(Lcom/google/common/cac ...

  8. android doc里面adb连接出现问题的解决方法

    第一保证连接的两边都是有网的 第二  就是网上常说的1.adb kill-server 2.adb start-server 3.adb remount 但是在运行adb remount有可能会提示 ...

  9. iOS开发之功能模块--Apns推送中的的json格式介绍

    在开发向苹果Apns推送消息服务功能,我们需要根据Apns接受的数据格式进行推送.下面接受我在进行apns推送时候总结的一点apns服务接受的Json数据格式 示例 1: 以下负载包含哦一个简单的 a ...

  10. android handler ,message消息发送方式

    1.Message msg =  Message.obtain(mainHandler) msg.obj=obj;//添加你需要附加上去的内容 msg.what = what;//what消息处理的类 ...