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

Unity Netcode主机与客户端伤害同步及UI更新异常修复求助

Unity Netcode 伤害同步UI更新异常问题修复

问题现象

使用Unity Netcode开发多人游戏时出现伤害同步异常:客户端攻击主机时功能正常,但主机攻击客户端时,health脚本内的NetworkVariable数值已变更,UI图标颜色却未同步更新。尝试使用ServerRpc与ClientRpc未解决问题。

相关代码

健康脚本(health.cs)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Unity.Netcode;

public class health : NetworkBehaviour
{
    [SerializeField] 
    private NetworkObject NetworkObject;
    [Space]
    [SerializeField] 
    private bool isHitBox;
    [SerializeField] 
    private health basicHealth;
    [SerializeField] 
    private int healthPartsNumber;
    [SerializeField] 
    private GameObject SelfCanvas;
    [Header("Basic")]
    [SerializeField] 
    private NetworkVariable<int>[] healthParts; 
    [SerializeField] 
    private Image[] iconParts;

    private void Start() 
    {
        if (!NetworkObject.IsLocalPlayer && !isHitBox)
        {
            SelfCanvas.SetActive(false);
        }
    }

    private void Update() 
    {
        if (!isHitBox && NetworkObject.IsLocalPlayer)
        {
            for (int i = 0; i < iconParts.Length; i++)
            {
                if (healthParts[i].Value == 100 && healthParts[i].Value > 61)
                {
                    iconParts[i].color = Color.green;
                }

                if (healthParts[i].Value <= 61 && healthParts[i].Value > 31)
                {
                    iconParts[i].color = Color.yellow;
                }

                if (healthParts[i].Value <= 31 && healthParts[i].Value > 1)
                {
                    iconParts[i].color = Color.red;
                }

                if (healthParts[i].Value <= 0)
                {
                    iconParts[i].color = Color.gray;
                }
            }
        }
    }

    [ServerRpc(RequireOwnership = false)]
    public void GetDamageServerRpc(int Damage)
    {
        basicHealth.healthParts[healthPartsNumber].Value -= Damage;
    }
}

枪械脚本(weapon.cs)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using Unity.Netcode;

public class weapon : MonoBehaviour
{
    [SerializeField] private NetworkObject NetworkObject;
    [Space]
    [SerializeField] private float range = 1000f;
    [SerializeField] private Transform raycastOrigin;
    [Header("Settings")]
    [SerializeField] private int damage;
    [Space]
    [SerializeField] private int bulletsMax;
    [Space]
    [SerializeField] private bool canChangeMode;
    [SerializeField] private ShootType shootType = 0;
    [Space]
    [SerializeField] private float fireRate;
    [Header("particles")]
    [SerializeField] private ParticleSystem muzzleFlash;
    [Header("gun recoil")]
    [SerializeField] private recoil recoil;
    [SerializeField] private float recoilX;
    [SerializeField] private float recoilY;
    [SerializeField] private float recoilZ;
    [Space]
    [SerializeField] private float snappiness;
    [SerializeField] private float returnSpeed;
    [Header("camera recoil")]
    [SerializeField] private recoil cameraRecoil;
    [SerializeField] private float camRecoilX;
    [SerializeField] private float camRecoilY;
    [SerializeField] private float camRecoilZ;
    [Space]
    [SerializeField] private float camSnappiness;
    [SerializeField] private float camReturnSpeed;
    

    private enum ShootType {auto, single}

    private int bulletNow;
    private bool canShoot;
    private float fireTime;
    private menu menu;
    private bool isOn;

    // for logs
    private TMP_Text shootTypeLog;
    private TMP_Text firedLog;
    private TMP_Text bulletsLog;

    private void Start() 
    {
        if (!NetworkObject.IsLocalPlayer)
        {
            enabled = false;
        }
        // logs
        shootTypeLog = GameObject.Find("shootTypeLog").GetComponent<TMP_Text>();
        firedLog = GameObject.Find("firedLog").GetComponent<TMP_Text>();
        bulletsLog = GameObject.Find("bulletsLog").GetComponent<TMP_Text>();
        
        bulletNow = bulletsMax;
    }

    private void FixedUpdate()
    {
        if (recoil != null)
        {
            recoil.RecoilParams(recoilX, recoilY, recoilZ, snappiness, returnSpeed);
        }
        else
        {
            recoil = GameObject.FindGameObjectWithTag("local recoil").GetComponent<recoil>();
        }

        if (cameraRecoil != null)
        {
            cameraRecoil.RecoilParams(camRecoilX, camRecoilY, camRecoilZ, camSnappiness, camReturnSpeed);
        }
        else
        {
            cameraRecoil = GameObject.FindGameObjectWithTag("local recoil cam").GetComponent<recoil>();
        }

        if (menu != null)
        {
            isOn = menu.GetMenuStatus();
        }
        else
        {
            menu = GameObject.Find("Canvas").GetComponent<menu>();
        }
        
        fireTimer();

        if (!isOn)
        {
            // change fire type
            if (Input.GetKeyDown(KeyCode.V) && canChangeMode)
            {
                if (shootType == ShootType.auto)
                {
                    shootType = ShootType.single;
                }
                else
                {
                    shootType = ShootType.auto;
                }
            }

            // reload
            if (Input.GetKey(KeyCode.R) && bulletNow != bulletsMax)
            {
                bulletNow = bulletsMax;
            }

            // shoot
            if (shootType == ShootType.auto)
            {
                if (Input.GetMouseButton(0) && bulletNow != 0 && canShoot)
                {
                    Fire();
                }
            }
            else
            {
                if (Input.GetMouseButtonDown(0) && bulletNow != 0 && canShoot)
                {
                    Fire();
                }
            }
        }
 
        //logs
        shootTypeLog.text = $"shootType: {shootType}";
        firedLog.text = $"fired: {canShoot}";
        bulletsLog.text = $"bullets: {bulletNow}";
    }

