游戏后端的房间管理与匹配系统设计
game-bass 的实时联机模块用 Hub 模式。所有 WebSocket 连接汇聚到一个 Hub,单 goroutine 事件循环统一调度。房间管理和匹配系统都跑在这个 Hub 上。 房间生命周期数据结构房间的核心定义在 internal/realtime/room.go: 1234567891011121314151617type RoomState intconst ( RoomStateWaiting RoomState = 0 // 等待玩家 RoomStatePlaying RoomState = 1 // 游戏中 RoomStateFinished RoomState = 2 // 已结束)type Room struct { ID string Players map[string]*Player Config RoomConfig State RoomState CreatedAt time.Time PlayerStates ...
UE5.7 DDC 配置实践:Zen Server 部署指南
“改了一个材质的 Roughness 参数,Cook 跑了 45 分钟。” 这是上个月在朋友工作室亲眼看到的——一个 12 人的独立团队,每次改点东西就要等大半个小时 Cook。检查之后发现:DDC 根本没配。所有资产,每次 Cook,全部重新算一遍。 他们花的钱买 build 机器,不如把 DDC 配好。 DDC 到底是什么?DDC(Derived Data Cache,派生数据缓存)是 UE5 对”已经算过的东西不再算第二遍”的实现。当你 Cook 一张贴图、编译一个 Shader、或者导入一个 Mesh 时,引擎会把处理后的结果缓存在 DDC 里。下次用到同样的输入,直接从缓存读,跳过计算。 DDC 的缓存键(Cache Key)由这些因素决定: 1DDC Key = Hash(Asset 源码) + Engine 版本 + 平台 + 编译配置 + DDC 版本号 任何一个变了,Key 就不同,等于 Cache Miss,要重算。所以: 升级引擎 → 全量 DDC Miss 改了资产 → 只 Miss 这一个 换了平台(Win64 → Linux) → 全量 Mi...
UE5 分布式编译选型:FASTBuild 还是 Horde?
“UE5 多机分布式编译打包,从哪个版本开始有?应该怎么用?推荐什么配置硬件?有什么短板?” 这几个问题,来自一个正在搭 UE5 项目的朋友。当时他一个人扛着全栈——引擎编译 40 分钟、Cook 2 小时起步,每次改完代码去倒杯水,回来发现还没结束。 但回答他的过程,远比我想的复杂。 不是”从哪个版本开始有”这么简单UE5 的分布式编译方案,其实有三代。以下时间线基于 UE 5.7 的视角回溯: 12345678 UE4.5 UE4.20 UE 5.2 UE 5.7 │ │ │ │XGE/IncrediBuild ───┤──────────────┤──────────────┤──────────────┤─→ 商业 License,已边缘化 │ │ │ │FAS...
SLG 聊天系统:WebSocket + gRPC 双协议的频道架构
SLG 游戏的聊天系统是玩家社交的核心——世界频道刷屏、联盟频道指挥、私聊交易,每秒可能有上百条消息。slg-go 的聊天服务用 WebSocket 做实时推送,gRPC 做跨服务调用,Hub 模式管理频道订阅。 双协议设计 graph LR subgraph "客户端" A["游戏客户端"] end subgraph "Chat 服务" B["WebSocket Handler"] --> C["Hub"] D["gRPC Handler"] --> C C --> E["Channel"] C --> F["Filter"] C --> G["History"] end A -->|"WebSocket&qu...
PackedFloat32Array:一个 Godot 联机游戏的同步架构
godot-game-one 的联机模式只支持双人协作(Host + 1 Client),但同步复杂度不低——50+ 个敌人、80+ 颗子弹、Boss 护盾、激光特效,全都要在 22ms 内同步一次。这篇文章拆解 main.gd 里 1811 行网络同步代码的核心设计。 同步频率与数据格式12345# main.gdconst ENTITY_SYNC_INTERVAL: float = 0.022 # 位置同步间隔(~45Hz)const ENTITY_SYNC_STRIDE: int = 3 # 位置数据步长 [eid, x, y]const ENTITY_SHIELD_SYNC_STRIDE: int = 3 # 护盾数据步长 [eid, shield, max_shield]const ENTITY_ROTATION_SYNC_STRIDE: int = 2 # 旋转数据步长 [eid, rotation] 所有实体数据用 PackedFloat32Array 打包——不是 ...
一个 Redis 管 100 个游戏:game-bass 的多租户隔离设计
game-bass 是一个给独立游戏团队用的后端模板,卖出去几十份。每份模板跑在同一个服务器上,但数据必须完全隔离——A 游戏的排行榜不能出现在 B 游戏里。解决方案是多租户架构:一个 Redis 实例,用 key 前缀隔离所有游戏项目的数据。 租户 = 游戏项目每个游戏项目注册后获得一组凭证: 12345678// internal/tenant/tenant.gotype Info struct { TenantID string `json:"tenant_id"` // t_1, t_2, ... AppID string `json:"app_id"` // app_ + 64-bit 随机 AppSecret string `json:"app_secret"` // sk_ + 128-bit 随机 Name string `json:"name"` CreatedAt ti...
百万玩家的缓存设计:sync.Map + Redis + singleflight 三级防线
SLG 游戏的 Logic 服务每秒要处理成千上万次玩家数据查询——登录、改名、升级、战斗,每个操作都要读玩家实体。如果每次都查 Redis,延迟从 0.1ms 涨到 2ms;如果穿透到数据库,直接飙到 20ms。slg-go 的 PlayerCache 用三层防线把 99% 的请求挡在了内存里。 三层防线总览 graph TD A["请求 Get(playerID)"] --> B{"本地缓存命中?"} B -->|"命中"| C["返回 clone 副本"] B -->|"未命中"| D{"singleflight 合并"} D --> E{"Redis 命中?"} E -->|"命中"| F["写入本地缓存"] F --> C E -->|"未命中"| G["...
用完即毁:一个 77 行的对象池为什么选择不回收
godot-game-one 每秒要发射几十颗子弹、产生十几个受击特效。如果每次都 instantiate() + queue_free(),GC 压力会让帧率从 60fps 掉到 45fps。解决方案是对象池——但和教科书不同,这个池子的对象用完就销毁,不回收复用。 教科书对象池 vs 这个项目教科书的经典实现: 1获取对象 → 使用 → 归还池子 → 下次复用 pool.gd 的实际实现: 1获取对象 → 使用 → 直接销毁(不归还) 为什么?因为 Godot 的 queue_free() 会把节点从场景树移除,但如果节点还被其他地方引用(比如子弹的 _on_body_entered 回调还没执行完),强制回收会导致悬空引用和崩溃。“用完即毁”比”回收复用”更安全,代价是每帧多几个 instantiate() 调用——但对 Godot 的场景实例化来说,这完全可以接受。 77 行的核心实现12345678910111213141516171819202122232425262728293031323334353637383940414243444546# pool.g...
SLG 聊天过滤:Aho-Corasick 自动机 + 热更新词库
SLG 游戏的聊天频道是玩家社区的命脉,也是运营的噩梦——一条未过滤的敏感词可能导致投诉、下架甚至法律风险。slg-go 的聊天过滤系统经历了三次迭代:最初用 strings.Contains 暴力匹配,后来换正则,最终落地到 Aho-Corasick 自动机。这篇记录最终方案的实现和踩坑。 为什么不用 strings.Contains?初版代码很简单: 1234567// ❌ 第一版:暴力匹配func filter(content string, words []string) string { for _, w := range words { content = strings.ReplaceAll(content, w, "***") } return content} 问题在于:100 个敏感词 × 1000 条消息 = 10 万次字符串扫描。词库涨到 500+ 时,单条消息过滤延迟从 0.1ms 涨到 2ms,1000 人频道就是 2 秒。 正则好一点,但 re...
Nacos + gRPC:游戏服务的注册与发现
slg-go 的三层服务分布在不同机器上,Gate 需要知道 Logic 在哪,Logic 需要知道 Battle 在哪。服务发现用 Nacos,层间通信用 gRPC。 Nacos 注册与发现每个服务启动时向 Nacos 注册,关闭时注销。其他服务通过 Nacos 查询可用节点: 123456789101112131415161718192021222324// internal/discovery/nacos.gotype Discovery struct { client naming_client.INamingClient}func (d *Discovery) Register(serviceName string, ip string, port int, metadata map[string]string) error { return d.client.RegisterInstance(constant.RegisterInstanceParam{ Ip: ip, ...