前言

文章来源于 AI 撰写,本人已仔细审阅,介意者勿阅!!!

可以简单回顾 lua 的基础语法,毕竟之前有接触过,目前更重要的是精通游戏开发中 lua 的应用,包括如下:

  • 语言机制层面:表、闭包、元表、协程、迭代器、多文件(模块化)
  • 嵌入与扩展:C/C++ 与 lua 的交换,常用的交互库,LuaJIT
  • 游戏服务器实践:lua 配表、脚本驱动逻辑、协程调度、状态机、热更新
  • 性能与设计:GC抖动、tabel频繁创建、全局变量污染与性能、closure 滥用
Day 学习要点 必做任务(Checklist) 验收标准(Done Criteria) Done
1 table 本质(array/hash)、nil 语义 □ 实现玩家列表(增删改查)
□ 写统计函数(max/avg)
□ 正确区分 array/hash
□ 不出现 nil 逻辑错误
✔️
2 closure、upvalue、模块化 □ 写 counter 闭包
□ 实现 math_utils 模块
□ 能解释 upvalue 生命周期
□ 模块可被 require 调用
✔️
3 metatable(__index) □ 实现 Player OOP
□ 创建多个实例测试
□ 正确理解 __index 查找链 ✔️
4 metatable 进阶 □ 实现只读表
□ 实现默认值表
□ 表行为可控(读写受限) ✔️
5 coroutine 基础 □ 写两个协程交替执行,打印执行顺序
□ 实现生产者-消费者模式
□ 练习 yield/resume 数据双向传递
□ 理解四种协程状态转换
□ 能徒手画出 yield/resume 数据流图
□ 能解释为什么协程不是线程
□ 能说出 create vs wrap 的区别
✔️
6 协程调度器 □ 实现 Scheduler(add/run/step)
□ 支持多任务轮询执行
□ 支持协程异常捕获和清理
□ 实现带优先级的调度
□ 3+ 协程能正确交替执行
□ 单协程异常不影响其他协程
□ 死协程能被正确清理
✔️
7 定时器机制 □ 实现协程式 sleep(n),协程可"等待"后再继续
□ 实现基于时间轮的定时器
□ 实现技能冷却的定时器应用
□ 理解增量时间(dt)驱动 vs 绝对时间驱动
□ sleep 精度可接受(秒级)
□ 时间轮 O(1) 添加任务
□ 理解定时器与协程调度的配合
✔️
8 Lua ↔ C 基础与进阶 □ C 调用 Lua 全局函数并获取返回值
□ Lua 调用 C 注册函数
□ 创建 full userdata 并附加元表(含 __gc)
□ 实现一个完整的 C 扩展库(编译为 .so)
□ 理解 lua_State 栈操作和 lua_pcall 错误处理
□ 能徒手画出 lua_State 虚拟栈的索引规则
□ 能解释 light userdata vs full userdata
□ 扩展库可被 require 正确加载
✔️
9 配置系统 □ 实现 ConfigLoader(支持 require 和 JSON 两种加载)
□ 实现配置缓存机制
□ 实现配置字段验证(类型/范围检查)
□ 建立配置索引(按类型/标签查找)
□ 将技能、物品、怪物配置独立为 config/*.lua
□ 配置与代码逻辑严格分离
□ 加载错误时有清晰报错
□ 配置修改后无需改动逻辑代码
✔️
10 技能系统 □ 设计技能配置表(含条件/目标/效果链/冷却)
□ 实现技能执行流程(检查→消耗→效果→冷却)
□ 实现效果分发系统(damage/heal/buff/dispel)
□ 实现公式引擎(解析配置中的公式字符串)
□ 实现冷却管理器(含公共冷却 GCD)
□ 添加新技能只需改配置,不改逻辑
□ 效果链按顺序正确执行
□ 公式引擎正确计算伤害
✔️
11 状态机 + AI □ 实现通用 FSM 框架(enter/update/exit/on_message)
□ 实现怪物 AI(Idle→Patrol→Chase→Attack→Dead)
□ 实现全局状态(HP检测、受控检测)
□ 了解行为树(BT)基本概念并实现简易版
□ 5 种状态能按条件正确切换
□ 全局状态始终运行,不被状态切换影响
□ 能解释 FSM vs 行为树的适用场景
✔️
12 性能优化 □ 将频繁访问的全局变量缓存为 local(测量性能差异)
□ 实现对象池并用于高频创建场景
□ 用 table.concat 替代循环字符串拼接
□ 使用弱引用表管理缓存映射
□ 加载阶段暂停 GC 后测量内存/耗时变化
□ 用 strict 模块检测全局变量污染
□ local 缓存比全局访问快 2-5 倍
□ 对象池避免高频 GC 抖动
□ 能解释 GC 增量式标记清除原理
✔️
13 热更新 + 整合 □ 实现基础模块热更新(清 package.loaded 后 re-require)
□ 实现带状态迁移的热更新(export_state / import_state)
□ 实现安全热更新(pcall + 失败回滚 + 自检)
□ 整合所有模块到 main.lua(含启动流程和主循环)
□ 测试:修改技能公式后热更新,无需重启
□ 热更新后旧状态不丢失
□ 热更新失败时自动回滚
□ main.lua 模块加载顺序正确
✔️
14 skynet 学习 □ 理解 Actor 模型和"一切皆服务"理念
□ 理解 skynet.call 背后的协程调度原理
□ 实现一个简化版 Actor 系统(消息队列+协程)
□ 画出 Skynet 服务架构图(Login/Gate/Agent/World/DB)
□ 阅读 skynet.lua 核心源码
□ 能脱离 Skynet 代码讲清其架构
□ 能解释 skynet.call 为什么看起来是同步却非阻塞
□ 自实现的简化 Actor 能正常收发消息
✔️

精通元表与OOP

概括

参考这条ChatGPT的聊天记录,可以仔细弄明白这些内容,并且尽可能去看一下 middleclass 和 ecs(Entity-Component-System)

元表和元方法

在 Lua 中,**元表(metatable)**是一个附加在普通表上的“控制结构”,用于定义当某些操作发生时的行为。

local t = {}
local mt = {}

setmetatable(t, mt)

一旦设置后,Lua 在执行某些操作时,会优先检查 mt 中是否定义了对应规则。元方法本质是操作的“拦截点”,它是元表中的特殊字段,用来定义某些操作的行为。常见元方法如下:

  • 运算相关
__add   -- +
__sub   -- -
__mul   -- *
__div   -- /
__unm   -- 负号
  • 比较相关
__eq    -- ==
__lt    -- <
__le    -- <=
  • 索引相关(至关重要)
__index     -- 读不存在的 key
__newindex  -- 写不存在的 key
  • 其他关键能力
__call      -- 表像函数一样调用
__tostring  -- tostring 行为
__pairs     -- 自定义遍历

一句话理解元表与元方法(核心认知)

当“默认语义无法完成操作时”,Lua 应该如何接管并继续执行。👉 本质统一为:Lua 默认做不了的事 -> 交给元方法

理解 __index

当执行:

t[k]

Lua 的逻辑是:

1. 查 t[k]
2. 如果不存在 -> getmetatable(t).__index

它存在的两种形态

mt.__index = otherTable -- 这个 otherTable 是另外一个表

等价于:

t[k] -> getmetatable(t)[k] -> otherTable[k]
  • 函数
mt.__index = function(t, k)
    return ...
end

OOP

假设如下代码:

function newPlayer(x, y)
    return {
        x = x,
        y = y,
        move = function(self, dx, dy)
            self.x = self.x + dx
            self.y = self.y + dy
        end
    }
end

创建 1000 个对象:

players[i] = newPlayer(...)
players[i]:move(1, 1) -- 等同于 players[i].move(players[i], 1, 1),语法糖,相当于实现了 oop

这会导致,每个实例都有一份 move 函数,会导致内存浪费、函数无法统一替换(热更新困难)、cache 不友好等问题。那么可以提取“类”的共同部分,即把“行为”抽出来:

Player = {} -- 这个就是所有 players[i] 的公共部分,可以放到一个新的表里面(即表 Player)

function Player:move(dx, dy)  -- lua 会认为等于 Player.move = function (self, dx, dy) ... end,语法糖
    self.x = self.x + dx
    self.y = self.y + dy
end

-- 实例只保留数据:
function newPlayer(x, y)
    return { x = x, y = y }
end

借助 __index 的力量:

Player.__index = Player

function newPlayer(x, y)
    local obj = { x = x, y = y }
    setmetatable(obj, Player)
    return obj
end

在这个基础上,进行调用拆解:p1:move(1,2) 到底发生了什么

p1:move(1,2)

等价于:

p1.move(p1, 1, 2) -- 注意,会把前面的调用者 p1 作为第一个参数传进来

先查找 move

p1.move

执行:

1. p1["move"] 是 nil,进入下一步
2. 查 getmetatable(p1).__index["move"],即 Player["move"]
3. Player["move"] 是函数,执行调用

再实际调用,对原式子展开实际是:

Player.move(p1, 1, 2)

对于其中的语法糖::. 的本质区别

写法 实际行为
p1:move(1,2) p1.move(p1,1,2)
function Player:move(dx, dy) Player.move = function(self, dx, dy),增加了 self 参数,可以在函数内部默认使用,指向了调用者

错误示例:

p1.move(1,2)

-- 等价于
getmetatable(p1).move(1, 2)

-- 等价于
Player.move(1, 2) -- 原型应该是 Player.move(self, dx, dy)

-- 等价于
-- self = 1, dx = 2, dy = nil,这是因为 lua 运行形参和实参不固定,多余的补充 nil

-- 👉 直接逻辑错误

注意一个关键点:

Lua 并没有 class,这只是基于 __index 的一种约定,Lua 中不管是类还是实例,本质都是 table

循环

由于在某个表中找不到 key,会去元表中的__index表寻找(如果__index也是表的话),还找不到,就回去__index表的元表的__index找,这样会形成循环依赖,lua默认是允许 2000 次深入寻找,否则就会报错找不到。

ECS

当对象越来越多:

Player
Enemy
BossEnemy
FlyingEnemy

会导致继承爆炸、行为难以组合、 数据与逻辑耦合等问题

延伸到 ECS(Entity Component System),实体、组件与系统。

ECS 的核心思想是:

把“数据”和“行为”彻底分离

下面使用 ECS 改写 Player:

  • Entity(实体)
player = 1
  • Component(数据)
Position = {
    [1] = {x = 0, y = 0}
}

Velocity = {
    [1] = {dx = 1, dy = 2}
}
  • System(行为)
function MovementSystem()
    for e, pos in pairs(Position) do
        local vel = Velocity[e]
        if vel then
            pos.x = pos.x + vel.dx
            pos.y = pos.y + vel.dy
        end
    end
end

核心变化就是从对象调用方法p1:move 变成了系统处理数据 MovementSystem,把所有有移动属性的实体都执行了移动

ECS vs OOP(工程视角)

oop的优点:

  • 直观
  • 易理解

oop的缺点:

  • 耦合高
  • 扩展困难

ecs的优点:

  • 高性能(cache-friendly)
  • 高组合性
  • 易扩展

ecs的缺点:

  • 抽象成本高
  • 初期理解困难

在真实项目中通常是:

Lua OOP(组织代码) + ECS(组织数据与行为)

推荐库阅读

  • middleclass:是一个面向对象的库
  • love2D:是一个使用 lua 实现的 2D 游戏库
  • Cocos2d-x Lua:一个商业级的游戏框架,有 lua 版本,国内手游应用广泛,包括 C++ 引擎和 Lua 脚本
  • Roblox:大规模商业游戏平台,有 lua

rawset/rawget

在设置了元表的情况下,如果一个key不存在于表中,直接使用会触发元表的__index(读取)/__newindex(写入),有时候这并不是我们想要的,我们有时候希望表不存在key就返回 nil 或者只是为了单纯写入一个新 key 罢了,那就需要用的 rawset/rawget

local t = {}
setmetatable(t, {
        __newindex = function() ... end,
        __index = function() ... end
    })
t.a = 10 -- 会触发 __newindex,是否真的会写入到 t 中,取决于 __newindex 的逻辑
rawset(t, "a", 10) -- rawset(table, key, value),只是为了写入某个 key,存在 t.a = 10

print(t.b) -- 会触发 __index
rawget(t, "b") -- rawget(table, key),只是为了获取某个 key,而不想要触发 __index,返回 nil

经典错误写法:

local t = {}
setmetatable(t, {
        __newindex = function(t, k, v)
            			t[k] = v -- 错误写法,会递归触发 __newindex
            			rawset(t, k, v) -- 正确写法,不存在就会写入对应的 k v
         			 end
    })

函数、闭包、Upvalue、迭代器

函数

Lua 中函数是第一类公民(first-class citizen):可以赋值给变量、可以作为参数传递、可以作为返回值返回、可以嵌套定义。在函数中传参是通过值或者“指针”传递的,额外的对“指针”进行解释:

  • 对于 table/userdata/function/thread/string 这些来说,都是传递“引用的拷贝”,可以把它理解成 C 语言中的指针。可以重新把它指定到其他的值,也可以通过对它操作来更多传入的值。

闭包

闭包是非常经典的用法了,指在其他编程语言中也经常出现的用法

function createCounter()
    local count = 0  -- 这是一个局部变量,本应该随着函数销毁
    -- 下面的匿名函数引用了外部的 count,此时 count 变成了 Upvalue
    return function()
        count = count + 1
        return count
    end
end

local c1 = createCounter()
local c2 = createCounter()

print(c1()) --> 1
print(c1()) --> 2
print(c2()) --> 1 (c2 是独立的,互不干扰)

通常函数执行完,局部变量(如 count)就该被销毁了。但因为内部函数(闭包)“捕获”了它,Lua 虚拟机决定不让它死,而是让它“活”在内存中,供闭包随时访问。每次调用 createCounter(),都会生成一个新的闭包实例,拥有自己独立的 count 变量。UpVal结构体如下:

typedef struct UpVal {
  CommonHeader;
  union {
    TValue *p;  /* points to stack or to its own value */
    ptrdiff_t offset;  /* used while the stack is being reallocated */
  } v;
  union {
    struct {  /* (when open) */
      struct UpVal *next;  /* linked list */
      struct UpVal **previous;
    } open;
    TValue value;  /* the value (when closed) */
  } u;
} UpVal;

闭包对象往往会持有这种对象的索引,随着闭包对象销毁,可以对UpVal进行GC。所以闭包+upval的方式往往会比较占据性能,不是性能优选,最好是通过返回一个表的方式直接把需要upval的值通过表返回。

迭代器

Lua 中的 for 本质如下,不过是 lua 提供的语法糖而已

for var1, var2, ... in iterator_func, state, init_value do
    -- body
end

-- 完全等价于
local f, s, var = iterator_func, state, init_value
while true do
    local var1, var2, ... = f(s, var)
    if var1 == nil then break end
    var = var1
    -- body 使用 var1, var2...
end
  • iterator_func 是一个迭代器函数,正确的写法是能够接收状态和值,并返回下一个值
  • state 是一个能够表示迭代器状态的,下标也好,什么都可以,主要取决于迭代器实现
  • init_value 是初始值,并且迭代器也会返回值,就是下一个值 var

进一步拆解 ipairs 和 pairs

-- pairs 的等价实现(最简洁)
function pairs(t)
    -- 直接返回 Lua 内置的 next 函数 + 表 + 初始 key(nil)
    return next, t, nil
end

-- ipairs 的等价实现(最常用版本)
function ipairs(t)
    -- 返回一个“迭代器函数” + 状态(表本身) + 初始值(0)
    return function(t, i)
        i = i + 1                    -- 每次把索引 +1
        local v = t[i]               -- 取出当前元素
        if v ~= nil then             -- 遇到 nil 就停止(ipairs 的核心规则)
            return i, v
        end
    end, t, 0
end
  • next 是 Lua 内置的底层函数,负责遍历 table 的下一个键值对。
  • ipairs 之所以要自己写一个闭包,是因为它必须严格按整数索引顺序遍历,并且在第一个 nil 处停止。
  • 这两个函数返回的正是泛型 for 循环需要的三个值:迭代器函数、状态、初始值。

迭代器分成有状态迭代器无状态迭代器,这两者区别在于有状态迭代器需要依赖捕获的外部UpVal才能进行迭代,而无状态迭代器则不需要

模块化

一级知识模块 二级知识点(聚合后的核心学习内容)
Lua 模块核心机制 模块本质(table/function/chunk)、模块生命周期、作用域与闭包、require 工作流程、模块缓存机制、package.loaded
Lua 加载与 package 系统 dofile/loadfile/load/require 区别、动态执行、package.path、package.cpath、package.searchers、自定义 Loader、模块搜索规则
模块设计与架构模式 return table 模式、闭包模块、OOP 模块、工厂/单例模块、无状态设计、高内聚低耦合、分层架构、MVC/MVVM
模块依赖与状态管理 模块共享状态、私有状态、生命周期管理、循环依赖、延迟 require、依赖注入、事件解耦
元表、环境与高级模块技巧 _ENV、setfenv/getfenv、环境隔离、沙箱、自动加载、延迟加载、代理模块、虚拟模块、动态模块生成
游戏开发模块化实践 配置系统、UI 模块、Service 模块、Actor 模型、消息驱动、协程调度、玩家/房间模块
热更新与运行时系统 package.loaded 清理、模块重载、函数替换、状态迁移、Upvalue 更新、LuaJIT、FFI、JIT 优化
工程结构与插件系统 项目目录结构、模块分层、公共模块设计、插件注册、动态加载、生命周期、Hook 系统
模块性能、调试与测试 require 性能、懒加载、缓存优化、依赖链分析、循环依赖定位、hook 调试、mock/stub、单元测试
Lua 工程生态与源码能力 xLua、tolua、Cocos Lua、Skynet、OpenResty、Lua require 源码、package 源码、Lua VM、GC 机制、框架设计

Lua 模块核心机制

模块本质:table / function / chunk

在 Lua 中,一个“模块”本质上就是一段代码块(chunk)执行后返回的一个 Lua 值。通常这个值是一个 table,里面存放了函数、变量等。但你也可以返回一个函数(例如单例工厂)或者任何类型。Lua 并没有为模块定义特殊的语法,只是通过代码组织与 require 的配合来实现模块化。

最简单的模块 - 返回 table:

-- mymath.lua
local M = {}
M.add = function(a, b) return a + b end
M.sub = function(a, b) return a - b end
return M

模块也可以是一个函数:

-- greeter.lua
return function(name)
    print("Hello, " .. name)
end

模块的本质是 chunk 返回值:当 require 加载一个文件时,它执行文件中的代码,并获取 return 的结果作为模块值。如果没有显式 return,require 会返回 true(早期 Lua 5.1 行为)?实际上,现代 Lua 中如果没有 return,require 返回的是 package.loaded[modname] 中设置的值,但显式 return 是最佳实践。模块的文件本身就是一个函数,通过 loadfile 等加载后执行,其返回值就是模块接口。

