
简介这是一份基于Unity3D引擎开发的双人联网跑酷游戏完整工程面向希望学习联网游戏开发的小白与进阶学习者可直接作为毕业设计、课程设计、大作业或工程实训项目使用。资源包共包含2005个文件压缩后约211.11MB其中353个fbx模型与93个tif贴图构成场景与角色美术资源346个prefab预制体、48个mat材质与38个cs脚本支撑游戏逻辑与联网同步另有controller、anim动画、asset配置及dll依赖等文件工程结构完整、模块划分清晰。目前已有279人学习下载。通过该工程读者可深入了解双人联网跑酷的同步机制、角色控制与关卡搭建思路掌握Unity网络组件与预制体组织方式并在此基础上进行二次开发或功能扩展是学习联网游戏开发与项目实战的实用参考。1. 双人联网跑酷从单机Demo到可联机对战的最小闭环跑酷游戏单机做起来快但一提到“双人联网”很多 Unity3D 开发者第一反应是“要不要上 Photon要不要自己写 Socket”。我见过太多项目卡在同步这一步角色位置一跳一跳、终点判定两边不一致、断线直接崩客户端。这个标题真正要解决的不是“跑酷怎么玩”而是两个玩家在不同设备上如何看到同一场比赛并且结果一致。适合已经会 Unity3D 基础操作、做过单机小游戏、想补上联网这块短板的从业者。读完你能拿到一套可复现的架构用 Unity3D 做客户端表现用 TCP 或 UDP 做状态同步用服务端权威判定防作弊最后在局域网或公网服务器上跑通双人竞速。下面按“先定同步模型再搭工程再填坑”的顺序拆开讲。2. 同步模型选型状态同步还是帧同步跑酷场景怎么定2.1 跑酷游戏对同步的真实需求跑酷的核心交互只有三件事横向移动、跳跃/下蹲、碰撞判定。双人联网时玩家 A 看到玩家 B 的位置必须足够平滑否则竞速体验直接崩。但跑酷又不像格斗游戏那样要求帧级精确它允许 100ms 左右的延迟只要终点判定不出现“我明明先到却判输”就行。常见做法有两种状态同步和帧同步。状态同步是客户端上报输入服务端计算位置后广播帧同步是只广播输入指令各客户端自己跑逻辑。跑酷游戏我一般选状态同步原因是跑酷地图长、机关多帧同步要求所有客户端逻辑完全一致浮点数误差累积后容易在终点线附近翻车状态同步虽然流量大一点但服务端权威判定干净。提示如果只是宿舍局域网双人玩状态同步的延迟完全可接受如果要上公网状态同步需要加插值和预测后面会讲。2.2 用 TCP 还是 UDP先看丢包代价Unity3D 自带的NetworkTransport或第三方库都支持 UDP但很多新手直接上 TCP。TCP 的问题是队头阻塞一个包丢了后面全卡住。跑酷游戏里玩家位置包丢一个最多是对方角色瞬移一下但如果用 TCP丢包会导致所有玩家位置一起延迟体验更差。我的选择是位置同步走 UDP关键事件走可靠通道。关键事件包括开始比赛、到达终点、触发陷阱。这些不能丢丢了就判定错误。实现上可以用 UDP 发位置用 TCP 发事件或者用支持可靠 UDP 的库如 ENet、LiteNetLib。下面是一个最小服务端广播逻辑的伪代码结构// Unity3D C# 服务端伪代码UDP 接收位置并广播 using System.Net; using System.Net.Sockets; UdpClient server new UdpClient(9050); IPEndPoint remoteEP new IPEndPoint(IPAddress.Any, 0); void Update() { while (server.Available 0) { byte[] data server.Receive(ref remoteEP); // data 格式玩家ID(1字节) x(4字节) y(4字节) z(4字节) // 直接广播给另一个客户端不做插值客户端自己处理 foreach (var client in connectedClients) { if (client ! remoteEP) { server.Send(data, data.Length, client); } } } }这段代码的逻辑是服务端只做转发不计算位置。参数说明端口 9050 可改data长度固定 13 字节玩家 ID 用来区分是谁。为什么不让服务端算位置因为跑酷移动逻辑简单客户端算完上报服务端只做合法性校验比如速度是否超上限这样服务端压力小延迟低。2.3 客户端插值让对手看起来不卡直接广播位置对方角色会一跳一跳。客户端需要做插值收到位置包后不立刻设置角色坐标而是记录两个包之间的时间差用Vector3.Lerp平滑过渡。常见参数是插值系数 0.1~0.3太小会延迟大太大会抖动。// 客户端插值示例 public Transform remotePlayer; private Vector3 targetPos; private float lerpFactor 0.2f; void OnReceivePosition(Vector3 pos) { targetPos pos; } void Update() { remotePlayer.position Vector3.Lerp(remotePlayer.position, targetPos, lerpFactor); }lerpFactor我一般设 0.2在 60fps 下大约 5 帧追上目标位置。如果网络抖动大可以改成根据延迟动态调整但新手先固定值跑通再说。3. 工程搭建Unity3D 双人跑酷的最小可跑通步骤3.1 场景与角色控制器的复用先做一个单机跑酷场景一条直道几个障碍物一个终点触发器。角色控制器用CharacterController或刚体都行我习惯用CharacterController因为跑酷不需要复杂物理。移动代码public class PlayerMove : MonoBehaviour { public float forwardSpeed 8f; public float laneDistance 3f; private CharacterController controller; private int currentLane 1; // 0左 1中 2右 void Start() { controller GetComponentCharacterController(); } void Update() { // 自动前进 Vector3 move Vector3.forward * forwardSpeed; // 左右切换 if (Input.GetKeyDown(KeyCode.A)) currentLane Mathf.Max(0, currentLane - 1); if (Input.GetKeyDown(KeyCode.D)) currentLane Mathf.Min(2, currentLane 1); float targetX (currentLane - 1) * laneDistance; move.x (targetX - transform.position.x) * 10f; controller.Move(move * Time.deltaTime); } }这段代码里forwardSpeed是前进速度laneDistance是跑道间距currentLane记录当前跑道。联网时这个脚本只在本地玩家身上启用输入远程玩家只接收位置。3.2 网络管理器的单例与消息协议建一个NetworkManager单例负责连接、收发。消息协议用最简单的二进制1 字节消息类型 变长负载。类型定义消息类型值负载内容玩家位置1玩家ID x y z开始比赛2无到达终点3玩家ID 用时心跳4无public class NetworkManager : MonoBehaviour { public static NetworkManager Instance; private UdpClient udp; private IPEndPoint serverEP; void Awake() { Instance this; udp new UdpClient(); serverEP new IPEndPoint(IPAddress.Parse(127.0.0.1), 9050); udp.Connect(serverEP); } public void SendPosition(int playerId, Vector3 pos) { byte[] data new byte[13]; data[0] 1; data[1] (byte)playerId; BitConverter.GetBytes(pos.x).CopyTo(data, 2); BitConverter.GetBytes(pos.y).CopyTo(data, 6); BitConverter.GetBytes(pos.z).CopyTo(data, 10); udp.Send(data, data.Length); } }参数说明playerId在连接时由服务端分配0 和 1 分别代表两个玩家。BitConverter默认小端序服务端和客户端保持一致即可。这段代码只发了位置实际项目中还要加时间戳防乱序但先跑通。3.3 双人竞速的判定逻辑终点判定必须由服务端做。客户端到达终点后发消息类型 3服务端记录第一个到达的玩家 ID然后广播结果。客户端收到结果后显示“玩家 X 获胜”。注意客户端不要自己判定胜负否则两边可能同时显示自己赢。// 服务端处理终点消息 if (msgType 3) { int playerId data[1]; if (winnerId -1) { winnerId playerId; // 广播给所有客户端 byte[] result new byte[2]; result[0] 3; result[1] (byte)winnerId; foreach (var client in clients) { server.Send(result, result.Length, client); } } }winnerId初始为 -1第一个到达的玩家设置后不再改变。这样即使两个客户端几乎同时发服务端按接收顺序判定公平且唯一。4. 避坑与排查双人联网跑酷最常见的 5 个翻车点4.1 现象两个玩家看到对方位置瞬移像幻灯片原因没有做插值或者插值系数设成了 1。解决按 2.3 节加Vector3.Lerp系数从 0.1 开始调。如果还卡检查 UDP 接收是否在主线程处理Unity3D 的UdpClient.Receive是阻塞的要放到子线程或异步。4.2 现象终点判定两边不一致A 显示自己赢B 也显示自己赢原因客户端各自判定胜负。解决所有胜负判定收归服务端客户端只发“我到了”等服务端广播结果。服务端要加一个winnerId锁第一个到达的写入后不再改。4.3 现象断线后客户端直接卡死无法重连原因没有心跳和超时处理。解决客户端每 1 秒发一次心跳类型 4服务端 3 秒没收到就标记该玩家掉线广播给另一个玩家“对手已离线”。客户端收到离线消息后显示提示允许返回主菜单重连。4.4 现象公网测试时延迟高角色像在水里游原因UDP 包发得太频繁或者服务端转发效率低。解决位置包限制在每秒 20 次50ms 一次不要每帧发。服务端用Socket异步接收不要用UdpClient的阻塞循环。另外公网服务器要选离玩家近的节点物理距离带来的延迟无法用代码消除。4.5 现象Unity3D 编辑器里正常打包到安卓后连不上原因安卓默认不允许明文 UDP或者防火墙拦截。解决检查Player Settings里Internet Access设为Require安卓 9 以上需要在AndroidManifest.xml加android:usesCleartextTraffictrue。如果是局域网确认手机和电脑在同一网段关掉电脑防火墙再试。5. 进阶技巧用服务端回放验证同步一致性跑酷游戏最怕的是“我觉得我到了服务端说我没到”。要验证同步是否可靠我习惯做一个服务端回放日志服务端把每个玩家每 100ms 的位置和输入事件写入一个文本文件比赛结束后可以回放整场比赛。具体做法// 服务端记录回放 using System.IO; StreamWriter replayLog new StreamWriter(replay_ DateTime.Now.Ticks .csv); replayLog.WriteLine(time,playerId,x,y,z,event); void LogPosition(int playerId, Vector3 pos, float time) { replayLog.WriteLine(${time},{playerId},{pos.x},{pos.y},{pos.z},pos); replayLog.Flush(); }参数说明time用服务端启动后的秒数event字段可以填pos、jump、finish。回放时用 Excel 或 Python 画轨迹图一眼就能看出两个玩家的位置是否在终点线附近出现“穿越”或“回退”。如果发现某个玩家在终点前位置突然跳变说明客户端预测和服务端校正冲突需要调整预测阈值。另一个技巧是延迟补偿服务端收到玩家 A 的终点消息时不要立刻判定而是等 100ms看玩家 B 是否也发了终点消息。如果两个消息在 100ms 内到达按服务端接收时间排序如果超过 100ms先到的赢。这个窗口可以根据网络延迟调整局域网设 50ms公网设 150ms。我自己的习惯是每次改完同步逻辑先跑 10 局回放看终点判定有没有争议。没有回放日志的联网项目出了问题就是黑匣子只能靠猜。希望帮到你。本文还有配套的精品资源点击获取