放置游戏离线收益:公式设计与实现
从玩家反馈说起“我昨天晚上攒了100万金币去睡觉,结果今天早上只收到了1万金币,是不是BUG?” 这是上周收到的玩家反馈。排查后发现,问题不在代码,而在我们的离线收益公式设计——收益曲线太平缓,玩家离线8小时和离线1小时的收益差距只有2倍,完全没有“离线时间越长收益越高”的正反馈。 公式藏在Rate里先看基础实现(internal/idle/idle.go 第114-121行): 12345// 计算离线收益elapsed := now - state.LastClaim // 离线秒数earned := state.Rate * float64(elapsed) // earned = 速率 × 时间state.Resources += earned // 累加到资源state.LastClaim = now // 更新时间戳 表面看很简单:earned = Rate × elapsed。但关键在Rate的计算(第336-345行): 12345678910func (s *St...
ELO + Lua 热更新:游戏竞技匹配的双引擎设计
game-bass 的竞技排位系统有两个核心问题要解决:评分算法怎么选和怎么不重启就改算法。解决方案是 Go 内置 ELO + Lua 热更新的双引擎架构。 ELO 算法:经典的 K=32 实现ELO 评分系统的核心公式: 12Ea = 1 / (1 + 10^((Rb - Ra) / 400))Ra_new = Ra + K * (Sa - Ea) Go 实现: 12345678910111213141516171819const kFactor = 32.0func builtinELO(ra, rb int, outcome string) (int, int) { ea := 1.0 / (1.0 + math.Pow(10, float64(rb-ra)/400.0)) eb := 1.0 - ea var sa, sb float64 switch outcome { case "a_win": sa, sb = 1.0, 0.0 case "b_win...
不做 Nakama:从零搭建游戏后端模板的原因
说实话,最开始我是想用 Nakama 的。 Nakama 功能确实全——认证、好友、排行榜、实时对战、存储、Lua 脚本、多语言 SDK,该有的都有。但当我真的拿它给一个 3 人独立团队做原型时,光配 PostgreSQL + Docker Compose 就花了大半天。团队里没有专职后端,每次改个字段都要跑迁移脚本,Debug 的时候要翻三套日志。 那一刻我意识到:Nakama 解决的问题规模和这个团队面对的问题规模不匹配。 一个真实的踩坑经历当时的情况是这样的: 团队要做一个放置类小游戏,需要排行榜、好友、简单的实时聊天。3 个人,一个策划一个美术一个程序,程序还是客户端转的,后端经验约等于零。 我推荐了 Nakama。然后: 第 1 天:Docker Compose 起服务,PostgreSQL 初始化,配了半天网络。程序小哥问我”为什么不用 MySQL”,我说 Nakama 要求用 PG。他说”那我本地没装 PG 怎么办”。 第 3 天:排行榜跑通了,但自定义字段要写 Lua 模块。小哥第一次写 Lua,语法倒是简单,但调试手段约等于零——print 大法,加 ...
OOM 事故后重构战斗调度器
事故现场:goroutine爆炸凌晨3点收到告警:战斗队列堆积超过5000,玩家等待时间从200ms飙升到8秒。排查发现,上一版的战斗调度器是简单的goroutine-per-request模式——每个战斗请求都创建一个新goroutine,没有上限控制。 高峰期goroutine数突破10万,内存占用暴涨 Lua VM没有复用,每次战斗都要重新加载脚本 同一战斗重复提交时,多个goroutine同时执行,结果不一致 “再这么下去,服务器迟早要OOM。”——运维同事的反馈。 为什么选channel做任务队列第一件事是加 Worker Pool。调度器的基础结构: 123456789101112type Scheduler struct { engine *engine.BattleEngine requests chan *BattleRequest // 带缓冲的 channel 作为任务队列 queueSize int highPriorityReserve...
Go 语言十问十答(二):从代码审查看实战陷阱
第一篇十问十答是语法基础,这一篇来自一次真实的 slg-go 项目代码审查——在 63 个 Go 源文件里找出了 40+ 个问题,从 P0 到 P4 分级修复。以下是最有代表性的 10 个实战陷阱。 Q11:sync.Mutex 真的能保护所有字段吗?场景:SessionManager.ValidateSeq 里先用 Get()(读锁)拿到 session,再 Lock() 修改 SeqNum。 1234567891011121314151617// 有竞态风险的老代码func (m *SessionManager) Get(playerID int64) (*PlayerSession, bool) { m.mu.RLock() defer m.mu.RUnlock() s, ok := m.sessions[playerID] return s, ok}func (m *SessionManager) ValidateSeq(playerID int64, seq uint32) bool { s, ok :...
Electron 实战:用 Vue 3 搭建桌面 TTS 工具的技术选型
这是「Electron 实战」系列的第一篇。我的项目 MiMo TTS Studio 是一个桌面配音工作台,集成小米 MiMo TTS 引擎,支持多角色音色管理、批量并发生成、波形可视化。本文从”为什么做这个工具”出发,聊聊技术选型的决策过程。 Q1:为什么选择 Electron 做一个桌面 TTS 工具?A: 起因很实际——我需要给游戏项目(godot-game-one)的 NPC 配音。 12345需求链路: 游戏需要 NPC 对话语音 → 需要大量台词的音频 → 手动逐条去网页端合成太慢 → 需要批量工具 → 需要管理多角色/多音色 → 需要可视化波形确认效果 → 最终产物 = 一个桌面应用 为什么不做成 Web 应用? 方案 优点 缺点 纯 Web 无需安装、跨平台 无法直接操作本地文件系统、无法调用系统原生 API Tauri 包体更小、内存占用低 Rust 生态对 TTS/音频处理库支持有限 Electron Node.js 生态丰富、文件 I/O 原生支持、前端技术栈无缝复用 包体较大(~150MB)...
Electron 实战:音频波形 Canvas 可视化完整实现
MiMo TTS Studio 的音频播放器里有一个波形显示组件——不是用任何图表库,而是从零手写 Canvas 渲染。本文完整拆解:从 ArrayBuffer 解码到 200 条 RMS 柱状图,再到 Retina 高清适配和点击跳转。 Q1:整体渲染流程是怎样的?A: 数据流是一条清晰的流水线: 1234567891011音频文件(file:// 或 HTTP URL) ↓fetchAudioBuffer() → ArrayBuffer(原始字节) ↓AudioContext.decodeAudioData() → AudioBuffer(解码后的 PCM 数据) ↓降采样 + RMS 计算 → [0.12, 0.45, 0.33, ..., 0.08] (200 个浮点数) ↓归一化 → [0.27, 1.00, 0.73, ..., 0.18] (最大值 = 1) ↓Canvas drawWaveform() → 柱状图绘制到屏幕 每一步都是纯原生 API,没有引入任何第三方库。 Q2:第一...
Electron 实战:批量 TTS 并发信号量池的实现
用户点击”全部生成”后,可能有几十条甚至上百条台词等待合成。如果同时发起所有请求,TTS API 会返回 429(限流错误)。MiMo TTS Studio 用一个 手写的信号量池 控制最大并发数为 3——不用任何第三方库。 Q1:为什么并发数限制为 3?不是越大越好吗?A: 并发数 效果 1(串行) 安全但太慢,100 条台词可能要 10+ 分钟 3(当前设置) 平衡点:充分利用 API 吞吐,基本不触发限流 10+ 大概率触发 429 限流 → 重试 → 更慢 无限制(全并行) API 直接封禁或返回大量错误 3 这个数字来自实测: MiMo API 单次请求平均耗时 3~8 秒 并发 3 时总吞吐约 每分钟 20~25 条 并发超过 5 后 429 错误率急剧上升 具体数字取决于你的 API 套餐和服务器负载。这个值做成常量 MAX_CONCURRENT 就是为了方便调整。 Q2:信号量池的核心实现只有 20 行?A: 核心逻辑确实非常简洁: 12345678910111213141516171819202122232425/...
Electron 实战:Pinia Store 设计 — 6 个 Store 的职责划分
MiMo TTS Studio 的前端状态管理用了 6 个 Pinia Store。这不是过度设计——每个 Store 对应一个清晰的业务域,Store 之间通过依赖注入(useXxxStore())协作。本文拆解每个 Store 的职责边界和协作关系。 Q1:6 个 Store 分别管什么?A: 先看全景图: 12345678910111213141516171819202122┌─────────────────────────────────────────────────┐│ Store 架构 ││ ││ ┌──────────────┐ ││ │ configStore │ ← 全局配置(Provider/导出格式) ││ └──────┬───────┘ ││ ...
Electron 实战:TTS 双 Provider 架构设计与实现
在 MiMo TTS Studio 中,我需要同时对接小米云端 TTS(MiMo)和本地/自托管模型(VoxCPM)。本文拆解这个双引擎架构的设计思路——从策略模式抽象到缓存、重试、安全日志的完整链路。 Q1:为什么需要双 Provider?一个 TTS 引擎不够吗?A: 够用,但不够灵活。实际使用场景决定了这个需求: 场景 需要的引擎 日常配音,追求音质和表现力 MiMo 云端(大模型,支持音色设计/克隆) 离线环境 / 数据安全要求高 VoxCPM 本地部署 对比测试两个引擎的效果 两者都要能切 API 配额用完时的 fallback 自动降级 与其写两套逻辑,不如一开始就抽象成 Provider 模式——前端只关心”给我一段音频”,不关心背后是谁在干活。 Q2:Provider 抽象层是怎么设计的?A: 核心思想很简单:定义接口契约,每个 Provider 自己实现细节。 1234567891011121314151617181920212223242526272829303132333435363738...