下载 App

《妖灵志》多人社交系统开发实录:从同屏到私聊的完整实现

精华修改于04/23148 浏览开发交流
一个人做 MMO 社交系统到底有多难?这篇帖子记录了我在《妖灵志》中实现多人同屏、世界聊天、好友系统、私聊功能的完整过程,包括踩过的坑和最终的架构方案。
horizontal linehorizontal line

先看效果

《妖灵志》是一款回合制 MMO 手游,玩家可以在多张地图自由探索、抓妖、战斗。社交系统是游戏的核心体验之一,目前已经实现了:
多人同屏 — 同一地图的玩家实时显示,走动流畅不卡顿
世界聊天 — 多频道聊天(世界、系统、传闻、帮派、跨服)
好友系统 — 搜索加好友、查看在线状态和角色信息、删除好友
好友私聊 — 一对一聊天、未读消息提醒、聊天记录
下面逐个拆解实现思路。
horizontal linehorizontal line

一、项目架构总览

整个多人系统采用 C/S 架构,服务端为常驻服(Persistent World),代码结构如下:
scripts/
├── main.lua                    # 客户端入口
├── server_main.lua             # 服务端入口
├── network/
│   ├── Client.lua              # 客户端网络逻辑
│   ├── Server.lua              # 服务端核心逻辑
│   ├── MultiplayerSync.lua     # 玩家状态同步
│   ├── Shared.lua              # 共享常量和工具
│   └── SaveEvents.lua          # 网络事件定义
├── game/
│   ├── ChatSystem.lua          # 聊天数据管理
│   ├── FriendSystem.lua        # 好友数据管理
│   └── ...
└── ui/
    ├── ChatOverlay.lua         # 聊天界面
    ├── FriendOverlay.lua       # 好友界面
    └── ...
网络通信通过 RemoteEvent 实现,一共定义了 17 个事件:
| 类别 | 事件 |
|------|------|
| 玩家同步 | SYNCPLAYERSTATE, SYNCWORLDSTATE, PLAYERJOINEDMAP, PLAYERLEFTMAP |
| 聊天 | CHATSEND, CHATBROADCAST |
| 好友 | FRIENDREQUESTSEND/RECV, FRIENDACCEPTSEND/RECV |
| 私聊 | WHISPERSEND, WHISPERRECV |
| 存档 | SAVECHARACTERS, LOADRESULT, SAVERESULT |
| 管理 | GMACTION, GMACTIONRESULT |
所有事件集中定义在 SaveEvents.lua,好处是客户端和服务端共享同一份事件名,改名不怕漏。
horizontal linehorizontal line

二、多人同屏同步

核心思路

客户端每 100ms(10Hz) 上传一次自己的位置、朝向、所在地图等状态给服务端。服务端收到后按地图分组,只把同一张地图里其他玩家的数据广播回来。
客户端 A 上传状态
       ↓
    服务端收集
       ↓
  按地图分组广播
       ↓
客户端 B 收到 A 的位置 → 插值渲染

同步的数据

每次同步只传必要字段,控制包体大小:
lua
{
    px, py,           -- 像素坐标(精确到 0.1)
    dir,              -- 朝向(8 方向:s/se/e/ne/n/nw/w/sw)
    scene,            -- 当前地图名
    charImgIdx,       -- 角色外观索引
    charName,         -- 角色名
    level,            -- 等级
    displayUID,       -- 6 位脱敏 UID
    factionId,        -- 门派 ID
    branchId          -- 分支 ID
}
带宽优化:服务端在广播前会把坐标取一位小数 math.floor(x * 10) / 10,减少无意义的精度传输。

客户端插值

收到其他玩家的目标位置后,不是直接跳过去,而是用 Lerp 插值平滑移动:
```lua
local lerpSpeed = 8.0
local dx = rp.targetPX - rp.px
local dy = rp.targetPY - rp.py
local dist = math.sqrt(dx * dx + dy * dy)
if dist > 1 then
    -- 还没到目标位置,平滑移动
    rp.px = rp.px + dx * math.min(lerpSpeed * dt, 1.0)
    rp.py = rp.py + dy * math.min(lerpSpeed * dt, 1.0)
    CharacterAnimator.SetState(rp.animator, "walk")
else
    -- 到了,停下来
    rp.px = rp.targetPX
    rp.py = rp.targetPY
    CharacterAnimator.SetState(rp.animator, "idle")
end
```
lerpSpeed = 8.0 是反复调出来的值——太小会有明显拖影,太大又会出现瞬移感。

