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); } }
问题分析
- NetworkVariable数组序列化失效:Unity无法正确序列化
NetworkVariable<int>[]类型数组,服务器与客户端的数组引用无法同步,导致客户端无法感知数值变化。 - UI更新逻辑冗余且不可靠:依赖
Update轮询检测数值变化,存在延迟或遗漏;同时第一个颜色判断条件healthParts[i].Value == 100 && healthParts[i].Value > 61逻辑错误,仅当数值为100时才触发绿色,不符合预期。 - 未利用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
相关产品推荐
相关产品推荐

