多人游戏联网时,客户端修改一个字段到底会有多严重?很多独立开发者或刚接触联机功能的同学,都以为改一下本地数据、同步一下就行了,但实际一上线就被外挂打爆。这篇我们专门聊聊“客户端本地修改NetworkBehaviour字段”到底会发生什么,为什么不同步,又怎么用数据校验和防作弊手段把漏洞补上。
一、先搞清楚一个核心:NetworkBehaviour的字段到底是谁说了算
Unity Netcode for GameObjects里,NetworkBehaviour是挂像NetworkObject那样的网络组件。它的核心运作思路是:服务器(host)拥有一份权威的数据,所有客户端都是“租客”,只能看到和操作这间屋子里的基础家具,但不能擅自改承重墙。
就好比打麻将,服务器是“庄家”,四个客户端是“玩家”。麻将桌(服务器)知道每个人手里有什么牌,谁胡了谁放炮,都由庄家统一判定。客户端自己手上的牌可以看,但不能直接改牌堆。谁敢把牌偷偷塞到自己手里,这就是作弊,大家都不跟你玩了。
1.1 NetworkBehaviour字段的同步机制
我们来看一个最简单的网络字段,用Unity官方推荐的NetworkVariable:
// 技术栈:Unity Netcode for GameObjects (C#)
using Unity.Netcode;
public class PlayerHealth : NetworkBehaviour
{
// 定义一个网络同步的HP变量
// NetworkVariable会自动在服务器和客户端之间同步
private NetworkVariable<int> health = new NetworkVariable<int>(
100, // 初始血量 = 100
NetworkVariableReadPermission.Everyone, // 所有人都能读
NetworkVariableWritePermission.ServerOnly // 只有服务器能写
);
public void TakeDamage(int damage)
{
// 只有在服务器上才允许修改
if (!IsServer)
{
return; // 客户端调用就直接忽略,改不了
}
health.Value -= damage;
}
}
注意看上面这段代码的注释。NetworkVariable有写入权限和读取权限。默认情况下,如果你写ServerOnly,只有服务器可以修改值。这个设计思路就是“服务器权威”。
可问题来了:很多刚开始接触Multiplayer的开发者,用的是普通C#字段,或者虽然用了NetworkVariable但把写入权限改成了Everyone,那客户端就能直接改。
// 技术栈:Unity Netcode for GameObjects (C#)
// 反面教材:这是一个允许客户端写入的网络变量
public class PlayerScore : NetworkBehaviour
{
public NetworkVariable<int> score = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone, // 谁都能读
NetworkVariableWritePermission.Everyone // 谁都能写 —— 这行就是漏洞大门
);
}
这一行Everyone意味着任何一个客户端可以直接修改自己的分数。而且更噎人的是,有时候你改了本地值,服务器上竟然也变了,因为默认是会同步的。但真正到了复杂的逻辑运算、碰撞判定、位置同步这些场景,就会炸得你头皮发麻。
二、典型的应用场景:看似正常,实际上到处是坑
我们拿一个赛车游戏举个最简单的生活化例子。赛车游戏里的车辆位置通常是一个NetworkTransform组件控制的,由服务器驱动,同步给所有人看。但有些开发者嫌服务器驱动延迟高,就直接在客户端本地修改车辆位置,或者在角色身上挂一个NetworkBehaviour,然后直接改一个自定义字段表示“我超速了”或者“我被打了”。
具体表现是什么样子的?
2.1 典型场景一:本地修改血量字段
暴力的外挂就是直接把health调成99999。用Cheat Engine挂到游戏进程上,找到一个内存地址,疯狂改数值。改完之后,其实服务器如果用的ServerOnly权限,压根不会鸟你,但你看到的是自己满血,别人打你永远不掉血。这是因为本地客户端渲染的数值是你改过的本地值,而服务器还是按照正确的血量在算。
我们详细演示一下为什么不同步:
// 技术栈:Unity Netcode for GameObjects (C#)
// 客户端本地错误修改示例(错误示范)
public class PlayerHacky : NetworkBehaviour
{
private NetworkVariable<int> bulletCount = new NetworkVariable<int>(
30,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Everyone // 危险权限
);
private int localAmmoPreview = 30;
void Update()
{
// 玩家按F键,试图把子弹加到9999
if (Input.GetKeyDown(KeyCode.F) && !IsServer)
{
// 直接修改网络变量
bulletCount.Value = 9999;
// 你还想在本地让自己看到立刻变成9999
localAmmoPreview = bulletCount.Value;
}
}
}
上面这段代码如果放进游戏里运行,会看到什么?看的人觉得你改了,自己也觉得改了。但请注意:服务器上的代码如果在别的地方也读取bulletCount去判定逻辑,比如“子弹够不够发射”,那你的作弊就真的生效了。因为你给的是EveryOne权限,服务器本人也认为你的子弹就是9999,你直接等于打开了后门。
2.2 典型场景二:手动改本地Transform位置
还有一种常见骚操作,客户端直接改transform.position。如果你没有用网络组件控制位置,而是手动给本地角色赋位置,接下来所有玩家的屏幕上你的位置都会出现穿墙、瞬移、站在空中。
// 技术栈:Unity Netcode for GameObjects (C#)
// 端口:错误地修改本地位置
public class NaughtyPlayerMove : NetworkBehaviour
{
private void FixedUpdate()
{
if (!IsLocalPlayer) return;
// 直接修改Unity本地位置组件
// 这个操作立刻改变了渲染表现,但只影响本地表现
float speed = 5f;
transform.position += new Vector3(Input.GetAxis("Horizontal") * speed * Time.fixedDeltaTime, 0f, 0f);
// 问题:根本没有跟服务器同步这个变化
// 服务器上的位置还是旧的,其他的客户端看到你飘在空中乱跑
}
}
很多新手第一次写联机移动就栽在这。位置确实动了,自己看到丝滑顺畅,但别的玩家看到你瞬移或者卡在原地。因为你绕过了服务器权威,抢了“庄家”的活,结果就是帐对不上。
我这种生活方式形容一下:这就好比你在办公室里用打印机打了一份自己盖章的工资单,财务那边的系统里根本没你的记录,你拿给门口保安看,保安不认,你的同事也不认。
三、为什么本地修改一定会导致不同步:深入剖析内在机理
我们追根问底,看看网络字段同步的底层数据流。一个网络对象的数据从客户端到服务器,再到其他客户端,要经过一个完整的“数据校验—序列化—传输—反序列化”流程。Netcode框架会在发送端生成数据快照,在接收端对比快照。如果你本地改了值,这个值本身可以脏标记同步给服务器,问题是服务器不认商店发票,它只认自己的账本。
3.1 State Synchronization(状态同步)工作原理
State同步就是每隔一段时间,把属性值变化的信息广播出去。Netcode有个默认的NetworkTick系统,每秒跑30帧逻辑。服务器会在每个Tick中,比对所有NetworkVariable的当前值是否发生变化,如果变了就打包同步给所有客户端。
如果你是客户端,修改了Everyone权限的NetworkVariable,框架会把修改消息发给服务器。服务器收到后,如果权限允许,它就会更新自己的值;然后服务器再把这个值广播给所有其他客户端。看起来是能同步,但正因为每个人都参与修改,数据的一致性就得不到保障。
举个例子,假如有两个客户端都在同时修改一个“玩家金币”字段:
// 技术栈:Unity Netcode for GameObjects (C#)
// 展示多人同时写入同一个 NetworkVariable 的灾难场景
public class Wallet : NetworkBehaviour
{
public NetworkVariable<int> coins = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Everyone // 谁能想到一改就出事
);
}
// 客户端A有100金币,想给自己加50
// 客户端B也有100金币,也想给自己加50
// 服务器收到的顺序可能是A先来:100+50=150,B后到:150+50=200
// 也可能B先到:100+50=150,A后到:150+50=200
// 两者结果一样,但如果是衰减操作呢?A扣50,B扣30,最终到底是70还是50?取决于顺序
// 一旦顺序不一致,所有客户端看到的结果就全乱了
然后如果客户端A网络拥堵,B先被处理了,结果又不一样。更可怕的是,如果你本地改了之后,因为某些原因反序列化失败,导致你本地和服务器不一致,Netcode会以服务器为准回滚,你会发现你刚加的99999突然消失,或者瞬移回去。某种意义上这是系统的自我保护,但对于玩家体验来说是“不同步”的直观体验。
3.2 客户端预测带来的陷阱
很多FPS或动作游戏为了减少延迟,让本地玩家操作时立即看到反馈,这叫客户端预测(Client Prediction)。这时候本地修改字段是常规操作。但你必须有服务器权威回滚机制,服务器每帧发真正的状态快照给客户端,客户端发现本地预测和服务器实际数据偏差过大,就要回滚校正。
你如果自己写NetworkBehaviour字段且不做回滚,那就是在预测和真实之间横跳。比如你按“前进”,本地位置变了,服务器同步还没到位,帧率一抖,你看自己穿过了一堵墙。画面虽然灵异,但服务器上下一次Tick把你拉回到墙外面,人眼感知就是“滑步”甚至“瞬移”。
四、数据校验与防作弊的实战方案
这部分才是硬菜。我们分四步走,层层递进,教你怎么建“堡垒”。
4.1 第一步:定义服务器权威的底层规则
一切数据修改以服务器为准。所有字段一律使用ServerOnly写入权限。任何需要变化的数值,都必须由服务器来改。
// 技术栈:Unity Netcode for GameObjects (C#)
// 正确的网络变量权限设置
public class PlayerStats : NetworkBehaviour
{
// 血量:所有人可读,仅服务器可写
public NetworkVariable<int> Health = new NetworkVariable<int>(
100,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.ServerOnly
);
// 分数:同上,服务器写
public NetworkVariable<int> Score = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.ServerOnly
);
// 位置偏移:如果你自己手动管理位置,也只用服务器写
public NetworkVariable<Vector3> Position = new NetworkVariable<Vector3>(
Vector3.zero,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.ServerOnly
);
}
注意这里有个细节:NetworkVariable本身只能同步服务器上的状态,如果改成ServerOnly,客户端发过来的修改请求是会被拒收的。
那你可能会问:客户端想移动怎么办?不能直接改字段,但可以发“请求”。
4.2 第二步:使用ServerRpc发送修改请求
客户端的正确姿势是发一个ServerRpc,让服务器去修改,改完再由服务器广播给大家。可以把它理解为“我向领导提交申请,领导审批通过后才生效”。
// 技术栈:Unity Netcode for GameObjects (C#)
// 通过ServerRpc发送修改请求给服务器的正确模式
public class PlayerController : NetworkBehaviour
{
private NetworkVariable<int> ammo = new NetworkVariable<int>(
30,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.ServerOnly
);
// 客户端调用这个方法申请加子弹
[ClientRpc]
public void RequestAddAmmoClientRpc(int amount)
{
// 只在客户端上执行
if (!IsLocalPlayer) return;
// 调用ServerRpc向服务器请求
AddAmmoServerRpc(amount);
}
// 这个代码运行在服务器上
[ServerRpc]
private void AddAmmoServerRpc(int amount)
{
// 服务器校验数值合法性
if (ammo.Value + amount > 999) // 假设上限999
{
return; // 非法请求直接拒绝
}
// 服务器修改
ammo.Value += amount;
// 服务器会自动同步给所有人
// 也可以发ClientRpc通知客户端处理一下UI特效
NotifyAmmoChangedClientRpc(ammo.Value);
}
[ClientRpc]
private void NotifyAmmoChangedClientRpc(int newAmmo)
{
// 所有客户端包括本地,更新UI
Debug.Log($"当前弹药:{newAmmo}");
}
}
这个例子里,服务器收到改弹药的请求,第一件做的事就是“校验”:数值是否越界?是否符合逻辑?比如你用枪射击,服务器要检查你有没有开枪的冷却、当前弹药是否够等。这个校验过程才是防作弊的核心。
再看一个移动的例子。客户端发位置,服务器校验速度是否超限:
// 技术栈:Unity Netcode for GameObjects (C#)
// 服务端权威移动位置同步
public class PlayerMove : NetworkBehaviour
{
private NetworkVariable<Vector3> serverPosition = new NetworkVariable<Vector3>(
Vector3.zero,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.ServerOnly
);
private float maxSpeed = 7f; // 允许的最大移动速度(米/秒)
void Update()
{
if (!IsLocalPlayer) return;
// 本地玩家每秒发送多次移动请求
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 moveInput = new Vector3(horizontal, 0f, vertical) * Time.deltaTime;
// 发送给服务器
RequestMoveServerRpc(moveInput);
}
[ServerRpc]
private void RequestMoveServerRpc(Vector3 deltaMove)
{
// 关键校验:检查速度是否超限
// 用帧间隔来估算移动速度
float magnitude = deltaMove.magnitude;
float estimatedSpeed = magnitude / Time.fixedDeltaTime;
if (estimatedSpeed > maxSpeed)
{
// 超速了!相当于想作弊瞬移
Debug.LogWarning("检测到非法移动速度,拒绝。");
return;
}
// 合理的移动才更新服务器位置
serverPosition.Value += deltaMove;
// 这个变量同步后,所有客户端(包括自己)都会收到新位置
// 这里不需要你手动赋值给 transform,你可以监听变量变化回调来更新
}
// 监听网络变量变化
private void Awake()
{
serverPosition.OnValueChanged += OnServerPositionChanged;
}
private void OnServerPositionChanged(Vector3 oldValue, Vector3 newValue)
{
// 同步到本地Transform
transform.position = newValue;
}
}
你可能已经发现了:客户端根本没有直接改位置的能力,它只能向服务器“申请”,服务器查“超不超速”,没问题才准。这就是“服务器权威+数据校验”,双重保险。
4.3 第三步:服务器定期校验 + 状态比对
如果服务器想再加一层保险,就需要“定期校验快照”。比如每5秒检查一次所有玩家数据的合理性,看血量、金币、位置是否越界。
// 技术栈:Unity Netcode for GameObjects (C#)
// 服务器定时快照校验
public class GameCheatGuard : NetworkBehaviour
{
private float checkTimer = 0f;
private const float CHECK_INTERVAL = 5f;
// 玩家状态快照字典
private Dictionary<ulong, PlayerSnapshot> snapshots = new Dictionary<ulong, PlayerSnapshot>();
private void Update()
{
if (!IsServer) return; // 只让服务器检查
checkTimer += Time.deltaTime;
if (checkTimer < CHECK_INTERVAL) return;
checkTimer = 0f;
ValidateAllPlayers();
}
private void ValidateAllPlayers()
{
// 遍历所有玩家,检查数据是否超出合理范围
foreach (var player in FindObjectsOfType<PlayerStats>())
{
// 检查血量范围
if (player.Health.Value > 100)
{
Debug.LogWarning($"玩家 {player.OwnerClientId} 血量异常,重置为100");
player.Health.Value = 100;
}
// 检查分数是否突变
if (player.Score.Value > snapshots[player.OwnerClientId].LastScore + 50)
{
Debug.LogWarning($"玩家 {player.OwnerClientId} 分数增长异常");
// 可以冻结或者上报反作弊系统
}
}
}
// 这个结构体用于保存玩家历史数据快照
private struct PlayerSnapshot
{
public int LastHealth;
public int LastScore;
public Vector3 LastPostion;
}
}
这种定期校验适合模拟类游戏、休闲游戏。缺点是实时性不强,但对于低成本防作弊足够了。
4.4 第四步:客户端本地预测的“可回滚”设计
如果你坚持要做客户端预测,那一定要把回滚做好。回滚的意思是我本地先算,服务器结果一来,如果不一样,本地就得乖乖用服务器的值覆盖。
实现方式可以这样:
// 技术栈:Unity Netcode for GameObjects (C#)
// 客户端预测 + 服务器权威位置同步(简化版)
public class PredictedPlayer : NetworkBehaviour
{
private NetworkVariable<Vector3> serverPosition = new NetworkVariable<Vector3>(
Vector3.zero,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.ServerOnly
);
// 本地预测的位置
private Vector3 predictedPosition;
private void Update()
{
if (!IsLocalPlayer) return;
float moveX = Input.GetAxis("Horizontal") * 5f * Time.deltaTime;
float moveZ = Input.GetAxis("Vertical") * 5f * Time.deltaTime;
// 1. 本地预测先行
predictedPosition += new Vector3(moveX, 0f, moveZ);
transform.position = predictedPosition;
// 2. 发送预测位置给服务器
RequestMoveServerRpc(predictedPosition);
}
[ServerRpc]
private void RequestMoveServerRpc(Vector3 requestedPosition)
{
// 服务器做碰撞检测和速度判定后,将合法位置同步回所有客户端
serverPosition.Value = SanitizePosition(requestedPosition);
}
private Vector3 SanitizePosition(Vector3 rawPosition)
{
// 撞墙检测、限定地图边界等
float x = Mathf.Clamp(rawPosition.x, -10f, 10f);
float z = Mathf.Clamp(rawPosition.z, -10f, 10f);
return new Vector3(x, 0f, z);
}
private void Awake()
{
// 监听服务器同步过来的新位置,如果和预测有偏差,就回滚
serverPosition.OnValueChanged += OnServerPositionChanged;
}
private void OnServerPositionChanged(Vector3 oldPos, Vector3 newPos)
{
// 如果偏差过大,就立即回滚到服务器位置
if (Vector3.Distance(predictedPosition, newPos) > 0.1f)
{
predictedPosition = newPos;
transform.position = newPos;
Debug.Log("位置偏差过大,回滚到服务器权威位置");
}
}
}
这样玩家操作时丝滑,又不至于产生作弊空间。这个方案适合动作类游戏。
五、关联技术扩展:真正的反作弊组合拳
数据校验是防作弊的基础,但它不是银弹。真实游戏项目一般还会搭配下面几项“关联技术”。
5.1 混淆/签名加密
即使你不让客户端直接写字段,外挂仍然可以Hook到内存里修改ServerRpc的参数,比如伪装成“合法请求”把金币+9999发给服务器。所以我们需要对消息做加密签名。
使用简单的Hmac签名:
// 技术栈:Unity + C# 消息签名示例(伪代码)
using System.Security.Cryptography;
using System.Text;
public static class MessageSigner
{
// 客户端和服务端共享密钥(需要更安全的分发方式)
private const string SharedKey = "your-secret-key";
public static string Sign(string message)
{
using HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(SharedKey));
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
return Convert.ToBase64String(hash);
}
public static bool Verify(string message, string signature)
{
string expected = Sign(message);
// 固定时间比较防止时间攻击
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature)
);
}
}
然后把ServerRpc的参数全部带上签名,服务端先验签,验签失败就丢弃请求,这样即使外挂抓到请求包也无法伪造。
5.2 心跳机制与玩家行为统计
服务器可以统计每个客户端发来的请求频率。比如一秒发20次移动请求是正常的,但如果一秒发200次请求,毫无疑问是脚本。可以做一个简单的异常行为检测:
// 技术栈:Unity Netcode (C#) 服务器检测请求频率
public class AntiScript : NetworkBehaviour
{
private Dictionary<ulong, Queue<DateTime>> requestTimes = new();
[ServerRpc]
public void MoveRequestServerRpc()
{
ulong clientId = NetworkManager.Singleton.LocalClientId;
if (!requestTimes.ContainsKey(clientId))
{
requestTimes[clientId] = new Queue<DateTime>();
}
var queue = requestTimes[clientId];
queue.Enqueue(DateTime.UtcNow);
// 保留最近1秒的请求记录
while (queue.Count > 0 && (DateTime.UtcNow - queue.Peek()).TotalSeconds > 1)
{
queue.Dequeue();
}
// 如果超过合理的请求次数,判定为脚本
if (queue.Count > 30) // 1秒内超过30次移动请求
{
Debug.LogWarning($"玩家 {clientId} 疑似使用脚本");
// 可踢出房间
}
}
}
5.3 配置项与热更新
不要把所有字段的合规值统统写死在客户端。因为客户端反编译后就能看到“服务器允许的最大金币是99999”,然后专门把协议卡到阈值边缘,细水长流地刷金币。应该把校验规则放在服务器端并且用配置中心动态下发,让校验规则也具备更新能力。但注意不要跑题太远,这里只需记住逻辑是“即使客户端知道上限,他也无法突破服务器的实时规则”。
六、技术优缺点分析
我们把整条方案摆在天平上称一下重量。
6.1 优点
第一,安全性大幅提升。服务器权威带来的默认行为就是“客户端改了等于白改”,让作弊成本翻倍。第二,数据一致性变好,不会出现“你自己看到满血,别人看到你死透”的撕裂画面。第三,逻辑集中在服务器,后期做热更新、活动配置、游戏规则迭代都非常方便。比如你不想让某种武器伤害降低,改服务器配置就行,不用每个客户端发版本。
第四,防止篡改内存的内存扫描强度降低,因为关键字段都在服务器上,客户端内存里只有副本,改了也没用。第五,Combo防作弊方案组合灵活,可以针对自家游戏类型挑选,运动类用位置校验,数值类用ServerRpc+限额,休闲类用定时快照。
6.2 缺点
第一,开发复杂度高,你得把百分之五六十的网络交互逻辑都迁到服务器上,服务器压力成倍增加。第二,玩家体验容易出现延迟感,因为所有操作都要服务器确认。你需要引入客户端预测、插值平滑,代码量蹭蹭往上涨。第三,对重逻辑、物理碰撞模拟类游戏(比如某些物理互动很强的派对游戏)来说,服务器回放物理效果非常消耗资源,需要极强的服务器架构能力。
第四,过分信任服务器可能导致服务器代码本身成为最大的安全短板,服务器被入侵那整个信任体系崩塌。第五,测试成本增加,你需要模拟各种路由丢包和恶意客户端注入请求,QA团队的杯子里总能冲出各种稀奇古怪的bug。
七、写代码时的注意事项
结合多年踩坑经验,我额外给你几条私人建议:
第一,不要用普通C#字段去做跨端同步。哪怕你再懒,也得用NetworkVariable或者自己封装网络字段结构体,否则代码逻辑在多人模式下会直接崩给你看。
第二,ServerRpc的参数列表里尽量不要传浮点数数组、字典这种复杂对象,网络包体变大,性能下降,而且反序列化过程中容易受攻击。
第三,一定要做客户端版本校验。如果你改了服务器端校验规则,得同步发新版本客户端,不然老客户端还在用旧的“合法操作流程”收发请求。不排除有人拿着旧版客户端,故意不升级,利用老规则做坏事。
第四,日志一定要留全。服务器检测到异常数据或作弊请求时,一定记得把日志打印出来,里面包含ClientId、时间戳、操作类型、请求参数。这样运营同学封号时有理有据,程序员定位Bug时也有迹可循。
第五,别全网广播玩家的私密数据。比如玩家的背包物品、内部评分这些不该被其他人看到的字段,放到单独的网络对象或使用目标Rpc定向发送,作为本机与服务器之间传输,别放NetworkVariable里同步给所有人。
第六,数据校验顺序一定要“先校验后更新”。任何ServerRpc第一行必须先验证参数,再执行数据修改。我见过很多同学先改了值,发现越界再改回来,中间那一步就让作弊者抓到机会利用竞态条件刷物品。
八、文章总结
总结成一句话:本地客户端里所有动来动去的NetworkBehaviour字段,都应该被服务器牢牢攥在手心。你要做的是给客户端开一扇“申请窗口”,而不是直接把保险箱钥匙发给每个玩家。客户端想改血量?请发申请;想改位置?请发申请;想改分数?请发申请。服务器收到申请,先查心率再捋袖子,合理就批,无理取闹就直接驳回。结合ServerRpc、权限控制、定期快照校验、消息签名,再辅以客户端预测与回滚,能将大部分常见的修改器、脚本、变速器挡在门外。
当然,服务器权威并不是免死金牌。对于特别狂热的作弊者来说,逆向是拦不住的。我们能做到的,是尽量提高作弊成本和门槛,让你的游戏在普通玩家群体里保持公平干净。架构落地的时候好好做数据校验,长远看来是给自己和你真正的玩家省了一大笔血泪钱。
评论
围绕“客户端修改本地NetworkBehaviour字段导致不同步,数据校验与防作弊”参与讨论