1.前言

Unity FpsSample Demo大约是2018发布,用于官方演示MLAPI(NetCode前身)+DOTS的一个FPS多人对战Demo。

Demo下载地址(需要安装Git LFS) :https://github.com/Unity-Technologies/FPSSample

下载完成后3-40GB左右,若大小不对可能下载不完整。

时间原因写的并不完整,但大致描绘了项目的框架轮廓。

1.1.附带文档与主配置界面

在项目根目录可以找到附带的文档:

在项目中的Fps Sample/Windows/Project Tools处可以打开主配置界面:

其中打包AssetBundle的方式值得一提,因为在资源底部标记AssetBundle的方式非常的不方便,

FpsSample将AssetBunlde通过Hash值存到了ScriptableObject里,并且区分Server/Client,

服务端打包AssetBundle时将用一些资源及耗费性能较少的替代文件,客户端打包的AssetBundle

则是完整版本。

2.GameLoop

可参考文档SourceCode.md,不同的GameLoop决定当前游戏下的主循环逻辑:

游戏内的几种GameLoop分别对应如下:

  • ClientGameLoop 客户端游戏循环
  • ServerGameLoop 服务端游戏循环
  • PreviewGameLoop 编辑器下执行关卡测试时对应的游戏循环(单机跑图模式)
  • ThinClientGameLoop 调试用的轻量版客户端游戏循环,内部几乎没有System

2.1 GameLoop触发逻辑

游戏的入口是Game.prefab:

GameLoop接口定义在Game.cs中:

public interface IGameLoop
{
bool Init(string[] args);
void Shutdown(); void Update();
void FixedUpdate();
void LateUpdate();
}

然后通过命令初始化所需要的GameLoop,内部会通过反射创建(Game.cs中):

void CmdServe(string[] args)
{
RequestGameLoop(typeof(ServerGameLoop), args);
Console.s_PendingCommandsWaitForFrames = 1;
}
IGameLoop gameLoop = (IGameLoop)System.Activator.CreateInstance(m_RequestedGameLoopTypes[i]);
initSucceeded = gameLoop.Init(m_RequestedGameLoopArguments[i]);

3.网络运行逻辑

3.1 ClientGameLoop

先来看下ClientGameLoop,初始化会调用Init函数,NetworkTransport为Unity封装的网络层,

NetworkClient为上层封装,附带一些游戏逻辑。

