Unity 使用 Mirror 框架做网络游戏客户端预测的角色控制器

这里写一个基于Mirror的完整PlayerNetworkController示范脚本,

集成了:

位置和旋转的客户端预测 + 服务器权威校正和平滑插值,

通过服务器同步的动画参数驱动客户端Animator(示范“移动速度”和“攻击状态”两个参数)

输入命令(包含移动方向和朝向旋转),带输入序号和重发机制,

代码注释详细,方便拖拽组件后直接使用 假设你的Animator有两个参数用于控制动画: Speed(float)- 移动速度大小 IsAttacking(bool)- 攻击状态,

代码简单移动,方便新手查看和进阶改造。

主要是新手对《客户端预测》这个看起来高大上的名词整的有点儿发怵,其实看看代码注释也能差不多懂了就。

主要是 Mirror 理解起来很容易,当你理解了之后,就可以自行切换其他网络框架了,当然高手自己写 TCP 或者 KCP 也行。

 using System.Collections.Generic;

using Mirror;
using UnityEngine;

public class NetworkPlayer : NetworkBehaviour
{
    // -------------------------------
    // 同步变量——服务器权威状态,同步给所有客户端。
  // [SyncVar]是Mirror里用于标记“变量需要在服务器与所有客户端间自动同步”的特性。它告诉Mirror这个字段的数据由服务器权威控制,任何变化都会自动广播更新给所有客户端。
  // hook参数的作用 当你给SyncVar加一个hook回调函数时,当这个变量的值在客户端发生变化时,除了自动刷新数据外,会额外调用命名为 OnPositionChanged 的方法,看hook也能大概懂这是个钩子回调吧。
[SyncVar(hook = nameof(OnPositionChanged))] private Vector3 syncPosition; [SyncVar(hook = nameof(OnRotationChanged))] private Quaternion syncRotation; [SyncVar(hook = nameof(OnSpeedChanged))] private float syncSpeed; // 本地预测变量 private Vector3 predictedPosition; private Quaternion predictedRotation; // 输入缓存 private struct InputData { public float horizontal; public float vertical; public int seq; } private List<InputData> pendingInputs = new List<InputData>(); // 当前输入序号 private int localInputSequence = 0; // 控制参数 public float moveSpeed = 5f; // 组件引用 private Animator animator; // 攻击动作锁定,防止重复攻击触发 private bool isAttacking = false; void Start() { animator = GetComponent<Animator>(); if (isLocalPlayer) { predictedPosition = transform.position; predictedRotation = transform.rotation; } } void Update() { if (isLocalPlayer) { GatherAndSendInput(); ApplyLocalPrediction(); // 本地输入攻击 if (Input.GetButtonDown("Fire1") && !isAttacking) { CmdRequestAttack(); isAttacking = true; } } else { // 插值移动其他玩家 SmoothMoveTo(syncPosition, syncRotation); } UpdateAnimation(); } // 收集输入,发送到服务器并缓存 private void GatherAndSendInput() { float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); InputData input = new InputData { horizontal = h, vertical = v, seq = localInputSequence }; localInputSequence++; pendingInputs.Add(input); CmdSendInput(input.horizontal, input.vertical, input.seq); } // 应用本地预测移动 private void ApplyLocalPrediction() { foreach (var input in pendingInputs) { Vector3 dir = new Vector3(input.horizontal, 0, input.vertical).normalized; predictedPosition += dir * moveSpeed * Time.deltaTime; if (dir != Vector3.zero) { predictedRotation = Quaternion.LookRotation(dir); } } transform.position = predictedPosition; transform.rotation = predictedRotation; } [Command] void CmdSendInput(float h, float v, int seq) { // 服务器权威处理移动 Vector3 dir = new Vector3(h, 0, v).normalized; Vector3 newPos = transform.position + dir * moveSpeed * Time.deltaTime; transform.position = newPos; syncPosition = newPos; if (dir != Vector3.zero) { Quaternion rot = Quaternion.LookRotation(dir); transform.rotation = rot; syncRotation = rot; } syncSpeed = dir.magnitude; // 给客户端确认输入消除延迟漂移 TargetConfirmPosition(connectionToClient, newPos, transform.rotation, seq); } [TargetRpc] void TargetConfirmPosition(NetworkConnection target, Vector3 pos, Quaternion rot, int seq) { if (!isLocalPlayer) return; // 清除已确认输入 pendingInputs.RemoveAll(i => i.seq <= seq); // 服务器权威位置与本地预测误差较大时平滑校正 float delta = Vector3.Distance(predictedPosition, pos); if (delta > 0.1f) { predictedPosition = Vector3.Lerp(predictedPosition, pos, 0.5f); predictedRotation = Quaternion.Slerp(predictedRotation, rot, 0.5f); transform.position = predictedPosition; transform.rotation = predictedRotation; } } // 位置变化回调,非本地玩家用插值平滑移动 void OnPositionChanged(Vector3 oldPos, Vector3 newPos) { if (isLocalPlayer) return; syncPosition = newPos; } void OnRotationChanged(Quaternion oldRot, Quaternion newRot) { if (isLocalPlayer) return; syncRotation = newRot; } void OnSpeedChanged(float oldSpeed, float newSpeed) { // 非本地玩家更新动画参数 if (!isLocalPlayer) { animator.SetFloat("Speed", newSpeed); } } void SmoothMoveTo(Vector3 targetPos, Quaternion targetRot) { transform.position = Vector3.Lerp(transform.position, targetPos, Time.deltaTime * 10f); transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, Time.deltaTime * 10f); } // 攻击命令服务器校验与广播 [Command] void CmdRequestAttack() { // 这里可以加服务器验证逻辑,比如冷却判断等 // 广播所有客户端播放攻击动画 RpcPlayAttackAnimation(); } // 所有客户端播放攻击动画 [ClientRpc] void RpcPlayAttackAnimation() { animator.SetTrigger("Attack"); // 主动重置本地本次攻击锁 if (isLocalPlayer) { StartCoroutine(ResetAttackLock()); } } // 重置攻击锁以允许下一次攻击触发 System.Collections.IEnumerator ResetAttackLock() { // 假设攻击动画长度约为0.7秒,实际可根据Animator设置 yield return new WaitForSeconds(0.7f); isAttacking = false; } // 同步动画速度参数到本地玩家animator void UpdateAnimation() { if (isLocalPlayer) { float speed = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).magnitude; animator.SetFloat("Speed", speed); } } }

 

posted @ 2025-11-06 13:53  孙公  阅读(4)  评论(0)    收藏  举报