Unity-Mirror网络框架-从入门到精通之Benchmark示例
文章目录
- 前言
- 什么是Benchmark?
- Benchmark 简要说明
- Benchmark示例
- BenchmarkNetworkManager
- MonsterMovement
- PlayerMovement
- InterestManagement
- 性能指标
- BenchmarkIdle示例
- BenchmarkPrediction示例
- BenchmarkStinkySteak示例
前言
在现代游戏开发中,网络功能日益成为提升游戏体验的关键组成部分。Mirror是一个用于Unity的开源网络框架,专为多人游戏开发设计。它使得开发者能够轻松实现网络连接、数据同步和游戏状态管理。本文将深入介绍Mirror的基本概念、如何与其他网络框架进行比较,以及如何从零开始创建一个使用Mirror的简单网络项目。
什么是Benchmark?
Benchmark示例 通常用于测试和评估Mirror网络框架在不同场景下的性能表现。该示例可以通过实施多种网络条件来测量服务器和客户端之间的数据传输速度、延迟、带宽使用情况以及不同数量的同时连接用户对整体性能的影响。
在测试过程中,开发者可以观察到游戏在进行高并发情况下的响应能力,例如在多人游戏场景中,如何处理大量玩家的输入、状态同步和事件广播等。同时,该示例还可以帮助开发者识别潜在的性能瓶颈,并对代码进行优化,以便实现更流畅的用户体验。
通过这些基准测试,开发者能够获取关键的性能指标,从而更好地调整游戏的网络架构和优化策略,以提高整个平台的稳定性和反应速度,同时确保用户在游戏中能够获得最佳的游戏体验。
Benchmark 简要说明
Mirror的Example包含哪些 基准测试 示例呢?
- 1.Benchmark:这个示例用于基准测试网络性能,通常会测试数据传输速率、延迟等网络参数,以帮助开发者评估他们的网络实现。
- 2.BenchmarkIdle:这个示例主要用于在空闲状态下进行基准测试,目的是评估在没有任何活动情况下的网络性能。这有助于理解网络在闲置时的行为情况。
- 3.BenchmarkPrediction:这个示例涉及到网络预测技术,通常用于展示如何在网络延迟情况下进行状态预测,以提高游戏的流畅度和响应性。
- 4.BenchmarkStinkySteak:这是一个更具趣味性的示例,通常用于展示服务器与客户端之间的交互。可能涉及一些有趣的机制或模拟网络延迟的场景,以帮助开发者理解网络通信是如何工作的。
这些示例可以帮助开发者更好地理解 Mirror 的功能和用法,并进行性能优化。
Benchmark示例
BenchmarkNetworkManager
基准测试的NetworkManager,主要是在StartServer后,创建了1000个自由飘荡的盒子,代表我们的大场景中的Monster。
逻辑很简单,就是SpawnAll,克隆1000个游戏对象,并且调用NetworkServer.Spawn(go),同步到网络中。
public class BenchmarkNetworkManager : NetworkManager
{
public GameObject spawnPrefab;
public int spawnAmount = 5000;
public float interleave = 1;
void SpawnAll()
{
float sqrt = Mathf.Sqrt(spawnAmount);
float offset = -sqrt / 2 * interleave;
int spawned = 0;
for (int spawnX = 0; spawnX < sqrt; ++spawnX)
{
for (int spawnZ = 0; spawnZ < sqrt; ++spawnZ)
{
if (spawned < spawnAmount)
{
GameObject go = Instantiate(spawnPrefab);
float x = offset + spawnX * interleave;
float z = offset + spawnZ * interleave;
go.transform.position = new Vector3(x, 0, z);
NetworkServer.Spawn(go);
++spawned;
}
}
}
}
public override void OnStartServer()
{
base.OnStartServer();
SpawnAll();
}
MonsterMovement
这个脚本是模拟MMORPG中的Monster,在大场景中自由的移动。
这里着重说一下,Update函数是模拟怪物自由移动的,
但是Update上面有一个[ServerCallback]修饰,代表这是一个服务器才会执行的函数。
在Mirror框架中,如果一个函数被[Server]或者[ServerCallback]修饰,那么他就代表仅在服务器执行,客户端不执行。
这里相当于在服务器直接模拟了大量的AI怪物的移动。保证了不同客户端中显示的AI物体的同步。
public class MonsterMovement : NetworkBehaviour
{
public float speed = 1;
public float movementProbability = 0.1f;
public float movementDistance = 20;
public override void OnStartServer()
{
start = transform.position;
}
[ServerCallback]
void Update()
{
}
}
PlayerMovement
PlayerMovement模拟的是玩家自由控制移动。纯客户端执行。
由于Player对象上有NetworkTransform组件,所以它的位置等信息会实时同步给其他客户端。
当我们在场景中走动的时候,我们发现可以看到的Monster,会根据自己的位置变化,距离Player位置近的会显示,距离远的会隐藏。这个是什么道理呢?
原来是Mirror给我们做了一个网络优化,在NetworkManager游戏对象上面,有一个组件名字叫:SpatialHashingInterestManagement(空间哈希兴趣管理),是一种用于优化网络游戏中对象的管理和交互的方法。它通过将游戏世界划分为多个小区域(或“哈希桶”),来决定哪个玩家需要接收哪些对象的信息,从而提高性能并减少不必要的数据传输。
他就类似在做大世界的MMORPG时,只显示距离我们近的范围的AI怪物,并非全地图绘制和执行所有怪物的逻辑的优化。
InterestManagement
Interest管理器,它的原理就是相当于把地图划分成了无数的格子,然后我们只能看到距离我们相近的9个格子内的网络对象。
如下图所示:
当我们在一个非常大的世界游戏中时,与其将完整的世界状态发送给每个玩家,不如考虑仅将玩家周围的内容发送给玩家。
Mirror给我们提供了InterestManagement的主要原因:
- 1.规模:想象一下魔兽世界。将整个世界发送给每个玩家将是疯狂的。为了扩展到数千个连接,我们只需要发送与任何给定玩家相关的内容。
- 2.可见性:在像 DotA/英雄联盟这样的 MOBA 游戏中,并不是每个人都应该一直看到其他人。玩家应该只看到自己的团队和周围的怪物。不仅如此,玩家还不应该看到墙后等。
- 3.作弊:在像 Counter-Strike 这样的游戏中,玩家自然看不到墙后的敌人,因为相机不会渲染它们。但是,如果整个世界状态在内存中都是已知的,那么黑客无论如何都可以利用这一点,将玩家显示在墙后。
InterestManagement是一个好主意。
Mirror给我们提供了很多内置的Interest Management组件
选择 Network Manager 并添加一个内置的 Interest Management 组件。
- 1.Spatial Hashing Interest Management:以前我们 Vector3.Distance 根据连接实体距离来判断。现在,我们将每个生成的实体放入一个 Grid 中,对于每个连接,我们将所有 8 个相邻的 Grid 条目都发送到客户端。这是非常快的。在早期的 uMMORPG 测试中,它比距离检查快 30 倍。该算法不太复杂,因此可以很好地扩展到大量实体。
- 2.Distance Interest Management:是简单的通过距离判断,来把范围内的连接发送给客户端,是InteractManager的最简单粗暴的做法。但是缺点就是,通过距离来判断每个连接成本较高。
- 3.Scene Interest Management:场景Interest 管理与附加场景一起使用,将对象联网到具有物理隔离的子场景。这意味着,即使您在服务器上加载了同一子场景的多个实例,对象之间的碰撞等也只会发生在该子场景内,而不会干扰其他子场景。
- 4.Scene Distance Interest Management:是上述 Scene 和 Distance 的组合。
- 5.Match Interest Management :匹配Interest管理,适用于非物理游戏,如纸牌、棋盘、街机游戏。
- 6.Team Interest Management:用于限制对团队成员的联网对象的可见性。这也可以用于仅限所有者的项目,取代 Network Owner Checker。
性能指标
在Benchmark示例中,您看到的参数主要用于监控和评估网络性能,具体含义如下:
- buffer:用于存储网络数据包的缓冲区。通常在网络传输中,数据包会被暂时存储在缓冲区中,以便后续处理或发送。
- driftEMA:表示“漂移的指数移动平均”(Exponential Moving Average of Drift)。这是一个用于平滑变量的统计量,帮助减少波动并提供更稳定的值,通常用于测量时间戳的漂移或延迟变化。
- DelTimeEMA:表示“延迟时间的指数移动平均”(Exponential Moving Average of Delay Time)。此参数用于跟踪网络延迟的平均值,以便评估网络响应的稳定性。
- BTM:可能代表“带宽时间测量”(Bandwidth Time Measurement),用于衡量网络在一定时间内能够传输的数据量,帮助评估网络性能。
- RTT:表示“往返时间”(Round-Trip Time),是从发送数据包开始,到接收到确认的时间。RTT是网络延迟的重要指标。
- PredErrUNADJ:表示“未调整的预测误差”(Unadjusted Prediction Error),用于测量在不调整任何参数的情况下,预测的值与实际值之间的差异。
- PredErrADJ:表示“调整后的预测误差”(Adjusted Prediction Error),与未调整的预测误差相似,但在计算时可能考虑了某些调整因素,从而得到更精确的预测效果。
这些参数共同用于评估和优化网络性能,特别是在多人游戏中,以帮助开发者理解网络延迟和数据传输的表现。
BenchmarkIdle示例
该示例,使用大多数空闲对象进行基准测试,以测试dirtyObjects技术。
这次创建的额对象都是空闲不动的,他们的数量大概有10000个
这产生了大量的流量。
该测试需要Telepathy才能不断开客户端。
这个示例的代码比较简单,就是纯创建10000个cube矩阵。
如下所示:
public class BenchmarkIdleNetworkManager : NetworkManager
{
[Header("Spawns")]
public int spawnAmount = 10_000;
public float interleave = 1;
public GameObject spawnPrefab;
[Range(0, 1)] public float spawnPositionRatio = 0.01f;
System.Random random = new System.Random(42);
void SpawnAll()
{
//for循环,创建10000个目标
}
public override Transform GetStartPosition()
{
startPositions.RemoveAll(t => t == null);
if (startPositions.Count == 0)
return null;
int index = random.Next(0, startPositions.Count); // DETERMINISTIC
return startPositions[index];
}
public override void OnStartServer()
{
base.OnStartServer();
SpawnAll();
}
BenchmarkPrediction示例
Mirror的Predicted预测基准测试,是针对低端设备/VR进行的优化。
在不与对象交互的情况下,开销为零!
在交互过程中,开销来自同步和校正。
这个基准预测了不断同步和校正的对象。
=>真实游戏中效果会比这个示例好,因为这是我们可以用于分析的最坏情况!
NetworkManagerPredictionBenchmark适用于创建1000个物理小球,
RandomForce:是物理小球上的网络组件,用于不同客户端,不间断的给物理小球不停地施加随机的力,代表了真实物理世界中,不同客户端的玩家,同时操作处理同一个物理网络对象,造成的延迟和不稳定。
public class RandomForce : NetworkBehaviour
{
public float force = 10;
public float interval = 3;
PredictedRigidbody prediction;
Rigidbody rb => prediction.predictedRigidbody;
void Awake()
{
prediction = GetComponent<PredictedRigidbody>();
}
public override void OnStartClient()
{
float randomStart = Random.Range(0, interval);
InvokeRepeating(nameof(ApplyForce), randomStart, interval);
}
[ClientCallback]
void ApplyForce()
{
// calculate force in random direction but always upwards
Vector2 direction2D = Random.insideUnitCircle;
Vector3 direction3D = new Vector3(direction2D.x, 1.0f, direction2D.y);
Vector3 impulse = direction3D * force;
rb.AddForce(impulse, ForceMode.Impulse);
CmdApplyForce(impulse);
}
[Command(requiresAuthority = false)] // everyone can call this
void CmdApplyForce(Vector3 impulse)
{
rb.AddForce(impulse, ForceMode.Impulse);
}
我们注意到,OnStartClient中,每个客户端都会开启一个定时器,不时的给小球施加一个力,然后调用了Command,把命令发送给服务器的同时,客户端自身也执行rb.AddForce,为什么这么做呢?
-
- 本地预测:
当客户端调用 rb.AddForce 时,它利用 PredictedRigidbody 组件在本地进行物理计算。这种方法让客户端可以立即感受到推动的效果,无需等待服务器的响应,从而提供更流畅、即时的反馈给玩家。
- 本地预测:
-
- 网络同步:
同时,CmdApplyForce 方法是一个 Command 方法,用于将力的应用请求发送到服务器。即使客户端立即对物体施加了力,该方法仍然会在服务器上执行一次相同的操作。这样,其他客户端也能看到这一变化,确保所有客户端的状态一致。
- 网络同步:
-
- 冲突说明:
由于物理计算是基于每个客户端本地进行的,所以如果网络延迟或不同的客户端施加的力不同,预测可能与服务器的实际状态有所偏差。
PredictedRigidbody 组件的设计宗旨是处理这些冲突。当服务器收到命令执行 rb.AddForce 时,物理效果会被应用,但如果与当前客户端的状态不一致,PredictedRigidbody 将自动处理这些调整,以确保同步。
- 冲突说明:
-
- 并发效果:
正如注释所述,这个方法允许每个连接的客户端对所有对象施加影响。因此,随着客户端数量的增加,场景的行为会变得更加复杂且动态。这种并发处理能够在基准测试时体现网络和同步机制的能力。
- 并发效果:
使用这种方式的目的是为了在保持快速响应和流畅玩家体验的同时,确保每个玩家都能看到一致的游戏状态,从而实现更好的用户体验和网络表现。