public bool Init(string[] args)
{
...
m_NetworkTransport = new SocketTransport();
m_NetworkClient = new NetworkClient(m_NetworkTransport);

3.1.1 NetworkClient内部逻辑

跟进去看下NetworkClient的结构,删了一些内容,部分接口如下:

public class NetworkClient
{
... public bool isConnected { get; }
public ConnectionState connectionState { get; }
public int clientId { get; }
public NetworkClient(INetworkTransport transport)
public void Shutdown() public void QueueCommand(int time, DataGenerator generator)
public void QueueEvent(ushort typeId, bool
reliable, NetworkEventGenerator generator)
ClientConnection m_Connection;
}

其中QueueCommand用于处理角色的移动、跳跃等信息,包含于Command结构中。

QueueEvent用于处理角色的连接、启动等状态。

3.1.2 NetworkClient外部调用

继续回到ClientGameLoop,在Update中可以看到NetworkClient的更新逻辑

public void Update()
{
Profiler.BeginSample("ClientGameLoop.Update"); Profiler.BeginSample("-NetworkClientUpdate");
m_NetworkClient.Update(this, m_clientWorld?.GetSnapshotConsumer()); //客户端接收数据
Profiler.EndSample(); Profiler.BeginSample("-StateMachine update");
m_StateMachine.Update();
Profiler.EndSample(); // TODO (petera) change if we have a lobby like setup one day
if (m_StateMachine.CurrentState() == ClientState.Playing && Game.game.clientFrontend != null)
Game.game.clientFrontend.UpdateChat(m_ChatSystem); m_NetworkClient.SendData(); //客户端发送数据

其中ClientGameLoop Update函数签名如下:

public void Update(INetworkClientCallbacks clientNetworkConsumer, ISnapshotConsumer snapshotConsumer)

参数1用于处理OnConnect、OnDisconnect等消息,参数2用于处理场景中各类快照信息。

3.1.3 m_NetworkClient.Update

进入Update函数看下接收逻辑:

public void Update(INetworkClientCallbacks clientNetworkConsumer, ISnapshotConsumer snapshotConsumer)
{
...
TransportEvent e = new TransportEvent();
while (m_Transport.NextEvent(ref e))
{
switch (e.type)
{
case TransportEvent.Type.Connect:
OnConnect(e.connectionId);
break;
case TransportEvent.Type.Disconnect:
OnDisconnect(e.connectionId);
break;
case TransportEvent.Type.Data:
OnData(e.connectionId, e.data, e.dataSize, clientNetworkConsumer, snapshotConsumer);
break;
}
}
}

可以看见具体逻辑处理在OnData中

3.1.4 m_NetworkClient.SendData

进入SendData函数,看下发送数据是如何处理的。

public void SendPackage<TOutputStream>() where TOutputStream : struct, NetworkCompression.IOutputStream
{
...if (commandSequence > 0)
{
lastSentCommandSeq = commandSequence;
WriteCommands(info, ref output);
}
WriteEvents(info, ref output);
int compressedSize = output.Flush();
rawOutputStream.SkipBytes(compressedSize); CompleteSendPackage(info, ref rawOutputStream);
}

可以看见,这里将之前加入队列的Command和Event取出写入缓冲准备发送。

3.2.ServerGameLoop

和ClientGameLoop一样,在Init中初始化Transport网络层和NetworkServer。

public bool Init(string[] args)
{
// Set up statemachine for ServerGame
m_StateMachine = new StateMachine<ServerState>();
m_StateMachine.Add(ServerState.Idle, null, UpdateIdleState, null);
m_StateMachine.Add(ServerState.Loading, null, UpdateLoadingState, null);
m_StateMachine.Add(ServerState.Active, EnterActiveState, UpdateActiveState, LeaveActiveState); m_StateMachine.SwitchTo(ServerState.Idle); m_NetworkTransport = new SocketTransport(NetworkConfig.serverPort.IntValue, serverMaxClients.IntValue);
m_NetworkServer = new NetworkServer(m_NetworkTransport);

注意,其中生成快照的操作在状态机的Active中。

Update中更新并SendData:

public void Update()
{
UpdateNetwork();//更新SQP查询服务器和调用NetWorkServer.Update
m_StateMachine.Update();
m_NetworkServer.SendData();
m_NetworkStatistics.Update();
if (showGameLoopInfo.IntValue > 0)
OnDebugDrawGameloopInfo();
}

3.2.1 Server - HandleClientCommands

来看一下接收客户端命令后是如何处理的,在ServerTick函数内,调用

HandleClientCommands处理客户端发来的命令

public class ServerGameWorld : ISnapshotGenerator, IClientCommandProcessor
{
...
public void ServerTickUpdate()
{
...
m_NetworkServer.HandleClientCommands(m_GameWorld.worldTime.tick, this);
}
public void HandleClientCommands(int tick, IClientCommandProcessor processor)
{
foreach (var c in m_Connections)
c.Value.ProcessCommands(tick, processor);
}

然后反序列化,加上ComponentData交给对应的System处理:

public class ServerGameWorld : ISnapshotGenerator, IClientCommandProcessor
{
  ...
public void ProcessCommand(int connectionId, int tick, ref NetworkReader data)
{
    ...
if (tick == m_GameWorld.worldTime.tick)
client.latestCommand.Deserialize(ref serializeContext, ref data);
if (client.player.controlledEntity != Entity.Null)
{
var userCommand = m_GameWorld.GetEntityManager().GetComponentData<UserCommandComponentData>(
client.player.controlledEntity);
userCommand.command = client.latestCommand;
m_GameWorld.GetEntityManager().SetComponentData<UserCommandComponentData>(
client.player.controlledEntity,userCommand);

}
}

4.Snapshot

4.1 Snapshot流程

项目中所有的客户端命令都发到服务器上执行,服务器创建Snapshot快照,客户端接收Snapshot快照同步内容。

Server部分关注ReplicatedEntityModuleServer和ISnapshotGenerator的调用:

public class ServerGameWorld : ISnapshotGenerator, IClientCommandProcessor
{
public ServerGameWorld(GameWorld world, NetworkServer networkServer, Dictionary<int, ServerGameLoop.ClientInfo> clients, ChatSystemServer m_ChatSystem, BundledResourceManager resourceSystem)
{
...
m_ReplicatedEntityModule = new ReplicatedEntityModuleServer(m_GameWorld, resourceSystem, m_NetworkServer);
m_ReplicatedEntityModule.ReserveSceneEntities(networkServer);
} public void ServerTickUpdate()
{
...
m_ReplicatedEntityModule.HandleSpawning();
m_ReplicatedEntityModule.HandleDespawning();
} public void GenerateEntitySnapshot(int entityId, ref NetworkWriter writer)
{
...
m_ReplicatedEntityModule.GenerateEntitySnapshot(entityId, ref writer);
} public string GenerateEntityName(int entityId)
{
...
return m_ReplicatedEntityModule.GenerateName(entityId);
}
}

Client部分关注ReplicatedEntityModuleClient和ISnapshotConsumer的调用:

foreach (var id in updates)
{
var info = entities[id];
GameDebug.Assert(info.type != null, "Processing update of id {0} but type is null", id);
fixed (uint* data = info.lastUpdate)
{
var reader = new NetworkReader(data, info.type.schema);
consumer.ProcessEntityUpdate(serverTime, id, ref reader);
}
}

4.2 SnapshotGenerator 流程

在ServerGameLoop中调用快照创建逻辑:

public class ServerGameWorld : ISnapshotGenerator, IClientCommandProcessor
{
void UpdateActiveState()
{
int tickCount = 0;
while (Game.frameTime > m_nextTickTime)
{
tickCount++;
m_serverGameWorld.ServerTickUpdate();
       ...
m_NetworkServer.GenerateSnapshot(m_serverGameWorld, m_LastSimTime);
}

在Server中存了所有的实体,每个实体拥有EntityInfo结构,结构存放了snapshots字段。

遍历实体并调用GenerateEntitySnapshot接口生成实体内容:

unsafe public class NetworkServer
{
unsafe public void GenerateSnapshot(ISnapshotGenerator snapshotGenerator, float simTime)
{
...
// Run through all the registered network entities and serialize the snapshot
for (var id = 0; id < m_Entities.Count; id++)
{
var entity = m_Entities[id]; EntityTypeInfo typeInfo;
bool generateSchema = false;
if (!m_EntityTypes.TryGetValue(entity.typeId, out typeInfo))
{
typeInfo = new EntityTypeInfo() { name = snapshotGenerator.GenerateEntityName(id), typeId = entity.typeId, createdSequence = m_ServerSequence, schema = new NetworkSchema(entity.typeId + NetworkConfig.firstEntitySchemaId) };
m_EntityTypes.Add(entity.typeId, typeInfo);
generateSchema = true;
} // Generate entity snapshot
var snapshotInfo = entity.snapshots.Acquire(m_ServerSequence);
snapshotInfo.start = worldsnapshot.data + worldsnapshot.length; var writer = new NetworkWriter(snapshotInfo.start, NetworkConfig.maxWorldSnapshotDataSize / 4 - worldsnapshot.length, typeInfo.schema, generateSchema);
snapshotGenerator.GenerateEntitySnapshot(id, ref writer);
writer.Flush();
snapshotInfo.length = writer.GetLength();

4.3 SnapshotConsumer 流程

在NetworkClient的OnData中处理快照信息

case TransportEvent.Type.Data:
OnData(e.connectionId, e.data, e.dataSize, clientNetworkConsumer, snapshotConsumer);
break;

对应的处理函数:

public void ProcessEntityUpdate(int serverTick, int id, ref NetworkReader reader)
{
var data = m_replicatedData[id]; GameDebug.Assert(data.lastServerUpdate < serverTick, "Failed to apply snapshot. Wrong tick order. entityId:{0} snapshot tick:{1} last server tick:{2}", id, serverTick, data.lastServerUpdate);
data.lastServerUpdate = serverTick; GameDebug.Assert(data.serializableArray != null, "Failed to apply snapshot. Serializablearray is null"); foreach (var entry in data.serializableArray)
entry.Deserialize(ref reader, serverTick); foreach (var entry in data.predictedArray)
entry.Deserialize(ref reader, serverTick); foreach (var entry in data.interpolatedArray)
entry.Deserialize(ref reader, serverTick); m_replicatedData[id] = data;
}

5.游戏模块逻辑

5.1 ECS System扩展

BaseComponentDataSystem.cs类中包含了各类System基类扩展:

  • BaseComponentSystem<T1 - T3> 筛选出泛型MonoBehaviour到ComponentGroup,但忽略已销毁的对象(DespawningEntity),可以在子类中增加IComponentData筛选条件
  • BaseComponentDataSystem<T1 - T5> 筛选出泛型ComponentData,其余与BaseComponentSystem一致
  • InitializeComponentSystem<T> 筛选T类型的MonoBehaviour然后执行Initialize函数,确保初始化只执行一次
  • InitializeComponentDataSystem<T,K> 为每个包含ComponentData T的对象增加ComponentData K,确保初始化只执行一次
  • DeinitializeComponentSystem<T> 筛选包含MonoBehaviour T和已销毁标记的对象
  • DeinitializeComponentDataSystem<T> 筛选包含ComponentData T和已销毁标记的对象
  • InitializeComponentGroupSystem<T,S> 同InitializeComponentSystem,但标记了AlwaysUpdateSystem
  • DeinitializeComponentGroupSystem<T> 同DeinitializeComponentSystem,但标记了AlwaysUpdateSystem

5.2 角色创建

以编辑器下打开Level_01_Main.unity运行为例。

运行后会进入EditorLevelManager.cs触发对应绑定的场景运行回调:

[InitializeOnLoad]
public class EditorLevelManager
{
static EditorLevelManager()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
...
static void OnPlayModeStateChanged(PlayModeStateChange mode)
{
if (mode == PlayModeStateChange.EnteredPlayMode)
{
...
case LevelInfo.LevelType.Gameplay:
Game.game.RequestGameLoop( typeof(PreviewGameLoop), new string[0]);
break;
}
}

在PreviewGameLoop中写了PreviewGameMode的逻辑,在此处若controlledEntity为空则触发创建:

public class PreviewGameMode : BaseComponentSystem
{
...
protected override void OnUpdate()
{
if (m_Player.controlledEntity == Entity.Null)
{
Spawn(false);
return;
}
}

最后调到此处进行创建:

CharacterSpawnRequest.Create(PostUpdateCommands, charControl.characterType, m_SpawnPos, m_SpawnRot, playerEntity);

在创建后执行到CharacterSystemShared.cs的HandleCharacterSpawn时,会启动角色相关逻辑:

public static void CreateHandleSpawnSystems(GameWorld world,SystemCollection systems, BundledResourceManager resourceManager, bool server)
{
systems.Add(world.GetECSWorld().CreateManager<HandleCharacterSpawn>(world, resourceManager, server)); // TODO (mogensh) needs to be done first as it creates presentation
systems.Add(world.GetECSWorld().CreateManager<HandleAnimStateCtrlSpawn>(world));
}

如果把这行代码注释掉,运行后会发现角色无法启动。

5.3 角色系统

角色模块分为客户端和服务端,区别如下:

Client Server 说明
UpdateCharacter1PSpawn   处理第一人称角色
PlayerCharacterControlSystem PlayerCharacterControlSystem 同步角色Id等参数
CreateHandleSpawnSystems CreateHandleSpawnSystems 处理角色生成
CreateHandleDespawnSystems CreateHandleDespawnSystems 处理角色销毁
CreateAbilityRequestSystems CreateAbilityRequestSystems 技能相关逻辑
CreateAbilityStartSystems CreateAbilityStartSystems 技能相关逻辑
CreateAbilityResolveSystems CreateAbilityResolveSystems 技能相关逻辑
CreateMovementStartSystems CreateMovementStartSystems 移动相关逻辑
CreateMovementResolveSystems CreateMovementResolveSystems 应用移动数据逻辑
UpdatePresentationRootTransform UpdatePresentationRootTransform 处理展示角色的根位置旋转信息
UpdatePresentationAttachmentTransform UpdatePresentationAttachmentTransform 处理附加物体的根位置旋转信息
UpdateCharPresentationState UpdateCharPresentationState 更新角色展示状态用于网络传输
ApplyPresentationState ApplyPresentationState 应用角色展示状态到AnimGraph
  HandleDamage 处理伤害
  UpdateTeleportation 处理角色位置传送
CharacterLateUpdate   在LateUpdate时序同步一些参数
UpdateCharacterUI   更新角色UI
UpdateCharacterCamera   更新角色相机
HandleCharacterEvents   处理角色事件

5.4 CharacterMoveQuery

角色内部用的还是角色控制器:

角色的生成被分到了多个System中,所以角色控制器也是单独的GameObject,

创建代码如下:

public class CharacterMoveQuery : MonoBehaviour
{
public void Initialize(Settings settings, Entity hitCollOwner)
{
//GameDebug.Log("CharacterMoveQuery.Initialize");
this.settings = settings;
var go = new GameObject("MoveColl_" + name,typeof(CharacterController), typeof(HitCollision));
charController = go.GetComponent<CharacterController>();

在Movement_Update的System中将deltaPos传至moveQuery:

class Movement_Update : BaseComponentDataSystem<CharBehaviour, AbilityControl, Ability_Movement.Settings>
{
protected override void Update(Entity abilityEntity, CharBehaviour charAbility, AbilityControl abilityCtrl, Ability_Movement.Settings settings )
{
// Calculate movement and move character
var deltaPos = Vector3.zero;
CalculateMovement(ref time, ref predictedState, ref command, ref deltaPos);
// Setup movement query
moveQuery.collisionLayer = character.teamId == 0 ? m_charCollisionALayer : m_charCollisionBLayer;
moveQuery.moveQueryStart = predictedState.position;
moveQuery.moveQueryEnd = moveQuery.moveQueryStart + (float3)deltaPos; EntityManager.SetComponentData(charAbility.character,predictedState);
}
}

最后在moveQuery中将deltaPos应用至角色控制器:

class HandleMovementQueries : BaseComponentSystem
{
protected override void OnUpdate()
{
...
var deltaPos = query.moveQueryEnd - currentControllerPos;
charController.Move(deltaPos);
query.moveQueryResult = charController.transform.position;
query.isGrounded = charController.isGrounded; Profiler.EndSample();
}
}

6.杂项

6.1 MaterialPropertyOverride

这个小工具支持不创建额外材质球的情况下修改材质球参数,

并且无项目依赖,可以直接拿到别的项目里用:

6.2 RopeLine

快速搭建动态交互绳节工具


参考:

https://www.jianshu.com/p/347ded2a8e7a

https://www.jianshu.com/p/c4ea9073f443

Unity FpsSample Demo研究的更多相关文章

  1. Unity 官方 Demo: 2DPlatformer 的 SLua 版本。

    9月份时,趁着国庆阅兵的假期,将 Unity 官方 Demo: 2DPlatformer 移植了一个 SLua 版本,并放在了我的 GitHub 账号下:https://github.com/yauk ...

  2. Unity AngryBots愤怒的机器人demo研究

    做为Unity早期的经典demo,一直从3.5以后沿用到4.7.x版本.但其内部一些做法十分不合理.比如使用过多的根目录, 创建怪物和玩家不用SpawnPoint.AI.CheckPoint的代码实现 ...

  3. Unity Web前端研究

    原地址:http://blog.csdn.net/libeifs/article/details/7200630 开发环境 Window7 Unity3D  3.4.1 MB525defy Andro ...

  4. Unity 跑酷Demo难题总结

    问题1:路面拼接处理 在拼接路的时候,如果两个路挨的太近就会出现贴图闪烁,如下所示 解决办法 如果把路改小就会出现断层,但不会出现贴图闪烁 PS:我是把贴图放在Cube上的,所以路是有厚度. 附注 刚 ...

  5. Unity制作FPS Demo

    等到把这个Unity FPS Demo[僵尸杀手]完成后再详细补充一下,使用Unity制作FPS游戏的经历,今天做个标识.

  6. Unity接入谷歌支付

    文章理由 前段时间负责Unity接入Google内购功能,一开始研究别人的技术博客时发现,他们的文章都有些年头了,有些细节的地方已经不像n年前那样了,技术永远是需要更新的,而这篇就作为2016年末的最 ...

  7. Unity LightmapParameters的使用

    Unity5的烘培十分不好用,今天看官方demo时发现可以用LightmapParameters对模型的GI配置进行单独覆写,介绍一下 LightmapParameters可以把全局光照的配置做成预设 ...

  8. C#程序员整理的Unity 3D笔记(十):Unity3D的位移、旋转的3D数学模型

    遇到一个想做的功能,但是实现不了,核心原因是因为对U3D的3D数学概念没有灵活吃透.故再次系统学习之—第三次学习3D数学. 本次,希望实现的功能很简单: 如在小地图中,希望可以动态画出Player当前 ...

  9. 【转】如何使用Unity创造动态的2D水体效果

    原文:http://gamerboom.com/archives/83080 作者:Alex Rose 在本篇教程中,我们将使用简单的物理机制模拟一个动态的2D水体.我们将使用一个线性渲染器.网格渲染 ...

  10. Unity 的 unitypackage 的存放路径

    Windows,C:\Users\<username>\AppData\Roaming\Unity\Asset Store Mac OS X,~/Library/Unity/Asset S ...

随机推荐

  1. Unity 编辑器中获取选中的文件夹、文件路径

    编辑器中获取选中的文件夹.文件路径 using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; public cl ...

  2. C# pythonnet(2)_傅里叶变换(FFT)

    Python代码如下 import pandas as pd import numpy as np import matplotlib.pyplot as plt # 读取数据 data = pd.r ...

  3. ubuntu insight卸载

    最近需要进行gdb调试,但感觉gdb命令行方式不够直观,于是按照教程下载安装了insight,期间各种出错和bug,好不容易安装完成,却发现安装后的gdb版本变成了6.8-1,与gcc版本不兼容. 无 ...

  4. 6.Class 与 Style 绑定

    操作元素的 class 列表和内联样式是数据绑定的一个常见需求. 因为它们都是属性,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可. 不过,字符串拼接麻烦且易错.因此,在 ...

  5. oeasy教您玩转vim - 11 - # 向前向后

    向前向后 回忆上节课内容 我们上次强化了起手势 回忆了基本的移动方式 hjkl 除 hjkl 外,据说还有更厉害的移动方式 是什么呢? 下素材 #这个素材,我们下载过,重温一下 wget github ...

  6. VUE小知识~作用域插槽

    作用域插槽可以为我们向组件内插入特定的标签,方便修改维护. 组件内需要使用 <slot></slot>进行插槽站位. 组件标签内需要使用<template > &l ...

  7. iframe嵌入svg图片动态更改文本样式并进行缩放等功能实现拓扑图

    好久没更了,近期开发遇到的需求,抽空梳理一下~ 需求:实现一个复杂的拓扑图,图中元素的个数,以及各个参数内容是动态展示的. 于是让ui提供了对应的svg图片. 解决思路:使用iframe嵌入svg图片 ...

  8. CCF 任务调度

    样例的示意图 查了一圈,然后很烦,不想学习方法,好卡.查了一些题解,发现都是10分,30分先搁置的博客,最后翻到 堪称官方标称的思路 (CCF题目pat题目各大poj/hdoj好戏那个都没有标程,这里 ...

  9. 【工具】SpringBoot项目如何查看某个maven依赖是否存在以及依赖链路

    当我在SpringBoot项目中想加个依赖,但是不确定现有依赖的依赖的依赖.....有没有添加过这个依赖,怎么办呢?如果添加过了但是不知道我需要的这个依赖属于哪个依赖的下面,怎么查呢? IDEA中提供 ...

  10. 支付宝小程序的级联选择器,对接简单操作,Cascader 级联选择器element_ui

    首先,对于element_ui 的动接,由于需要数据格式是 但是支付宝提的接口返回的数据是另一种格式,并且支付宝的三级联动接口是先只有一个列表,点击列表项再发现请求,生成另外一个下拉选择, 需要这个三 ...