4、Lua脚本入门:Lua语法基础、变量与表、函数定义、协程基础
好,咱们进入第四章节。前面聊了事件轮换的触发逻辑,现在得动手写脚本了。我选 Lua 作为教学语言,原因很简单——它在游戏开发里太常见了。Unity 的 xLua、ToLua,还有 Cocos2d-x,底层基本都是它。我自己最早接触 Lua 是在做一款 MMORPG 的 UI 系统时,那时候每天跟 .lua 文件打交道,说实话,一开始挺别扭的,但用顺手了就觉得真香。
4.1 Lua 语法基础:极简但够用
Lua 的设计哲学就是「小而美」。它的语法结构非常精简,你学完今天这一节,基本就能看懂大部分游戏脚本了。
先看一个完整的例子:
-- 这是单行注释
--[[
这是多行注释
可以写好几行
]]
-- 打印输出
print("Hello, Event System!")
-- 变量声明(默认全局)
health = 100
name = "Player"
-- 局部变量用 local
local damage = 50
local isAlive = true
嗯,这里有个坑我得提一下。Lua 的变量默认是全局的,哪怕你只是在函数里写了个 count = 1,它也会污染全局空间。我曾经在项目里遇到过因为忘记加 local 导致两个模块互相覆盖变量的 bug,查了整整一个下午。所以我的习惯是:所有变量一律加 local,除非你明确需要全局共享。
数据类型就八种:
| 类型 | 示例 | 说明 |
|---|---|---|
| nil | nil |
空值,未赋值的变量默认是 nil |
| boolean | true / false |
布尔值,注意只有 false 和 nil 为假 |
| number | 3.14 / 100 |
双精度浮点数,整数也是它 |
| string | "hello" / 'world' |
字符串,单引号双引号都行 |
| table | {} |
表,Lua 唯一的数据结构 |
| function | function() end |
函数,一等公民 |
| userdata | C 对象 | 宿主语言(如 C#)传过来的数据 |
| thread | 协程 | 独立执行流 |
控制流也很直白:
-- if 判断
if health <= 0 then
print("角色死亡")
elseif health < 30 then
print("血量危险")
else
print("状态安全")
end
-- for 循环(数字型)
for i = 1, 5 do
print("第 " .. i .. " 次轮询")
end
-- for 循环(泛型)
local items = {"剑", "盾", "药水"}
for index, value in ipairs(items) do
print(index, value)
end
-- while 循环
local count = 0
while count < 3 do
print("重试次数: " .. count)
count = count + 1
end
.. 是字符串拼接操作符。如果你在游戏里看到 "玩家" .. name .. " 造成了 " .. damage .. " 点伤害",那就是在拼字符串。我个人更推荐用 string.format(),可读性更好。
4.2 变量与表:Lua 的灵魂
表(table)是 Lua 里唯一的数据结构。数组、字典、对象、模块……全是它。说白了,Lua 就是用一张表打天下。
表的两种用法:
-- 1. 当作数组(索引从 1 开始!)
local skills = {"火球术", "冰冻", "闪电链"}
print(skills[1]) -- 输出 "火球术",不是 0
-- 2. 当作字典
local player = {
name = "勇者",
level = 10,
hp = 100,
mp = 50
}
print(player.name) -- 输出 "勇者"
print(player["hp"]) -- 也可以这样访问
这里有个新手必踩的坑:Lua 的数组索引从 1 开始。我记得第一次用 Lua 写循环时,习惯性地从 0 开始,结果第一个元素死活取不到。你想想看,如果你在事件系统里用 events[0] 去拿第一个事件,拿到的永远是 nil,然后整个逻辑就崩了。
表的增删改查:
local eventQueue = {}
-- 添加元素
eventQueue[1] = "登录事件"
eventQueue[2] = "点击事件"
table.insert(eventQueue, "移动事件") -- 自动追加到末尾
-- 修改
eventQueue[1] = "登出事件"
-- 删除
eventQueue[2] = nil -- 直接赋 nil 就是删除
table.remove(eventQueue, 1) -- 删除并自动重排
-- 遍历
for k, v in pairs(eventQueue) do
print(k, v)
end
pairs 遍历时,顺序是不保证的。如果你需要按插入顺序遍历,用 ipairs 只遍历连续整数键,或者自己维护一个索引列表。我在做事件轮询系统时,就因为这个顺序问题导致事件触发顺序错乱,后来改用数组 + 独立索引才解决。
4.3 函数定义:灵活到有点任性
Lua 的函数非常灵活。它支持多返回值、可变参数,还能作为变量传递。说白了,函数在 Lua 里就是「可以执行的变量」。
基本定义方式:
-- 标准定义
local function onDamage(target, damage)
print(target .. " 受到了 " .. damage .. " 点伤害")
end
-- 匿名函数(赋值给变量)
local onHeal = function(target, amount)
print(target .. " 恢复了 " .. amount .. " 点生命")
end
-- 多返回值
local function getPlayerInfo()
return "勇者", 100, 50 -- 返回名字、血量、蓝量
end
local name, hp, mp = getPlayerInfo()
可变参数:
local function logEvent(...)
local args = {...} -- 把可变参数收集到表里
for i, v in ipairs(args) do
print("参数" .. i .. ": " .. tostring(v))
end
end
logEvent("点击", "坐标(10,20)", "左键")
我在项目中经常用多返回来传递事件数据。比如一个事件处理函数返回 isHandled, resultData,调用方根据第一个布尔值决定是否继续传递。这种模式在事件轮换系统里特别实用。
4.4 协程基础:轻量级并发
协程(coroutine)是 Lua 最强大的特性之一。它不是真正的多线程,而是一种「可暂停/可恢复」的函数。你想想看,在游戏里,一个动画播放到一半要等用户输入,或者一个 AI 行为要分几步执行——用协程就特别自然。
协程的三种状态:
| 状态 | 说明 |
|---|---|
| suspended | 挂起(刚创建或刚 yield 回来) |
| running | 运行中 |
| dead | 结束(函数执行完毕或出错) |
基本用法:
-- 创建一个协程函数
local function enemyAI()
print("AI: 开始巡逻")
coroutine.yield() -- 暂停,让出控制权
print("AI: 发现玩家,进入战斗")
coroutine.yield()
print("AI: 战斗结束,返回巡逻")
end
-- 创建协程
local co = coroutine.create(enemyAI)
-- 启动/恢复协程
coroutine.resume(co) -- 输出 "AI: 开始巡逻"
coroutine.resume(co) -- 输出 "AI: 发现玩家,进入战斗"
coroutine.resume(co) -- 输出 "AI: 战斗结束,返回巡逻"
coroutine.resume(co) -- 什么也不输出,协程已 dead
传递数据:
local function eventProcessor()
while true do
local eventType, eventData = coroutine.yield()
print("处理事件: " .. eventType .. " 数据: " .. eventData)
end
end
local co = coroutine.create(eventProcessor)
-- 第一次 resume 启动协程,执行到 yield 暂停
coroutine.resume(co)
-- 后续 resume 传入数据
coroutine.resume(co, "点击", "按钮A")
coroutine.resume(co, "移动", "坐标(5,10)")
pcall 包裹可能出错的代码。否则你的事件处理会悄无声息地「消失」。
协程 vs 普通函数:
| 特性 | 普通函数 | 协程 |
|---|---|---|
| 执行方式 | 一次性执行到底 | 可暂停、可恢复 |
| 状态保持 | 不保持(局部变量销毁) | 保持所有局部变量 |
| 适用场景 | 简单计算、一次性处理 | 分步 AI、动画序列、事件队列 |
| 性能开销 | 低 | 略高(需要维护协程状态) |
下面这张图展示了协程在事件轮换系统中的典型工作流程:
嗯,协程这块内容不少,但核心就一句话:协程让你能用同步的方式写异步逻辑。在事件轮换系统里,你可以把每个事件的处理流程写成一个协程,需要等待时就 yield,主循环继续处理其他事件。等条件满足了再 resume 回来继续执行。这样代码可读性比回调嵌套好太多了。