模块生命周期

一个模块从被加载到被卸载(很少显式卸载)会经历:注册(放入 package.loaded)、初始化执行(模块文件中的代码运行)、缓存使用可能的重载(热更新时清理缓存重新加载)。生命周期管理对热更新和资源释放非常重要。

  • 加载注册:第一次 require 时,Lua 先检查 package.loaded[modname],若存在则直接返回。不存在则查找模块文件(或加载器),执行 chunk,将返回值存入 package.loaded[modname] 并返回。
  • 初始化:模块代码执行时,可以初始化内部状态、注册全局服务、连接事件等。
  • 运行期:多次 require 只会得到同一个缓存实例。
  • 重载:通过从 package.loaded 中删除条目,并重新 require 可实现重载,但需要注意遗留引用和 upvalue 问题(后续热更新章节会详细讲)。

作用域与闭包

Lua 采用词法作用域(lexical scoping),函数可以访问定义时所在的外部变量。模块正是利用闭包来隐藏私有状态,只暴露公共接口。这是模块封装的基础。

-- counter.lua
local count = 0   -- 私有变量,外部无法直接访问
local function increment()
    count = count + 1
    return count
end
local function get()
    return count
end
return { inc = increment, get = get }

使用时每次 require 该模块(实际上只会执行一次初始化),闭包中的 count 被模块实例持有,实现了有状态的模块。如果不希望模块之间有状态污染,可以设计成工厂函数(每次调用返回新实例)。

深究原理:Lua 中的函数是闭包(closure),每个闭包包含一个函数原型和一个 upvalue 数组。对于 counter.lua 中返回的 incget 函数,它们都会捕获外层局部变量 count。当模块被加载时,count 变量被创建并常驻内存,直到模块被释放。这解释了为什么模块级别的局部变量可以充当全局共享状态(在模块实例内部)。

require 工作流程

require 是 Lua 加载模块的核心 API。其执行步骤如下(简化版):

  1. 检查 package.loaded[modname],若不为空则直接返回该值。
  2. package.searchers(或旧版 package.loaders)序列中依次尝试查找模块加载器。
  3. 找到一个合适的加载器(通常是文件搜索器),获得一个 loader 函数(实际上就是执行模块 chunk 的函数)。
  4. 调用 loader,得到模块值。
  5. 将模块值存入 package.loaded[modname](如果模块返回值是 nil 或 false 则存入 true)。
  6. 返回该模块值。

我们可以模拟一个简化的 require:

function my_require(name)
    if package.loaded[name] then
        return package.loaded[name]
    end
    for _, searcher in ipairs(package.searchers) do
        local loader, arg = searcher(name)
        if type(loader) == "function" then
            package.loaded[name] = true   -- 预占位,避免循环依赖时重复加载
            local result = loader(arg)    -- 执行模块
            if result == nil then
                result = true
            end
            package.loaded[name] = result
            return result
        end
    end
    error("module " .. name .. " not found")
end

模块缓存机制与 package.loaded

package.loaded 是一个普通的 Lua table,所有已加载模块都被缓存在这里。你可以直接读写它,这为热更新、模块重载、依赖覆盖提供了强大的能力。

  • 查看已加载模块:遍历 package.loaded 可以查看当前 Lua 状态中所有已加载模块。
  • 强制重新加载package.loaded["mymodule"] = nil 后再次 require 会重新执行模块初始化。
  • 预注册模块:可以在加载前手动向 package.loaded 写入假模块,拦截后续 require。
-- 预注册一个 Mock 模块用于测试
package.loaded["database"] = { query = function() return "mock data" end }
require("database")  -- 直接返回上面 mock 对象,不会实际加载 database.lua

实现原理:require 首先查询 package.loaded,如果存在(非 nil 且非 false),就直接返回,不会再执行搜索和加载步骤。所以通过操作 package.loaded 可以完全控制模块的装载行为。


Lua 加载与 package 系统

dofile / loadfile / load / require 区别

  • dofile:读取文件内容并执行,返回 chunk 的返回值(通常不用于模块化)。它会每次重新执行,无缓存。
  • loadfile:编译文件为函数,不执行;返回一个函数,调用该函数即执行文件代码,可多次执行。
  • load:从字符串或函数中加载 chunk,类似 loadfile 但源来自字符串。
  • require:专门用于模块加载,有缓存,搜索路径复杂,并处理循环依赖。
-- dofile: 立即执行
dofile("test.lua")

-- loadfile: 先编译,后续执行
local f = loadfile("test.lua")
f()  -- 执行
f()  -- 再次执行

-- load: 从字符串加载代码
local chunk = load("print('hello world')")
chunk()

-- require: 智能模块加载
require("mymodule")

动态执行与安全注意事项

loadloadfile 支持指定环境,可以用于沙箱。而 dofile 会在全局环境下执行,可能产生污染。load 默认在全局环境,但可以传入 _ENV 参数控制。

local sandbox_env = { print = print }  -- 只提供 print
local chunk = load("print('safe')", nil, "t", sandbox_env)
chunk()  -- 在受限环境中执行

package.path 与 package.cpath

package.path 是 Lua 纯脚本模块的搜索路径模板字符串,package.cpath 是 C 扩展动态库的搜索路径。默认值通常类似:

package.path = "./?.lua;./?/init.lua;/usr/local/share/lua/5.4/?.lua;..."

路径中的 ? 会被模块名(. 替换成路径分隔符)替换。例如 require("foo.bar") 会搜索 foo/bar.luafoo/bar/init.lua 等。

我们可以在运行时修改路径:

package.path = package.path .. ";./modules/?.lua"
require("my.custom.module")  -- 会在 ./modules/my/custom/module.lua 中搜索

原理:require 调用文件搜索器(package.searchers[2] 或旧版 searcher[2])时,会利用 package.path 生成可能的文件路径,并检查文件是否存在。

package.searchers 与自定义 Loader

package.searchers 是一个数组,存储了多个搜索器函数,按顺序调用,每个搜索器返回一个 loader 函数和附加参数。Lua 5.2+ 默认四个搜索器:

  1. 预加载搜索器(package.preload 表,用于手动注册模块)。
  2. Lua 文件搜索器(利用 package.path 搜索 .lua 文件)。
  3. C 扩展搜索器(利用 package.cpath 搜索动态库)。
  4. 组合加载器(用于加载 C 库中的 Lua 模块)。

我们可以自定义搜索器,实现从数据库、网络、嵌入式资源等加载模块。

-- 添加一个从字符串表加载模块的自定义搜索器
local my_modules = {
    hello = "return { say = function() print('hello') end }"
}
table.insert(package.searchers, 2, function(modname)
    local code = my_modules[modname]
    if code then
        return load(code, modname, "t", _ENV)
    end
end)

require("hello").say()  -- 输出 hello

深入理解:自定义 searcher 极大地扩展了 Lua 模块的来源,也是许多游戏引擎(如 Unity 的 xLua)实现从 AssetBundle 加载模块的底层机制。

模块搜索规则与优先级

简单总结:require(modname) 首先把 modname 中的点换成路径分隔符,然后按 package.searchers 顺序尝试:

  1. package.preload[modname] 是否存在,若有则直接以该函数作为 loader。
  2. package.path 中搜索 ?.lua,找到第一个存在的文件,加载执行。
  3. package.cpath 中搜索 ?.so?.dll,找到后通过 Lua 的 C 扩展机制加载。
  4. 若 C 库里含有对应模块的 luaopen_* 函数,则调用之。

如果所有搜索器都失败,抛出错误。


模块设计与架构模式

return table 模式(经典模块模式)

这是最常用的模块设计:在文件末尾返回一个 table,其中包含对外公开的函数和变量。私有变量通过局部变量实现。前面已展示。

优点:简单直观,适合大多数场景。缺点:无法支持继承和多态。

闭包模块

利用返回包含函数的 table,但将更多私有变量封装在闭包中,本质上和 return table 模式无本质区别,只是写法更函数式。实际上经典模块模式就使用了闭包。

OOP 模块(基于元表与类库)

需要使用 Lua 的面向对象模拟(通过元表 __index)。常见做法:模块返回一个类表,然后提供 new 方法创建实例。

-- person.lua
local Person = {}
Person.__index = Person

function Person.new(name)
    local self = setmetatable({}, Person)
    self.name = name
    return self
end

function Person:sayHello()
    print("I am " .. self.name)
end

return Person

使用时:

local Person = require("person")
local p = Person.new("Lua")
p:sayHello()

工厂模块与单例模块

  • 工厂模块:模块返回一个创建其他对象的函数,不暴露内部细节。
  • 单例模块:直接返回一个实例 table,全局唯一。依赖 package.loaded 缓存自动保证单例性质。
-- logger.lua (单例)
local instance = {
    level = "INFO",
    log = function(self, msg) print(self.level, msg) end
}
return instance

工厂风格允许创建多个独立实例:

-- create_counter.lua
return function(init)
    local count = init or 0
    return { inc = function() count = count + 1; return count end }
end

无状态设计、高内聚低耦合与分层架构

  • 无状态模块:模块内没有可变状态,所有函数只依赖输入参数,便于测试和复用。
  • 高内聚低耦合:一个模块只负责一个明确的功能,通过清晰的接口与外部交互,减少直接依赖全局变量。
  • 分层架构:将系统分为表现层、业务逻辑层、数据层。每层之间通过模块接口通信。
-- 数据层模块
local DataLayer = {}
function DataLayer.getUser(id) ... end
-- 业务层依赖数据层接口,不直接操作数据库

MVC / MVVM 在 Lua 中的应用

在做界面(如 Cocos2d-xLua、Unity xLua)时,可以将视图(View)、控制器(Controller)、模型(Model)拆成不同模块。模型模块负责数据与业务逻辑,视图模块负责显示,控制器模块协调二者。模块化后可以单独替换视图实现。


模块依赖与状态管理

模块共享状态(全局/模块级)

模块内部的局部变量在模块被加载后一直存在,且被所有使用该模块的代码共享,这就是模块级的共享状态。合理利用可以搭建全局服务,但过度使用会造成耦合。

私有状态与作用域隔离

利用局部变量和闭包即可实现私有。如下:

local my_private_state = {}
local function private_func() end

外部无法访问,只能通过模块返回的公共接口操作。

生命周期管理

模块可能有初始化、运行、销毁阶段。可以在模块中定义 initshutdown 函数,由系统或入口脚本主动调用,实现资源释放、停止协程等。

-- service_module.lua
local timer = nil
local function tick() ... end
local M = {}
function M.start()
    timer = timer or love.timer.step()  -- 假设游戏引擎
end
function M.stop()
    if timer then timer:cancel(); timer = nil end
end
return M

循环依赖与延迟 require

如果模块 A 依赖 B,B 又依赖 A,在加载时可能出现互相等待或一方拿到未初始化完成的模块。Lua 的 require 通过预先把 package.loaded[modname] 设为 true 来解决部分问题,但仍需谨慎。

解决方案:

  1. 重构:提取公共依赖为 C 模块,消除循环。
  2. 延迟 require:在函数内部 require,而不是在模块顶层。
-- a.lua
local B
local M = {}
function M.useB()
    B = B or require("b")
    B.doSomething()
end
return M

这样当 A 加载时不会立即加载 B,而是在调用 useB 时才加载,此时可能 B 已经加载完成或相互调用形成安全局面。

依赖注入

通过外部显式传递依赖,而不是在模块内部直接 require,能提高模块的可测试性和可替换性。

-- GameController.lua
local GameController = {}
function GameController.new(deps)
    local deps = deps or {}
    local playerService = deps.playerService or require("player_service")
    -- ...
end

事件解耦

使用事件总线(消息系统)让模块间通过发布/订阅通信,而不是直接调用。Lua 可以轻松实现简易事件模块:

-- event_bus.lua
local listeners = {}
function on(event, callback)
    listeners[event] = listeners[event] or {}
    table.insert(listeners[event], callback)
end
function emit(event, ...)
    for _, cb in ipairs(listeners[event] or {}) do
        cb(...)
    end
end
return { on = on, emit = emit }

元表、环境与高级模块技巧

_ENV 与环境隔离

从 Lua 5.2 开始,每个 chunk 都有一个环境变量 _ENV,它实际上是一个 table,所有全局变量都相当于 _ENV.var。改变 _ENV 可以控制代码看到的全局变量。

local print = print  -- 为了在新环境中使用
local sandbox = setmetatable({}, { __index = _G })
sandbox.print = print
-- 将 _ENV 设置为 sandbox 后,代码无法访问未显式提供的全局变量
local code = load("print(debug)", nil, "t", sandbox)
code()  -- debug 在 sandbox 中不存在,会报 nil 错误?

实际上 __index 只会读取,但不会阻止对未知全局变量的赋值,需要配合 __newindex 来限制。

setfenv / getfenv(Lua 5.1 及兼容)

在 Lua 5.1 中,使用 setfenv 修改函数环境。在需要兼容旧代码的项目中依然会见到。

-- 5.1 风格
local env = { print = print }
local f = loadstring("print('hello')")
setfenv(f, env)
f()

现代 Lua 推荐使用 _ENV 方式。

环境隔离与沙箱实现

创建一个干净的沙箱环境,剥夺危险函数(如 os.executeio.open 等),然后使用 loadloadfile 传入该环境。这是 Lua 安全嵌入的基础。

local sandbox_env = {
    print = print,
    math = math,
    string = string,
    -- 故意不提供 os, io, debug 等
}
local user_code = load(user_input, nil, "t", sandbox_env)
local ok, res = pcall(user_code)

自动加载(懒加载)与代理模块

当模块很大时,可以在第一次访问模块的某个字段时才真正加载该子模块。通过 __index 元方法实现代理。

-- big_module.lua
local module = {}
local real_module = nil

setmetatable(module, {
    __index = function(_, key)
        if not real_module then
            real_module = require("real_big_module")
        end
        return real_module[key]
    end
})
return module

虚拟模块与动态模块生成

根据配置动态生成不同实现的模块。比如根据平台返回不同的实现。

-- platform.lua
local platform
if love.system.getOS() == "Windows" then
    platform = require("win_platform")
elseif love.system.getOS() == "Android" then
    platform = require("android_platform")
end
return platform

游戏开发模块化实践

游戏引擎(如 Cocos2d-x + Lua、Unity + xLua、LÖVE)强烈依赖模块化。

配置系统

将所有数值、字符串配置抽取为独立的模块,便于策划修改,也支持热更新。例如:

-- config/items.lua
return {
    [1001] = { name = "Sword", price = 100 },
    [1002] = { name = "Shield", price = 150 }
}

UI 模块

每个 UI 面板对应一个 Lua 模块,负责事件绑定、显示逻辑、资源管理。模块之间通过消息系统交互。

-- ui/hero_panel.lua
local UIManager = require("ui.manager")
local HeroPanel = {}
function HeroPanel.show(heroData)
    -- 创建界面、绑定按钮
end
UIManager.register("hero", HeroPanel)
return HeroPanel

Service 模块

游戏中的网络、音频、数据存储等独立服务以单例模块形式提供。

-- services/audio.lua
local Audio = {}
function Audio.playBGM(name) ... end
function Audio.playEffect(name) ... end
return Audio

Actor 模型与消息驱动

每个游戏角色(Actor)作为一个模块,拥有自己的状态机、消息队列,通过向消息总线发送消息来交互。

-- actor/base.lua
local BaseActor = {}
BaseActor.__index = BaseActor
function BaseActor:send(event, data)
    EventBus.emit(event, data)
end

协程调度模块

游戏中使用协程管理异步行为(延时、Tween)。可以设计一个调度器模块统一管理。

-- scheduler.lua
local tasks = {}
function scheduler.run(coroutineFunc) ... end
function scheduler.update(dt) ... end

玩家/房间模块

多人游戏中,每个玩家或房间的数据和行为封装在独立的模块实例中,由管理器模块创建和销毁。

-- room.lua
local Room = {}
Room.__index = Room
function Room.new(roomId)
    return setmetatable({ id = roomId, players = {} }, Room)
end
function Room:addPlayer(uid) ... end
return Room

热更新与运行时系统

热更新是 Lua 在游戏开发中最大的优势之一,无需重新编译整个应用程序即可更新逻辑。

package.loaded 清理

最简单的重载:移除 package.loaded[modname],然后重新 require。

function reload_module(modname)
    package.loaded[modname] = nil
    return require(modname)
end

但这只会让后续调用拿到新模块,已经持有的旧模块引用仍然指向旧模块。

模块重载与函数替换

对于已经引用了旧模块的地方,需要主动替换函数。可以设计模块本身支持热替换,通过全局注册表。

-- mymodule.lua
local M = {}
function M.doWork() print("v1") end
return M

-- reload 时遍历所有持有者(困难),更简单做法是模块提供一个替换表

状态迁移

如果模块内部状态必须保留(如玩家数据),直接重载会丢失。策略:重载前将旧模块状态导出,新模块重新导入。

local old_mod = require("player")
local state = old_mod.save_state()
package.loaded["player"] = nil
local new_mod = require("player")
new_mod.load_state(state)

Upvalue 更新

如果一个模块的函数被其他函数闭包捕获,替换模块函数后旧闭包内的 upvalue 仍指向旧函数。需要遍历调试库 debug 获取 upvalue 并修改,或更简单地不依赖 upvalue。

利用 debug.getupvaluedebug.setupvalue 可以实现运行时替换函数体内的闭包,但极其危险且依赖调试库,通常建议设计时避免这种深层依赖。

LuaJIT、FFI 与 JIT 优化

LuaJIT 的 FFI 允许直接调用 C 函数,在热更新时需要考虑 C 函数无法热更新。热更新只适用于 Lua 函数。另需注意 JIT 编译后的代码被替换后可能仍执行旧的机器码,需要关闭 JIT 或强制重新编译。


工程结构与插件系统

项目目录结构示例

project/
├── main.lua
├── lib/           第三方库
├── modules/       业务模块
│   ├── player/
│   │   ├── init.lua
│   │   └── battle.lua
│   └── ui/
├── common/        公共模块(事件、工具)
├── config/        配置
└── plugin/        插件系统

模块分层:common (底层) → modules (中层) → main (入口),原则下层不依赖上层。

插件注册与动态加载

插件系统定义标准接口(如 on_load, on_unload, on_event),插件模块必须提供这些函数。主程序扫描插件目录,遍历 require 并调用 on_load

-- plugin_manager.lua
function load_plugins(plugin_dir)
    local plugins = {}
    for _, filename in ipairs(lfs.dir(plugin_dir)) do
        if filename:match("%.lua$") then
            local plug = require("plugins." .. filename:sub(1, -5))
            if plug.on_load then plug:on_load() end
            table.insert(plugins, plug)
        end
    end
    return plugins
