效果描述
一个服务器,两个客户端,让他们连接后分别生成不同的Pawn,并且在不同的位置生成。
意义
这是个项目需求,但是我发现如果能够彻底理解并制作出这个功能,会对虚幻4内置的网络功能以及一些重要的Gameplay 的类有更深入的了解。
目前已有解决方案
在Google上也搜了好久,但是相关信息并不多,比较靠谱的最终都指向同一个wiki页面:Spawn Different Pawns For Players in Multiplayer,但是他的解决方案有个缺陷:开发过程中不好测试——原因是他的方案需要一个外部存储的数据或者SaveGame,而开发时你不可能把一个工程复制两份。除非发布以后,把发布好的文件复制两份去测试,但这样也很麻烦。
我的解决方案概述
我采用了蓝图实现。区分不同的客户端主要是依靠OnPostLogin的连接顺序。
在GameMode中设置一个int变量作为用户索引,每次OnPostLogin后递增1。因为服务器(Listen Server)的PostLogin肯定是第一个(没哪个客户端的连接速度会快过本机),所以服务端连接时索引为0。而其他两个客户端在项目中并不需要立即区分其角色,只需要先分别给一个不同的角色,后面如果需要修改,在服务端提供界面进行手动修改即可。
预备知识
- 首先要熟悉UE4中的网络/复制/RPC相关概念以及引擎GameMode,PlayerController等类在网络环境中的表现,这些信息需要仔细阅读和理解官方的文档:
NetWorking and Multiplayer 其中的ActorReplication章节非常重要需要仔细阅读和理解,MultiPlayer in Blueprints章节是个引导概况性质的章节,也需要仔细读
- 其次要阅读GameModeBase源码,了解从PostLogin到真正生成Pawn之间都发生了什么,这里我已经总结成了一幅图:
具体实施步骤
准备工作
先使用ThirdPerson模板创建工程,然后创建ThirdPersonCharacter的三个子类,给不同的颜色作为标记,分别是红,绿,蓝,其中红色准备作为服务器的Pawn,其他两个作为客户端的。
以GameModeBase为基类创建一个GameMode蓝图,这里命名为MyGameMode
以PlayerController为基类窗机一个Controller蓝图,这里明明为MyPlayerController
创建一个Enum,命名为ClientType,包含三个值:Server, ClientA, ClientB
在ThirdPersionExampleMap中,删掉场景中的Character,然后把PlayerStart复制2个出来,摆放好位置,分别给三个PlayerStart的Player Start Tag属性设置为Server,ClientA,ClientB
关键步骤
MyPlayerController
在MyPlayerController中创建一个变量:
MyClientType:ClientType枚举类型,设置为Replicated,用于标记该PlayerController的类型
MyGameMode
在MyGameMode中,创建6个变量:
ClientIndex : int型,默认设置,用于标记不同的客户端连接,每次OnPostLogin后会递增1
PlayerStarts:PlayerStart引用类型,数组,其他默认,用于存放所有的PlayerStart
ServerPawnClass: Pawn类类型,默认设置,表示服务器端Pawn的类
ClientAPawnClass: Pawn类类型,默认设置,表示客户端APawn的类
ClientBPawnClass: Pawn类类型,默认设置,表示客户端BPawn的类
CurrentPlayer: MyPlayerController引用类型,默认设置,临时存放传入的PlayerController
创建两个关于获取PlayerStart的函数:
GetAllPlayerStarts
GetPlayerStartByTag:
这两个函数的含义如其名称,功能也比较简单。不过需要注意调用时机。有人可能会想,直接在BeginPlay里调用GetAllPlayerStarts就可以了,实际上这样不行,因为OnPostLogin事件会在BeginPlay之前发生。
右键搜索OnPostLogin, 创建Event OnPostLogin事件,连接如下图:
步骤释义:
- 当有玩家连接进来后(包括服务器自身连接自身),把PlayerController存入一个临时变量Current Player。
- 根据Client Index设置Current Player的MyClientType,依次设置为Server,ClientA,ClientB。
- 然后把Client Index自增1。
- 判断Controller是否已经拥有了Pawn,如果有则销毁。
- 调用Restart Player重新生成该Controller的Pawn(注意看上文中的流程图,Restart Player之后进行了什么操作)
到这步之后,Restart之后并没有改变要使用的Pawn的类。
根据上文中的流程图,Pawn的类是在GetDefaultPawnClassForController函数中获取的,在三处都使用了该函数来返回Pawn的类型,因此我们需要覆盖这个函数,点"Functions"中的Override按钮,覆盖该函数。
函数截图如下:
步骤释义:
获取PlayerController,转换为MyPlayerController,根据刚才存入的MyClientType来返回不同的Pawn类型。使用MyGameMode里的三个Pawn Class 变量。
到这里,Pawn类别已经可以正常区分了,但是起始点还不行,都是在同一个位置生成。下面要解决的就是区分PlayerStart。
看上文中的流程图,可以看到,在Restart Player函数中是通过调用Find Player Start函数来决定使用哪个PlayerStart。因此要覆盖FindPlayerStart函数。
在MyGameMode里的Functions里点"Override按钮,覆盖FindPlayerStart函数,覆盖后的截图如下:
因为之前已经给不同的PlayerController分配了不同的角色,所以这步比较简单,也是区别My Client Type,返回不同的PlayerStart即可。
到此为止就完成了