视觉区分

为了让玩家一眼区分自己和别人:
自己:使用自定义名字颜色
其他玩家:绿色名字标签 {140, 230, 180},脚下绿色阴影圆圈

场景分组

服务端按地图名分组广播,同一张地图才能看到对方。这样做的好处:
节省带宽:不在同一张地图的玩家互不干扰
逻辑清晰:玩家切换地图时自动从旧地图消失、在新地图出现
```lua
-- 服务端:按地图分组
local sceneGroups = {}
for connKey, pState in pairs(playerStates_) do
    local scene = pState.state.scene
    if not sceneGroups[scene] then
        sceneGroups[scene] = {}
    end
    table.insert(sceneGroups[scene], { connKey = connKey, state = pState.state })
end
-- 只广播给同场景的其他玩家
for sceneName, players in pairs(sceneGroups) do
    if #players >= 2 then
        for i, p in ipairs(players) do
            local others = {}
            for j, q in ipairs(players) do
                if i ~= j then
                    table.insert(others, q.state)
                end
            end
            -- 发给玩家 i
        end
    end
end
```
horizontal linehorizontal line

三、世界聊天系统

频道设计

聊天系统支持 5 个频道,其中世界和帮派可发言,系统和传闻只读:
| 频道 | 说明 | 可发言 |
|------|------|--------|
| 世界 | 全服广播 | 是 |
| 系统 | 系统公告、战斗日志 | 否 |
| 传闻 | 服务器事件通知 | 否 |
| 帮派 | 帮派内部聊天 | 是(预留) |
| 跨服 | 跨服聊天 | 是(预留) |

消息类型与颜色编码

不同来源的消息用不同颜色显示,一眼就能分清:
lua
MSG_COLORS = {
    system  = { 255, 220, 100 },   -- 金色:系统消息
    whisper = { 100, 220, 255 },   -- 天蓝:私聊
    world   = { 200, 255, 200 },   -- 浅绿:世界聊天
    battle  = { 255, 140, 100 },   -- 橙红:战斗信息
}

频道过滤

每个频道标签页会聚合相关的消息类型:
lua
CHANNEL_FILTER = {
    world   = { "world", "player", "npc" },
    system  = { "system", "battle" },
}
切到"世界"标签只看世界频道的聊天,切到"系统"只看系统公告和战斗日志。

消息流转

玩家 A 发消息 → CHAT_SEND → 服务端
                              ↓
              遍历所有在线玩家(排除发送者)
                              ↓
               CHAT_BROADCAST → 玩家 B, C, D...
服务端代码很直白:
lua
function HandleChatSend(eventType, eventData)
    -- 广播给除发送者外的所有人
    for ck, conn in pairs(serverConnections_) do
        if ck ~= connKey then
            conn:SendRemoteEvent(EVENTS.CHAT_BROADCAST, true, vm)
        end
    end
end

消息上限

客户端最多保存 100 条消息,超过自动淘汰最旧的。聊天记录不做持久化存储——重新登录就清空,这对于世界聊天来说完全够用。
horizontal linehorizontal line

四、好友系统

功能列表

1、搜索好友:支持按角色名或 6 位 UID 搜索
2、发送/接受/拒绝好友请求
3、好友列表:按在线状态排序(在线 > 忙碌 > 离线),同状态按等级排
4、查看好友信息:等级、门派、最后在线时间
5、删除好友:二次确认弹窗

上限

lua
MAX_FRIENDS = 50  -- 最多 50 个好友

好友数据结构

lua
{
    name,           -- 角色名
    level,          -- 等级
    status,         -- 状态:online / busy / offline
    lastOnline,     -- 最后在线时间
    charImgIdx,     -- 角色外观
    uid,            -- 用户 ID
    displayUID,     -- 6 位脱敏 UID
    factionId,      -- 门派
    branchId,       -- 分支
    faction         -- 门派名称
}

好友请求流程