end

生命周期 Hook 系统

在关键业务点(如进入战斗、玩家升级)定义钩子,其他模块可以注册回调来扩展行为,而无需修改原模块。

-- hook.lua
local hooks = {}
function register_hook(point, callback)
    hooks[point] = hooks[point] or {}
    table.insert(hooks[point], callback)
end
function call_hook(point, ...)
    for _, cb in ipairs(hooks[point] or {}) do
        cb(...)
    end
end
return { register = register_hook, call = call_hook }

模块性能、调试与测试

require 性能与懒加载

大量模块在启动时一次性加载会拖慢启动速度。懒加载(如前文代理模块)可以按需加载。也可以使用 package.searchers 配合缓存优化。

测量加载耗时:

local start = os.clock()
require("heavy_module")
print("cost:", os.clock() - start)

缓存优化与依赖链分析

分析依赖关系可使用 debug.getinfo 递归查询,或借助 package.loaded 倒推。依赖链过长可能导致重新加载困难。利用工具如 laye 检查。

循环依赖定位

在 require 内部添加日志:

local old_require = require
function require(name)
    print("require " .. name)
    return old_require(name)
end

当循环依赖发生时,日志中会出现相互 require 的模式。

hook 调试与 mock / stub

使用 debug.sethook 可以跟踪模块函数的调用。单元测试时可以 package.loaded 重载依赖模块为 mock 版本。

单元测试框架

Lua 有 busted、luaunit 等。针对模块的测试:

-- test_mymath.lua
local mymath = require("mymath")
assert(mymath.add(2,3) == 5)

Lua 工程生态与源码能力

热门框架简析:xLua / tolua / Cocos Lua

  • xLua(Unity 热更新方案):为 Unity 提供了 Lua 虚拟机和 C# 交互,通过自定义 loader 从 AssetBundle 加载 Lua 模块,并实现大量生成代码来提升性能。
  • tolua / slua 类似,都是将 C++ 对象绑定到 Lua。
  • Cocos2d-x Lua 使用 Lua 模块化开发游戏场景和逻辑。

Skynet 服务框架

Skynet 是一个轻量级的并发框架,每个服务就是一个独立的 Lua 状态机,服务间通过消息通信。服务模块化的设计非常典型。

OpenResty 中的 Lua 模块

Nginx + LuaJIT,每个请求在一个独立的 coroutine 中运行,模块被缓存于 package.loaded 中,共享于同一 worker。

require 源码浅读

Lua 官方 C 源码中,require 的实现位于 loadlib.c,核心函数为 ll_require。它会调用 luaL_findtable 获取 package.loaded,搜索器逻辑在 package.searchers

Lua VM 与 GC 机制

模块中的局部变量、闭包、table 都由 Lua GC 管理。大量模块频繁加载会产生垃圾,需要合理管理生命周期。理解 GC 的 step 和 collectgarbage 有助于优化。

成为框架设计师的基石

掌握上述所有内容后,你可以设计自己的应用框架:统一的模块生命周期、配置热更新、协程调度、事件总线、依赖注入容器等。模块化不仅是一种语法技巧,更是工程化思维。

补充:Lua 全局变量、基础函数与标准库完全清单

作为模块化学习的延伸,全面理解 Lua 暴露的所有全局名称是成为熟练开发者必要的一步。下面我将按照功能分类,列出所有重要的内置全局变量、函数、表以及它们的用途和版本注意事项(主要基于 Lua 5.3 / 5.4,同时注明 5.1 和 5.2+ 的差异)。请注意,真正的“全局变量”是指存在于全局环境 _G 中的字段,但为了完整性,我们也会包括一些本质上是全局表的字段(如 math),它们同样可以直接访问。


一、核心全局变量

全局名称 类型 用途说明 版本/备注
_G table 全局环境表。所有的全局变量都是 _G 的字段,例如 _G.print 就是 print。任何未通过 local 声明的变量都会写入 _G 所有版本
_ENV table 当前 chunk 的环境表。Lua 5.2+ 中,每个 chunk 有一个上值 _ENV,全局变量查找实际是 _ENV.var_ENV 通常指向 _G,但可以修改以实现沙箱。 5.2+,5.1 使用 setfenv
_VERSION string 当前 Lua 解释器的版本字符串,如 "Lua 5.4" 所有版本
arg table 命令行参数表。当 Lua 脚本作为主程序运行时,arg[0] 是脚本名,arg[1]arg[2]… 是传入的参数。arg[-1] 等表示解释器选项。 所有版本

二、基础函数(全局可调用的函数)

这些函数不属于任何表,直接作为全局名称提供。

函数 用途
print(...) 将参数转换为字符串输出到标准输出(stdout),自动添加换行。
type(v) 返回变量 v 的类型字符串:"nil", "boolean", "number", "string", "table", "function", "thread", "userdata"
tostring(v) 将任意值转换为字符串。对于表、函数等,默认返回类似 "table: 0x..." 的地址,但可通过 __tostring 元方法自定义。
tonumber(v [, base]) 将字符串或数字转换为数字。如果提供 base(2-36),则按该进制解释字符串。转换失败返回 nil
assert(v [, message]) vnilfalse,则触发错误并输出可选的 message,否则返回 v。常用于参数校验。
error(message [, level]) 抛出一个错误,message 为错误信息,level 指出错误位置(1=调用者,2=调用者的调用者等)。
pcall(f, ...) 保护模式调用函数 f,捕获任何错误。返回 (true, 返回值...)(false, 错误信息)
xpcall(f, err, ...) pcall,但错误发生时调用 err 处理函数,通常用 debug.traceback 获取调用栈。
select(index, ...) index 是数字,返回从该位置开始的所有剩余参数;若 index 是字符串 "#",返回可变参数的总个数。
next(table [, key]) 返回表中的下一个键值对(用于遍历)。若没有下一项则返回 nil。通常不直接使用,而是用 pairs
pairs(t) 返回迭代器(next, t, nil),用于遍历表的所有键值对。
ipairs(t) 返回迭代器(闭包, t, 0),用于遍历序列(连续整数键,从1开始直到第一个空缺)。
load(chunk [, chunkname [, mode [, env]]]) 从字符串或函数中加载代码块,编译后返回一个函数。若失败返回 nil 和错误信息。
loadfile([filename [, mode [, env]]]) 从文件加载 Lua 代码块,返回一个函数。不执行文件。
dofile([filename]) 加载并执行 Lua 文件,同时返回文件 chunk 的返回值。每次都会重新执行。
require(modname) 加载模块,返回模块值,有缓存(package.loaded)。是 Lua 模块化核心中的核心函数。
rawget(table, key) 不触发元表 __index,直接获取 table[key] 的值。
rawset(table, key, value) 不触发元表 __newindex,直接设置 table[key] = value
rawlen(v) 返回对象 v 的原始长度(对于表,不触发 __len 元方法;对于字符串,返回字节数)。Lua 5.3+。
getmetatable(object) 返回对象的元表。
setmetatable(table, metatable) 设置表的元表,返回该表。若 metatablenil 则移除元表。
getfenv([f]) Lua 5.1 及兼容模式中返回函数或调用栈级别处的环境表。5.2+ 中已废弃,使用 _ENV 代替。
setfenv(f, table) 5.1 中设置函数的环境表。5.2+ 中不可用。
collectgarbage([opt [, arg]]) 垃圾回收控制函数,可执行停止、重启、步进、完整回收等操作。
coroutine.create(f) 创建协程。返回线程(thread)对象。
coroutine.resume(co [, ...]) 启动或恢复协程 co
coroutine.yield(...) 让出当前协程。
coroutine.status(co) 返回协程状态:"suspended", "running", "dead", "normal"
coroutine.wrap(f) 创建协程并返回一个函数,每次调用该函数就恢复协程(自动处理 resume/yield)。

三、标准库(全局表)

这些表是预先定义的,包含大量实用函数。它们的字段并非直接全局变量,但表本身是全局的。

表名 用途
math 数学函数库:math.sin, math.cos, math.sqrt, math.random, math.pi, math.huge 等。
string 字符串处理库:string.sub, string.find, string.gsub, string.format, string.match, string.gmatch 等。支持模式匹配。
table 表操作库:table.insert, table.remove, table.sort, table.concat, table.pack, table.unpack 等。
io 输入输出库:io.open, io.read, io.write, io.lines, io.popen(部分平台)。
os 操作系统库:os.time, os.date, os.clock, os.execute, os.remove, os.rename, os.exit。注意沙箱中可能被屏蔽。
debug 调试库:debug.getinfo, debug.getlocal, debug.setlocal, debug.getupvalue, debug.setupvalue, debug.traceback, debug.sethook。生产环境谨慎使用。
coroutine 协程库:上面已列出核心函数,此外还有 coroutine.isyieldable
utf8 UTF-8 支持库(Lua 5.3+):utf8.char, utf8.codepoint, utf8.len, utf8.offset 等。
package 模块加载系统核心表:包含 package.loaded, package.path, package.cpath, package.searchers(或 loaders),package.preload, package.config 等。require 和模块搜索器依赖此表。

四、其他全局变量(特定场景)

全局变量 用途 备注
debug 注意 debug 本身也是一个全局表,但某些安全环境中会被移除或留空。 不适合生产。
math.randomseed math 库的函数,不是独立全局。
废弃的 module() Lua 5.1 中用于定义模块的函数,Lua 5.2 后废弃。现在只需直接返回表。 避免使用。

五、隐藏全局变量(实现相关)

某些 Lua 实现可能额外提供以下全局,但标准不建议依赖:

  • _LOADED:旧版(Lua 5.0)的模块缓存表,已被 package.loaded 替代。
  • _REQUIREDNAME:在模块中用来获取模块名(老式)。

六、环境相关的特殊全局概念

  1. _G_ENV 的关系
    默认情况下,_ENV == _G。但是如果你在 loadloadfile 中指定了不同的 env,那么该 chunk 中的 _ENV 指向你提供的表,而 _G 仍然指向原始全局环境。所以在受到限制的沙箱中,_G 可能包含沙箱无法访问的函数,需要注意。

  2. 修改 _ENV 实现沙箱

    local sandbox = { print = print, error = error }
    local f = load("print(x)", nil, "t", sandbox) -- _ENV = sandbox
    f()  -- x 不存在于 sandbox,会报 nil 错误
    
  3. 访问未声明的全局变量检查
    通过元表监控 _ENV__index__newindex,可以捕获意外的全局读写。


七、如何查看当前 Lua 环境的所有全局变量?

在交互式解释器或脚本中执行:

for k, v in pairs(_G) do
    print(k, type(v))
end

这将列出所有全局名称。若想排除标准库,可以对比一个干净的 Lua 环境(如新建一个状态,不加载任何库)。

协程(Coroutine)

概括

协程(coroutine)是 Lua 中最强大的特性之一,它为异步编程、状态机、游戏逻辑调度提供了优雅的解决方案。与操作系统线程不同,Lua 协程是**协作式(cooperative)**的——协程主动让出控制权,而非被抢占。理解协程是成为 Lua 高手的分水岭。

协程核心概念

线程(Thread)类型

在 Lua 中,协程是一个 thread 类型的值,通过 coroutine.create(f) 创建。注意它不是操作系统线程,而是一个独立的执行上下文(独立的栈、指令指针),但始终运行在单一 OS 线程上。

local co = coroutine.create(function()
    print("Hello from coroutine")
end)
print(type(co))  --> thread

四种核心状态

状态 含义 进入方式
suspended 协程已创建但未开始,或已 yield coroutine.create() 后,或调用 coroutine.yield()
running 协程正在执行 调用 coroutine.resume()
normal 协程 A resume 了协程 B,此时 A 处于 normal A 在 coroutine.resume(B) 时变为 normal
dead 协程函数执行完毕或抛出未捕获错误 函数 return 或 error
local co = coroutine.create(function()
    print("1. " .. coroutine.status(co))   --> running
    coroutine.yield()
    print("3. " .. coroutine.status(co))   --> running
end)

print("0. " .. coroutine.status(co))       --> suspended
coroutine.resume(co)                       --> 输出 1
print("2. " .. coroutine.status(co))       --> suspended
coroutine.resume(co)                       --> 输出 3
print("4. " .. coroutine.status(co))       --> dead

coroutine.create vs coroutine.wrap

两者的本质区别:

特性 create wrap
返回类型 返回 thread 对象 返回普通 function
错误处理 resume 返回 (bool, error),可手动捕获 错误会直接向上层抛出(无保护)
resume 方式 coroutine.resume(co, ...) 直接调用返回的函数 f(...)
底层实现 返回原始协程 thread 内部包装了 create + resume,去掉了首位的 bool
-- create 方式:完全控制,适合精细管理
local co = coroutine.create(function(a, b)
    print(a + b)
    return a * b
end)
local ok, result = coroutine.resume(co, 3, 4)
print(ok, result)  --> true    12

-- wrap 方式:简洁,适合简单场景
local f = coroutine.wrap(function(a, b)
    print(a + b)
    return a * b
end)
local result = f(3, 4)
print(result)  --> 12

wrap 的实现本质上等价于:

function coroutine.wrap(f)
    local co = coroutine.create(f)
    return function(...)
        local status, result = coroutine.resume(co, ...)
        if not status then
            error(result)  -- 将错误重新抛出
        end
        return result
    end
end

yield 与 resume 的数据交换

协程最精妙的设计在于 yieldresume 之间的双向数据传递

local co = coroutine.create(function(initial)
    print("收到初始值:", initial)          -- 收到初始值: hello
    local received = coroutine.yield(1, 2) -- 返回 (1, 2) 给 resume 调用者
    print("收到第二次传入:", received)       -- 收到第二次传入: world
    return "done"
end)

-- resume 的额外参数传给协程函数的形参
-- resume 的返回值是 yield 传回的值
local ok, a, b = coroutine.resume(co, "hello")
print(ok, a, b)  --> true   1   2

-- 第二次 resume 的参数会作为 yield 的返回值传给协程内部
local ok, result = coroutine.resume(co, "world")
print(ok, result)  --> true   done

数据流关键理解:

主线程                   协程内部
  |                        |
  |-- resume(co, arg) ---->| arg 作为协程函数的参数
  |                        | yield(ret) 返回 ret 到主线程
  |<--- ret ------------- |
  |                        |
  |-- resume(co, arg2) --> | arg2 作为 yield() 的返回值
  |                        | return final
  |<--- final ----------- |

多返回值与可变参数

yield 可以返回多个值,resume 也接收多个值:

