多人游戏联网时,客户端修改一个字段到底会有多严重?很多独立开发者或刚接触联机功能的同学,都以为改一下本地数据、同步一下就行了,但实际一上线就被外挂打爆。这篇我们专门聊聊“客户端本地修改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、权限控制、定期快照校验、消息签名,再辅以客户端预测与回滚,能将大部分常见的修改器、脚本、变速器挡在门外。

当然,服务器权威并不是免死金牌。对于特别狂热的作弊者来说,逆向是拦不住的。我们能做到的,是尽量提高作弊成本和门槛,让你的游戏在普通玩家群体里保持公平干净。架构落地的时候好好做数据校验,长远看来是给自己和你真正的玩家省了一大笔血泪钱。