玩家 A 发送好友请求
     ↓  FRIEND_REQUEST_SEND
   服务端查找目标玩家
     ↓  FRIEND_REQUEST_RECV
   玩家 B 收到请求弹窗
     ↓  接受 → FRIEND_ACCEPT_SEND
   服务端通知玩家 A
     ↓  FRIEND_ACCEPT_RECV
   双方互相添加
好友关系是双向的,A 加了 B,B 的列表里也会有 A。

在线状态排序

好友列表不是乱序的,按状态 + 等级双重排序:
lua
local order = { online = 1, busy = 2, offline = 3 }
table.sort(friends_, function(a, b)
    local oa = order[a.status] or 3
    local ob = order[b.status] or 3
    if oa ~= ob then return oa < ob end
    return a.level > b.level
end)
在线的好友永远排在最前面,方便快速找到能一起玩的人。

界面设计

好友界面分 4 个标签页:
| 标签 | 功能 |
|------|------|
| 最近联系 | 最近私聊过的好友,按时间排序 |
| 好友列表 | 全部好友,按状态排序 |
| 好友请求 | 待处理的请求 |
| 添加好友 | 搜索框 + 搜索结果 |
horizontal linehorizontal line

五、好友私聊

消息存储

每个好友独立维护一份聊天记录,最多保存 50 条:
```lua
MAXPRIVATEMSGS = 50
-- 数据结构
privateMessages_ = {
    ["好友名A"] = {
        { sender = "我", content = "在干嘛", time = 1700000000 },
        { sender = "好友名A", content = "打副本呢", time = 1700000005 },
    },
}
```

消息路由

私聊走的是点对点转发,不是广播:
玩家 A → WHISPER_SEND → 服务端
                          ↓
         在 playerStates_ 中查找目标玩家
                          ↓
         WHISPER_RECV → 玩家 B(仅目标玩家)
服务端只做转发,不存储私聊内容。

未读消息追踪

这是私聊体验的关键细节:
1、收到私聊时,如果聊天窗口没打开,未读计数 +1
2、好友列表里显示红色未读角标
3、好友按钮上显示总未读数
4、打开和某个好友的聊天窗口后,该好友的未读清零
```lua
-- 收到私聊
function FriendSystem.AddPrivateMessage(friendName, sender, content)
    -- 存储消息
    -- 更新最近联系人时间戳
end
-- 未读管理
FriendSystem.AddUnread(senderName)    -- +1
FriendSystem.ClearUnread(friendName)  -- 清零
FriendSystem_.GetTotalUnread()         -- 总数(用于角标)
```

聊天 UI

采用经典的气泡聊天布局:
自己的消息:靠右,蓝色背景
对方的消息:靠左,深色背景
底部输入框 + 发送按钮
horizontal linehorizontal line

六、屏蔽与管理

玩家屏蔽

可以屏蔽指定玩家,屏蔽后:
1、世界聊天里看不到他的消息
2、屏蔽列表持久化到云存档

GM 管理面板

服务端支持 GM 操作,通过白名单控制权限:
| 操作 | 说明 |
|------|------|
| 禁言 | 指定时长,到期自动解除 |
| 封禁 | 踢出并禁止登录 |
| 踢出 | 强制断开连接 |
禁言和封禁都是基于时间戳的,到期后自动清除,不需要手动解封:
```lua
mutedUsers_[userId] = os.time() + duration * 60
-- 检查时自动清理
if muteEnd and os.time() >= muteEnd then
    mutedUsers_[userId] = nil
end
```
horizontal linehorizontal line

七、数据持久化

玩家数据(包括好友列表)通过 serverCloud
API 持久化到云端:
lua
CLOUD_KEYS = {
    CHARACTERS   = "characters_v1",
    LAST_PLAYED  = "last_played_v1",
    AUDIO        = "audio_settings_v1",
}
Key 带 _v1 版本后缀,方便后续数据结构升级时做向后兼容。
登录时服务端批量拉取所有存档:
lua
serverCloud:BatchGet(userId)
    :Key(KEYS.CHARACTERS)
    :Key(KEYS.LAST_PLAYED)
    :Key(KEYS.AUDIO)
    :Fetch({
        ok = function(values)
            -- 一次性发给客户端
        end
    })
horizontal linehorizontal line

八、踩过的坑

1. 网络节点必须用 Dispose()

