《InsideUE4》-9-GamePlay架构(八)Player
你们对力量一无所知
引言
回顾上文,我们谈完了World和Level级别的逻辑操纵控制,如同分离组合的AController一样,UE在World的层次上也采用了一个分离的AGameMode来抽离了游戏关卡逻辑,从而支持了逻辑的组合。本篇我们继续上升一个层次,考虑在World之上,游戏还需要哪些逻辑控制?
暂时不考虑别的功能系统(如社交系统,统计等各种),单从游戏性来讨论,现在闭上眼睛,想象我们已经藉着UE的伟力搭建了好了一个个LevelWorld,嗯,就像《西部世界》一样,场景已经搭建好了,世界规则故事也编写完善,现在需要干些什么?当然是开始派玩家进去玩啦!
大家都是老玩家了,想想我们之前玩的游戏类型:
- 玩家数目是单人还是多人
- 网络环境是只本地还是联网
- 窗口显示模式是单屏还是分屏
- 输入模式是共用设备还是分开控制(比如各有手柄)
- 也许还有别的不同
假如你是个开发游戏引擎的,会怎么支持这些不同的模式?以笔者见识过的大部分游戏引擎,解决这个问题的思路就是不解决,要嘛是限制功能,要嘛就是美名其曰让开发者自己灵活控制。不过想了一下,这也不能怪他们,毕竟很少有引擎能像UE这样历史悠久同时又能得到足够多的游戏磨练,才会有功夫在GamePlay框架上雕琢。大部分引擎还是更关注于实现各种绚丽的功能,至于怎么在上面开展游戏逻辑,那就是开发者自己的事了。一个引擎的功能是否强大,是基础比拼指标;而GamePlay框架作为最高层直面用户的对接接口,是一个引擎的脸面。所以有兴趣游戏引擎研究的朋友们,区分一个引擎是否“优秀”,第二个指标是看它是否设计了一个优雅的游戏逻辑编写框架,一般只有基础功能已经做得差不多了的引擎开发者才会有精力去开发GamePlay框架,游戏引擎不止渲染!
言归正传,按照软件工程的理念,没有什么问题是不能通过加一个间接层解决的,不行就加两层!所以既然我们在处理玩家模式的问题,理所当然的是加个间接层,将玩家这个概念抽象出来。
那么什么是玩家呢?狭义的讲,玩家就是真实的你,和你身旁的小伙伴。广义来说,按照图灵测试理论,如果你无法分辨另一方是AI还是人,那他其实就跟玩家毫无区别,所以并不妨碍我们将网络另一端的一条狗当作玩家。那么在游戏引擎看来,玩家就是输入的发起者。游戏说白了,也只是接受输入产生输出的一个程序。所以有多少输入,这些输入归多少组,就有多少个玩家。这里的输入不止包括本地键盘手柄等输入设备的按键,也包括网线里传过来的信号,是广义的该游戏能接受到的外界输入。注意输出并不是玩家的必要属性,一个玩家并不一定需要游戏的输出,想象你闭上眼睛玩马里奥或者有个网络连接不断发送来控制信号但是从来不接收反馈,虽然看起来意义不大,但也确实不能说这就不是游戏。
在UE的眼里,玩家也是如此广义的一个概念。本地的玩家是玩家,网络联机时虽然看不见对方,但是对方的网络连接也可以看作是个玩家。当然的,本地玩家和网络玩家毕竟还是差别很大,所以UE里也对二者进行了区分,才好更好的管理和应用到不同场景中去,比如网络玩家就跟本地设备的输入没多大关系了嘛。
UPlayer
让我们假装自己是UE,开始编写Player类吧。为了利用上UObject的那些现有特性,所以肯定是得从UObject继承了。那能否是AActor呢?Actor是必须在World中才能存在的,而Player却是比World更高一级的对象。玩游戏的过程中,LevelWorld在不停的切换,但是玩家的模式却是脱离不变的。另外,Player也不需要被摆放在Level中,也不需要各种Component组装,所以从AActor继承并不合适。那还是保持简单吧:
如图可见,Player和一个PlayerController关联起来,因此UE引擎就可以把输入和PlayerController关联起来,这也符合了前文说过的PlayerController接受玩家输入的描述。因为不管是本地玩家还是远程玩家,都是需要控制一个玩家Pawn的,所以自然也就需要为每个玩家分配一个PlayerController,所以把PlayerController放在UPlayer基类里是合理的。
ULocalPlayer
然后是本地玩家,从Player中派生下来LocalPlayer类。对本地环境中,一个本地玩家关联着输入,也一般需要关联着输出(无输出的玩家毕竟还是非常少见)。玩家对象的上层就是引擎了,所以会在GameInstance里保存有LocalPlayer列表。
UE4里的ULocalPlayer也如图所见,ULocalPlayer比UPlayer多了Viewport相关的配置(Viewport相关的内容在渲染章节讲述),也终于用SpawnPlayerActor实现了创建出PlayerController的功能。GameInstance里有LocalPlayers的信息之后,就可以方便的遍历访问,来实现跟本地玩家相关操作。
关于游戏的详细加载流程目前不多讲述(按惯例在相应引擎流程章节讲述),现在简单了解一下LocalPlayer是怎么在游戏的引擎的各个环节发挥作用的。UE在初始化GameInstance的时候,会先默认创建出一个GameViewportClient,然后在内部再转发到GameInstance的CreateLocalPlayer:
ULocalPlayer* UGameInstance::CreateLocalPlayer(int32 ControllerId, FString& OutError, bool bSpawnActor)
{
ULocalPlayer* NewPlayer = NULL;
int32 InsertIndex = INDEX_NONE;
const int32 MaxSplitscreenPlayers = (GetGameViewportClient() != NULL) ? GetGameViewportClient()->MaxSplitscreenPlayers : 1;
//已略去错误验证代码,MaxSplitscreenPlayers默认为4
NewPlayer = NewObject<ULocalPlayer>(GetEngine(), GetEngine()->LocalPlayerClass);
InsertIndex = AddLocalPlayer(NewPlayer, ControllerId);
if (bSpawnActor && InsertIndex != INDEX_NONE && GetWorld() != NULL)
{
if (GetWorld()->GetNetMode() != NM_Client)
{
// server; spawn a new PlayerController immediately
if (!NewPlayer->SpawnPlayActor("", OutError, GetWorld()))
{
RemoveLocalPlayer(NewPlayer);
NewPlayer = NULL;
}
}
else
{
// client; ask the server to let the new player join
NewPlayer->SendSplitJoin();
}
}
return NewPlayer;
}
可以看到,如果是在Server模式,会直接创建出ULocalPlayer,然后创建出相应的PlayerController。而如果是Client(比如Play的时候选择NumberPlayer=2,则有一个为Client),则会先发送JoinSplit消息到服务器,在载入服务器上的Map之后,再为LocalPlayer创建出PlayerController。
而在每个PlayerController创建的过程中,在其内部会调用InitPlayerState:
void AController::InitPlayerState()
{
if ( GetNetMode() != NM_Client )
{
UWorld* const World = GetWorld();
const AGameModeBase* GameMode = World ? World->GetAuthGameMode() : NULL;
//已省略其他验证和无关部分
if (GameMode != NULL)
{
FActorSpawnParameters SpawnInfo;
SpawnInfo.Owner = this;
SpawnInfo.Instigator = Instigator;
SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnInfo.ObjectFlags |= RF_Transient; // We never want player states to save into a map
PlayerState = World->SpawnActor<APlayerState>(GameMode->PlayerStateClass, SpawnInfo );
// force a default player name if necessary
if (PlayerState && PlayerState->PlayerName.IsEmpty())
{
// don't call SetPlayerName() as that will broadcast entry messages but the GameMode hasn't had a chance
// to potentially apply a player/bot name yet
PlayerState->PlayerName = GameMode->DefaultPlayerName.ToString();
}
}
}
}
这样LocalPlayer最终就和PlayerState对应了起来。而网络联机时其他玩家的PlayerState是通过Replicated过来的。
我们谈了那么久的玩家就是输入,体现在在每个PlayerController接受Player的时候:
void APlayerController::SetPlayer( UPlayer* InPlayer )
{
//[...]
// Set the viewport.
Player = InPlayer;
InPlayer->PlayerController = this;
// initializations only for local players
ULocalPlayer *LP = Cast<ULocalPlayer>(InPlayer);
if (LP != NULL)
{
// Clients need this marked as local (server already knew at construction time)
SetAsLocalPlayerController();
LP->InitOnlineSession();
InitInputSystem();
}
else
{
NetConnection = Cast<UNetConnection>(InPlayer);
if (NetConnection)
{
NetConnection->OwningActor = this;
}
}
UpdateStateInputComponents();
// notify script that we've been assigned a valid player
ReceivedPlayer();
}
可见,对于ULocalPlayer,APlayerController内部会开始InitInputSystem(),接着会创建相应的UPlayerInput,BuildInputStack等初始化出和Input相关的组件对象。现在先明白到LocalPlayer才是PlayerController产生的源头,也因此才有了Input就够了,特定的Input事件流程分析在后续章节再细述。
思考:为何不在LocalPlayer里编写逻辑?
作为游戏开发者,相信大家都有这么个体会,往往在游戏逻辑代码中总会有一个自己的Player类,里面放着这个玩家的相关数据和逻辑业务。可是在UE里为何就不见了这么个结构?也没见UE在文档里有描述推荐你怎么创建自己的Player。
这个可能有两个原因,一是UE从FPS-Specify游戏起家,不像现在的各种手游有非常重的玩家系统,在UE的眼中,Level和World才是最应该关注的对象,因此UE的视角就在于怎么在Level中处理好Player的逻辑,而非在World之外的额外操作。二是因为在一个World中,上文提到其实已经有了Pawn-PlayerController和PlayerState的组合了,表示、逻辑和数据都齐备了,也就没必要再在Level掺和进Player什么事了。当然你也可以理解为PlayerController就是Player在Level中的话事人。
凡事留一线,日后好相见。尽管如此,UE还是给了我们自定义ULocalPlayer子类的机会:
//class UEngine:
/** The class to use for local players. */
UPROPERTY()
TSubclassOf<class ULocalPlayer> LocalPlayerClass;
/** @todo document */
UPROPERTY(globalconfig, noclear, EditAnywhere, Category=DefaultClasses, meta=(MetaClass="LocalPlayer", DisplayName="Local Player Class"))
FStringClassReference LocalPlayerClassName;
你可以在配置中写上LocalPlayer的子类名称,让UE为你生成你的子类。然后再在里面写上一些特定玩家的数据和逻辑也未尝不可,不过这部分额外扩展的功能就得用C++来实现了。
UNetConnection
非常耐人寻味的是,在UE里,一个网络连接也是个Player:
包含Socket的IpConnection也是玩家,甚至对于一些平台的特定实现如OculusNet的连接也可以当作玩家,因为对于玩家,只要能提供输入信号,就可以当作一个玩家。
追根溯源,UNetConnection的列表保存在UNetDriver,再到FWorldContext,最后也依然是UGameInstance,所以和LocalPlayer的列表一样,是在World上层的对象。
本篇先前瞻一下结构,对于网络部分不再细述。
总结
本篇我们抽象出了Player的概念,并依据使用场景派生出了LocalPlayer和NetConnection这两个子类,从此Player就不再是一个虚无缥缈的概念,而是UE里的逻辑实体。UE可以根据生成的Player对象的数量和类型的不同,在此上实现出不同的玩家控制模式,LocalPlayer作为源头Spawn出PlayerController继而PlayerState就是实证之一。而在网络联机时,把一个网络连接看作是一个玩家这个概念,把在World之上的输入实体用Player统一了起来,从而可以实现出灵活的本地远程不同玩家模式策略。
尽管如此,UPlayer却像是深藏在UE里的幕后功臣,UE也并不推荐直接在Player里编程,而是利用Player作为源头,来产生构建一系列相关的机制。但对于我们游戏开发者而言,知道并了解UE里的Player的概念,是把现实生活同游戏世界串联起来的很重要的纽带。我们在一个个World里向上仰望,还能清楚的看见一个个LocalPlayer或NetConnection仿佛在注视着这片大地,是他们为World注入了生机。
已经到头了?并没有,我们继续向上逆风飞翔,终将得见游戏里的神:GameInstance。
引用
UE4.14
知乎专栏:InsideUE4
UE4深入学习QQ群: 456247757(非新手入门群,请先学习完官方文档和视频教程)
个人原创,未经授权,谢绝转载!
《InsideUE4》-9-GamePlay架构(八)Player的更多相关文章
- 《InsideUE4》GamePlay架构(十)总结
世界那么大,我想去看看 引言 通过对前九篇的介绍,至此我们已经了解了UE里的游戏世界组织方式和游戏业务逻辑的控制.行百里者半九十,前述的篇章里我们的目光往往专注在于特定一个类或者对象,一方面固然可以让 ...
- 《InsideUE4》-10-GamePlay架构(九)GameInstance
一人之下,万人之上 引言 上篇我们讲到了UE在World之上,继续抽象出了Player的概念,包含了本地的ULocalPlayer和网络的UNetConnection,并以此创建出了World中的Pl ...
- 《InsideUE4》-8-GamePlay架构(七)GameMode和GameState
我的世界,我做主 引言 上文我们说到在Actor层次,UE用Controller来充当APawn的逻辑控制者,也有了可以接受玩家输入的PlayerController,和能自行行动的AIControl ...
- 《InsideUE4》-7-GamePlay架构(六)PlayerController和AIController
PlayerController:你不懂,伴君如伴虎啊 AIController:上来,我自己动 引言 上文我们谈到了Component-Actor-Pawn-Controller的结构,追溯了ACo ...
- 《InsideUE4》-6-GamePlay架构(五)Controller
<InsideUE4>-6-GamePlay架构(五)Controller Tags: InsideUE4 GamePlay 那一天 Pawn又回想起了 被Controller所支配的恐惧 ...
- 《InsideUE4》-5-GamePlay架构(四)Pawn
<InsideUE4>-5-GamePlay架构(四)Pawn Tags: InsideUE4 我像是一颗棋 进退任由你决定 我不是你眼中唯一将领 却是不起眼的小兵 引言 欢迎来到Game ...
- [翻译]了解ASP.NET底层架构(八)
原文地址:http://www.cnblogs.com/tmfc/archive/2006/09/04/493304.html [翻译]了解ASP.NET底层架构(完) [翻译]了解ASP.NET底层 ...
- 《InsideUE4》-4-GamePlay架构(三)WorldContext,GameInstance,Engine
Tags: InsideUE4 UE4深入学习QQ群: 456247757 引言 前文提到说一个World管理多个Level,并负责它们的加载释放.那么,问题来了,一个游戏里是只有一个World吗? ...
- 《InsideUE4》-3-GamePlay架构(二)Level和World
UE4深入学习QQ群: 456247757 引言 上文谈到Actor和Component的关系,UE利用Actor的概念组成一片游戏对象森林,并利用Component组装扩展Actor的能力,让世界里 ...
随机推荐
- 从Insider计划看Win10的发展
Windows 10 Insider计划是微软为了更好的倾听用户的需求而推出的用户测试项目,参与该项目的 Insider可以免费使用Windows 10 预览版.同时这些用户还需要对 Windows ...
- html的留言板制作(js)
这次留言板运用到了最基础的localstorage的本地存储,展现的效果主要有: 1.编写留言2.留言前可以编辑自己的留言昵称.不足之处: 1.未能做出我喜欢的类似于网易的叠楼功能. 2.未能显示评论 ...
- C#中实现对象间的更新操作
最近工作的时候遇到一个问题,根据Web端接收到的对象obj1,更新对应的对象值ogj2.先判断obj1中属性值是否为null, 若不等于null,则更新obj2中对应属性值:若等于null,则保持ob ...
- luogg_java学习_13_GUI
本文为博主辛苦总结,希望自己以后返回来看的时候理解更深刻,也希望可以起到帮助初学者的作用. 转载请注明 出自 : luogg的博客园 谢谢配合! GUI 容器 JFrame , JPanel , JS ...
- C#-Socket监听消息处理
TCP/IP:Transmission Control Protocol/Internet Protocol,传输控制协议/因特网互联协议,又名网络通讯协议.简单来说:TCP控制传输数据,负责发现传输 ...
- ahjesus web动态icon
刚刚逛插件无意间发现的,记录下,里面有demo可以直接run了看效果 http://nicolasbize.com/faviconx/ http://www.miaofree.com/
- WebAPI生成可导入到PostMan的数据
一.前言 现在使用WebAPI来作为实现企业服务化的需求非常常见,不可否认它也是很便于使用的,基于注释可以生成对应的帮助文档(Microsoft.AspNet.WebApi.HelpPage),但是比 ...
- jquery给div的innerHTML赋值
$("#id").html()=""; //或者 $("#id").html("test");
- 今天大雪 看雪花飘落HTML5特效
今天大雪,弄一个下雪的特效.html5飘落的雪花堆积动画特效 查看效果:http://hovertree.com/texiao/js/snow.htm 以下是完整源代码,保存到HTML文件也可以看效果 ...
- CSS3中flexbox如何实现水平垂直居中和三列等高布局
最近这些天都在弥补css以及css3的基础知识,在打开网页的时候,发现了火狐默认首页上有这样一个东西.