You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity C#多人游戏对象交互实现咨询(附代码片段)

Unity C# 多人游戏对象交互代码解析与完善

嘿,我看你这段代码是个基础的可交互对象基类,但有未完成的逻辑和多人游戏下的同步漏洞,我帮你补全并优化了,完全适配UNET的网络规则,确保服务器权威和多客户端同步,一起来拆解下:

原代码的问题点

  • Update里的距离判断写到一半断了
  • 没考虑服务器权威——多人游戏里不能让客户端直接触发交互,不然很容易被作弊
  • 缺少玩家选中交互对象的焦点逻辑
  • 没有处理交互状态的网络同步

完善后的完整代码

using UnityEngine;
using UnityEngine.Networking;

[RequireComponent(typeof(Collider))]
public class Interactable : NetworkBehaviour
{
    [Header("交互配置")]
    public float interactionRadius = 2f;
    public Transform interactionPoint;

    // 用SyncVar同步焦点状态到所有客户端,钩子函数处理状态变化
    [SyncVar(hook = nameof(OnFocusChanged))]
    private bool _isFocused;
    private Transform _localPlayer;
    private bool _hasInteracted;

    private void Awake()
    {
        // 默认用自身位置当交互点,避免空引用
        if (interactionPoint == null)
        {
            interactionPoint = transform;
            Debug.LogWarning($"[{gameObject.name}] 未设置交互点,默认使用自身位置");
        }
        // 强制碰撞体为触发器,用来检测玩家进入范围
        GetComponent<Collider>().isTrigger = true;
    }

    // 玩家进入范围时,本地客户端设置焦点并同步到服务器
    [ClientCallback]
    private void OnTriggerEnter(Collider other)
    {
        if (!isLocalPlayer) return;
        
        var playerIdentity = other.GetComponent<NetworkIdentity>();
        if (playerIdentity != null && playerIdentity.isLocalPlayer)
        {
            _localPlayer = other.transform;
            CmdSetFocus(true);
        }
    }

    // 玩家离开范围时取消焦点
    [ClientCallback]
    private void OnTriggerExit(Collider other)
    {
        if (!isLocalPlayer) return;
        
        var playerIdentity = other.GetComponent<NetworkIdentity>();
        if (playerIdentity != null && playerIdentity.isLocalPlayer)
        {
            CmdSetFocus(false);
            _hasInteracted = false;
            _localPlayer = null;
        }
    }

    // 本地客户端处理交互输入
    [ClientCallback]
    private void Update()
    {
        // 只允许本地玩家、在焦点状态下、未交互过的情况触发
        if (!isLocalPlayer || !_isFocused || _hasInteracted) return;

        float distance = Vector3.Distance(_localPlayer.position, interactionPoint.position);
        // 检测是否在交互范围内,且按下E键(可自行修改按键)
        if (distance <= interactionRadius && Input.GetKeyDown(KeyCode.E))
        {
            CmdPerformInteraction();
            _hasInteracted = true; // 本地标记已交互,防止重复触发
        }
    }

    // 向服务器发送设置焦点的请求
    [Command]
    private void CmdSetFocus(bool isFocused)
    {
        _isFocused = isFocused;
    }

    // 向服务器发送交互请求,服务器验证后执行逻辑
    [Command]
    private void CmdPerformInteraction()
    {
        // 服务器二次验证玩家是否真的在交互范围内,防止作弊
        if (_localPlayer == null) return;
        float distance = Vector3.Distance(_localPlayer.position, interactionPoint.position);
        if (distance > interactionRadius) return;

        // 执行核心交互逻辑(子类重写这个方法)
        ServerInteract();
        // 同步交互结果到所有客户端
        RpcOnInteracted();
    }

    // 服务器端的核心交互逻辑,子类重写实现具体功能
    protected virtual void ServerInteract()
    {
        Debug.Log($"[Server] 玩家{_localPlayer.name}与{transform.name}完成交互");
    }

    // 客户端同步交互完成的反馈(比如播放音效、动画)
    [ClientRpc]
    private void RpcOnInteracted()
    {
        OnClientInteracted();
    }

    // 客户端交互反馈,子类可重写
    protected virtual void OnClientInteracted()
    {
        Debug.Log($"[Client] 与{transform.name}交互完成");
    }

    // SyncVar钩子,焦点状态变化时触发(所有客户端都会执行)
    private void OnFocusChanged(bool oldValue, bool newValue)
    {
        _isFocused = newValue;
        // 这里可以加UI反馈,比如高亮对象
        if (newValue)
        {
            Debug.Log($"[{gameObject.name}] 被玩家选中");
        }
        else
        {
            Debug.Log($"[{gameObject.name}] 取消选中");
        }
    }

    // 编辑器里绘制交互范围,方便调试
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(interactionPoint.position, interactionRadius);
    }
}

关键逻辑拆解

1. 服务器权威是核心

多人游戏里绝对不能让客户端说了算,所有关键操作必须经过服务器验证:

  • 客户端只负责发请求(比如按下E键后发CmdPerformInteraction)
  • 服务器先验证玩家是否真在交互范围内,再执行交互
  • 用[ClientRpc]把交互结果同步给所有客户端,保证大家看到的状态一致

2. 焦点状态同步

用[SyncVar]标记_isFocused,配合钩子函数OnFocusChanged,让所有客户端都能同步看到对象是否被选中,避免本地状态不一致。

3. 子类怎么扩展?

如果要做具体的交互对象(比如开门、开宝箱),直接继承这个基类重写两个方法就行:

public class Door : Interactable
{
    [SyncVar] private bool _isOpen;
    public AudioClip openSound;

    protected override void ServerInteract()
    {
        _isOpen = !_isOpen;
        // 服务器端处理门的旋转逻辑
        transform.Rotate(0, _isOpen ? 90f : -90f, 0);
    }

    protected override void OnClientInteracted()
    {
        // 客户端播放开门音效
        AudioSource.PlayClipAtPoint(openSound, transform.position);
    }
}

4. 注意事项

  • 玩家对象必须加NetworkIdentity,并且勾选Local Player Authority
  • 交互对象也要加NetworkIdentity,但不要勾选Local Player Authority(服务器权威)
  • 如果你是新项目,更推荐用Unity Netcode for GameObjects(UNET是旧框架),但核心逻辑思路是一样的

内容的提问来源于stack exchange,提问作者calimses

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:36:17