简介这是一份面向Unity初学者的轻量级联网聊天室教学Demo聚焦网络通信基础实践帮助开发者快速掌握Unity Networking API的核心用法。资源包含完整可运行项目涵盖服务器端消息中转、客户端UI交互、消息实时收发等关键环节适用于网络编程入门、课设开发或多人互动应用原型搭建。压缩包共435个文件主体为11个C#网络脚本实现NetworkManager、消息广播与同步逻辑、23个Unity asset资源含场景、预制体与UI组件、138个info配置文件及52个bin二进制文件整体仅3.74MB结构紧凑、无冗余依赖。已有1207人学习下载项目代码清晰、注释充分附带完整场景配置与网络管理器预设开箱即用学习者可直接运行双实例验证通信效果亦可逐模块修改调试深入理解连接建立、状态同步与异常处理等实战要点。1. 这不是“Hello World”式聊天框一个能真连局域网、跑通消息收发、带基础UI状态反馈的 Unity 聊天室 DemochatDemo.zip你试过在 Unity 里点开一个“聊天室 Demo”双击运行界面上弹出个输入框和发送按钮——然后无论怎么敲字、点发送控制台静得像关了麦NetworkManager 没报错、也没连上任何东西这不是你环境有问题而是很多标着“Unity 聊天室”的资源本质是 UI 布局练习册压根没接网络层。而这个chatDemo.zip不同它用 Unity 自带的Unity Transport非旧版 UNET也非第三方插件实现轻量级 TCP 连接服务端是 C# 控制台程序客户端可多开支持局域网内真实消息广播、连接状态可视化绿色/红色指示灯、消息时间戳与来源标识。它不追求 WebRTC 音视频或百万并发但能让你 10 分钟内看到“张三发你好”实时出现在李四窗口——对刚学完NetworkVariable、卡在“怎么让两个实例真正说话”阶段的开发者这是少有的、能立刻验证“联网逻辑闭环”的最小可行包。适合 Unity 中级入门者做二次开发起点也适合作为某高校课程设计中“网络通信模块”的参考基线。2. 从解压到首条消息完整运行链路与核心组件定位这个压缩包不是单个.unitypackage而是一个结构清晰的工程快照。解压后你会看到三个并列文件夹ClientUnity 客户端工程、ServerC# 控制台服务端、Docs含简要配置说明。它不依赖 Unity Cloud 或外部服务器所有通信走本地局域网 IP这意味着你不需要注册账号、不用配域名、甚至不用联网——只要两台设备在同一 WiFi 下就能完成端到端测试。2.1 服务端启动3 行命令跑起消息中转站服务端是纯 .NET 6 控制台程序无 GUI靠日志输出体现运行状态。进入Server文件夹后执行# 确保已安装 .NET 6 SDK验证dotnet --version 应输出 6.x.x dotnet restore dotnet build -c Release dotnet run --project ChatServer.csproj提示若提示dotnet命令未找到请先从 https://dotnet.microsoft.com/download/dotnet/6.0 下载安装 SDK。不要用 .NET 7本项目未做兼容性升级.NET 7 运行时会因UdpNetworkDriver初始化失败而直接退出。成功启动后终端将打印[INFO] ChatServer started on 0.0.0.0:7777 [INFO] Waiting for clients...这表示服务端已在本机所有网卡的7777端口监听。注意0.0.0.0是通配地址意味着同一局域网内任意设备如手机、另一台电脑都能连接它只要知道运行服务端那台机器的局域网 IP例如192.168.1.105。2.2 客户端配置改一行 IP 就能连上真实服务端Unity 客户端工程Client基于 Unity 2021.3.33f1 LTS 构建打开前请确认你的 Unity Hub 已安装该版本其他 2021.3.x 版本大概率兼容但 2022 版本因 Transport API 微调需手动适配。关键配置在Assets/Scripts/Network/ChatClient.cs的第 22 行public string serverIp 127.0.0.1; // ← 修改此处局域网测试时填服务端实际IP如 192.168.1.105 public int serverPort 7777;若你在同一台机器上测试服务端和客户端都运行在本机保留127.0.0.1即可若想用笔记本当服务端、用台式机当客户端则需将台式机客户端里的serverIp改为笔记本的局域网 IP通过ipconfigWindows或ifconfigmacOS/Linux查得serverPort必须与服务端启动日志中显示的端口号一致默认7777可在Server/Program.cs第 15 行修改。改完保存回到 Unity 编辑器点击 Play 按钮。你会看到 UI 右上角出现一个圆形状态灯初始为红色几秒后变为绿色并在下方日志区显示[Connected] to 192.168.1.105:7777—— 这是连接成功的明确信号。2.3 发送第一条消息UI 交互与底层协议的映射关系客户端 UI 由Canvas下的InputField输入框、Button发送按钮、ScrollView消息历史容器构成。所有逻辑集中在ChatClient.cs。当你在输入框输入文字并点击发送实际触发的是// ChatClient.cs 中的 OnSendButtonClick 方法 private void OnSendButtonClick() { if (string.IsNullOrWhiteSpace(inputField.text)) return; // 1. 构造消息结构体固定长度避免粘包 var msg new ChatMessage { messageType MessageType.Text, senderId clientId, // 客户端自动生成的唯一IDUUID字符串 timestamp DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), contentLength (ushort)inputField.text.Length, content inputField.text }; // 2. 序列化为字节数组使用 MemoryWriter非 JSON var writer new FastBufferWriter(256, Allocator.Temp); writer.WriteValueSafe(msg); // 3. 通过 NetworkDriver.Send 发送至服务端 if (connection ! null connection.State NetworkConnection.State.Connected) { driver.BeginSend(connection, out var writerRef); writerRef.WriteBytes(writer.ToByteArray()); driver.EndSend(writerRef); } inputField.text ; // 清空输入框 }这里的关键点在于它没有用JsonUtility或Newtonsoft.Json做序列化而是用 Unity Transport 自带的FastBufferWriter写入二进制流。ChatMessage是一个[StructLayout(LayoutKind.Sequential)]标记的结构体确保内存布局严格对齐服务端才能用MemoryReader精准解析。这种设计牺牲了可读性但换来零 GC 分配和毫秒级解析速度——这也是它能在低端设备上稳定跑 30 客户端的原因。3. 消息如何不丢、不乱、不错Transport 层关键参数与心跳机制Unity Transport 并非开箱即用的“傻瓜式”网络库它的可靠性高度依赖驱动配置。chatDemo.zip在Client/Assets/Scripts/Network/NetworkManager.cs中做了针对性调优这些参数直接影响你是否遇到“消息发了但对方收不到”、“断网后无法自动重连”等玄学问题。3.1 Driver 配置超时、重传与缓冲区的黄金三角NetworkManager.cs的InitializeDriver()方法中NetcodeNetworkConfig对象定义了核心行为var config new NetcodeNetworkConfig { // 关键1连接建立超时单位毫秒 ConnectTimeoutMS 5000, // 默认 3000 太短局域网偶尔抖动就失败 // 关键2数据包重传策略TCP 模式下此参数被忽略但 UDP 模式必须设 MaxRetransmissionCount 3, // 重传 3 次后放弃避免无限等待 // 关键3发送/接收缓冲区大小字节 SendQueueCapacity 1024 * 1024, // 1MB 发送队列防突发消息堆积 ReceiveQueueCapacity 1024 * 1024, // 1MB 接收队列防消息洪峰丢失 // 关键4心跳间隔单位毫秒 HeartbeatTimeoutMS 10000, // 10秒无响应判定断连比默认 5000 更稳 };为什么这些值重要ConnectTimeoutMS5000实测发现某些路由器在首次建立 TCP 连接时会有 2~3 秒延迟3000ms 超时会导致频繁“连接失败”假警报SendQueueCapacity1MB当用户连续快速点击发送比如连发 50 条测试消息小缓冲区会直接丢弃后续包而 1MB 能平滑吞吐HeartbeatTimeoutMS10000若设为默认 5000Wi-Fi 切换瞬间如从客厅走到卧室极易触发误判断连10 秒给了网络恢复缓冲期。3.2 心跳包的实现位置与调试技巧心跳不是独立线程轮询而是由 Transport 驱动内置机制触发。你可以在ChatClient.cs的Update()中看到显式心跳发送逻辑private float heartbeatTimer 0f; private const float HEARTBEAT_INTERVAL 5f; // 每5秒发一次心跳 void Update() { heartbeatTimer Time.deltaTime; if (heartbeatTimer HEARTBEAT_INTERVAL connection.State NetworkConnection.State.Connected) { var heartbeatMsg new ChatMessage { messageType MessageType.Heartbeat, senderId clientId, timestamp DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; var writer new FastBufferWriter(64, Allocator.Temp); writer.WriteValueSafe(heartbeatMsg); driver.BeginSend(connection, out var writerRef); writerRef.WriteBytes(writer.ToByteArray()); driver.EndSend(writerRef); heartbeatTimer 0f; } }注意服务端收到MessageType.Heartbeat后不会返回内容仅更新该客户端的最后活跃时间戳。你在服务端日志中看不到“收到心跳”但若客户端断网服务端会在HeartbeatTimeoutMS10秒后主动关闭连接并打印[DISCONNECTED] Client: xxx。3.3 消息顺序保证TCP 模式下的天然优势本 Demo 强制使用 TCP 协议NetworkDriverType.Tcp而非 UDP。这意味着消息绝对按发送顺序到达msg A先发msg B后发 → 服务端一定先收 A 后收 B不会出现“部分消息丢失但其余正常”的情况TCP 要么全到要么连接中断无需在应用层实现序列号、ACK 确认等复杂逻辑。这也是它比“UDP 自研可靠传输”方案更适合教学的原因省去理解滑动窗口、拥塞控制等计算机网络底层细节聚焦在“如何组织消息结构”和“如何处理连接状态”这两个最痛的点上。4. 避坑指南5 条血泪经验总结现象→原因→解决开发和测试过程中我踩过不少坑。以下是最常复现、最容易卡住新手的 5 个问题每一条都来自真实翻车现场。4.1 现象客户端 UI 状态灯一直红色控制台报Failed to connect to server原因服务端防火墙拦截了7777端口或 Windows Defender 防火墙默认阻止了新程序联网。解决Windows打开「Windows 安全中心」→「防火墙和网络保护」→「允许应用通过防火墙」→ 找到dotnet.exe或ChatServer.exe勾选「专用」和「公用」网络macOS系统设置 → 隐私与安全性 → 防火墙 → 防火墙选项 → 解锁后勾选dotnet验证在客户端机器上执行telnet 192.168.1.105 7777Windows或nc -zv 192.168.1.105 7777macOS/Linux若返回Connected则端口通畅。4.2 现象消息能发出去但服务端日志无记录客户端也收不到回显原因客户端ChatClient.cs中serverIp填了localhost或127.0.0.1但服务端运行在另一台机器导致连接被路由到本机环回地址而非真实局域网 IP。解决务必用ipconfig/ifconfig查服务端机器的真实局域网 IP形如192.168.x.x或10.x.x.x填入客户端脚本禁用localhost和127.0.0.1除非纯本机测试。4.3 现象发送中文消息后对方收到乱码如ä½ å¥½英文正常原因ChatMessage.content字段在序列化时未指定编码FastBufferWriter默认用 UTF-8但content是string类型其内部字节长度计算未考虑 UTF-8 多字节特性。解决在ChatMessage结构体中将content改为byte[]并在序列化前显式编码// 修改前错误 public string content; // 修改后正确 public byte[] contentBytes; // 替换原 content 字段 // 发送时 var utf8Bytes System.Text.Encoding.UTF8.GetBytes(inputField.text); msg.contentBytes utf8Bytes; msg.contentLength (ushort)utf8Bytes.Length;对应地接收端用System.Text.Encoding.UTF8.GetString(bytes)还原。本 Demo 原始包已修复此问题但若你自行修改结构体务必检查此处。4.4 现象多开客户端时第二个客户端连接后第一个客户端消息消失或重复刷屏原因服务端未为每个连接分配独立的消息缓冲区所有客户端共用一个Liststring存储历史广播时遍历该列表导致状态污染。解决服务端ChatServer.cs中为每个NetworkConnection创建独立的ClientSession对象包含clientId、lastHeartbeat、messageHistory私有历史不用于广播。全局广播只用ConcurrentQueueChatMessage存储待分发消息每个客户端连接的UpdateLoop独立消费该队列互不干扰。4.5 现象Unity 编辑器 Play 模式下一切正常但 Build 出的 EXE 运行时报DllNotFoundException: Unity.Networking.Transport原因Unity 构建时未自动打包 Transport 插件的原生 DLL如Unity.Networking.Transport.dll对应的libunitytransport.dll。解决打开Edit → Project Settings → Player → Other Settings找到Scripting Backend确保为MonoIL2CPP 模式需额外配置本 Demo 未适配在Publishing Settings中勾选Development Build和Script Debugging仅调试用最关键一步在Client/Assets/Plugins文件夹下确认存在x86_64子文件夹内含libunitytransport.dllWindows或libunitytransport.dylibmacOS。若缺失从 Unity 安装目录Editor/Data/PlaybackEngines/NetworkingPlayer/Plugins/复制对应平台 DLL 进来。5. 从“能跑”到“能改”三个可立即落地的二次开发方向拿到chatDemo.zip很多人停在“运行成功”就结束了。但它的真正价值在于作为一块可塑性极强的基板。下面三个方向我都在线上项目中验证过改动量均小于 50 行代码却能显著提升实用性。5.1 方向一添加用户名输入框替代默认 UUID3 步15 行代码当前客户端用System.Guid.NewGuid().ToString()生成clientId显示为一长串哈希值如a1b2c3d4...不利于识别。改成用户自定义昵称只需在Canvas下新增InputField命名为UsernameInput设Placeholder为“请输入昵称”修改ChatClient.cs在Start()中读取该输入框值作为clientIdpublic InputField usernameInput; // 在 Inspector 中拖入 void Start() { // ...原有初始化代码 if (!string.IsNullOrWhiteSpace(usernameInput.text)) { clientId usernameInput.text.Trim(); } else { clientId Guest_ Random.Range(1000, 9999); // 无输入时给随机访客名 } }在OnSendButtonClick()中将senderId改为clientId已存在并修改 UI 显示逻辑// 在 AddMessageToUI() 中 messageText.text $[{clientId}] {msg.content} ({FormatTime(msg.timestamp)});效果用户启动时输入“张三”所有消息前缀变为[张三]且服务端日志也同步显示。无需改服务端因为senderId字段本就透传。5.2 方向二实现私聊功能点对点消息非广播当前所有消息走BroadcastToAllClients()要支持私聊需扩展协议。服务端已预留MessageType.Private类型只需补充路由逻辑步骤操作代码位置1客户端发送时解析目标用户名如输入/msg 李四 你好ChatClient.csOnSendButtonClick()2构造ChatMessage时设messageType MessageType.PrivatetargetId 李四同上3服务端HandleIncomingMessage()中若类型为Private则只向targetId匹配的客户端发送而非广播ChatServer.cs关键代码片段服务端if (msg.messageType MessageType.Private) { var targetClient clientSessions.FirstOrDefault(c c.clientId msg.targetId); if (targetClient ! null) { SendToClient(targetClient.connection, msg); } else { // 发送失败提示给发送方 var failMsg new ChatMessage { messageType MessageType.System, content $[私聊失败] 用户 {msg.targetId} 不在线 }; SendToClient(clientConnection, failMsg); } }提示私聊不改变现有广播逻辑老客户端仍能收公聊新客户端可发私聊——协议完全向前兼容。5.3 方向三接入本地 SQLite 存储聊天记录离线可用Unity 客户端默认不存历史关闭再开就清空。用 SQLite 实现本地持久化只需引入sqlite-net-pclNuGet 包Unity 2021.3 支持 .NET Standard 2.0在Client/Assets/Plugins下放入SQLite-net.dll创建ChatDatabase.cs定义MessageRecord表public class MessageRecord { [PrimaryKey, AutoIncrement] public int Id { get; set; } public string Sender { get; set; } public string Content { get; set; } public long Timestamp { get; set; } public string Type { get; set; } // public, private, system } // 初始化数据库 var dbPath Path.Combine(Application.persistentDataPath, chat.db); var db new SQLiteConnection(dbPath); db.CreateTableMessageRecord();在AddMessageToUI()后追加存储db.Insert(new MessageRecord { Sender msg.senderId, Content msg.content, Timestamp msg.timestamp, Type msg.messageType.ToString() });效果重启客户端后调用db.TableMessageRecord().OrderByDescending(m m.Timestamp).Take(100)即可加载最近 100 条填充到ScrollView。存储路径Application.persistentDataPath保证跨平台Windows/macOS/iOS/Android一致。从那以后我每次接手新网络 Demo第一件事就是打开NetworkManager.cs查HeartbeatTimeoutMS和ConnectTimeoutMS——这两个值不对后面所有功能都是黑匣子你永远不知道是逻辑错了还是网络抖了一下。而chatDemo.zip把它们设成了经过局域网实测的合理值省去了我三天抓包调参的时间。希望帮到你。本文还有配套的精品资源点击获取