local co = coroutine.create(function(...)
    local args = {...}
    print("协程收到", #args, "个参数")
    local result = coroutine.yield(args[1] + args[2], args[1] * args[2])
    print("协程继续:", result)
    return "finished"
end)

local ok, sum, product = coroutine.resume(co, 10, 20)
print("主线程收到:", sum, product)  --> 主线程收到: 30  200

coroutine.resume(co, "next step")
--> 协程继续: next step

协程的应用模式

模式一:生产者-消费者(Producer-Consumer)

这是协程最经典的应用。协程天然适合表达"按需生产"的逻辑:

-- 生产者:生成斐波那契数列
local function fibonacci()
    return coroutine.wrap(function()
        local a, b = 0, 1
        while true do
            coroutine.yield(b)
            a, b = b, a + b
        end
    end)
end

-- 消费者:按需获取
local fib = fibonacci()
for i = 1, 10 do
    print(fib())  --> 1 1 2 3 5 8 13 21 34 55
end

模式二:非抢占式多任务调度

多个协程在同一个线程中交替执行,模拟并发:

local tasks = {
    coroutine.wrap(function()
        for i = 1, 5 do
            print("任务A - 第" .. i .. "步")
            coroutine.yield()
        end
    end),
    coroutine.wrap(function()
        for i = 1, 5 do
            print("任务B - 第" .. i .. "步")
            coroutine.yield()
        end
    end),
}

-- 调度器
while #tasks > 0 do
    for i = #tasks, 1, -1 do
        local status, err = pcall(tasks[i])
        if not status then
            print("任务" .. i .. "结束")
            table.remove(tasks, i)
        end
    end
end

模式三:在循环体内部 yield

协程可以在多层嵌套函数中 yield(Lua 5.2+):

local function inner()
    coroutine.yield("from inner")
end

local function outer()
    inner()
    return "from outer"
end

local co = coroutine.wrap(outer)
print(co())  --> from inner (pause)
print(co())  --> from outer (resume 后继续)

这一点非常重要——它意味着你可以在深层调用的任意位置 yield,Lua 会自动保存和恢复完整的调用栈。

模式四:coroutine.isyieldable

在某些上下文中不能 yield(比如在 pcall 的 err 处理函数中),可以提前检查:

if coroutine.isyieldable() then
    coroutine.yield()
else
    -- 不能 yield,执行替代逻辑
    print("当前上下文不能 yield")
end

无法 yield 的场景包括:

  • 主协程(main thread)
  • 某些 C 函数的保护调用中
  • __gc 元方法中
  • 无法 yield 的 C 调用内部

协程调度器

概括

在游戏服务器中,不可能手动管理数十个协程的 resume。我们需要一个统一的**协程调度器(Scheduler)**来管理所有协程的生命周期、调度和异常处理。

基础调度器实现

版本一:简单轮询调度器

-- scheduler.lua
local Scheduler = {}
Scheduler.__index = Scheduler

function Scheduler.new()
    local self = setmetatable({}, Scheduler)
    self._tasks = {}      -- 待执行协程列表
    self._running = nil   -- 当前运行的协程
    return self
end

-- 添加一个协程任务
function Scheduler:add(f, ...)
    local co = coroutine.create(f)
    local task = {
        co = co,
        status = "waiting",
        args = {...},
        callback = nil,     -- 任务完成时的回调
    }
    table.insert(self._tasks, task)
    -- 立即启动,使其运行到第一个 yield
    self:_resume_task(task)
    return task
end

-- 内部 resume 一个任务
function Scheduler:_resume_task(task)
    self._running = task
    task.status = "running"
    local ok, msg = coroutine.resume(task.co, table.unpack(task.args or {}))
    if not ok then
        print("协程错误:", msg)
        task.status = "dead"
    elseif coroutine.status(task.co) == "dead" then
        task.status = "dead"
        if task.callback then
            task.callback()
        end
    else
        task.status = "waiting"
    end
    self._running = nil
end

-- 步进一次:恢复所有等待中的协程
function Scheduler:step()
    for i = #self._tasks, 1, -1 do
        local task = self._tasks[i]
        if task.status == "dead" then
            table.remove(self._tasks, i)
        elseif task.status == "waiting" then
            self:_resume_task(task)
        end
    end
end

-- 运行直到所有任务完成
function Scheduler:run()
    while #self._tasks > 0 do
        self:step()
    end
end

return Scheduler

使用示例:

local Scheduler = require("scheduler")
local sched = Scheduler.new()

sched:add(function()
    for i = 1, 3 do
        print("协程A:", i)
        coroutine.yield()  -- 每步主动让出
    end
end)

sched:add(function()
    for i = 1, 2 do
        print("协程B:", i)
        coroutine.yield()
    end
end)

sched:run()
-- 输出:
-- 协程A: 1
-- 协程B: 1
-- 协程A: 2
-- 协程B: 2
-- 协程A: 3

版本二:带优先级的调度器

function Scheduler:add_priority(f, priority, ...)
    local task = {
        co = coroutine.create(f),
        status = "waiting",
        priority = priority or 0,
        args = {...},
    }
    -- 按优先级排序插入
    local inserted = false
    for i, t in ipairs(self._tasks) do
        if (t.priority or 0) < task.priority then
            table.insert(self._tasks, i, task)
            inserted = true
            break
        end
    end
    if not inserted then
        table.insert(self._tasks, task)
    end
    self:_resume_task(task)
    return task
end

版本三:按帧数调度的调度器(游戏场景)

在游戏帧循环中,每帧执行一定数量的协程,避免卡顿:

function Scheduler:update(dt, max_tasks_per_frame)
    local count = 0
    local limit = max_tasks_per_frame or 10

    for i = #self._tasks, 1, -1 do
        if count >= limit then break end
        local task = self._tasks[i]
        if task.status == "dead" then
            table.remove(self._tasks, i)
        elseif task.status == "waiting" then
            self:_resume_task(task)
            count = count + 1
        end
    end
end

协程调度的深层原理

从 Lua 源码理解协程实现

在 Lua 虚拟机中,每个协程是一个独立的 lua_State

// lstate.h (简化)
typedef struct lua_State {
    StkId top;           // 栈顶
    StkId stack_last;    // 栈底
    StkId stack;         // 栈数组
    int stacksize;       // 栈大小
    struct lua_longjmp *errorJmp;  // 错误跳转
    CallInfo *ci;        // 当前调用信息
    CallInfo base_ci;    // 基础调用信息
    // ...
} lua_State;

而在协程(lua_State)之上,主线程持有的 global_State 管理着所有协程的 GC、全局表等。当 coroutine.yield() 被调用时:

  1. 当前协程的整个栈、指令指针、调用链被保存
  2. 控制权返回到 coroutine.resume() 的调用者
  3. GC 将 yield 的协程视为可达对象,不会回收其栈上的值

这就是为什么协程可以"暂停"而状态不丢失——它有自己完整的 Lua 栈。

yield 的 C 层面限制

虽然 Lua 5.2+ 允许在嵌套 Lua 函数中 yield,但在 C 函数层面有限制:

  • 可 yield 的 C 函数:通过 lua_yieldk 实现,使用了 continuation function
  • 不可 yield 的 C 函数:大多数标准库的 C 函数(如 table.sort 的回调)
  • 如果在不可 yield 的上下文中调用 coroutine.yield(),会抛出 "attempt to yield across a C-call boundary" 错误

定时器机制

概括

定时器是游戏服务器中最常用的功能之一:技能冷却、Buff 持续时间、定时任务、超时检测等。利用协程可以优雅地实现"暂停等待"的语义。

基础实现:sleep(n)

版本一:带调度器的 sleep

-- timer.lua
local Timer = {}

function Timer.new(scheduler)
    local self = setmetatable({}, { __index = Timer })
    self._scheduler = scheduler
    self._timers = {}     -- 延时任务列表
    return self
end

-- 添加一个延时任务
function Timer:add(delay, callback, ...)
    table.insert(self._timers, {
        expire = os.time() + delay,
        callback = callback,
        args = {...},
    })
end

-- 每帧更新,检查过期任务
function Timer:update()
    local now = os.time()
    for i = #self._timers, 1, -1 do
        local t = self._timers[i]
        if now >= t.expire then
            t.callback(table.unpack(t.args))
            table.remove(self._timers, i)
        end
    end
end

return Timer

版本二:协程式 sleep(最优雅)

让协程"等待"一段时间后再继续:

function Scheduler:sleep(seconds)
    -- 将自己挂起,注册一个定时器
    local task = self._running
    task.wakeup = os.time() + seconds
    task.status = "sleeping"
    coroutine.yield() -- 主动让出,等待被唤醒
end

-- 在 step 中处理 sleeping 状态
function Scheduler:step()
    local now = os.time()
    for i = #self._tasks, 1, -1 do
        local task = self._tasks[i]
        if task.status == "dead" then
            table.remove(self._tasks, i)
        elseif task.status == "sleeping" then
            if now >= task.wakeup then
                task.status = "waiting"
                self:_resume_task(task)
            end
        elseif task.status == "waiting" then
            self:_resume_task(task)
        end
    end
end

使用效果:

local sched = Scheduler.new()

sched:add(function()
    print(os.date("%X") .. " 开始")
    sched:sleep(2)
    print(os.date("%X") .. " 2秒后")
    sched:sleep(1)
    print(os.date("%X") .. " 再1秒后")
    print("完成!")
end)

while #sched._tasks > 0 do
    sched:step()
end
-- 输出:
-- 10:30:00 开始
-- 10:30:02 2秒后
-- 10:30:03 再1秒后
-- 完成!

版本三:高精度游戏定时器

游戏通常使用 dt(delta time,帧间隔时间)来驱动,而非 os.time():

function Scheduler:sleep_dt(seconds)
    local task = self._running
    task.remaining = seconds    -- 剩余等待时间
    task.status = "sleeping"
    coroutine.yield()
end

function Scheduler:update(dt)
    for i = #self._tasks, 1, -1 do
        local task = self._tasks[i]
        if task.status == "sleeping" then
            task.remaining = task.remaining - dt
            if task.remaining <= 0 then
                task.status = "waiting"
                self:_resume_task(task)
            end
        end
    end
end

基于时间轮的高性能定时器

当定时器数量巨大时(如百万个 Buff 计时),逐一遍历检查效率太低。**时间轮(Timing Wheel)**是工业级解决方案。

时间轮原理

将时间划分为固定大小的槽(slot),每个槽是一个任务链表。指针每走一格,处理当前槽的所有任务:

-- timing_wheel.lua
local TimingWheel = {}

function TimingWheel.new(slot_count, tick_interval)
    local self = setmetatable({}, { __index = TimingWheel })
    self._slot_count = slot_count or 60   -- 60个槽
    self._tick_interval = tick_interval or 1  -- 每格1秒
    self._slots = {}
    self._current = 1                      -- 当前指针位置
    self._elapsed = 0                      -- 累计经过时间
    for i = 1, self._slot_count do
        self._slots[i] = {}
    end
    return self
end

-- 添加一个延时任务
function TimingWheel:add(delay, callback, ...)
    local slot = (self._current + math.ceil(delay / self._tick_interval) - 1)
                 % self._slot_count + 1
    table.insert(self._slots[slot], {
        callback = callback,
        args = {...},
        rounds = math.floor(delay / (self._slot_count * self._tick_interval)),
    })
end

-- 指针前进一格
function TimingWheel:tick()
    local slot = self._slots[self._current]
    local remaining = {}
    for _, task in ipairs(slot) do
        if task.rounds <= 0 then
            -- 到期执行
            task.callback(table.unpack(task.args))
        else
            task.rounds = task.rounds - 1
            table.insert(remaining, task)
        end
    end
    self._slots[self._current] = remaining
    self._current = self._current % self._slot_count + 1
end

return TimingWheel

复杂度分析:添加任务 O(1),执行过期任务 O(k),k 为该槽任务数。相比 O(n) 遍历,时间轮在大量定时器场景下有数量级的性能优势。

多层级时间轮(Hierarchical Timing Wheel)

对于跨度极大的时间需求(从毫秒到小时),可以使用多层时间轮:层级从秒→分钟→小时,层层降级,类似于时钟的秒针、分针、时针。

定时器在游戏中的实战应用

技能冷却

sched:add(function()
    while true do
        -- 等待玩家输入
        if input == "skill_a" then
            if skill_a:can_use() then
                skill_a:cast()
                sched:sleep(skill_a.cooldown)  -- 冷却等待
            end
        end
        coroutine.yield()
    end
end)

超时重试

local function request_with_timeout(url, timeout)
    local co = coroutine.running()
    local result = nil
    local done = false

    -- 发起异步请求
    async_http_get(url, function(res)
        result = res
        done = true
        if coroutine.status(co) ~= "dead" then
            coroutine.resume(co)
        end
    end)

    -- 超时检测
    local start = os.time()
    while not done and os.time() - start < timeout do
        coroutine.yield()
    end

end

Lua ↔ C 基础与进阶

概括

Lua 与 C/C++ 的交互是其最强大的能力之一。Lua 作为脚本语言负责逻辑,C/C++ 负责性能关键路径(数学运算、网络 IO、图像处理)。理解这个桥梁是游戏服务器开发的核心技能。

Lua 虚拟栈 — 一切交互的基础

Lua 与 C 之间的一切数据交换都通过**虚拟栈(Virtual Stack)**进行。这是一个 LIFO 结构,C 代码通过索引来操作栈上的元素。

栈索引规则

栈顶 (索引 -1)
  |  元素5   (索引 5, 负索引 -1)
  |  元素4   (索引 4, 负索引 -2)
  |  元素3   (索引 3, 负索引 -3)
  |  元素2   (索引 2, 负索引 -4)
  |  元素1   (索引 1, 负索引 -5)
栈底 (伪索引 LUA_REGISTRYINDEX)
  • 正索引:从栈底开始,1 是第一个压入的元素
  • 负索引:从栈顶开始,-1 是栈顶
  • 伪索引:特殊常量如 LUA_REGISTRYINDEX(注册表)、LUA_RIDX_GLOBALS(全局表)

核心栈操作 API

// 压栈
void lua_pushnil(lua_State *L);
void lua_pushboolean(lua_State *L, int b);
void lua_pushnumber(lua_State *L, lua_Number n);
void lua_pushinteger(lua_State *L, lua_Integer n);
void lua_pushlstring(lua_State *L, const char *s, size_t len);
void lua_pushstring(lua_State *L, const char *s);
void lua_pushcfunction(lua_State *L, lua_CFunction f);
void lua_pushlightuserdata(lua_State *L, void *p);

// 读取栈值
int lua_toboolean(lua_State *L, int index);
lua_Number lua_tonumber(lua_State *L, int index);
lua_Integer lua_tointeger(lua_State *L, int index);
const char *lua_tolstring(lua_State *L, int index, size_t *len);
lua_CFunction lua_tocfunction(lua_State *L, int index);
void *lua_touserdata(lua_State *L, int index);

// 栈管理
int lua_gettop(lua_State *L);               // 返回栈顶索引(元素数量)
void lua_settop(lua_State *L, int index);   // 设置栈顶位置
void lua_pushvalue(lua_State *L, int index); // 复制指定位置的值到栈顶
void lua_rotate(lua_State *L, int idx, int n); // 旋转栈元素 (5.3+)
void lua_remove(lua_State *L, int index);   // 移除指定位置
void lua_insert(lua_State *L, int index);   // 栈顶元素插入到指定位置
void lua_replace(lua_State *L, int index);  // 栈顶元素替换指定位置

// 类型检查
int lua_type(lua_State *L, int index);       // 返回类型常量
const char *lua_typename(lua_State *L, int tp); // 类型名
int lua_isnumber(lua_State *L, int index);
int lua_isstring(lua_State *L, int index);
int lua_istable(lua_State *L, int index);
int lua_isfunction(lua_State *L, int index);

// Lua 5.2+ 新增栈管理
void lua_copy(lua_State *L, int fromidx, int toidx);  // 复制
int lua_absindex(lua_State *L, int idx);  // 将负索引转正

栈操作状态机图解

初始栈(空): []
pushnumber(L, 3.14):        [3.14]                    top=1
pushstring(L, "hello"):     [3.14, "hello"]           top=2
pushvalue(L, 1):            [3.14, "hello", 3.14]     top=3
remove(L, 2):               [3.14, 3.14]              top=2
pop(L) → lua_settop(L, 1):  [3.14]                    top=1

关键原则:C 函数接收参数时,它们已经在栈上了(正索引 1, 2, 3…);C 函数返回结果时,把返回值压入栈中,并返回数量。

C 调用 Lua 函数

基础流程

// 假设 Lua 中有:function add(a, b) return a + b end

lua_getglobal(L, "add");      // 1. 将函数 push 到栈顶
lua_pushnumber(L, 3);         // 2. 压入参数1
lua_pushnumber(L, 5);         // 3. 压入参数2

// 4. 调用:nargs=2, nresults=1(LUA_MULTRET 表示返回所有值)
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
    fprintf(stderr, "调用失败: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

// 5. 获取返回值
double result = lua_tonumber(L, -1);
lua_pop(L, 1);  // 清理栈
printf("结果: %f\n", result);

调用 Lua 表的函数(方法调用)

// 假设:tbl = { mul = function(self, factor) return self.value * factor end }
//      obj = { value = 10 }; setmetatable(obj, {__index = tbl})

// 方式1:手动模拟 obj:mul(2)
lua_getglobal(L, "obj");        // push obj
lua_getfield(L, -1, "mul");     // push obj.mul(触发 __index)
lua_pushvalue(L, -2);           // push obj(作为 self)
lua_pushnumber(L, 2);           // push factor
lua_pcall(L, 2, 1, 0);         // 调用
// 栈: [obj, nil, nil]
lua_settop(L, 0);              // 清理

lua_call vs lua_pcall

函数 错误处理 使用场景
lua_call(L, nargs, nresults) 不处理,错误直接传播 确定性环境,信任代码
lua_pcall(L, nargs, nresults, msgh) 捕获错误,返回 LUA_OK 或错误码 生产代码必须使用
// 错误处理函数索引(msgh=0 表示无特殊处理)
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
    // 栈顶现在是错误信息字符串
    fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

Lua 调用 C 函数

注册 C 函数

C 函数签名:int func(lua_State *L),返回值是压入栈的结果数量。

// 一个简单的 C 函数:add(a, b)
static int l_add(lua_State *L) {
    // 参数在栈上:索引 1 = a, 索引 2 = b
    double a = luaL_checknumber(L, 1);  // 带类型检查
    double b = luaL_checknumber(L, 2);
    lua_pushnumber(L, a + b);
    return 1;  // 返回1个结果
}

// 注册到 Lua
lua_pushcfunction(L, l_add);
lua_setglobal(L, "add");

luaL_Reg 批量注册

// 使用 luaL_Reg 结构体数组批量注册
static const struct luaL_Reg mylib[] = {
    {"add", l_add},
    {"sub", l_sub},
    {"mul", l_mul},
    {"get_version", l_get_version},
    {NULL, NULL}  // 哨兵
};

// 两种注册方式

// 方式1:注册为全局函数
int luaopen_mymath(lua_State *L) {
    luaL_newlib(L, mylib);  // 创建新 table 并注册所有函数
    return 1;
}

// 方式2:注册到已有表
luaL_setfuncs(L, mylib, 0);

参数校验利器:luaL_check*

static int l_process_player(lua_State *L) {
    // luaL_check* 系列:类型不匹配时抛出描述性错误
    luaL_checktype(L, 1, LUA_TTABLE);  // 检查表类型
    const char *name = luaL_checkstring(L, 2);
    int level = luaL_checkinteger(L, 3);
    double hp = luaL_optnumber(L, 4, 100.0);  // 可选参数,默认100

    // 检查表字段
    lua_getfield(L, 1, "pos");
    luaL_checktype(L, -1, LUA_TTABLE);
    lua_getfield(L, -1, "x");
    double x = luaL_checknumber(L, -1);
    // ... 处理

    return 0;  // 不返回值
}

C 函数操作 Lua 表

// 在 C 中构建并返回一个 Lua 表
static int l_create_player(lua_State *L) {
    lua_createtable(L, 0, 2);  // narrec=0, nrec=2 (hash部分预分配)

    // 设置字段
    lua_pushstring(L, "John");
    lua_setfield(L, -2, "name");

    lua_pushinteger(L, 42);
    lua_setfield(L, -2, "level");

    // 设置嵌套表
    lua_createtable(L, 0, 2);
    lua_pushnumber(L, 100.0);
    lua_setfield(L, -2, "x");
    lua_pushnumber(L, 200.0);
    lua_setfield(L, -2, "y");
    lua_setfield(L, -2, "position");

    return 1;  // 返回构建的表
}

Userdata — Lua 与 C 对象桥梁

Userdata 是 Lua 中表示 C 数据的类型。分为两种:

Light Userdata

等价于一个 void* 指针的包装,没有元表,没有独立的内存,GC 不管:

// C 侧:推送一个指针包装
MyStruct *obj = malloc(sizeof(MyStruct));
lua_pushlightuserdata(L, obj);

// Lua 侧:拿到的是一个 userdata,无法附加方法
local ptr = get_my_object()
-- ptr 只是地址,Lua 不知道它的结构

适用场景:C 资源的句柄、不会在 Lua 中直接操作的数据。

Full Userdata

一块由 Lua 分配、Lua GC 管理的原始内存块,可以附加元表:

// 创建 full userdata
typedef struct {
    double x, y;
    int id;
} Vector;

// 创建 userdata 并设置元表
static int l_new_vector(lua_State *L) {
    double x = luaL_checknumber(L, 1);
    double y = luaL_checknumber(L, 2);

    // 分配 sizeof(Vector) 字节的 userdata 内存
    Vector *v = (Vector *)lua_newuserdata(L, sizeof(Vector));
    v->x = x;
    v->y = y;
    v->id = 0;

    // 设置元表(使 Lua 可以对其调用方法)
    luaL_getmetatable(L, "VectorMT");
    lua_setmetatable(L, -2);

    return 1;  // 返回 userdata
}

// Userdata 的元方法
static int l_vector_add(lua_State *L) {
    Vector *a = (Vector *)luaL_checkudata(L, 1, "VectorMT");  // 带类型检查
    Vector *b = (Vector *)luaL_checkudata(L, 2, "VectorMT");
    lua_pushnumber(L, a->x + b->x);
    lua_pushnumber(L, a->y + b->y);
    return 2;
}

static int l_vector_tostring(lua_State *L) {
    Vector *v = (Vector *)luaL_checkudata(L, 1, "VectorMT");
    lua_pushfstring(L, "Vector(%f, %f)", v->x, v->y);
    return 1;
}

// __gc 元方法:userdata 被 GC 时调用
static int l_vector_gc(lua_State *L) {
    Vector *v = (Vector *)luaL_checkudata(L, 1, "VectorMT");
    printf("Vector(%d) 被回收\n", v->id);
    // 如果有额外的 C 资源(如文件句柄),在这里释放
    return 0;
}

// 注册 Vector 类型
static const struct luaL_Reg vector_methods[] = {
    {"add", l_vector_add},
    {"__tostring", l_vector_tostring},
    {"__gc", l_vector_gc},
    {NULL, NULL}
};

void register_vector(lua_State *L) {
    // 创建元表
    luaL_newmetatable(L, "VectorMT");

    // 设置 __index = 元表本身(方法查找)
    lua_pushvalue(L, -1);
    lua_setfield(L, -2, "__index");

    // 注册方法
    luaL_setfuncs(L, vector_methods, 0);

    lua_pop(L, 1);  // 弹出元表
}

Lua 侧使用:

local v1 = Vector.new(3, 5)
local v2 = Vector.new(1, 2)
local x, y = v1:add(v2)
print(tostring(v1))  --> Vector(3.000000, 5.000000)

Light Userdata vs Full Userdata

特性 Light Userdata Full Userdata
内存分配 C 侧手动管理(malloc/free) Lua 分配,GC 管理
元表 不支持 支持(每个类型各自有 metatable)
GC 不管 参与 GC(无引用时回收)
__gc 不支持 支持(释放 C 资源)
相等性 按指针地址比较 按指针地址比较(除非重载 __eq
性能 更快(无 GC 开销) 稍慢(GC 追踪)
典型用途 C 对象句柄、指针传递 需要方法和生命周期的 C 对象

注册表(Registry)

注册表是一个特殊的 Lua 表,只能通过 C API 访问(不在 Lua 全局空间中)。用于存储 C 代码之间共享的状态:

// 获取注册表
lua_pushstring(L, "MyModule_Singleton");
lua_gettable(L, LUA_REGISTRYINDEX);

// 如果还没设置,创建并存入
if (lua_isnil(L, -1)) {
    lua_pop(L, 1);
    // 创建单例对象
    MyModule *mod = lua_newuserdata(L, sizeof(MyModule));
    // ... 初始化
    lua_pushstring(L, "MyModule_Singleton");
    lua_pushvalue(L, -2);
    lua_settable(L, LUA_REGISTRYINDEX);
}

引用系统(Reference System)

luaL_ref / luaL_unref 提供了一种更方便的方式在注册表中存储和取回值:

// 创建一个引用
lua_pushstring(L, "some_value");
int ref = luaL_ref(L, LUA_REGISTRYINDEX);  // 存入注册表,返回整数引用

// 后续取回
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);    // 压回栈
// 使用...

// 释放引用
luaL_unref(L, LUA_REGISTRYINDEX, ref);

错误处理与保护调用

C 代码中的错误处理

static int l_safe_divide(lua_State *L) {
    double a = luaL_checknumber(L, 1);
    double b = luaL_checknumber(L, 2);

    if (b == 0.0) {
        // luaL_error 会执行 longjmp,不会返回
        luaL_error(L, "division by zero");
        // return 0;  // 不会执行到这里
    }

    lua_pushnumber(L, a / b);
    return 1;
}

资源保护的 pcall 模式

// 在 C 中安全地调用 Lua 函数并清理资源
char *buffer = malloc(1024);

lua_getglobal(L, "process");
lua_pushstring(L, buffer);   // 参数

if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
    fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

free(buffer);  // 确保资源释放

// 获取返回值
const char *result = lua_tostring(L, -1);
lua_pop(L, 1);

实践:C 扩展库的完整结构

// myphysics.c
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

// ========== 内部实现 ==========
typedef struct {
    double mass;
    double velocity_x, velocity_y;
} RigidBody;

// 创建函数
static int l_new_body(lua_State *L) {
    double mass = luaL_checknumber(L, 1);
    RigidBody *body = lua_newuserdata(L, sizeof(RigidBody));
    body->mass = mass;
    body->velocity_x = 0;
    body->velocity_y = 0;
    luaL_getmetatable(L, "RigidBodyMT");
    lua_setmetatable(L, -2);
    return 1;
}

// 方法函数
static int l_body_set_velocity(lua_State *L) {
    RigidBody *body = luaL_checkudata(L, 1, "RigidBodyMT");
    body->velocity_x = luaL_checknumber(L, 2);
    body->velocity_y = luaL_checknumber(L, 3);
    return 0;
}

static int l_body_get_kinetic_energy(lua_State *L) {
    RigidBody *body = luaL_checkudata(L, 1, "RigidBodyMT");
    double v2 = body->velocity_x * body->velocity_x
              + body->velocity_y * body->velocity_y;
    lua_pushnumber(L, 0.5 * body->mass * v2);
    return 1;
}

static int l_body_gc(lua_State *L) {
    RigidBody *body = luaL_checkudata(L, 1, "RigidBodyMT");
    printf("[GC] RigidBody(mass=%.2f) freed\n", body->mass);
    return 0;
}

// ========== 模块注册 ==========
static const luaL_Reg rigidbody_methods[] = {
    {"set_velocity", l_body_set_velocity},
    {"get_kinetic_energy", l_body_get_kinetic_energy},
    {"__gc", l_body_gc},
    {NULL, NULL}
};

static const luaL_Reg myphysics_functions[] = {
    {"new_body", l_new_body},
    {NULL, NULL}
};

// ========== 模块入口 ==========
int luaopen_myphysics(lua_State *L) {
    // 1. 创建 RigidBody 元表
    luaL_newmetatable(L, "RigidBodyMT");
    lua_pushvalue(L, -1);
    lua_setfield(L, -2, "__index");
    luaL_setfuncs(L, rigidbody_methods, 0);
    lua_pop(L, 1);

    // 2. 创建模块表
    luaL_newlib(L, myphysics_functions);
    return 1;
}

编译和使用:

# 编译为动态库
gcc -shared -fPIC -o myphysics.so myphysics.c -llua

# Lua 中使用
local physics = require("myphysics")
local body = physics.new_body(10)
body:set_velocity(5, 0)
print(body:get_kinetic_energy())  --> 125.0

Lua 值在 C 中的内存管理

关键要点:

  1. 栈上的值:在 C 函数返回后,如果不再被引用就会被 GC。如果你需要长期持有,要么存入注册表/全局表,要么使用 luaL_ref

  2. String 的生命周期lua_tolstring 返回的指针在字符串被 GC 回收后失效。不要跨 Lua 调用持有它。

  3. Userdata:Full userdata 的内存由 Lua 分配,GC 管理。C 代码不应 free 它(用 __gc 元方法代替)。Light userdata 的指针由 C 管理。

// 危险!跨调用持有字符串指针
const char *str = lua_tostring(L, -1);
lua_pop(L, 1);
lua_gc(L, LUA_GCCOLLECT, 0);  // 强制 GC
printf("%s\n", str);           // 可能已失效!

// 安全做法:复制
const char *str = lua_tostring(L, -1);
char *copy = strdup(str);
lua_pop(L, 1);
// 使用 copy...
free(copy);

C++ 与 Lua 的集成(工业级方案)

概括

上一章用 C API 讲清了 Lua ↔ C 的底层机制,但在实际工业项目中,几乎没有人会直接手写 C API 来做绑定——栈操作繁琐、类型不安全、容易内存泄漏、代码量爆炸。C++ 社区发展出了一系列优秀的绑定库,它们的共同目标是:让 Lua 调用 C++ 如同调用 Lua 原生函数,让 C++ 操作 Lua 数据如同操作 STL 容器

为什么工业界不用裸 C API

C API 痛点 C++ 绑定库方案
手动压栈/弹栈,序号管理心智负担大 RAII 自动管理栈,类型推导零心智负担
luaL_check* 逐个校验,代码冗长 模板自动推断类型并校验
userdata 的 __gc 容易遗漏资源 智能指针(shared_ptr/unique_ptr)自动管理生命周期
每次导出函数都要写 boilerplate lambda / 成员函数直接导出,零样板代码
遍历 Lua table 像写汇编 sol::table 如同 std::map,直接 range-for

结论:学 C API 是为了理解底层机制,生产代码用 C++ 绑定库是为了效率和正确性。

工业级 C++ ↔ Lua 绑定库全景图

Github Stars(近似) 风格 需 C++ 版本 特点 适用场景
sol2 / sol3 ~3500+ 现代模板元编程 C++17+ 功能最全,文档完善,header-only,生态最好 首选推荐,几乎所有项目
LuaBridge ~1600+ 轻量级,简洁 API C++11+ 极简 header-only,学习成本最低 轻量级嵌入,功能需求不多时
kaguya ~250+ 模板元编程 C++11/14 日系风格,功能全面 不支持 C++17 的旧项目
LuaIntf ~200+ 简洁 API C++11 接口直觉化,类似 Java 反射 小型项目快速集成
OOLua ~80+ 代码生成 C++03 从 C++ 头文件自动生成绑定 大量遗留 C++ 类需要导出
tolua++ (老牌) 代码生成(.pkg) C++03 Cocos2d-x 使用,定义 .pkg → 生成胶水代码 旧项目(Cocos2d-x)、已有 .pkg 文件
LuaBind Boost 重度依赖 C++03 最早期的工业级方案,模板元编程鼻祖 遗留项目(已不再维护)

选型建议

新项目 → sol3(功能最全,学习投入回报率最高)
轻嵌入(只暴露十几个函数) → LuaBridge(够用且极简)
老项目/无 C++17 编译器 → kaguya 或 LuaIntf
在 Cocos2d-x 系做手游 → tolua++(继承项目)

sol3 完全使用指南(工业首选)

sol3 是 sol2 的继任者,C++17+ 的头文件库。它几乎以零 boilerplate 覆盖了 Lua C API 的全部功能,同时提供了 C++ 开发者所期望的类型安全和 RAII。

一、快速上手

1.1 环境搭建

# 直接拉取头文件(sol3 只有头文件)
git clone https://github.com/ThePhD/sol2.git
# include 路径指向 sol2/include 即可

# 或者 CMake FetchContent
include(FetchContent)
FetchContent_Declare(sol3
    GIT_REPOSITORY https://github.com/ThePhD/sol2.git)
FetchContent_MakeAvailable(sol3)
target_link_libraries(your_project sol3::sol3)
// 最小示例
#define SOL_ALL_SAFETIES_ON 1  // 开启所有安全检查(开发期)
#include <sol/sol.hpp>

int main() {
    sol::state lua;            // 创建 Lua 虚拟机
    lua.open_libraries();      // 加载所有标准库(math, string, table...)

    // 直接执行 Lua 代码
    lua.script("print('Hello from Lua!')");

    // 执行并获取返回值
    auto result = lua.script("return 42 + 8");
    int value = result;        // 隐式转换
    return 0;
}

1.2 编译注意事项

# 必须链接 Lua 库,指定 C++17
g++ -std=c++17 -I/path/to/sol2/include -I/path/to/lua/include \
    main.cpp -L/path/to/lua/lib -llua -ldl -o main

# 如果是 LuaJIT
g++ -std=c++17 ... -lluajit-5.1 -o main

二、C++ 调用 Lua

2.1 获取全局变量

sol::state lua;
lua.script(R"(
    player = {
        name = "John",
        level = 42,
        position = { x = 100, y = 200 }
    }
    damage_formula = "atk * 1.5 - def * 0.5"
    max_hp = 9999
)");

// 基本类型直接取
int level = lua["player"]["level"];        // 42
std::string name = lua["player"]["name"];  // "John"

// 嵌套表
int x = lua["player"]["position"]["x"];   // 100

// 安全的可选取值
auto hp = lua["max_hp"].get_or(100);       // 不存在时返回默认值
sol::optional<int> maybe_level = lua["player"]["level"];
if (maybe_level) {
    int lv = maybe_level.value();
}

2.2 调用 Lua 函数

lua.script(R"(
    function add(a, b) return a + b end
    function multiply(a, b) return a * b end
    function get_player_info(id)
        return { name = "Player" .. id, hp = 100 * id }
    end
)");

// 直接调用
int sum = lua["add"](10, 32);              // 42

// sol::function 持有函数引用(性能更好,避免反复查找)
sol::function multiply = lua["multiply"];
int product = multiply(6, 7);              // 42

// 多返回值
sol::function get_info = lua["get_player_info"];
sol::table info = get_info(5);
std::string name = info["name"];           // "Player5"
int hp = info["hp"];                       // 500

// 使用 tie 解包多返回值
int a, b;
sol::tie(a, b) = lua.script("return 10, 20");

2.3 在 C++ 中操作 Lua 表

// 创建表
sol::table inventory = lua.create_table();
inventory["gold"] = 1000;
inventory["items"] = lua.create_table();
inventory["items"][1] = "Sword";
inventory["items"][2] = "Shield";

// 遍历表 (range-for 直接用,比 C API 优雅 100 倍)
sol::table config = lua["player"];
for (auto& kv : config) {
    sol::object key = kv.first;
    sol::object value = kv.second;
    std::cout << "Key: " << key.as<std::string>() << std::endl;
}

// 遍历数组部分
for (int i = 1; i <= inventory["items"].size(); i++) {
    std::string item = inventory["items"][i];
}

三、Lua 调用 C++

3.1 导出自由函数

// C++ 函数
int add(int a, int b) { return a + b; }
void log_message(const std::string& msg) { std::cout << "[LOG] " << msg << std::endl; }
std::string greet(const std::string& name) { return "Hello, " + name; }

// 导出到 Lua
lua.set_function("add", add);
lua.set_function("log", log_message);
lua.set_function("greet", greet);

// Lua 侧直接调用
lua.script(R"(
    print(add(3, 4))          --> 7
    log("server started")     --> [LOG] server started
    print(greet("World"))     --> Hello, World
)");

3.2 导出 lambda

lua.set_function("clamp", [](double val, double min, double max) {
    return std::max(min, std::min(val, max));
});

// 捕获外部变量的 lambda
int call_count = 0;
lua.set_function("tracked_call", [&call_count](const std::string& name) {
    call_count++;
    std::cout << "Called: " << name << " (total: " << call_count << ")" << std::endl;
});

3.3 重载函数

// sol3 自动处理同名不同参
lua.set_function("print_value",
    sol::overload(
        [](int v)    { std::cout << "int: " << v << std::endl; },
        [](double v) { std::cout << "double: " << v << std::endl; },
        [](const std::string& v) { std::cout << "string: " << v << std::endl; },
        [](sol::table t) { std::cout << "table with " << t.size() << " entries" << std::endl; }
    )
);

3.4 可变参数

lua.set_function("sum_all", [](sol::variadic_args args) {
    double total = 0;
    for (auto arg : args) {
        total += arg.as<double>();
    }
    return total;
});

// Lua: sum_all(1, 2, 3, 4, 5) => 15

四、C++ 类/结构体绑定(最核心场景)

游戏开发中最大的需求就是把 C++ 的类暴露给 Lua。sol3 做到了几乎零样板代码。

4.1 基础类导出

struct Vec3 {
    double x, y, z;
    Vec3() : x(0), y(0), z(0) {}
    Vec3(double x, double y, double z) : x(x), y(y), z(z) {}

    double length() const {
        return std::sqrt(x*x + y*y + z*z);
    }
    Vec3 normalized() const {
        double len = length();
        return Vec3(x/len, y/len, z/len);
    }
    void set(double nx, double ny, double nz) {
        x = nx; y = ny; z = nz;
    }
};

// 导出到 Lua(USERTYPE 宏 + 链式调用,极简)
lua.new_usertype<Vec3>("Vec3",
    // 构造函数
    sol::constructors<Vec3(), Vec3(double, double, double)>(),
    // 成员变量(直接暴露,Lua 可读写)
    "x", &Vec3::x,
    "y", &Vec3::y,
    "z", &Vec3::z,
    // 成员函数
    "length", &Vec3::length,
    "normalized", &Vec3::normalized,
    "set", &Vec3::set
);

Lua 侧使用:

local v1 = Vec3.new(3, 4, 0)
print(v1:length())        --> 5.0

local v2 = Vec3.new(1, 2, 3)
print(v2.x, v2.y, v2.z)   --> 1.0  2.0  3.0

v2:set(10, 20, 30)        -- C++ 成员函数被正确调用
print(v2.x)               --> 10.0

-- 属性读写(因为直接暴露了成员变量)
v2.x = 100
print(v2.x)               --> 100.0

4.2 属性访问器

class Player {
    int hp_;
public:
    Player() : hp_(100) {}
    int get_hp() const { return hp_; }
    void set_hp(int v) { hp_ = std::max(0, std::min(v, max_hp_)); }
    static constexpr int max_hp_ = 9999;
};

// 暴露为属性,Lua 侧可以 player.hp = 500
lua.new_usertype<Player>("Player",
    sol::constructors<Player()>(),
    "hp", sol::property(&Player::get_hp, &Player::set_hp),
    "max_hp", sol::readonly(&Player::max_hp_)  // 只读属性
);
local p = Player.new()
print(p.hp)      --> 100
p.hp = 500
print(p.hp)      --> 500
print(p.max_hp)  --> 9999
-- p.max_hp = 100  -- 报错!只读属性

4.3 继承关系

struct Entity {
    int id;
    std::string name;
    Entity(int id, const std::string& name) : id(id), name(name) {}
    virtual ~Entity() = default;
    virtual std::string get_type() { return "Entity"; }
};

struct Monster : Entity {
    int atk, def;
    Monster(int id, const std::string& name, int atk, int def)
        : Entity(id, name), atk(atk), def(def) {}
    std::string get_type() override { return "Monster"; }
};

// 导出基类
lua.new_usertype<Entity>("Entity",
    sol::constructors<Entity(int, std::string)>(),
    "id", &Entity::id,
    "name", &Entity::name,
    "get_type", &Entity::get_type
);

// 导出派生类(sol::bases 指定基类)
lua.new_usertype<Monster>("Monster",
    sol::constructors<Monster(int, std::string, int, int)>(),
    sol::base_classes, sol::bases<Entity>(),  // 继承声明
    "atk", &Monster::atk,
    "def", &Monster::def,
    "get_type", &Monster::get_type            // 虚函数覆盖
);

Lua 中:

local m = Monster.new(1001, "Goblin", 30, 10)
print(m:get_type())    --> Monster  (多态生效)
print(m.id, m.name)    --> 1001  Goblin  (继承的字段可用)
print(m.atk, m.def)    --> 30  10

4.4 智能指针与生命周期

工业代码几乎必用 shared_ptr,sol3 原生支持:

std::vector<std::shared_ptr<Monster>> monsters;

lua.set_function("spawn_monster",
    [&](int id, const std::string& name, int atk, int def) {
        auto m = std::make_shared<Monster>(id, name, atk, def);
        monsters.push_back(m);
        return m;  // 返回 shared_ptr
    });

lua.set_function("find_monster",
    [&](int id) -> std::shared_ptr<Monster> {
        for (auto& m : monsters)
            if (m->id == id) return m;
        return nullptr;
    });
-- shared_ptr 进入 Lua 后,Lua GC 和 C++ shared_ptr 协同管理
local m1 = spawn_monster(2001, "Dragon", 100, 50)
local m2 = find_monster(2001)    -- m1 和 m2 指向同一个 C++ 对象
print(m1 == m2)                  --> true  (同一指针)

-- m1 和 m2 都被 GC 且 C++ 侧也释放后,Monster 才会真正销毁

4.5 Factory / 对象工厂模式

// 场景:Lua 拿到 Entity 基类指针,需要根据类型调用派生类方法
lua.set_function("get_entity", [&](int idx) -> std::shared_ptr<Entity> {
    if (idx >= 0 && idx < static_cast<int>(monsters.size()))
        return monsters[idx];  // 返回派生类指针
    return nullptr;
});

// 注册 usertype 时需要声明所有可能的派生类
lua.new_usertype<Entity>("Entity",
    // ... 
);
lua.new_usertype<Monster>("Monster",
    sol::base_classes, sol::bases<Entity>(),
    // ...
);

这样 Lua 中拿到的 Entity 就是多态的,可以安全调用 get_type()

五、常用进阶技巧

5.1 把 C++ 容器暴露为可迭代对象

std::vector<Vec3> positions;

lua.set_function("add_position", [&](double x, double y, double z) {
    positions.emplace_back(x, y, z);
});

// 返回整个 vector,Lua 可 range-for
lua.set_function("get_all_positions", [&]() -> std::vector<Vec3>& {
    return positions;
});
add_position(1, 2, 3)
add_position(4, 5, 6)

for i, pos in ipairs(get_all_positions()) do
    print(i, pos.x, pos.y, pos.z)
end
-- 1   1.0  2.0  3.0
-- 2   4.0  5.0  6.0

5.2 枚举导出

enum class DamageType { PHYSICAL, MAGICAL, TRUE_DAMAGE };

lua.new_enum<DamageType>("DamageType", {
    { "PHYSICAL", DamageType::PHYSICAL },
    { "MAGICAL", DamageType::MAGICAL },
    { "TRUE_DAMAGE", DamageType::TRUE_DAMAGE },
});

// Lua: DamageType.MAGICAL

5.3 异常安全与错误处理

// sol3 自动将 C++ 异常转换为 Lua 错误
class DatabaseException : public std::runtime_error {
    using std::runtime_error::runtime_error;
};

lua.set_function("query_db", [](int id) -> sol::table {
    if (id < 0) throw DatabaseException("Invalid ID: " + std::to_string(id));
    sol::table result = lua.create_table();
    result["id"] = id;
    return result;
});

// Lua 侧 pcall 可以捕获 C++ 抛出的异常
local ok, result_or_err = pcall(query_db, -1)
print(result_or_err)  --> "Invalid ID: -1"

5.4 协程支持

// 创建 Lua 协程
sol::thread runner = sol::thread::create(lua);
sol::function script = runner.load("return coroutine.yield(42)");

int result1 = script();           // 42
// 注意:协程在 C++ 侧的完整控制需要配合 sol::coroutine
sol::coroutine co = runner.state().script(R"(
    return coroutine.yield("step1", "step2")
)");
std::string s1, s2;
auto status = co(s1, s2);        // "step1", "step2"

5.5 配置表一次性导出

// 实战:将游戏常量配置用 sol::table 快速暴露
lua["GameConfig"] = lua.create_table_with(
    "MAX_LEVEL", 100,
    "MAX_PARTY_SIZE", 4,
    "TICK_RATE", 30,
    "DEFAULT_HP", 1000,
    "SKILL_SLOTS", 8
);

六、性能注意事项

技巧 说明
缓存 sol::function 不要反复 lua["func_name"],存为成员变量或 static
批量操作减少跨语言调用 一次传递 table 代替多次 set/get
大数据用 lightuserdata 或指针 数值数组用 std::vector 直接映射
关闭安全检查(Release 构建) #define SOL_ALL_SAFETIES_ON 0
sol::table::size() 而不是遍历计数 Lua 5.1 无原生 # 时注意区别
userdata 不要频繁创建销毁 配合对象池使用

七、总结:sol3 在游戏服务器中的典型分层

┌─────────────────────────────────────────────┐
│  Lua 脚本层(逻辑、配表、protobuf、AI BT)  │
├─────────────────────────────────────────────┤
│  sol3 绑定层(Vec3, Entity, Component...)  │
├─────────────────────────────────────────────┤
│  C++ 引擎层(网络、物理、碰撞、navmesh)     │
└─────────────────────────────────────────────┘

关键认知:sol3 的价值不在于"让你少写代码",而在于让你把 Lua ↔ C++ 的边界降到几乎不可见的代价,从而把精力集中在真正的游戏逻辑上。


配置系统

概括

游戏开发中,策划需要频繁调整数值(伤害、血量、掉落率)、字符串(名称、描述)、逻辑开关。将这些从代码中抽离为配置表,是游戏工程化的第一步。

配置表设计原则

1. 配置与逻辑严格分离

-- ❌ 错误:逻辑嵌入配置
config = {
    [1001] = {
        name = "火球术",
        damage = function(caster, target)
            return caster.attack * 1.5 - target.defense * 0.5
        end
    }
}

-- ✅ 正确:配置只描述数据,逻辑由系统执行
config = {
    [1001] = {
        name = "火球术",
        damage_formula = "atk * 1.5 - def * 0.5",
        cooldown = 3,
        mana_cost = 30
    }
}

2. 单一数据源

所有同类型配置放在一起,通过 ID 索引:

-- config/items.lua
return {
    [1001] = { name = "铁剑",   type = "weapon", atk = 15, price = 100 },
    [1002] = { name = "皮甲",   type = "armor",  def = 10, price = 80 },
    [1003] = { name = "生命药水", type = "potion", hp_restore = 50, price = 20 },
}

3. 可读性与可维护性

-- config/constants.lua
local M = {}
M.ITEM_TYPE = { WEAPON = 1, ARMOR = 2, POTION = 3 }
M.EQUIP_SLOT = { WEAPON = 1, HEAD = 2, CHEST = 3 }
return M

配置加载系统

基础加载器

-- config_loader.lua
local ConfigLoader = {}

function ConfigLoader.load(name)
    local ok, config = pcall(require, "config." .. name)
    if not ok then
        error("加载配置失败: " .. name .. "\n" .. config)
    end
    return config
end

-- 缓存已加载配置
local config_cache = {}
function ConfigLoader.load_cached(name)
    if not config_cache[name] then
        config_cache[name] = ConfigLoader.load(name)
    end
    return config_cache[name]
end

return ConfigLoader

从 JSON/CSV 加载

实际项目中,策划用 Excel 编辑,导出为 JSON 或 Lua:

local json = require("json")

function ConfigLoader.load_json(filepath)
    local f = io.open(filepath, "r")
    if not f then
        error("无法打开配置文件: " .. filepath)
    end
    local content = f:read("*a")
    f:close()
    return json.decode(content)
end

配置验证

function ConfigLoader.validate(config, schema)
    for key, rules in pairs(schema) do
        local val = config[key]
        if rules.required and val == nil then
            error("配置缺少必要字段: " .. key)
        end
        if rules.type and type(val) ~= rules.type then
            error(string.format("字段 %s 类型错误: 期望 %s, 实际 %s",
                key, rules.type, type(val)))
        end
        if rules.range then
            local min, max = rules.range[1], rules.range[2]
            if val < min or val > max then
                error(string.format("字段 %s 数值越界: %d 不在 [%d, %d]",
                    key, val, min, max))
            end
        end
    end
end

配置查询优化(索引)

function ConfigLoader.build_index(config_list, field)
    local index = {}
    for id, data in pairs(config_list) do
        local key = data[field]
        if key then
            if not index[key] then index[key] = {} end
            table.insert(index[key], id)
        end
    end
    return index
end

-- 快速查找所有武器
local items = ConfigLoader.load("items")
local by_type = ConfigLoader.build_index(items, "type")
local weapon_ids = by_type["weapon"]

技能系统

技能配置

-- config/skills.lua
return {
    [1001] = {
        id = 1001,
        name = "火球术",
        cast_condition = { mana_cost = 30, min_level = 5 },
        targeting = { type = "enemy_single", range = 800 },
        effects = {
            { type = "damage", formula = "atk * 2.0 + 50", element = "fire" },
            { type = "buff", buff_id = 2001, duration = 3, trigger_chance = 0.3 },
        },
        cooldown = 4.0,
        gcd = 1.5,     -- 公共冷却
        cast_time = 1.0,
    },
    [1002] = {
        id = 1002, name = "治愈术",
        targeting = { type = "ally_single", range = 600 },
        cast_condition = { mana_cost = 25 },
        effects = {
            { type = "heal", formula = "matk * 1.8 + 30" },
            { type = "dispel", debuff_types = {"poison", "bleed"} },
        },
        cooldown = 3.0, cast_time = 1.5,
    },
}

技能执行流程

function SkillSystem.cast_skill(caster, skill_id, target_id)
    local skill_cfg = config[skill_id]
    if not skill_cfg then return false, "技能不存在" end

    -- 1. 冷却检查
    if caster:is_on_cooldown(skill_id) then
        return false, "技能冷却中"
    end

    -- 2. 施法条件
    if not check_condition(caster, skill_cfg.cast_condition) then
        return false, "不满足施法条件"
    end

    -- 3. 目标验证
    local target = resolve_target(caster, skill_cfg.targeting, target_id)
    if not target then return false, "无效目标" end

    -- 4. 消耗资源
    consume(caster, skill_cfg.cast_condition)

    -- 5. 执行效果链
    for _, effect in ipairs(skill_cfg.effects) do
        apply_effect(caster, target, effect)
    end

    -- 6. 冷却
    caster:start_cooldown(skill_id, skill_cfg.cooldown, skill_cfg.gcd)
    return true, "施法成功"
end

效果系统(Effect Dispatch)

local effect_handlers = {}

function register_effect(effect_type, handler)
    effect_handlers[effect_type] = handler
end

function apply_effect(caster, target, effect)
    local handler = effect_handlers[effect.type]
    if not handler then error("未知效果类型: " .. (effect.type or "nil")) end
    return handler(caster, target, effect)
end

-- 伤害效果
register_effect("damage", function(caster, target, effect)
    local dmg = Formula.calc(effect.formula, caster, target)
    local is_crit = math.random() < (caster.crit_rate or 0.05)
    if is_crit then dmg = dmg * (caster.crit_dmg or 1.5) end
    target:take_damage(dmg, effect.element)
    return { type = "damage", value = dmg, crit = is_crit }
end)

-- Buff 效果
register_effect("buff", function(caster, target, effect)
    if math.random() <= (effect.trigger_chance or 1.0) then
        target:add_buff(effect.buff_id, effect.duration or 5, caster)
    end
end)

-- 驱散效果
register_effect("dispel", function(caster, target, effect)
    local removed = target:remove_buffs_by_type(effect.debuff_types)
    return { type = "dispel", count = removed }
end)

伤害公式引擎

local Formula = {}

local variable_providers = {
    atk  = function(e) return e.attack end,
    def  = function(e) return e.defense end,
    matk = function(e) return e.magic_attack end,
    mdef = function(e) return e.magic_defense end,
    level = function(e) return e.level end,
    max_hp = function(e) return e.max_hp end,
}

function Formula.calc(formula_str, caster, target)
    local expr = formula_str
    for var, provider in pairs(variable_providers) do
        expr = expr:gsub("caster%." .. var, tostring(provider(caster)))
        expr = expr:gsub("target%." .. var, tostring(provider(target)))
    end
    local result = load("return " .. expr)()
    return math.max(0, result)
end

return Formula

冷却管理

local CooldownManager = {}

function CooldownManager.new()
    local self = setmetatable({}, { __index = CooldownManager })
    self._cooldowns = {}
    self._global_cd = 0
    return self
end

function CooldownManager:start(skill_id, cd_time, gcd_time)
    local now = os.time()
    self._cooldowns[skill_id] = now + cd_time
    if gcd_time then self._global_cd = now + gcd_time end
end

function CooldownManager:can_use(skill_id)
    local now = os.time()
    if now < self._global_cd then return false, self._global_cd - now end
    local cd_end = self._cooldowns[skill_id]
    if cd_end and now < cd_end then return false, cd_end - now end
    return true, 0
end

function CooldownManager:get_remaining(skill_id)
    local cd_end = self._cooldowns[skill_id]
    return (cd_end and os.time() < cd_end) and (cd_end - os.time()) or 0
end

return CooldownManager

状态机与 AI

有限状态机(FSM)

核心概念

  • 状态(State):实体在某一时刻所处的模式
  • 转换(Transition):从一个状态变到另一个状态的条件
  • 事件(Event):触发转换的外部输入

基础 FSM 实现

-- state_machine.lua
local StateMachine = {}

function StateMachine.new(owner)
    local self = setmetatable({}, { __index = StateMachine })
    self.owner = owner
    self.states = {}
    self.current = nil
    self.previous = nil
    self.global = nil
    return self
end

function StateMachine:register(name, state)
    -- state = { enter, update, exit, on_message }
    self.states[name] = state
end

function StateMachine:change(name, ...)
    local new_state = self.states[name]
    if not new_state then error("状态不存在: " .. name) end
    if self.current and self.current.exit then
        self.current.exit(self.owner)
    end
    self.previous = self.current
    self.current = new_state
    if new_state.enter then new_state.enter(self.owner, ...) end
end

function StateMachine:revert(...)
    if self.previous then
        for name, state in pairs(self.states) do
            if state == self.previous then
                self:change(name, ...); return
            end
        end
    end
end

function StateMachine:update(dt)
    if self.global and self.global.update then
        self.global.update(self.owner, dt)
    end
    if self.current and self.current.update then
        self.current.update(self.owner, dt)
    end
end

function StateMachine:handle_message(msg, ...)
    if self.current and self.current.on_message then
        return self.current.on_message(self.owner, msg, ...)
    end
    return false
end

function StateMachine:is_in(name)
    return self.current == self.states[name]
end

return StateMachine

实战:怪物 AI 状态机

local function create_monster_ai(monster)
    local fsm = StateMachine.new(monster)

    fsm:register("idle", {
        enter = function(self)
            self.idle_timer = 0
        end,
        update = function(self, dt)
            self.idle_timer = self.idle_timer + dt
            local enemy = self:find_nearest_enemy(self.aggro_range)
            if enemy then self.fsm:change("chase", enemy)
            elseif self.idle_timer > self.idle_duration then
                self.fsm:change("patrol")
            end
        end,
    })

    fsm:register("patrol", {
        enter = function(self) self:set_patrol_target() end,
        update = function(self, dt)
            if self:has_reached(self.patrol_target) then
                self.fsm:change("idle")
            end
            local enemy = self:find_nearest_enemy(self.aggro_range)
            if enemy then self.fsm:change("chase", enemy) end
        end,
    })

    fsm:register("chase", {
        enter = function(self, target) self.chase_target = target end,
        update = function(self, dt)
            local target = self.chase_target
            if not target or target:is_dead() then
                self.fsm:change("idle"); return
            end
            local dist = self:distance_to(target)
            if dist <= self.attack_range then
                self.fsm:change("attack", target)
            elseif dist > self.aggro_range * 1.5 then
                self.fsm:change("idle")
            else self:move_toward(target, dt) end
        end,
        exit = function(self) self.chase_target = nil end,
    })

    fsm:register("attack", {
        enter = function(self, target)
            self.attack_target = target; self.attack_timer = 0
        end,
        update = function(self, dt)
            local target = self.attack_target
            if not target or target:is_dead() then
                self.fsm:change("idle"); return
            end
            self.attack_timer = self.attack_timer + dt
            local dist = self:distance_to(target)
            if dist > self.attack_range * 1.2 then
                self.fsm:change("chase", target)
            elseif self.attack_timer >= self.attack_interval then
                self:perform_attack(target)
                self.attack_timer = 0
            end
        end,
        exit = function(self) self.attack_target = nil end,
    })

    fsm:register("dead", {
        enter = function(self)
            self:play_death_animation(); self:drop_loot()
        end,
    })

    fsm:change("idle")
    return fsm
end

全局状态(始终运行)

fsm.global_state = {
    update = function(self, dt)
        if self.hp <= 0 and not fsm:is_in("dead") then
            fsm:change("dead")
        end
    end,
    on_message = function(self, msg, ...)
        if msg == "stunned" then
            fsm:change("stunned"); return true
        end
        return false
    end,
}

行为树(Behavior Tree)简介

当 AI 行为复杂度超出 FSM 舒适区时使用。

核心节点类型

Selector (选择): 依次执行子节点, Success 立即返回, Failure 继续
Sequence (序列): 依次执行子节点, Failure 立即返回, Success 继续  
Condition (条件): 检查条件, 返回 Success/Failure
Action (动作): 执行动作, 返回 Running/Success/Failure

Lua 简易实现

local BT = {}

function BT.sequence(children)
    return function(entity, dt)
        for _, child in ipairs(children) do
            local status = child(entity, dt)
            if status ~= "success" then return status end
        end
        return "success"
    end
end

function BT.selector(children)
    return function(entity, dt)
        for _, child in ipairs(children) do
            local status = child(entity, dt)
            if status == "success" then return "success"
            elseif status == "running" then return "running" end
        end
        return "failure"
    end
end

function BT.condition(check_fn)
    return function(entity, dt)
        return check_fn(entity) and "success" or "failure"
    end
end

function BT.action(action_fn)
    return function(entity, dt)
        return action_fn(entity, dt)
    end
end

return BT

FSM vs 行为树 选型

场景 推荐 理由
简单怪物 AI FSM 状态少,转换清晰
Boss 多阶段 FSM + 分层 阶段切换天然是 FSM
复杂 AI 行为 行为树 模块化,策划可配置
UI 状态管理 FSM 互斥状态,清晰切换
实际项目 混合 FSM 管大状态,行为树管细节

性能优化

1. 局部变量优化(最有效)

-- ❌ 慢:每次全局查找 math.sin / math.pi
for i = 1, 1000000 do
    local val = math.sin(math.pi * i / 100)
end

-- ✅ 快:缓存为局部
local sin = math.sin; local pi = math.pi
for i = 1, 1000000 do
    local val = sin(pi * i / 100)
end

模块顶部缓存模式

local type, pairs, ipairs = type, pairs, ipairs
local t_insert = table.insert
local s_format = string.format

2. Table 优化

避免频繁创建 Table

-- ❌ 每帧创建新表
function update_effects(entity)
    for _, effect in ipairs(entity.effects) do
        local pos = { x = entity.x, y = entity.y }  -- GC 压力!
        render_effect(effect, pos)
    end
end

-- ✅ 复用表
local temp_pos = { x = 0, y = 0 }
function update_effects(entity)
    temp_pos.x = entity.x; temp_pos.y = entity.y
    for _, effect in ipairs(entity.effects) do
        render_effect(effect, temp_pos)
    end
end

预分配大小

-- ✅ Lua 5.4+
local list = table.create(10000, 0)

-- ✅ 通用版本
local list = {}
for i = 1, 10000 do list[i] = 0 end

反向遍历删除

-- ✅ 正确:反向遍历
for i = #t, 1, -1 do
    if should_remove(t[i]) then table.remove(t, i) end
end

3. 字符串优化

-- ❌ 循环拼接:每次创建新字符串,旧字符串变垃圾
local result = ""
for i = 1, 10000 do
    result = result .. i .. ","
end

-- ✅ table.concat 一次性分配
local parts = {}
for i = 1, 10000 do parts[i] = tostring(i) end
local result = table.concat(parts, ",")

4. GC 优化

增量式 GC(Lua 5.2+)

collectgarbage("stop")     -- 暂停 GC
collectgarbage("restart")  -- 重启 GC
collectgarbage("collect")  -- 执行一次完整 GC
collectgarbage("step", n)   -- 执行 n KB 的 GC 步进
print(collectgarbage("count"))  -- 当前内存 (KB)
collectgarbage("setpause", 200) -- GC 触发阈值
collectgarbage("setstepmul", 200) -- GC 步进倍数

加载阶段暂停 GC

collectgarbage("stop")
for i = 1, 10000 do entities[i] = create_entity(i) end
collectgarbage("restart")
collectgarbage("collect")

弱引用表

-- 弱键表:键被回收时条目自动移除
local cache = setmetatable({}, { __mode = "k" })
-- 弱值表
local listeners = setmetatable({}, { __mode = "v" })

-- 游戏典型用法:对象→UI 映射,对象销毁时 UI 自动清理
local entity_panels = setmetatable({}, { __mode = "k" })

5. 对象池(Object Pool)

local ObjectPool = {}

function ObjectPool.new(create_fn, reset_fn, initial_size)
    local self = setmetatable({}, { __index = ObjectPool })
    self._create, self._reset = create_fn, reset_fn
    self._free, self._active = {}, {}
    for _ = 1, (initial_size or 10) do
        table.insert(self._free, create_fn())
    end
    return self
end

function ObjectPool:acquire(...)
    local obj = (#self._free > 0) and table.remove(self._free) or self._create()
    if self._reset then self._reset(obj, ...) end
    self._active[obj] = true
    return obj
end

function ObjectPool:release(obj)
    if self._active[obj] then
        self._active[obj] = nil
        table.insert(self._free, obj)
    end
end

return ObjectPool

6. 全局变量污染检测

-- strict.lua
local mt = {
    __index = function(t, n)
        error("尝试访问未声明的全局变量: " .. n, 2)
    end,
    __newindex = function(t, n, v)
        error("尝试创建未声明的全局变量: " .. n, 2)
    end,
}
setmetatable(_G, mt)

7. Closure 滥用问题

闭包的性能代价:每个闭包 = 函数原型 + upvalue 数组(内存) + GC 对象(GC) + 间接寻址(访问延迟)。

-- ❌ 循环中创建 1000 个独立闭包
for i = 1, 1000 do
    local handler = function() return i * 2 end
    table.insert(handlers, handler)
end

-- ✅ 使用参数代替闭包捕获
local function compute(i) return i * 2 end

热更新与项目整合

核心机制:替换 package.loaded

-- hotfix.lua
function reload_module(modname)
    local old = package.loaded[modname]
    package.loaded[modname] = nil
    local ok, new = pcall(require, modname)
    if not ok then
        package.loaded[modname] = old
        error("热更新失败: " .. modname .. "\n" .. new)
    end
    return old, new
end

状态迁移

-- 模块设计成支持 export/import 状态
-- player.lua
local Player = {}
local state = { online_count = 0, player_list = {} }

function Player.export_state() return state end
function Player.import_state(s) state = s end
return Player

-- 热更新时迁移
function migrate_module(modname)
    local old = package.loaded[modname]
    local saved = (old.export_state and old.export_state()) or nil
    package.loaded[modname] = nil
    local new = require(modname)
    if saved and new.import_state then new.import_state(saved) end
    return old, new
end

分层热更新策略

Level 1 - 纯数据更新:reload 配置模块(最安全)
Level 2 - 纯函数更新:替换无状态工具函数(安全,接口不变)
Level 3 - 有状态模块更新:需状态迁移(复杂)
Level 4 - 架构级更新:模块间依赖变化(需完整重启)

安全热更新

function safe_update(modname)
    local backup = package.loaded[modname]
    local ok, err = pcall(function()
        reload_module(modname)
    end)
    if not ok then
        package.loaded[modname] = backup
        error("热更新失败,已回滚: " .. err)
    end
    -- 自检验证
    local mod = package.loaded[modname]
    if type(mod.self_check) == "function" then
        local ck_ok, ck_err = pcall(mod.self_check)
        if not ck_ok then
            package.loaded[modname] = backup
            error("自检失败,已回滚: " .. ck_err)
        end
    end
    return true
end

Upvalue 更新(进阶)

-- 使用调试库更新闭包捕获的旧函数
function update_upvalue(func, name, new_value)
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then break end
        if n == name then
            debug.setupvalue(func, i, new_value)
            return true
        end
    end
    return false
end

项目整合:main.lua

-- main.lua
package.path = "./src/?.lua;./src/?/init.lua;" .. package.path
package.cpath = "./lib/?.so;" .. package.cpath

require("common.strict")
local EventBus  = require("common.event_bus")
local Config    = require("common.config_loader")
local Scheduler = require("core.scheduler")

-- 加载配置
Config.load("config.items")
Config.load("config.skills")

-- 业务模块
local Players  = require("game.player_manager")
local Skills   = require("game.skill_system")
local Combat   = require("game.combat_system")
local AI       = require("game.ai_manager")

-- 初始化
local function init()
    Scheduler:init()
    Players:init()
    AI:init()
    print("[Server] 初始化完成")
end

-- 主循环
local function main_loop(dt)
    EventBus:process()
    Scheduler:update(dt)
    Players:update(dt)
    AI:update(dt)
    Combat:update(dt)
end

init()
-- 帧循环由引擎或服务器框架提供

Skynet 架构深度解析

云风(风云)设计的轻量级并发框架,国内大量游戏公司在用。理解 Skynet 的底层原理,是你从"会写 Lua"到"能搭服务器"的跨越。

一、设计哲学与核心概念

1.1 一切皆服务(Service)

Skynet 的核心思想:整个服务器由许多**服务(Service)**组成,每个服务是一个独立 Actor。

┌─────────────────────────────────────────────────────┐
│                    Skynet 进程                      │
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐      │
│  │ Login  │ │  Gate  │ │ Agent  │ │ World  │ ...  │
│  │Service │ │Service │ │Service │ │Service │      │
│  │(lua VM)│ │(lua VM)│ │(lua VM)│ │(lua VM)│      │
│  └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘      │
│      └──────────┼──────────┼──────────┘            │
│           ┌─────┴─────┐                             │
│           │  消息队列   │  (二级队列:全局→服务)     │
│           └─────┬─────┘                             │
│           ┌─────┴─────┐                             │
│           │ Worker线程 │                             │
│           └───────────┘                             │
└─────────────────────────────────────────────────────┘

1.2 Actor 模型核心约定

特性 说明
独立 Lua VM 每个 Service 有独立 lua_State,全局变量互不干扰
无共享内存 服务间只能通过消息通信,不存在 data race
串行处理 同一服务的消息串行处理,无需加锁
故障隔离 单个服务崩溃(Lua error)不会影响其他服务
并发模型 多 Worker 线程并发处理不同服务的消息

这个模型就是 Actor 模型(与 Erlang、Akka 同源):

Actor = 状态 + 行为 + 信箱。状态私有,行为通过消息触发,信箱串行化处理。

1.3 地址与名字系统

每个 Service 有两套标识:

-- 数字地址:32 位整数,由 Skynet 分配,全局唯一
local handle = 0x01000001  -- 十六进制,高 8 位 = 节点 ID,低 24 位 = 服务序号

-- 名字别名:通过 .register / .queryservice 使用
skynet.register(".login")            -- 注册名字
local addr = skynet.queryservice(".login")  -- 通过名字查找地址
assert(addr == skynet.self())        -- 自己也可以获取自己的地址

名字服务本质上是维护在 Skynet 内核中的一个 hash 表,.name 映射到 handle。


二、Skynet 启动流程(源码级理解)

Skynet 的入口是 skynet_start.c,启动流程是理解全局的关键。

2.1 启动时序图

skynet_start()
  ├─ skynet_node_init()        // 初始化全局节点状态
  ├─ skynet_mq_init()          // 初始化全局消息队列
  ├─ skynet_module_init()      // 初始化模块加载器(.so 动态库)
  ├─ skynet_timer_init()       // 初始化定时器系统
  ├─ skynet_socket_init()      // 初始化网络层
  ├─ skynet_harbor_init()      // 初始化集群通信
  ├─ bootstrap(ctx, config->bootstrap)  // 启动引导服务(通常是 snlua)
  │     └─ snlua 加载 config 中指定的启动 Lua 脚本
  │           └─ skynet.start(function() ... end)
  └─ skynet_start()            // 启动 Worker 线程
        ├─ thread_timer()      // 定时器线程 (1个)
        ├─ thread_socket()     // 网络线程 (1个)
        └─ thread_worker() × N // 工作线程 (N个,可配置)

2.2 关键源码结构

// skynet-src/skynet_start.c (简化)
void skynet_start(struct skynet_config *config) {
    // 1. 注册信号处理 (SIGHUP 等用于热更新)
    skynet_handle_init(config->harbor);   // 初始化 handle 存储
    // 2. 全局消息队列
    struct message_queue *mq = skynet_mq_create(0);
    skynet_globalmq_push(mq);
    // 3. 启动 logger 服务(C 编写的简单服务)
    struct skynet_context *logctx = skynet_context_new("logger", config->logger);
    // 4. bootstrap 服务:加载 config 中指定的 lua 入口文件
    bootstrap(ctx, config->bootstrap);
    // 5. 启动各线程
    create_thread(&pid[0], thread_timer, m);
    create_thread(&pid[1], thread_socket, m);
    for (int i = 2; i < thread_count; i++)
        create_thread(&pid[i], thread_worker, m);
}

2.3 服务上下文(skynet_context)

每个 Service 的核心 C 结构体:

struct skynet_context {
    void *instance;              // 模块实例 (snlua 时是 lua_State)
    struct skynet_module *mod;   // 模块指针
    uint32_t handle;            // 32 位服务地址
    char result[32];            // 存放 session 响应结果
    struct message_queue *queue; // 私有消息队列
    bool init;                  // 是否已完成初始化
    bool endless;               // 是否无限循环
    int profile;                // 性能分析标记
    int session_id;             // session 生成器
};

一个 Service 对应:

  • 1 个 skynet_context
  • 1 个私有的二级消息队列 message_queue
  • 1 个 lua_State(对于 snlua 类型的服务)

三、消息队列机制(skynet_mq.c)

这是 Skynet 性能的核心,必须深入理解。

3.1 二级消息队列架构

┌──────────────────────────────────────────┐
│            Global MQ (全局队列)           │
│  [Service_1_mq] → [Service_2_mq] → ...   │
│  只要有消息的服务队列就会在此排队         │
│  Worker 线程从这里竞争取出需要处理的队列   │
└────────────┬─────────────────────────────┘
             │ Worker 线程拿到某个 Service 的私有队列
             ▼
┌──────────────────────────────────────────┐
│        Secondary MQ (服务私有队列)        │
│  [msg1] → [msg2] → [msg3] → ...          │
│  该服务所有待处理消息按顺序排列            │
│  Worker 从中逐条取出,交给该服务的 Lua VM  │
└──────────────────────────────────────────┘

关键设计点

  1. 全局队列只存"有消息的 Service 队列指针"(轻量)
  2. Worker 线程首先竞争全局队列拿到某个 Service 的私有队列
  3. 然后独占处理这个 Service 的一条消息(串行保证)
  4. 处理完后如果该 Service 还有消息,重新放入全局队列

3.2 消息结构体

struct skynet_message {
    uint32_t source;    // 发送方地址
    int session;        // 会话 ID(0 = send,非 0 = call)
    void *data;         // 消息数据(已序列化)
    size_t sz;          // 数据大小
};

3.3 send 与 call 的区别(精确语义)

-- skynet.send(addr, "lua", "CMD", args...)
--   → session = 0,不等待响应,非阻塞,发送完立刻返回
--   → 适用场景:通知、广播、不关心结果的操作

-- skynet.call(addr, "lua", "CMD", args...)
--   → session ≠ 0,挂起当前协程,等待目标返回响应包
--   → 适用场景:查询、需要确认的操作
--   → 内部有超时机制,超时后自动唤醒协程并报错

-- 接收方统一通过 skynet.dispatch 处理
-- 在 dispatch 回调中:
--   session == 0 → 不需要回复 (send 来的)
--   session ≠ 0 → 需要调用 skynet.ret(pack(...)) 回复 (call 来的)

3.4 消息处理流程(全路径)

发送方: skynet.call(target, "lua", "HELLO")
  ├─ 生成 session_id
  ├─ 将消息 push 到 target 的私有队列
  ├─ 将 target 的队列 push 到全局队列(如果不在里面)
  ├─ 挂起当前协程 (coroutine.yield)
  └─ 在 session_map 中记录 session_id → coroutine

Worker 线程:
  ├─ 从全局队列 pop 一个 Service 的私有队列
  ├─ 从私有队列 pop 一条消息
  ├─ 调用 skynet_context_push → 让该 Service 的 Lua VM 处理
  ├─ Lua VM 执行 dispatch 回调
  │     ├─ 匹配 cmd: "HELLO"
  │     └─ 调用 skynet.ret(skynet.pack("World"))
  └─ 如果 session ≠ 0, 生成响应消息发送回原服务

接收响应:
  ├─ 原服务收到响应消息
  ├─ 根据 session_id 找到挂起的协程
  ├─ 将响应数据 push 到协程参数
  └─ coroutine.resume(co, data...)

四、协程调度核心(skynet.lua 深度剖析)

4.1 skynet.lua 的协程池设计

Skynet 不每次都创建新协程,而是维护一个协程池

-- skynet.lua 核心逻辑(精简版)
local coroutine_pool = {}  -- 空闲协程池

local function co_create(f)
    local co = table.remove(coroutine_pool)
    if co == nil then
        co = coroutine.create(function(...)
            f(...)
            -- 执行结束后将自己放回池中
            while true do
                local func = coroutine.yield("EXIT")
                func(coroutine.yield())
            end
        end)
    else
        -- 复用协程:修改协程内函数为新的 f
        coroutine.resume(co, f)
    end
    return co
end

协程复用的好处

  • 避免频繁创建/销毁协程的 GC 开销
  • 避免 lua_State 多次分配
  • 在高频消息场景下性能提升显著

4.2 raw_dispatch_message 完整流程

-- skynet.lua 中的消息分发
local function raw_dispatch_message(prototype, msg, sz, session, source)
    if prototype == "lua" then
        local co = co_create(dispatch_func)    -- 从池中取协程
        local p = proto[prototype]
        local response = p.dispatch(co, session, source, p.unpack(msg, sz))
        -- 需要回复则发送响应
        if session ~= 0 and response ~= nil then
            ...
        end
    end
end

4.3 为什么 skynet.call 看起来像同步但其实是异步

-- 本质是 "协程挂起 + 消息回调恢复"
function skynet.call(addr, typename, ...)
    local session = c.intcommand("SESSION")   -- C 层生成唯一 session
    local p = proto[typename]
    -- 发送消息到目标服务
    c.send(addr, p.id, session, p.pack(...))
    
    -- 注册等待:当前协程挂起,等待响应
    local co = coroutine.running()
    sending_requests[session] = co
    c.intcommand("SUSPEND", co, coroutine.resume, coroutine.status)
    
    -- 挂起!!等待被唤醒
    local ok, msg, sz = coroutine.yield()
    
    sending_requests[session] = nil
    -- 被唤醒后拿到响应数据
    return p.unpack(msg, sz)
end

-- 收到响应时
local function wakeup(co, ok, msg, sz)
    -- 通过 c.intcommand 机制
    -- Worker 线程收到响应消息后,解包 session
    -- 找到对应协程并 resume
    coroutine.resume(co, true, msg, sz)
end

一句话总结skynet.call = 写入消息 + 挂起协程 + 等待对方写回响应 + 被唤醒。看起来是同步函数调用,实际上是协程级别的异步


五、网络层:Gate + Watchdog 模式

这是 Skynet 最经典的网络模式,理解它等于理解了一半 Skynet。

5.1 架构图

                        Internet
                           │
                    ┌──────▼──────┐
                    │   TCP 连接   │
                    └──────┬──────┘
                           │ fd
                    ┌──────▼──────┐
                    │  Gate 服务   │  (C 编写的网络收发)
                    │ socket.listen│  ← 负责 accept / recv / send
                    │ socket.start │  ← 不处理任何业务逻辑
                    └──────┬──────┘
                           │ fd 通知 (socket_data 消息)
                    ┌──────▼──────┐
                    │  Watchdog   │  (Lua 服务,连接管理)
                    │  服务        │  ← 断线重连 / 心跳检测
                    └──────┬──────┘
                           │ 转发消息
                    ┌──────▼──────┐
                    │  Agent 服务  │  (Lua 服务,业务逻辑)
                    │ (每个连接1个)│  ← 登录/消息处理/逻辑
                    └─────────────┘

5.2 Gate 服务详细代码

-- gate.lua
local skynet = require "skynet"
local socket = require "skynet.socket"

skynet.start(function()
    -- 1. 监听端口
    local listen_fd = socket.listen("0.0.0.0", 8888)
    socket.start(listen_fd, function(fd, addr)
        -- 2. 有新连接
        print("新连接来自: " .. addr)
        socket.start(fd)  -- 开始接收该 fd 的数据
        
        -- 3. 创建 Agent 处理该连接
        local agent = skynet.newservice("agent")
        
        -- 4. 建立 fd ↔ agent 映射
        --    Gate 负责接收数据并转发给 Agent
        --    Agent 通过 Gate 发送数据
    end)
    
    -- 5. 注册协议处理器,处理 socket 消息
    skynet.dispatch("lua", function(session, source, cmd, ...)
        -- Gate 收到来自 Agent 的消息,转发到对应的 fd
    end)
end)

5.3 Watchdog 服务

-- watchdog.lua
local skynet = require "skynet"
local socket = require "skynet.socket"

local agent_pool = {}

skynet.register(".watchdog")

skynet.start(function()
    local listen_fd = socket.listen("0.0.0.0", 8888)
    socket.start(listen_fd, function(fd, addr)
        -- 创建 Agent 处理该客户端
        local agent = skynet.newservice("agent")
        skynet.send(agent, "lua", "start", fd, addr)
        agent_pool[fd] = agent
    end)
    
    -- 断线处理
    skynet.dispatch("lua", function(_, _, cmd, fd)
        if cmd == "DISCONNECT" then
            local agent = agent_pool[fd]
            if agent then
                skynet.send(agent, "lua", "disconnect")
                agent_pool[fd] = nil
            end
        end
    end)
end)

5.4 Agent 服务(玩家业务逻辑)

-- agent.lua
local skynet = require "skynet"
local socket = require "skynet.socket"
local netpack = require "skynet.netpack"

local CMD = {}
local fd
local player = { name = "", level = 1, hp = 100 }

function CMD.start(_fd, addr)
    fd = _fd
    print("玩家连接: " .. addr)
    
    -- 向客户端发送欢迎消息
    local pack = netpack.pack("欢迎来到游戏服务器!")
    socket.write(fd, pack)
    
    -- 注册 socket 消息处理
    socket.start(fd)
end

function CMD.disconnect()
    print("玩家断开: " .. player.name)
    skynet.exit()  -- Agent 服务退出
end

-- 接收并处理客户端数据
skynet.dispatch("socket", function(_, _, _fd)
    -- 粘包处理
    local msg = netpack.read(_fd)
    if msg then
        local cmd, args = netpack.unpack(msg)
        -- 根据 cmd 处理不同协议
        if cmd == "LOGIN" then
            player.name = args.name
            socket.write(fd, netpack.pack("LOGIN_OK"))
        elseif cmd == "CHAT" then
            -- 转发到聊天服务
            skynet.send(".chat", "lua", "say", skynet.self(), player.name, args.msg)
        end
    end
end)

skynet.dispatch("lua", function(_, _, cmd, ...)
    CMD[cmd](...)
end)

5.5 粘包拆包(netpack 原理)

TCP 是流式协议,数据会粘在一起。Skynet 的 netpack 使用长度前缀

客户端发送: [2字节长度] + [数据体]
        例: \x00\x05HELLO  →  长度5,内容"HELLO"

服务端:
  socket.read(fd)  →  读取并缓冲
  netpack.read(fd) →  解析长度前缀,提取完整包
// skynet-src/skynet_netpack.c 核心逻辑
// 每个 fd 有一个缓冲队列 queue
// 收到数据 → 追加到 queue
// netpack_read → 读前 2 字节得长度 → 读长度指定的数据 → 返回完整包

六、Harbor 与集群通信

多台物理机上的 Skynet 进程通过 Harbor 通信。

6.1 架构

┌──────────────────┐         ┌──────────────────┐
│   节点 A (harbor=1)│ ←TCP→ │   节点 B (harbor=2)│
│                  │         │                  │
│  Service_01000001│ ────→  │  Service_02000001│
│  Service_01000002│ ←────  │  Service_02000002│
└──────────────────┘         └──────────────────┘

6.2 Handle 编码规则

32 位 Handle = [8位 harbor_id] [24位 service_index]

0x01000001 = harbor=1, index=1
0x01000002 = harbor=1, index=2
0x02000001 = harbor=2, index=1

Harbor 通过 handle >> 24 判断目标是否在本机:

  • 本机 → 直接投递消息到本地队列
  • 远程 → 序列化消息,通过 TCP 发送到目标节点的 harbor 服务

6.3 集群用法

-- config 中配置 harbor
-- harbor = 1  表示本节点 ID 为 1

-- 启动 harbor 服务监听其他节点连接
skynet.uniqueservice("harbor")

-- 远程调用:地址使用完整 handle
local remote_addr = 0x02000001  -- 节点2的服务
skynet.call(remote_addr, "lua", "CMD", args)

七、DataCenter 与 Sharedata

7.1 DataCenter(跨服务小数据共享)

DataCenter 本身就是一个 Service,其他服务通过消息读写:

local datacenter = skynet.uniqueservice("datacenterd")

-- 写入
skynet.call(datacenter, "lua", "SET", "online_count", 1234)

-- 读取
local count = skynet.call(datacenter, "lua", "GET", "online_count")

-- 适用场景:全局状态(在线人数、服务器开关状态)

注意:每次读写都是一次 skynet.call,不适合高频访问。

7.2 Sharedata(只读共享内存)

对于大量只读数据(如配置表),DataCenter 的消息开销太大。Sharedata 通过内存映射实现多服务共享:

-- 主服务加载配置并写入 sharedata
local sharedata = require "skynet.sharedata"
sharedata.new("skill_config", skills)     -- 写入

-- 其他服务只读访问
local cfg = sharedata.query("skill_config")
local fireball = cfg[1001]
-- cfg 是只读的,多服务共享同一份 C 内存,不需要消息通信

原理:数据以 C 结构体形式存储在共享内存中,通过 COW (Copy-on-Write) 实现安全更新。读操作零开销,更新时创建新版本后原子替换指针。


八、定时器系统(skynet_timer.c)

8.1 实现原理

Skynet 使用时间轮算法,单独一个线程运行:

// skynet_src/skynet_timer.c
// 定时器线程: 每 2.5ms (可配置) 唤醒一次
// 时间轮有 TIME_NEAR (256) + TIME_LEVEL × 4 (64×4) 共 5 级

struct timer_event {
    uint32_t handle;     // 目标服务
    int session;         // session ID
    uint32_t time;       // 到期时间 (tick)
};

// 添加定时器
int skynet_timeout(uint32_t handle, int time, int session) {
    // 将 timer_event 插入时间轮,O(1)
}

// 到期后:时间轮线程发送 TIMEOUT 消息到目标服务

8.2 Lua 层使用

-- 一次性定时器
skynet.timeout(100, function()
    print("100 厘秒后执行")    -- 1 tick = 10ms
end)

-- 周期性定时器(需要递归注册)
local function tick()
    print("每秒执行一次")
    skynet.timeout(100, tick)  -- 100 * 10ms = 1s
end
skynet.timeout(100, tick)

8.3 定时器精度

精度级别 tick 值 用途
高精度 1 (10ms) 帧同步、技能释放
中精度 10 (100ms) Buff 计时、状态检测
低精度 100 (1s) 离线检测、排行榜刷新

九、服务生命周期管理

9.1 创建与销毁

-- 创建新服务(模板名 + 参数)
local agent = skynet.newservice("agent")

-- 唯一服务(同一类型的全局单例)
local db = skynet.uniqueservice("db")    -- 全局只此一个

-- 查询已有服务
local db_addr = skynet.queryservice("db")  -- 或 skynet.queryservice(true)

-- 退出
skynet.exit()  -- 当前服务退出

-- 强制杀死其他服务
skynet.kill(addr)

9.2 服务状态监控

-- 监控其他服务
skynet.monitor("agent", true)  -- 监控 .agent 名字的服务

-- 注册退出回调
skynet.register_protocol {
    name = "system",
    id = skynet.PTYPE_SYSTEM,
    unpack = skynet.tostring,
    dispatch = function(_, _, msg)
        -- msg 可能为 "EXIT" 等系统消息
    end
}

9.3 优雅退出流程

正常退出:
  Service.exit() → 清理资源 → 发送 EXIT 给 monitor → 释放 Lua VM

异常退出 (Lua error):
  skynet_pcall 捕获 → 日志记录 → 同正常退出流程

被 kill:
  收到 SIGKILL 消息 → 清理 → 退出

十、Debug Console(调试控制台)

Skynet 内置 telnet 调试接口,端口在 config 中配置。

telnet 127.0.0.1 8000

10.1 常用命令

-- 列出所有服务
list          -- 显示: :handle type  (名字)
              -- 例:  :01000001 snlua logger
              --      :01000002 snlua bootstrap

-- 查看服务内存
mem           -- 每个服务的 Lua VM 内存占用
mem 0100000a  -- 特定服务的内存详情

-- 查看服务状态
stat          -- 消息数量、CPU 占用等

-- 注入 Lua 代码到特定服务
inject 0100000a print("debug info:", player.name)  -- 小心使用

-- GC 状态
gc            -- 全局 GC 状态

-- 查看任务队列
task          -- 待处理消息队列状态

-- 退出 Skynet
exit          -- 优雅退出

10.2 性能分析 (Profile)

-- 在 skynet 配置中开启 profile
-- profile = true

-- console 中查看
-- 会显示每个服务的 CPU 耗时百分比
-- 可以看到哪个服务是热点

十一、完整游戏服务器架构实战

11.1 典型服务拆分

┌────────────────────────────────────────────────────────────┐
│                     Skynet 集群                             │
│                                                            │
│  ┌──────────┐                                              │
│  │  Login   │  登录验证 (HTTP/gRPC 对接账号系统)            │
│  └────┬─────┘                                              │
│       │ 验证通过,返回 token                               │
│  ┌────┴──────────────────────────────────────┐            │
│  │              Gate 集群 (1~N)              │            │
│  │  Gate1 (port 8888)  Gate2 (port 8889)...  │            │
│  └────┬──────────────────────────────────────┘            │
│       │                                                    │
│  ┌────┴─────────┐                                         │
│  │  Watchdog    │  连接管理 (心跳、断线重连)                │
│  └────┬─────────┘                                         │
│       │                                                    │
│  ┌────┴──────────────────────────────┐                    │
│  │         Agent 集群 (1 per player)  │                    │
│  │  Agent1  Agent2  Agent3  ...      │                    │
│  └────┬──────────────────────────────┘                    │
│       │                                                    │
│  ┌────┼────────┬──────────┬─────────┐                     │
│  │    │        │          │         │                     │
│  ▼    ▼        ▼          ▼         ▼                     │
│ World Chat   Scene      Match      Mail                   │
│ (世界服务) (场景服务)  (匹配服务) (邮件服务)                │
│  │    │        │          │         │                     │
│  └────┼────────┴──────────┴─────────┘                     │
│       │                                                    │
│  ┌────┴─────────┬──────────────┐                          │
│  │   DB Proxy   │   DB Proxy   │                          │
│  │   (Redis)    │   (MySQL)    │                          │
│  └──────────────┴──────────────┘                          │
└────────────────────────────────────────────────────────────┘

11.2 服务消息流向

「客户端→服务器」
  客户端 TCP → Gate(接收fd数据) → netpack解包 → Agent(业务处理)

「服务器→客户端」
  Agent 调用 socket.write → Gate → TCP → 客户端

「服务间通信」
  Agent.skynet.call(World, "lua", "ENTER_SCENE", player_data)
  → World 收到后 → 处理 → skynet.ret 返回
  → Agent 的协程被恢复 → 拿到返回数据 → socket.write 回客户端

11.3 为什么 Agent 每个玩家一个

优势 说明
故障隔离 单玩家逻辑崩溃不影响其他玩家
资源隔离 玩家 Lua VM 独立,数据不交叉污染
无需锁 同一玩家的消息串行,天然线程安全
按需调度 活跃玩家 Agent 常驻内存,不活跃可挂起或踢出
热更新 可以按玩家粒度灰度更新

十二、常见陷阱与反模式

12.1 不要在 dispatch 中执行耗时操作

-- ❌ 错误:阻塞整个服务的消息处理
skynet.dispatch("lua", function(_, _, cmd)
    if cmd == "HEAVY" then
        -- 大量计算阻塞了后续消息
        for i = 1, 10000000 do
            heavy_calc(i)
        end
    end
end)

-- ✅ 正确:耗时任务用 skynet.fork 异步处理
skynet.dispatch("lua", function(_, _, cmd)
    if cmd == "HEAVY" then
        skynet.fork(function()
            for i = 1, 10000000 do
                heavy_calc(i)
                if i % 1000 == 0 then
                    skynet.yield()  -- 主动让出
                end
            end
        end)
    end
end)

12.2 不要依赖服务启动顺序

-- ❌ 错误:假设对方已存在
local db = skynet.queryservice("db")
skynet.call(db, "lua", "QUERY")  -- 可能对方还没启动!

-- ✅ 正确:等待对方启动或使用超时
local function wait_for_service(name, timeout)
    local start = skynet.now()
    while skynet.now() - start < timeout do
        local addr = skynet.queryservice(name)
        if addr then return addr end
        skynet.sleep(10)
    end
    error("Service " .. name .. " not ready")
end

12.3 避免消息死锁

Service A: skynet.call(B, "GET")  ← 等待 B 返回
Service B: skynet.call(A, "GET")  ← 等待 A 返回
→ 两个服务的协程同时挂起,互相等待,永远无法恢复

解决方案

  • 设计单向依赖,避免环形调用
  • skynet.send 代替 skynet.call 打破闭环
  • 设置 skynet.call 的超时

12.4 Sharedata 更新策略

-- 不要在运行时高频率更新 sharedata(COW 会有短暂的内存翻倍)
-- 正确做法:配置更新低频进行(如策划发布配置)

-- 如果需要频繁更新共享状态,使用 DataCenter 或自定义服务

十三、Skynet 源码阅读路线(由浅入深)

阶段 文件 需要理解的内容
Level 1 service/gate.lua
service/watchdog.lua
service/agent.lua
理解 Gate/Watchdog/Agent 模式,netpack 使用
Level 2 lualib/skynet.lua 协程池、session 管理、dispatch 流程、fork/timeout 实现
Level 3 skynet-src/skynet_start.c
skynet-src/skynet_mq.c
启动流程、消息队列二级调度
Level 4 skynet-src/skynet_server.c 服务上下文创建、消息分发到 Lua VM
Level 5 skynet-src/skynet_timer.c 定时器时间轮实现
Level 6 skynet-src/skynet_socket.c
skynet-src/skynet_harbor.c
网络层 epoll/多路复用、跨节点 harbor
Level 7 service-src/service_snlua.c Lua 服务的 C 层实现,理解 lua_State 如何与框架桥接

推荐阅读顺序:先 Lua 层再 C 层,先业务再框架,循序渐进。


十四、进阶:从 Skynet 学到的架构思想

14.1 Actor 模型在游戏服务器中为什么好

传统多线程 Actor 模型
共享内存 + 锁 消息传递,无锁
容易死锁/竞争 天然避免死锁(串行处理)
调试困难 单线程执行逻辑,可重现
扩展靠加锁粒度 扩展靠加 Actor 实例

14.2 协程 vs 回调

-- 回调风格 (Node.js 式)
do_query(db_addr, "GET_USER", uid, function(result)
    do_query(db_addr, "GET_ITEMS", uid, function(items)
        process(result, items)
    end)
end)

-- Skynet 协程风格 (同步写法)
local user = skynet.call(db_addr, "lua", "GET_USER", uid)
local items = skynet.call(db_addr, "lua", "GET_ITEMS", uid)
process(user, items)

协程写法的优势:代码可读性接近同步,错误处理用 pcall,调试栈完整。

14.3 设计可复用的服务

-- 服务接口规范示例
-- db.lua: 数据库代理服务
-- 接口:
--   SET(key, value)     → ok/error
--   GET(key)            → value/nil
--   DEL(key)            → ok/error
--   HGET(hash, field)   → value/nil
--   HSET(hash, field, v)→ ok/error

-- 定义清晰的接口协议 → 可独立开发、测试、热更新

Skynet 学习路线

  1. Day 1-2:搭建 Skynet 环境,跑通 example,理解 config 配置
  2. Day 3-4:深入 Gate/Watchdog/Agent 示例,理解网络层模型
  3. Day 5:手写 skynet.call / skynet.send / skynet.fork 的简化实现
  4. Day 6-7:阅读 skynet.lua 源码(~1500 行),画出协程调度流程图
  5. Day 8-9:阅读 skynet_mq.c + skynet_server.c,理解消息队列机制
  6. Day 10-11:实现一个最小完整项目:Login + Gate + Agent + Chat
  7. Day 12:加入 World(场景) + DB Proxy + Cluster 配置
  8. Day 13:压测(用 skynet 自带的 cluster agent 做压力测试),调优
  9. Day 14:阅读 skynet_timer.cskynet_socket.cskynet_harbor.c

附录:学习资源与进阶

必读资源

资源 说明
《Programming in Lua》(4th Ed.) Lua 圣经,建议通读
Lua 5.4 源码 (~2万行) 短小精悍,推荐精读 lapi.c、lvm.c、lgc.c
LuaJIT 官方文档 JIT 编译原理、FFI 使用
Skynet 源码 + Wiki 工业级游戏服务器框架,必读 skynet.lua 和 skynet_mq.c
sol3 官方文档 C++ ↔ Lua 绑定库圣经
Lua Users Wiki 各种模式和实践

进阶方向

  1. LuaJIT FFI:直接调用 C 函数,省去 C API boilerplate
  2. sol3 实战:大型项目的 C++ ↔ Lua 分层架构,智能指针生命周期管理
  3. Lua 5.4 新特性const 变量、<close> 闭包(to-be-closed)、分代 GC
  4. 序列化与协议:Protobuf + Lua,自定义二进制协议,netpack 深入
  5. Skynet 集群:多节点 Harbor 通信,数据一致性方案
  6. OpenResty / Nginx + Lua:Web 服务器端 Lua
  7. ECS 深入:数据导向架构,配合 Lua table 操作优势

C++ / sol3 专项资源

资源 说明
sol3 GitHub 官方仓库,examples 目录即是教程
sol3 Tutorial 官方快速入门教程(1 小时可读完)
sol3 headers (include/sol/) 源码即文档,推荐读 usertype.hppfunction.hpp
《C++ Templates: The Complete Guide》 理解 sol3 的模板魔法必备基础

Skynet 专项资源

资源 说明
Skynet GitHub 云风官方仓库
Skynet Wiki 官方文档,虽然简略但全是干货
云风博客 有很多 Skynet 设计理念的文章
skynet/examples/ 官方示例,跑通 simplemonitorsimpledb
skynet/lualib/skynet.lua 必读,~1500 行,是理解 Skynet 的关键
skynet-src/skynet_mq.c 消息队列实现,~400 行,极其精巧
skynet-src/skynet_start.c 启动流程,理解线程模型
skynet-src/skynet_timer.c 定时器时间轮,经典数据结构的工程应用

自检清单(完成本章后应能做到)

  1. 徒手写出 Lua table 的 OOP 实现(__index + self
  2. 徒手画出 coroutine.yield / coroutine.resume 之间的数据流
  3. 用 C API 注册一个 full userdata 类型并导出给 Lua 调用
  4. 用 sol3 将一个 C++ class Player(含继承)完整暴露给 Lua
  5. 徒手画出 Skynet 的 Gate → Watchdog → Agent 消息流向图
  6. 解释 skynet.call 为什么是协程级别的异步(而非线程/回调)
  7. 解释 Skynet 全局消息队列 + 服务私有队列的二级调度原理
  8. 用 Skynet 搭建 Login + Gate + Agent + 聊天 的完整 Demo