ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

Unity + C# Socket 实战:从零搭建 TCP 多人联机合作生存游戏 我想先说一个结论如果你准备投 Unity 客户端、游戏服务端或 C# 后端相关岗位把「TCP 多人联机合作生存游戏Unity C# Socket 服务端」作为实习项目是很划算的一条路。原因是它覆盖了客户端玩法、UI、Socket 网络编程、服务端连接管理、消息协议、多人同步这一整套链路而很多应届简历上缺少的恰恰是这种“能讲清完整链路”的项目。适合看这篇内容的人主要是 27 届毕业生正在为实习或校招准备项目已经会一点 Unity但还没碰过 Socket 网络编程以及想用 C# 同时把客户端和服务端统一起来降低技术栈切换成本的同学。最值得提前判断的一点是这个项目真正的难点不在建模不在 UI而在消息协议、连接管理和同步策略。只要你能把这三块讲透面试时就能把项目撑起来。1. 求职简历上的亮点在于一条完整链路而不是单机 Demo1.1 这个项目为什么适合 27 届求职实习现在很多学生的 Unity 项目停留在“跟着教程做单机闯关”或者“做一个小游戏”。但单机项目把所有逻辑都放在客户端本地面试官很难判断你到底有没有工程意识。多人联机会多出一整层复杂度网络不可靠、消息有边界、服务端要同时管理多个连接、客户端要处理异步回调。这些问题不是背概念能解决的必须在真实项目里踩过一遍才有感觉。对 27 届同学来说还有一个很现实的问题时间并不宽裕。与其做一个“做了 3 个玩法但每个都没跑通”的大项目不如做一个“小范围但完整闭环”的项目。这个项目的合理边界是4 人房间、角色移动同步、资源采集、基础怪物、生命值和饥饿值同步。这样既能展示客户端能力也能展示服务端能力面试官追问时你也基本能接住。不建议一上来就做无缝大世界、复杂技能特效、怪物 AI 系统、存档系统、排行榜匹配。这些功能单拆出来都很大放进一个实习项目里只会把完成度拉低。更现实的做法是先跑通最小闭环再把能体现亮点的部分做扎实。1.2 核心范围界定要做哪些模块不做哪些模块我建议把项目拆成三个持续维护的部分。客户端场景角色控制器、相机控制、交互 UI、物品显示、血量和饥饿值面板、其他玩家对象管理。服务端TCP 监听、连接管理、消息协议解析、房间管理、玩家状态、地图资源对象、怪物生成逻辑。网络桥接层连接、心跳、断线重连、消息回调调度、日志。不需要在一开始就考虑的问题包括数据库持久化、分布式部署、多服架构、反作弊系统。不是说这些没用而是实习项目阶段把“单人开发出一个能稳定多人联机的小游戏”这个目标完成已经能证明你的基础能力。传奇服务端、QQ 飞车服务端这类商业项目复杂度非常高但那是团队协作、海量并发、长期迭代后的结果不适合实习项目直接借鉴。你要做的是一个房间制小型服务端逻辑清晰、可调式、可演示这就够了。2. 先把多人联机拆成单机原型、服务端、网络桥接三层2.1 环境准备Unity 版本、C# 工程与调试工具Unity 版本不要追最新 Beta 版。建议装官方当前最稳定的 LTS 版本。LTS 的优势是资料多、插件兼容性稳定遇到 Texture、UI、动画状态机这类常见报错很容易搜到解决方案。如果你看了一下网上教程发现版本差异不大那其实无所谓关键是别选一个还在高频迭代的版本给自己添乱。C# 服务端我强烈建议做成一个独立的 .NET 控制台应用不要直接塞进 Unity 工程里。原因是服务端不依赖 UnityEngine 类型单独建工程后可以用 Visual Studio 或 Rider 单独启动、单独调试。以后想换语言做服务端只要协议不变业务逻辑基本可以平移到 Go 或 Java。调试工具方面你需要能在同一台机器上开两个进程一个跑 Unity一个跑服务端控制台。然后用127.0.0.1回环地址测试先不碰局域网。只有当本机联调通过后再考虑打包客户端、开放防火墙端口、走真实局域网 IP 测试。如果你后续想出 Android 端要注意 Unity 里的 Minimum API Level 和 Target API Level 配置。现在很多新手机的 Target API 要求已经比较高配置太低可能导致安装时提示版本不兼容甚至运行起来闪退。但这个不是当前重点PC 端先把逻辑跑通再说。2.2 确定玩法边界和网络模型再开始写代码很多新手拿到“多人联机合作生存游戏”这个题目第一反应是直接打开 Unity 开始拖场景。这样后面很容易失控。我建议先写一句话把玩法定下来。比如“4 名玩家进入一个房间在有限资源里采集树木和矿石抵御夜里刷出的怪物共享生存天数。”然后选择同步方式。合作生存类游戏一般用状态同步这是主流方案。状态同步的思路是客户端把位置、操作请求发给服务端服务端更新权威状态后广播给所有人。帧同步则更适合格斗游戏、RTS 这类需要极低延迟、逻辑确定且输入种类少的玩法。你选状态同步实现简单也更容易和服务端权威校验结合。注意一个细节合作生存里的“合作”不是每个客户端互相广播自己的数值而是“所有客户端连接同一个服务端看到同一个权威世界”。服务端是世界状态的持有者客户端只是表现端。这个思路确定后后面所有模块都会清晰很多。3. 本地生存玩法先跑通联机之前先有可玩的底子3.1 角色控制、生存数值与物品收集的最小实现不要一开始就接网络。先用 Unity 做一个本地能玩的版本。角色不变的话用一个 Capsule 或自己的简易模型都行重点是能移动、能转向。视角建议做第三人称或俯视角第一人称会增加碰撞和镜头处理的成本但对核心网络逻辑帮助不大。生存数值先做两个生命值、饥饿值。再做一个物品集合比如木材和石头用 Integer 存数量即可。关键是要把数值放在一个可序列化的数据类里不要散落在各个场景物体的字段中。为什么这么做因为后面服务端也要用相同字段做同步如果客户端数据结构没有统一接网络时会非常痛苦。食物、物品收集的逻辑先本地模拟按 E 采集树木木材数量 1饥饿值随时间下降饥饿值归零后生命值缓慢下降。这个阶段不需要做得很精致但要让整个玩法循环能跑起来角色能移动、能采集、数值会变化、UI 能刷新。你只有先有一个可玩的单机底子后面接入 Socket 时才能判断“网络问题”和“玩法问题”是谁造成的。3.2 哪些逻辑必须留给服务端哪些可以放在客户端表现本地原型可以做得很随意但你要很早就分清哪些逻辑将来必须上收。我的判断标准很简单凡是影响“所有玩家共同看到的世界”的数值都必须由服务端裁决客户端只负责输入和表现。举个例子。角色移动客户端收到 WASD 输入后把操作方向发给服务端服务端更新坐标再广播给所有人。你可以在客户端做本地预测让角色响应更跟手但最终坐标以服务端广播为准。生命值扣减客户端被怪物攻击时先播一个受伤表现但实际扣多少血由服务端计算后广播。物品采集客户端请求采集树木服务端判断树木是否还在、玩家距离是否够近、采集冷却是否结束然后统一扣减树木数量并广播。为什么这么分因为客户端一旦成为可信端就没办法防住改内存、改包体刷资源的问题。实习项目虽然不需要做复杂反作弊但面试官很可能会问你“怎么防止玩家修改客户端拿到无限资源”。你只要回答“物品数值在服务端统一管理客户端只发操作请求”就已经体现出服务端权威意识了。4. C# Socket 服务端从 Accept 到连接管理的核心代码4.1 用长度前缀解决粘包和半包C# Socket 服务端最核心的问题不是 Accept而是消息边界。TCP 是流式协议它本身不知道“一条消息”从哪里开始到哪里结束。如果你每隔 100ms 发送一次 JSON接收端可能一次读到好几条消息也可能一条消息被拆成两次到达。这就是常说的粘包和半包。解决方式常用长度前缀发送前把 JSON 字符串转成 UTF8 字节数组。在消息开头写 4 个字节表示消息体长度。接收端先读满 4 个字节得到长度 N。再读满 N 个字节得到一条完整 JSON。发送端的示例代码byte[] body Encoding.UTF8.GetBytes(json); byte[] header BitConverter.GetBytes(body.Length); await stream.WriteAsync(header, 0, 4); await stream.WriteAsync(body, 0, body.Length);接收端要注意不能只调用一次Read就当读满。Read返回的是本次读到的字节数可能小于你要的长度。要循环读取int read 0; while (read len) { int count await stream.ReadAsync(buffer, read, len - read); if (count 0) throw new Exception(连接已断开); read count; }这也是网上很多人搜“C# byte char”“C# 截取字节流”会碰到的问题。本质不是不会用 API而是没有建立“TCP 流式读取”的意识。为什么用 UTF8 而不用默认字符串编码因为 C# 里 string 默认是 UTF-16直接用流发送中文会出现乱码。统一转成 UTF8 字节对中英文消息都稳定。4.2 连接管理、心跳超时与消息分发服务端主循环可以做得很简单TcpListener listener new TcpListener(IPAddress.Any, 7777); listener.Start(); while (true) { TcpClient client await listener.AcceptTcpClientAsync(); _ HandleClientAsync(client); }但主循环简单不代表后续简单。你需要用 ConcurrentDictionary 保存所有连接。为什么不用普通 Dictionary因为多客户端连接各自有接收线程同时增删节点时普通 Dictionary 会抛“集合已修改”异常。每个连接对应一个连接 ID这个 ID 是消息路由的唯一句柄。常见结构ConcurrentDictionaryint, ClientConnection _clients new();每个客户端对象里保存TcpClient、NetworkStream、房间号、玩家信息、最后心跳时间。消息分发时可以给每条消息一个 type 字段{type: C2S_JoinRoom, code: AB1234}服务端收到后根据 type 找到对应 handler。这种写法的好处是协议清晰扩展起来也方便。以后想加聊天、加交互只需要新增 type 和对应处理方法。心跳机制一定要有。每 5 秒客户端发一条 Ping服务端更新该连接的 lastPingTime如果 15 秒内没有收到任意消息就判定连接失效主动断开并通知同房间其他玩家。为什么 TCP 本身不处理因为 TCP 连接在拔网线、程序崩溃、中间代理失效时不一定立刻通知对端。应用层心跳是保证连接状态真实可用的常见手段。4.3 房间与玩家数据结构服务端数据结构要和客户端约定好。玩家模型可以这样设计public class Player { public int ConnId; public string Name; public Vector3Data Position; public int Hp; public int Hunger; } public class Room { public int RoomId; public string Code; public Dictionaryint, Player Players new(); public int MaxPlayers 4; }注意 Vector3Data 不要直接引 UnityEngine.Vector3。服务端是纯 C# 工程没有 UnityEngine 引用。自己定义一个包含 X、Y、Z 的简单类即可。这也算是一个小坑提前绕开能省不少时间。房间码建议用 6 位大写字母加入时输入更方便。房间满员后服务端要返回“房间已满”的错误消息。房间不存在时返回“房间不存在”。这些错误码要提前定义好方便客户端 UI 提示。5. Unity 客户端接入 Socket网络层不能直接碰场景物体5.1 NetworkManager 单例与连接流程客户端需要建一个 NetworkManager通常做成 MonoBehaviour 单例。它负责连接、发送、接收、断线重连。连接建议用异步方式_client new TcpClient(); await _client.ConnectAsync(ip, port); _stream _client.GetStream();不要在 Unity 主线程里同步等待连接成功否则界面会卡死。连接地址和端口可以做成一个 ScriptableObject 或配置类方便切换本机和局域网环境。连接之后不要立刻发业务消息。先发一条 C2S_Join 握手等服务端回 Welcome 或 JoinOK 后再进入房间流程。这样可以避免服务端还没准备好、客户端就开始疯狂发采集消息的情形。5.2 回调线程调度到主线程避免 Unity 崩溃这是 Unity 网络开发里最常见的问题。Socket 接收线程收到消息后如果直接去修改 GameObject、改 UI、执行 InstantiateUnity 会报错提示“只能从主线程访问”。原因是 Unity 引擎对象不允许跨线程操作。通用解法是做一个主线程消息队列ConcurrentQueueAction mainThreadQueue new(); void Update() { while (mainThreadQueue.TryDequeue(out Action action)) { action?.Invoke(); } } void OnReceive(string json) { mainThreadQueue.Enqueue(() HandleMessage(json)); }接收线程只负责解析 JSON、封装成 Action、丢进队列。Update 里每帧消费队列。这样做还有额外好处所有消息处理都集中在一个地方方便加日志和统计。如果消息量很大比如频繁广播位置Update 里一次性处理上百条会导致掉帧。可以加一个“每帧最多处理条数”的开关。这其实就是很多人在做循环采集数据时遇到的 UI 卡顿问题原因往往是回调直接阻塞了接收线程或者主线程每帧处理太密集。5.3 同屏玩家对象的创建与简单插值收到 PlayerJoin 广播后根据 PlayerId 生成一个网络角色对象。注意排除自己的 PlayerId。自己的角色由本地控制器驱动别人的角色由网络数据驱动。收到位置同步数据时不要直接把目标位置赋给 transform.position。那样会看到瞬移、抖动、出现和消失。最简单的方式是用 Lerp 做平滑transform.position Vector3.Lerp(transform.position, target, 10 * Time.deltaTime);同步频率先按 10 次每秒设置也就是每 100 毫秒发一次位置。10Hz 对实习项目来说足够能明显降低带宽和服务端广播压力。如果你做得更细可以设计成“坐标变化超过阈值才发包”但一开始不需要。上线、下线消息也要处理。玩家加入时要实例化角色玩家断线后要销毁角色。不要只处理加入忘记处理离开否则整个房间会越挤越多。6. 多人合作生存的联机逻辑房间制、状态同步和服务端校验6.1 房间码创建与加入先实现房间创建和加入这是多人联机的入口。创建房间客户端发 C2S_CreateRoom服务端生成 6 位房间码返回 JoinOK并把该连接加入房间。加入房间客户端发 C2S_JoinRoom服务端按房间码查找。存在且未满则加入不存在或已满返回错误码。加入成功后服务端要做两件事把房间现有玩家的完整信息发给新玩家让新玩家能生成所有已有角色。把新玩家加入的消息广播给房间内其他玩家让他们生成新角色。这里经常出现一个问题新玩家加入时其他玩家已经在采集资源或打怪。如果每个客户端各自维护一份资源状态新玩家看到的资源和老玩家不一致。所以服务端要保存一个“世界对象列表”新玩家加入时一次性拿到快照。6.2 物资、生命值和怪物的同步策略物资采集的逻辑建议这样设计客户端按 E发送 C2S_Collect包含 targetId。服务端收到后判断目标是否存在、离玩家距离是否合理、采集冷却是否结束。通过后服务端更新物资数量向房间广播 B2C_ResourceUpdate。所有客户端收到后刷新 UI 和场景物品状态。生命值扣减类似。玩家被怪物攻击时客户端播放动画但实际扣血由服务端进行。服务端扣完血后广播最新 Hp。如果 Hp 降到 0服务端广播玩家死亡消息。怪物生成由服务端定时器触发比如每隔 30 秒在房间随机位置生成 1 只怪物。生成后广播 SpawnEnemy带上怪物 ID 和坐标。所有客户端都根据同一份怪物列表渲染才不会出现“一个人面前有怪另一个人面前没有”的错位。这些功能看起来繁琐但本质只有一句话所有影响世界状态的数据都从服务端出发再广播到客户端。客户端之间不直接通信。6.3 为什么作弊问题要从服务端校验开始考虑哪怕只是个实习项目也要养成“客户端不可信”的思维习惯。面试官大概率会问“如果玩家修改客户端给自己加木头怎么办”正确回答思路是服务端不信任客户端上报的结果服务端只信任客户端上报的操作并对操作做合法性校验。比如采集树木客户端说是“采集了 50 次”服务端不会直接加 50而是每次采集都校验树是否存在、距离是否够、冷却是否结束然后由服务端统一更新数量。这样就算玩家伪造了采集请求服务端也会因为距离、冷却等校验条件不满足而拒绝。当然防作弊不止这一层。实际开发还有加密、签名、防调试等做法但对实习项目来说你只要做到了“服务端权威状态 操作合法性校验”就已经具备正确的工程观念。7. 测试与排错先看端口再看连接再看消息7.1 多开客户端做联机测试的方法联机测试必须真的有两个客户端同时在线。最常用的方法是Unity 编辑器作为客户端 A再打一个 Development Build 作为客户端 B。两个客户端都连同一个服务端。不要尝试直接打开两个 Unity 编辑器跑同一个项目会遇到 Library 锁定。虽然 ParrelSync 这类插件能复制工程但如果你只是测联机打一个 exe 更直接。打包时勾选 Development Build这样能看到调试日志。服务端启动后先用命令确认监听成功。Windows 上netstat -ano | findstr 7777Linux 或 macOS 上lsof -i :7777如果服务端没监听客户端连接会直接失败后续所有问题都不用排查了。本机测试通过后再做局域网测试。把服务端监听改成 IPAddress.Any客户端连接地址改为运行服务端那台机器的局域网 IP。此时 Windows 防火墙很可能默认拦截入站端口需要手动放行 TCP 端口。不放行的表现是连接超时不是拒绝比较容易被误判成代码问题。7.2 常见报错与排查顺序联机项目排错最忌讳一上来就改代码。我建议按这个顺序检查端口是否被占用。报bind: only one usage of each socket address时说明有另一个进程占用了当前端口。查到 PID结束进程或换个端口。服务端是否在监听。netstat里看不到 LISTENING先解决启动问题。地址和端口是否匹配。客户端连127.0.0.1和连局域网 IP 不是一回事分别测能缩小范围。防火墙。局域网联机超时优先看防火墙是否拦截端口。连接被重置。tcp connection reset by peer这类错误通常是服务端主动断开或中间设备丢弃了连接。去服务端看日志有没有 Accept 记录。能连上但收不到数据。先确认服务端有没有收到消息服务端有没有广播客户端有没有进入主线程队列。位置抖动和瞬移。同步频率低或没有做插值。这里给一个简单的现象和排查对照表现象优先排查方向常见处理连接超时防火墙、IP 不可达、服务端未启动放行端口确认监听检查 IP连接被拒绝端口没监听、服务端启动失败看服务端日志确认监听端口连接被重置服务端主动断开、中间设备拦截看服务端日志确认 Accept 和握手流程能连上没数据协议长度、解析、主线程队列分别看服务端收发包和客户端收包日志位置瞬移同步频率、插值缺失降低同步间隔给远程角色加 LerpUI 卡顿接收线程阻塞、主线程处理过多消息入队列限制每帧处理条数7.3 心跳、断线重连和日志要提前设计联机调试时日志越早设计越好。服务端每条连接、加入、广播、断开都要有日志。客户端连接状态变化也要有日志和 UI 提示。服务端日志示例[00:00:01] Accept client: 192.168.1.10:56789, connId1 [00:00:01] JoinRoom roomCodeAB1234 connId1 playerCount2 [00:00:10] Collect tree_001 count9 connId1 [00:00:15] Timeout no ping, close connId1分析问题时先看日志里有没有你期待的那条记录。如果服务端日志里连 Accept 都没有客户端再怎么改也没用。断线重连要考虑。简单的退避策略是第 1 秒重连第 2 秒第 4 秒第 8 秒最多尝试 5 次。每次重连前要清理旧 Socket 和旧 NetworkStream否则容易出 SocketException。重连成功后客户端要重新发送加入房间请求因为服务端已经在上次断开时清理了房间数据。8. 简历怎么写面试官可能追问哪些问题8.1 项目描述的一种直接写法简历上的项目描述不要只写“我开发了一个多人联机游戏”要按“技术栈 功能范围 关键设计”的结构来写。可以用这个结构项目名称基于 Unity 与 C# Socket 的 TCP 多人合作生存游戏技术栈Unity、C#、System.Net.Sockets、TCP/IP、JSON 消息协议项目简介实现 4 人房间制合作生存玩法包含角色移动同步、资源采集同步、怪物生成同步、生命值与饥饿值同步。关键职责独立设计并实现客户端网络层使用长度前缀方案解决 TCP 粘包半包问题。实现服务端连接管理、心跳检测、消息分发和房间管理。使用服务端权威校验资源采集和血量计算避免客户端数值被篡改。通过主线程消息队列处理 Socket 回调保证 Unity 运行稳定。有一件事必须提醒简历里写的数字要真实。比如“支持 4 人同时在线”“连续运行 2 小时未断开”“局域网同步延迟稳定”都必须是你在自己环境里实测出来的不要随便写“支持万人并发”。面试官在这个项目上会默认真实一旦发现数字对不上整个项目信任度都会受损。8.2 面试追问点与回答思路围绕这个项目面试官最可能问这些问题。为什么用 TCP不用 UDP回答思路合作生存玩法对可靠性和顺序要求更高。TCP 自带可靠性保证先做 TCP 可以先把玩法做完整如果之后做实时对抗更重的玩法再研究 KCP 或 UDP 方案。这也说明你知道 TCP 不是唯一答案。TCP 三次握手和四次挥手在你的项目里体现在哪回答思路客户端 ConnectAsync 时发生三次握手服务端 Accept 后连接建立。断开时客户端或服务端 Close TcpClient触发四次挥手。能结合自己项目的日志说明更佳。粘包是怎么产生的怎么解决回答思路TCP 是流式协议没有消息边界发送方可能多条合并接收方可能一次读不完整。用长度前缀先读 4 字节长度再循环读满消息体。服务端如果 100 个玩家同时在线会怎样回答思路先承认当前项目是学习型设计几十连接量级可跑然后说瓶颈每条连接一个接收线程锁竞争、广播量会增大。后续可以用异步 Socket、事件循环、消息队列、房间分片去优化。重点是别吹自己支持百万并发。两个玩家同时采集同一棵树怎么处理回答思路服务端统一裁决。同一棵树的采集请求同时到达时服务端只让距冷却判断、避免重复减数量客户端只显示服务端广播后的结果。只要把这些问题过一遍面试时即使追问到细节你也能答得比较从容。项目本身不需要很复杂关键是每个设计决定你都能说出“为什么”。最后留一句经验做这个项目时代码可以糙日志和方案思路不能乱。联机项目的调试成本比单机高很多越早加日志、越早做小范围测试后面越省时间。你能把这套流程完整走一遍在 27 届求职实习里就足够有说服力了。
返回列表