资讯动态

Unity C# Socket实战:从零搭建TCP多人联机合作生存游戏

发布时间:2026/9/9 23:22:55 来源:尧图企业网站定制
如果你现在打开一份 Unity 游戏开发岗位的面试简历会发现不少候选人写着“熟悉 TCP/IP”“了解 Socket 网络编程”。但面试官追问一句“你用 Socket 做过什么完整项目”很多人只能答出“写过回显 Demo”。更尴尬的是那个 Demo 在现场跑不起来既没有服务端日志也没有第二台客户端完全没法证明你真的理解网络通信。这个问题的根源在于大多数求职者只做了单机项目网络编程只停留在概念层面。真正的区分度在于你有没有一个能完整跑通的、包含服务端和客户端的多人联机项目。对一个面向 27 届求职实习的候选人来说Unity C# Socket 做 TCP 多人联机合作生存游戏是一个性价比很高的选择。这篇文章会从零开始拆解这个项目的完整链路网络拓扑怎么选、服务端怎么设计、消息协议怎么定、粘包拆包怎么写、Unity 客户端怎么接入以及最终如何作为面试作品展示。读完你可以照着搭出一个最小可玩的双人合作生存游戏并且能应对面试官关于 TCP、Socket、C# 并发和游戏同步的大部分追问。1. 这个项目真正的价值在哪里先说结论这个项目最大的价值不是游戏玩法而是它覆盖了游戏研发中最难自学的“网络层”。一个普通单机项目能展示的是 C# 基础、Unity 操作、UGUI、简单 AI。而多人联机项目会让你接触四块硬核内容传输层协议的现实应用TCP 为什么可靠、哪里会粘包、为什么需要心跳。C# 异步与并发BeginAccept、BeginReceive、多线程回调、共享数据保护。架构设计客户端与服务端如何分层、消息如何路由、房间如何管理。工程排错能力联调失败时怎么定位是网络问题、协议问题还是业务逻辑问题。这四块内容恰好是游戏客户端、游戏服务端、后端开发、甚至是普通 C# 工程师面试都会考察的方向。用一个项目同时覆盖四条技术线在实习简历里非常占优势。另一个现实原因是可演示性。面试官不喜欢只能看截图的项目但多人联机项目天然可以现场演示你打开服务端控制台再开两个 Unity 客户端一台移动另一台实时跟随。这种效果比任何项目描述都有说服力。当然这个项目也有不适合的场景。如果你投的是纯渲染、TA、图形学方向花大量时间做网络层意义不大。但如果你投的是游戏客户端、游戏服务器、通用服务端开发这个项目方向完全值得投入。2. TCP 多人联机的基础架构与关键决策2.1 网络拓扑客户端-服务端模型多人联机有两种常见拓扑P2P客户端之间直接通信没有中心节点。客户端-服务端所有客户端连接同一个服务端服务端做权威判断和消息转发。对这个项目我建议直接采用客户端-服务端模型原因有三反作弊更简单服务端拥有最终决定权例如玩家是否扣血、资源是否刷新。消息转发逻辑清晰客户端只需要维护一条到服务端的连接。面试容易展开服务端模型的难点更集中能和面试官聊“权威服务器”的设计理念。2.2 为什么选 TCP 而不是 UDP游戏联机经常被拿来对比 TCP 和 UDP。很多人以为 TCP 只适合弱网、UDP 只适合 FPS这是一个常见的误解。TCP 的特点是面向连接、可靠传输、保证顺序。它的缺点是存在头部阻塞和重传延迟极端情况下延迟会比 UDP 高很多。UDP 则是无连接、不可靠、不保证顺序但延迟低、开销小。对于合作生存游戏玩家需要采集资源、建造、对抗怪物这些操作对可靠性要求远高于实时性。哪怕一个位置包偶尔延迟几十毫秒也不会有明显体验问题但如果资源状态丢包了玩家采集了一个服务端根本没认可的“假资源”体验会更差。所以这个项目选 TCP 是完全合理的面试时你能说出“为什么我的游戏选 TCP 不选 UDP”就已经比大多数候选人强了。对比项TCPUDP连接状态面向连接无连接可靠性可靠、有序不可靠、可能乱序传输效率稍低较高适合场景资源同步、房间操作、消息可靠实时位置、语音、FPS 射击本项目定位主协议可扩展用于高频位置同步2.3 同步方式状态同步还是帧同步状态同步和帧同步是游戏联机里绕不开的问题。状态同步指客户端把“我的位置、血量、动作状态”发给服务端服务端广播给其他客户端。帧同步则是所有客户端只同步“操作指令”每帧在本地执行相同逻辑保证结果一致。帧同步适合格斗、RTS 这类对操作确定性要求极高的游戏但它的难点是浮点运算一致性、回放和反外挂都很难。状态同步实现简单、调试方便也更贴近大多数商业游戏的做法。合作生存游戏采用状态同步是更稳妥的选择。这个技术选型也更好面试你可以明确说出“我选择状态同步因为开发效率高、逻辑容易验证后续需要时可以局部替换为混合同步”。3. 环境准备与项目结构服务端使用 C# 控制台项目客户端使用 Unity 工程两者共用一套消息定义但物理上分成两个工程。这样的好处是服务端逻辑不依赖 UnityEngine可以直接在 Visual Studio / Rider 中启动调试不打开 Unity 也能跑。环境准备大致如下版本请以实际安装为准Unity 2022.3 LTS 或 Unity 6模板选择 3D Core。.NET SDK 6.0 或更高版本用于创建服务端控制台项目。IDEVisual Studio、JetBrains Rider或者 VS Code。平台Windows / macOS 皆可局域网联调建议两台设备没有两台设备也能在同一台机器开两个客户端进程。项目目录建议按下面这样组织CoopSurvival/ ├── Server/ # 服务端控制台工程 │ ├── Program.cs │ ├── NetServer.cs │ ├── ClientConnection.cs │ ├── PacketParser.cs │ ├── MessageDefine.cs │ └── RoomManager.cs └── UnityClient/ # Unity 工程 ├── Assets/ │ ├── Scripts/ │ │ ├── Net/ │ │ │ ├── NetClient.cs │ │ │ ├── PacketParser.cs │ │ │ └── MessageDefine.cs │ │ └── Game/ │ │ ├── PlayerController.cs │ │ ├── PlayerSync.cs │ │ └── UIManager.cs │ └── Scenes/ └── ProjectSettings/服务端和客户端各保留一份 MessageDefine.cs虽然存在重复但好处是服务端不依赖 Unity 程序集可以独立启动和调试。如果你后期工程变大可以再把公共代码抽成一个 .NET Standard 类库。4. 服务端核心C# Socket 异步架构4.1 为什么用异步 SocketC# Socket 编程有三种常见方式同步阻塞、异步回调、async/await。这个项目建议使用异步回调BeginAccept、BeginReceive。同步阻塞模式不适合网络服务器因为一个客户端的读阻塞会卡住整个线程。异步回调则不占用额外线程接收由系统 IO 完成端口驱动适合高并发连接。不建议一上来就用 async/await 的另一个原因是异步回调的时序关系更直观能让你真正理解 “收到数据 - 拆包 - 处理 - 继续接收” 的回环结构。面试官如果追问底层原理你也可以讲得更清楚。4.2 服务端启动与监听先写一个最简服务端启动类// 文件路径Server/NetServer.cs using System; using System.Net; using System.Net.Sockets; public class NetServer { private Socket _listenSocket; private bool _running; public void Start(int port) { _listenSocket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listenSocket.Bind(new IPEndPoint(IPAddress.Any, port)); _listenSocket.Listen(100); _running true; Console.WriteLine($[Server] listening on 0.0.0.0:{port}); _listenSocket.BeginAccept(AcceptCallback, null); } private void AcceptCallback(IAsyncResult ar) { if (!_running) return; try { Socket clientSocket _listenSocket.EndAccept(ar); Console.WriteLine($[Server] client connected: {clientSocket.RemoteEndPoint}); ClientConnection connection new ClientConnection(clientSocket); ClientManager.Instance.Add(connection); connection.StartReceive(); } catch (Exception ex) { Console.WriteLine($[Server] accept exception: {ex}); } finally { _listenSocket.BeginAccept(AcceptCallback, null); } } public void Stop() { _running false; _listenSocket.Close(); } }这里有一个关键细节在 AcceptCallback 的 finally 里重新调用 BeginAccept。这个回环很重要如果漏掉服务端只能接收第一个连接之后就无响应了。4.3 连接对象与数据接收每个客户端连接封装为一个 ClientConnection。它负责接收数据、解析粘包、心跳检测和发送消息。// 文件路径Server/ClientConnection.cs using System; using System.Net.Sockets; public class ClientConnection { private Socket _socket; private byte[] _receiveBuffer new byte[4096]; private PacketParser _parser new PacketParser(); public DateTime LastHeartbeatTime { get; private set; } DateTime.UtcNow; public int PlayerId { get; set; } public int RoomId { get; set; } -1; public ClientConnection(Socket socket) { _socket socket; } public void StartReceive() { try { _socket.BeginReceive(_receiveBuffer, 0, _receiveBuffer.Length, SocketFlags.None, ReceiveCallback, null); } catch (Exception ex) { Console.WriteLine($[Server] begin receive error: {ex}); Close(); } } private void ReceiveCallback(IAsyncResult ar) { try { int length _socket.EndReceive(ar); if (length 0) { Close(); return; } _parser.Append(_receiveBuffer, 0, length, OnPacket); StartReceive(); } catch (Exception ex) { Console.WriteLine($[Server] receive error: {ex}); Close(); } } private void OnPacket(byte[] message) { // 在这里解析消息并路由到房间、登录等业务逻辑 LastHeartbeatTime DateTime.UtcNow; MessageRouter.Instance.Dispatch(this, message); } public void Send(byte[] data) { try { _socket.Send(data); } catch (Exception ex) { Console.WriteLine($[Server] send error: {ex}); Close(); } } public void CheckHeartbeat() { double idleSeconds (DateTime.UtcNow - LastHeartbeatTime).TotalSeconds; if (idleSeconds 30) { Console.WriteLine($[Server] client {_socket.RemoteEndPoint} heartbeat timeout, close.); Close(); } } public void Close() { if (_socket ! null) { try { _socket.Shutdown(SocketShutdown.Both); } catch { } _socket.Close(); } ClientManager.Instance.Remove(this); Console.WriteLine($[Server] client closed, current count: {ClientManager.Instance.Count}); } }这里的 Send 使用的是同步发送。项目演示阶段消息频率不高这样做没有问题。如果后续要支撑更高并发应该把 Send 改成异步或者用发送队列加独立发送线程。心跳检测需要配合一个定时器。在服务端 Program.cs 里可以启动一个 Timer每 5 秒遍历一次客户端连接// 文件路径Server/Program.cs using System; using System.Threading; class Program { static void Main(string[] args) { int port 8888; NetServer server new NetServer(); server.Start(port); Timer heartbeatTimer new Timer(state { ClientManager.Instance.CheckAllHeartbeat(); }, null, 5000, 5000); Console.WriteLine(Press Enter to stop server...); Console.ReadLine(); server.Stop(); } }到这里服务端已经有了监听、接收、拆包回调、心跳检查这几个基础能力。你会发现服务端本身没有写任何“游戏玩法”这是正确的。网络层和业务层应该分开后续扩展房间和资源点逻辑时才不会把代码堆成一团。5. 消息协议设计与粘包拆包实现5.1 协议格式长度头 消息体网络传输的是一串字节流TCP 不保证一次接收恰好是一个完整消息。所以通信双方必须约定消息边界。本项目采用最简单的长度前缀法| 4 字节消息长度 | 消息体 | 消息体内部 | 2 字节 msgId | 业务数据 |用 BitConverter 在二进制中消息长度和 msgId 都采用小端序。C# 在小端平台上是默认小端你在 Windows、Linux、macOS 上跑都没问题。后面如果涉及跨语言通信需要统一约定字节序并显式处理。5.2 消息定义建议把消息号单独放到一个文件方便客户端和服务端共用。// 文件路径Server/MessageDefine.cs public static class MsgId { public const ushort Heartbeat 0; public const ushort LoginRequest 1; public const ushort LoginResponse 2; public const ushort CreateRoom 3; public const ushort JoinRoom 4; public const ushort RoomEntered 5; public const ushort PositionSync 6; public const ushort LeaveRoom 7; }位置同步消息体定义为字段类型大小PlayerIdint4 字节Xfloat4 字节Yfloat4 字节Zfloat4 字节5.3 发送封装封装一个 MessageBuilder把业务数据转成外层包。// 文件路径Server/MessageBuilder.cs using System; using System.IO; public static class MessageBuilder { /// summary /// 构造带 msgId 的消息体 /// /summary public static byte[] BuildBody(ushort msgId, byte[] payload) { using (MemoryStream ms new MemoryStream()) using (BinaryWriter writer new BinaryWriter(ms)) { writer.Write(msgId); writer.Write(payload ?? Array.Emptybyte()); return ms.ToArray(); } } /// summary /// 把消息体封装成完整网络包4 字节长度 消息体 /// /summary public static byte[] EncodePacket(byte[] body) { byte[] packet new byte[4 body.Length]; byte[] head BitConverter.GetBytes(body.Length); Buffer.BlockCopy(head, 0, packet, 0, 4); Buffer.BlockCopy(body, 0, packet, 4, body.Length); return packet; } public static void Send(ClientConnection conn, ushort msgId, byte[] payload) { byte[] body BuildBody(msgId, payload); conn.Send(EncodePacket(body)); } }5.4 粘包拆包器粘包是指多个消息被合并到一个 TCP 段里拆包是指一个消息被拆成多个段。为了处理这两种情况需要一个缓冲数组。// 文件路径Server/PacketParser.cs using System; public class PacketParser { private byte[] _cache new byte[4096]; private int _cacheLength; /// summary /// 追加新收到的数据解析出完整消息后回调 /// /summary public void Append(byte[] data, int start, int count, Actionbyte[] onPacket) { if (count 0) return; if (_cacheLength count _cache.Length) { Resize(_cacheLength count); } Buffer.BlockCopy(data, start, _cache, _cacheLength, count); _cacheLength count; int offset 0; while (_cacheLength - offset 4) { int bodyLength BitConverter.ToInt32(_cache, offset); if (bodyLength 0 || bodyLength 1024 * 1024) { throw new Exception($invalid body length: {bodyLength}); } if (_cacheLength - offset - 4 bodyLength) { break; // 数据不完整等下一批 } byte[] body new byte[bodyLength]; Buffer.BlockCopy(_cache, offset 4, body, 0, bodyLength); onPacket(body); offset 4 bodyLength; } if (offset 0) { int rest _cacheLength - offset; Buffer.BlockCopy(_cache, offset, _cache, 0, rest); _cacheLength rest; } } private void Resize(int required) { int newSize _cache.Length * 2; while (newSize required) newSize * 2; byte[] newCache new byte[newSize]; Buffer.BlockCopy(_cache, 0, newCache, 0, _cacheLength); _cache newCache; } }这个拆包器有四个关键细节每次新增数据后优先检查缓存区是否够用。消息长度要加合法性校验防止恶意客户端发送超大长度导致内存异常。只有确认缓存中有一个完整消息时才回调否则等待下一次 Receive 填充。解析完成后把未处理的数据移到数组头部避免缓慢积累导致数组越界。服务端和客户端可以复用同一个拆包器代码。如果想让代码更加工程化可以把这部分抽成公共类库。6. Unity 客户端接入 Socket6.1 NetClient 封装Unity 端的网络封装要解决一个核心问题Socket 回调运行在系统 IO 线程而 Unity 的 API 只能在主线程调用。如果直接在回调里操作 GameObject、Transform、UI会产生随机报错或者卡死。我的处理方式是网络回调只负责解析包然后把消息体放入一个线程安全队列Unity 主线程在 Update 里取出并处理。// 文件路径UnityClient/Assets/Scripts/Net/NetClient.cs using System; using System.Collections.Concurrent; using System.IO; using System.Net.Sockets; using UnityEngine; public class NetClient : MonoBehaviour { public static NetClient Instance { get; private set; } private TcpClient _tcpClient; private NetworkStream _stream; private PacketParser _parser new PacketParser(); private byte[] _receiveBuffer new byte[4096]; private ConcurrentQueuebyte[] _messageQueue new ConcurrentQueuebyte[](); private void Awake() { Instance this; } private void Update() { while (_messageQueue.TryDequeue(out byte[] body)) { HandleMessage(body); } } public void Connect(string ip, int port) { _tcpClient new TcpClient(); _tcpClient.BeginConnect(ip, port, ar { try { _tcpClient.EndConnect(ar); _stream _tcpClient.GetStream(); _stream.BeginRead(_receiveBuffer, 0, _receiveBuffer.Length, ReadCallback, null); Debug.Log($[NetClient] connected to {ip}:{port}); } catch (Exception ex) { Debug.LogError($[NetClient] connect failed: {ex.Message}); } }, null); } private void ReadCallback(IAsyncResult ar) { try { int length _stream.EndRead(ar); if (length 0) { Debug.Log([NetClient] disconnected by server); return; } _parser.Append(_receiveBuffer, 0, length, body { _messageQueue.Enqueue(body); }); _stream.BeginRead(_receiveBuffer, 0, _receiveBuffer.Length, ReadCallback, null); } catch (Exception ex) { Debug.LogError($[NetClient] read error: {ex.Message}); } } public void Send(ushort msgId, byte[] payload) { if (_stream null) return; byte[] body MessageBuilder.BuildBody(msgId, payload); byte[] packet MessageBuilder.EncodePacket(body); _stream.Write(packet, 0, packet.Length); } private void HandleMessage(byte[] body) { using (MemoryStream ms new MemoryStream(body)) using (BinaryReader reader new BinaryReader(ms)) { ushort msgId reader.ReadUInt16(); byte[] payload reader.ReadBytes((int)(ms.Length - ms.Position)); GameMessageDispatcher.Instance.Dispatch(msgId, payload); } } private void OnDestroy() { _tcpClient?.Close(); } }这里有一个值得注意的设计Update 里把消息转发给 GameMessageDispatcher再由它调用当前场景里的玩家同步逻辑。这样做的好处是网络模块不依赖具体游戏对象换场景时 NetClient 可以常驻而具体消息处理器可以按场景注册。6.2 玩家移动与位置同步Unity 场景中放置一个地面两个 Cube 代表玩家。本地玩家用 WASD 控制移动服务端会把位置同步给同房间其他客户端。// 文件路径UnityClient/Assets/Scripts/Game/PlayerController.cs using UnityEngine; public class PlayerController : MonoBehaviour { public float moveSpeed 5f; void Update() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 dir new Vector3(h, 0, v); if (dir.magnitude 0.01f) { transform.position dir.normalized * (moveSpeed * Time.deltaTime); } } }// 文件路径UnityClient/Assets/Scripts/Game/PlayerSync.cs using System.IO; using UnityEngine; public class PlayerSync : MonoBehaviour { public int playerId; private float _sendTimer; private const float SyncInterval 0.1f; // 10 次/秒 void Update() { _sendTimer Time.deltaTime; if (_sendTimer SyncInterval) { _sendTimer 0f; SendPosition(); } } private void SendPosition() { Vector3 pos transform.position; using (MemoryStream ms new MemoryStream()) using (BinaryWriter writer new BinaryWriter(ms)) { writer.Write(playerId); writer.Write(pos.x); writer.Write(pos.y); writer.Write(pos.z); NetClient.Instance.Send(MsgId.PositionSync, ms.ToArray()); } } /// summary /// 由 GameMessageDispatcher 调用更新远端玩家位置 /// /summary public void ApplyRemotePosition(byte[] payload) { using (MemoryStream ms new MemoryStream(payload)) using (BinaryReader reader new BinaryReader(ms)) { int remoteId reader.ReadInt32(); float x reader.ReadSingle(); float y reader.ReadSingle(); float z reader.ReadSingle(); if (remoteId playerId) return; // 自己的位置由本地控制 transform.position new Vector3(x, y, z); } } }在 GameMessageDispatcher 里收到 PositionSync 消息后调用对应玩家对象的 ApplyRemotePosition。这样本地玩家移动依靠 WASD远端玩家位置依靠网络包整个联机同步链路就通了。7. 房间管理与合作玩法扩展7.1 房间管理思路多人合作游戏需要一个简单的房间系统。服务端可以用一个 RoomManager 管理房间每个房间维护一个玩家连接列表。// 文件路径Server/RoomManager.cs using System.Collections.Generic; public class Room { public int RoomId { get; set; } public string RoomName { get; set; } public ListClientConnection Players { get; } new ListClientConnection(); public void Broadcast(byte[] packet, ClientConnection except null) { foreach (var player in Players) { if (player except) continue; player.Send(packet); } } } public class RoomManager { private Dictionaryint, Room _rooms new Dictionaryint, Room(); private int _nextRoomId 1; public Room CreateRoom(string roomName) { Room room new Room { RoomId _nextRoomId, RoomName roomName }; _rooms[room.RoomId] room; return room; } public Room FindRoom(int roomId) { _rooms.TryGetValue(roomId, out Room room); return room; } }在实际项目中你可以在此基础上扩展“加入房间后进入同一场景”“房间满员提示”“离开房间通知”等逻辑。但注意不要把房间逻辑堆在 ClientConnection 里保持单一职责会让项目更容易维护。7.2 合作生存玩法落地建议完成了位置同步和房间系统后合作玩法可以按下面的顺序逐步加入资源点同步服务端维护资源点列表玩家采集时发送请求服务端扣除资源并广播。怪物刷新服务端定时生成怪物把怪物的类型、位置、血量广播给房间内玩家。玩家状态同步血量、饥饿值等数值变化通过消息同步不在客户端本地直接修改。胜负条件比如合作建造基地、抵御指定波次怪物服务端记录关卡状态。这些玩法本身不需要新增网络知识重点是继续沿用“客户端发起请求、服务端做逻辑判断、再广播结果”这个流程。8. 联调运行与效果验证8.1 运行步骤第一步启动服务端。在控制台项目目录执行dotnet run看到类似输出代表启动成功[Server] listening on 0.0.0.0:8888第二步在 Unity 中打开客户端场景设置 NetClient 的 IP 和端口。如果是本机联调IP 填127.0.0.1如果是局域网联调填服务端机器的局域网 IP例如192.168.1.100。第三步运行第一个 Unity 客户端连接后自动创建房间。第四步在 Unity Editor 中点击 Play 运行第二个客户端或者打包一个独立 Build 再运行连接后加入同一房间。第五步控制第一个客户端移动观察第二个客户端中对应玩家对象是否跟随移动。同时观察服务端控制台能看到连接建立、心跳正常、玩家加入房间等信息。8.2 如何判断成功服务端显示两个客户端连接连接数不为 -1。两个客户端能进入同一个 RoomId。客户端 A 移动客户端 B 能看到一个 Cube 同步移动。定期输出心跳日志没有“heartbeat timeout”断开记录。手动关掉一个客户端服务端能在 30 秒内识别断线并清理连接。如果其中任何一步不符合预期优先检查服务端日志和 Unity Console 窗口大部分问题在日志中能看到明确异常。9. 常见问题与排查思路问题现象可能原因排查方式解决方案服务端 Bind 时提示端口被占用上次服务端进程未退出在命令行执行netstat -ano查看端口占用杀掉占用进程或更换端口客户端提示 connect failedIP 写错、服务端没启动、防火墙拦截在客户端机器执行ping 服务端IP再用telnet IP 端口测试核对 IP确认服务端启动放行防火墙端口服务端只能接收第一个连接AcceptCallback 里没有重新 BeginAccept查看服务端代码是否在 finally 里接续监听在 finally 中重新调用 BeginAccept收到的位置乱跳、角色瞬移位置同步频率过高或解析字段顺序错误打印服务端收到的坐标日志核对二进制读写顺序统一字段顺序客户端网络回调里操作 UI 报错跨线程访问 Unity API查看报错堆栈是否来自网络线程使用 ConcurrentQueue 切换主线程处理心跳正常但仍然断线心跳包没有正确携带 msgId抓包或打印心跳消息内容确认客户端心跳发送携带 MsgId.Heartbeat消息体长度解析异常发送端打出长度头和消息体不一致打印发送端和接收端长度前缀统一 EncodePacket 逻辑封装到公共类手机或第二台电脑连不上服务端监听 127.0.0.1 而不是 0.0.0.0查看服务端监听地址日志Bind 使用 IPAddress.Any即 0.0.0.010. 简历怎么写与面试高频追问10.1 简历项目描述参考不要只写“完成了多人联机游戏”要突出能验证的技术点项目TCP 多人联机合作生存游戏 技术栈Unity / C# / Socket / TCP / 二进制协议 职责与成果 - 独立实现基于异步 Socket 的 TCP 游戏服务端支持多客户端并发连接、心跳检测、断线清理。 - 设计长度头消息协议解决粘包拆包问题客户端与服务端共用同一套协议定义。 - 采用状态同步方案实现玩家位置实时同步与多人房间管理。 - 将网络回调解耦到主线程消息队列避免 Unity 跨线程操作 UI/GameObject 引起的异常。这里每个点都是面试官能顺着往下问的写上去的每一句都必须能展开讲。10.2 面试官大概率会问的问题TCP 三次握手和四次挥手的过程是怎样的TCP 为什么会有粘包怎么解决为什么你的位置同步用 10 次/秒这个频率怎么定的状态同步和帧同步的区别你的项目为什么选状态同步为什么用异步 Socket 而不是多线程阻塞多个线程同时操作 List 或 Dictionary 会不会有问题你是怎么避免的客户端收到消息后为什么放到队列而不是直接处理如果现在要支持 100 人同屏你的协议可以怎么优化服务端收到位置同步消息后是怎么决定广播给谁的如果玩家断网服务端多久会发现心跳超时时间设置为多少合理这些问题都不需要背标准答案只要你在开发过程中真正理解了每一步就能自然回答。如果面试官问到你没做过的优化可以大方承认并说出自己的思考方向这比编造经验更受认可。11. 后续扩展方向与实用建议第一优先级是补上断线重连。现在的实现中服务端会在 30 秒心跳超时后断开客户端。你可以设计一个掉线保护机制客户端检测到连接断开后自动重连服务端在短时间内允许玩家回到原房间而不是直接解散房间。第二优先级是消息压缩与批量同步。目前位置同步是一个包发一次带宽浪费比较高。你可以把多个玩家的位置打包成一个 BatchSync 消息减少包头数量和网络往返。第三优先级是在同一个项目中对比帧同步。如果你是投客户端岗位能主动写出“UDP 帧同步版本的故事原型”会更有加分但那属于进阶操作不要在基础版还没跑通时急着做。对 27 届求职的同学来说建议按这个节奏推进花两天把服务端监听、客户端连接、心跳、位置同步跑通。花一天补上房间系统和一个采集玩法点。花半天整理代码结构和面试问题清单。在面试前自己录一段双人联机演示视频。这个项目不要追求大而全核心是把网络层链路讲透。能回答清楚 TCP 为什么适合这个场景、粘包拆包怎么处理、异步回调怎么保证线程安全面试官对这个项目的评价就已经超过大多数候选人了。把最小闭环做完、跑熟再根据自己投递方向做针对性扩展才是这类求职项目最正确的打开方式。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价