多人场景中创建的网络节点,销毁时不能用 Remove(),必须用 Dispose(),否则会内存泄漏。这个问题调了很久才发现。

2. 同步频率的平衡

最初用 20Hz 同步,带宽撑不住。后来降到 10Hz,配合客户端插值,视觉效果几乎没有差异,但带宽直接砍半。

3. 坐标精度

浮点数直接传输会有很多无意义的小数位,math.floor(x * 10) / 10 取一位小数就够了,包体更小。

4. UID 脱敏

不能把真实用户 ID 暴露给其他玩家,所以做了一层 6 位哈希映射。用 XOR 混淆 + 取模生成,简单但够用。
horizontal linehorizontal line

九、总结

整个社交系统的代码量分布:
| 模块 | 文件 | 职责 |
|------|------|------|
| MultiplayerSync.lua | 同步逻辑 | 上传/接收/插值/渲染 |
| ChatSystem.lua | 聊天数据 | 消息存储、频道过滤 |
| ChatOverlay.lua | 聊天 UI | 频道标签、消息列表、输入框 |
| FriendSystem.lua | 好友数据 | 好友 CRUD、私聊记录、未读追踪 |
| FriendOverlay.lua | 好友 UI | 列表、搜索、聊天气泡 |
| server_main.lua | 服务端 | 广播、转发、GM、云存档 |
| SaveEvents.lua | 事件定义 | 17 个网络事件 |
几个关键的设计决策:
1. 10Hz 同步 + 客户端插值 — 带宽和体验的最佳平衡点
2. 场景分组广播 — 不在同一张地图就不同步,大幅节省带宽
3. 事件集中定义 — 客户端和服务端共享事件名,减少出错
4. 依赖注入 — 每个模块通过 `Init(deps)` 注入依赖,方便测试和替换
5. 时间戳自动过期 — 禁言、封禁到期自动清除,不需要定时任务
如果你也在做多人游戏,希望这篇帖子能给你一些参考。有问题欢迎评论区讨论。
猜你想搜
taptap 制造多人同屏实现
别再烧积分了!从每次 2000 到每次 50 的血泪经验截图
别再烧积分了!从每次 2000 到每次 50 的血泪经验
适用场景:所有在用 TapTap Code 开发游戏的小白 难度:★☆☆☆☆ 背景最近群里总有人"炫耀"一次操作花了几千积分,说实话这不是值得高兴的事——这是在告诉所有人你的开发方式有根本性问题。我也走过这条弯路,所以来说说怎么改。 积分消耗的本质是什么积分 = AI 处理的 token 数量 = 你发给 AI 的内容 + AI 回给你的内容所以烧积分的本质只有两种: 你让 AI 做了大量无效/重
精华
68 赞
21 回复
03:06
为了不被斩杀,我只能做到这些了截图
为了不被斩杀,我只能做到这些了
之前一直申请不到计划3,之前说太花哨,后面说体量完成度不足,在我一番小作文下,编辑终于给了我解答,什么原生质感,玩家开局体验啥的。 本来计划2也够用,准备以后再优化了,结果计划2赠送的积分要砍半了,学生党实在消费不起,于是只能再冲一波计划3了 从周六中午到现在一天半时间,优化了一遍角色卡,战斗卡,还有抽卡界面,优化了新手教程,该说不说确实比之前要舒服些。 前半部分为新版,后半部分为旧版。 明天再次
35 赞
6 回复
01:27
《大二暑假,我用AI做了一款属于自己的游戏》截图
《大二暑假,我用AI做了一款属于自己的游戏》
本人自制游戏现已加入taptap封闭测试(仅适配安卓机型)欢迎大家前来尝试(八月十号可以正式开始游玩) 如果有建议或者bug反馈可以加入qq群928501443 #发现好游戏 #游戏日常 #浅评一下 #今天游戏圈发生了啥
3 赞
如何提升你游戏中的UI美术水平截图
如何提升你游戏中的UI美术水平
一、请为你的游戏挑选一款合适的字体 很多开发者没有注意到其实UI界面中最重要的元素是字体,一款合适的字体能给整个游戏的界面感官带来蜕变。 请看案例 那么如何挑选适合自己的字体呢? 我总结出了一些小经验可供参考: 黑体:通用 宋体:古风、武侠、修仙 楷体:古风、武侠、修仙 圆体:卡通、动漫、Q版 科技:科幻、未来、电子 顺手分享一个免费可商用字体网站: 当然 如果你不知道自己的游戏适合什么风格
精华
123 赞
44 回复
00:26
塔啦啦科研:三渲二(风格1)截图
塔啦啦科研:三渲二(风格1)
最近在研究像素风格的三渲二,一直想做小丑牌那种风格,但是塔啦啦一直做不出来,发现是有一部分的功能做不了,只能实现简单的材质制作和描边,因此,我从零直接搭建了一整套引擎基础。目前进度可以看视频。 @嗒啦啦 什么时候出shader 评论区随意提问 #TapTap制造 #TapTap制造社区 #TapTap制造
4 赞
5 回复
开发心得-界面布局心得截图
开发心得-界面布局心得
大家好,我正在用 TapTap 制造制作一款2D 治愈林间徒步旅行游戏《徒步荒野》。 我分享一下我的开发心得,希望能有用,下面是一些界面布局的心得。 第一步:定版式、操作 先确定游戏是横版还是竖版、需要哪些操作按钮,敲定整体玩法思路。 我的玩法灵感参考《边境之旅》,主打散步看风景、沿途触发随机事件。 原版是 3D,考虑到自己是新手,选择2D 横版街机式来做,制作更简单、好上手。 第二步:敲定全局美
6 赞
3 回复
2D 等轴测视角:完整理论截图
2D 等轴测视角:完整理论
2D 等轴测视角:完整理论 一、视角的本质 什么是等轴测 人站在高处斜着往下看地面,地面上的方格子不再是正方形,而变成了菱形。这就是等轴测的视觉本质。 俯视(正上方看下去): 等轴测(斜上方看下去): ┌──┬──┬──┐ ◇ ├──┼──┼──┤ ◇ ◇ ├──┼──┼──┤ ◇ ◇ ◇ └──┴──┴──┘ ◇ ◇ ◇ 正方形网格经过旋转 + 压缩,变成了菱形网格。 为什么是 2:1
6 赞
【Tap make 塔拉拉之立项干货】萌新看过来!截图
【Tap make 塔拉拉之立项干货】萌新看过来!
题外话:本人也是tap make 使用者之一。塔啦啦忠实信徒、天哥忠实拥护者、八年游戏策划经验的低级策划。但是!我已经做出来一款5000+热度、且稳定可更新的框架、并且还在持续产出更多游戏内容的橘猫! 这几天正好在开新坑。我是3月初加入的tapmake大家庭。两周时间于3月25日将这个作品上架。当时的全凭感觉摸索。如今,自己也是一个有自己项目的小菜鸡了。今天,就来分享一下,如何高效的制作一款高大上
投票精华
68 赞
24 回复
「游戏上新」秦王的游戏截图
「游戏上新」秦王的游戏
你是秦王嬴政身边的廷臣。 方士徐福献上一匣【帝王卡牌】,四张凶牌封死命运: 屠戮|淫欲|奢华|征伐 七日死限,必须一一折断卡牌对应的欲望任务。 办不成? 秦王殿上长剑出鞘,君臣性命,顷刻倾覆。 ⚔游戏核心玩法 ▪回合卡牌策略,抉择牵动秦廷朝堂势力 ▪朝堂派系博弈:王权、法家、军功贵族、方士暗流角力 ▪多重结局:功成辅秦王一统,或是任务失败血染咸阳宫 ▪随机事件触发,每一局嬴政的猜忌程度截然
4 赞
2 回复
01:29
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,截图
官方阅,申请积分计划2,希望官方继续支持我完成这款游戏,
视频内容只是一部分, 本游戏耗时3个月,目前完成进度百分之80, 是生存游戏,进入修仙界,走剧情找到宗门加入, 这时游戏才是真正开始, 提升实力境界后就可以变强,获取功法法术法宝, @视频内是游戏里挑战妖族入侵一种主流玩法,游戏就是围绕着这个来进行, 还有很多地方可以战斗,获得资源修炼变强,比如去藏书阁学习功法, 去法宝楼抽法宝,每一个宗门数十个法宝抽取 去修仙界探索,获得各自的资源,可能遇见妖兽
1 赞
3 回复