    private void Fire()
    {
        bulletNow--;
        canShoot = false;
        recoil.RecoilFire();
        cameraRecoil.RecoilFire();

        ParticleSystem mz = Instantiate(muzzleFlash, raycastOrigin);
        // mz.GetComponent<NetworkObject>().Spawn(true);
        mz.GetComponent<connectToOrigin>().ConnectOrigin(raycastOrigin);

        RaycastHit hit;
        if (Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out hit, range))
        {
            health health = hit.collider.GetComponent<health>();
            if (health != null)
            {
                health.GetDamageServerRpc(damage);
            }
        }
    }
    
    private void fireTimer()
    {
        if (!canShoot)
        {
            if (fireTime >= 0)
            {
                fireTime -= Time.deltaTime;
                canShoot = false;
            }
            else
            {
                canShoot = true;
                fireTime = fireRate;    
            }
        }
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawRay(raycastOrigin.position, raycastOrigin.forward * range);
    }
}

问题分析

  1. NetworkVariable数组序列化失效:Unity无法正确序列化NetworkVariable<int>[]类型数组,服务器与客户端的数组引用无法同步,导致客户端无法感知数值变化。
  2. UI更新逻辑冗余且不可靠:依赖Update轮询检测数值变化,存在延迟或遗漏;同时第一个颜色判断条件healthParts[i].Value == 100 && healthParts[i].Value > 61逻辑错误,仅当数值为100时才触发绿色,不符合预期。
  3. 未利用NetworkVariable变化事件:没有监听NetworkVariable的OnValueChanged回调,无法在数值变更时立即触发UI更新。

解决方案

1. 替换NetworkVariable数组为NetworkList

使用Netcode官方支持的NetworkList<int>替代数组,它能正确实现网络同步与序列化:

// 替换原NetworkVariable<int>[]声明
[SerializeField] private int maxHealthPerPart = 100;
public NetworkList<int> healthParts = new NetworkList<int>();

在网络对象生成时初始化列表并绑定监听事件:

public override void OnNetworkSpawn()
{
    base.OnNetworkSpawn();
    if (IsServer)
    {
        // 服务器端初始化每个部位血量
        for (int i = 0; i < iconParts.Length; i++)
        {
            healthParts.Add(maxHealthPerPart);
        }
    }
    // 绑定血量变化监听
    healthParts.OnListChanged += OnHealthPartsChanged;
}

public override void OnNetworkDespawn()
{
    healthParts.OnListChanged -= OnHealthPartsChanged;
    base.OnNetworkDespawn();
}

2. 改用事件驱动的UI更新逻辑

移除Update中的轮询代码,通过NetworkList的变化事件触发UI更新:

private void OnHealthPartsChanged(NetworkListEvent<int> changeEvent)
{
    if (!isHitBox && NetworkObject.IsLocalPlayer)
    {
        UpdateHealthIcon(changeEvent.Index);
    }
}

private void UpdateHealthIcon(int index)
{
    int healthValue = healthParts[index];
    if (healthValue > 61)
    {
        iconParts[index].color = Color.green;
    }
    else if (healthValue > 31)
    {
        iconParts[index].color = Color.yellow;
    }
    else if (healthValue > 1)
    {
        iconParts[index].color = Color.red;
    }
    else
    {
        iconParts[index].color = Color.gray;
    }
}

3. 修正伤害处理ServerRpc

修改GetDamageServerRpc直接操作NetworkList,确保数值同步:

[ServerRpc(RequireOwnership = false)]
public void GetDamageServerRpc(int Damage)
{
    if (basicHealth != null && healthPartsNumber >= 0 && healthPartsNumber < basicHealth.healthParts.Count)
    {
        int newHealth = basicHealth.healthParts[healthPartsNumber] - Damage;
        basicHealth.healthParts[healthPartsNumber] = Mathf.Max(newHealth, 0);
    }
}

4. 初始化本地玩家UI状态

在网络对象生成时初始化本地玩家的UI图标颜色:

public override void OnNetworkSpawn()
{
    base.OnNetworkSpawn();
    if (!NetworkObject.IsLocalPlayer && !isHitBox)
    {
        SelfCanvas.SetActive(false);
    }
    else if (NetworkObject.IsLocalPlayer && !isHitBox)
    {
        // 初始化所有图标颜色
        for (int i = 0; i < iconParts.Length; i++)
        {
            UpdateHealthIcon(i);
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 22:22:05