高级特性
这部分内容主要面向高级开发者,如果您需要进一步深挖地图的表现力,或者提升游戏运行性能,可以参考本章节。
并行计算
辅助计算线程
当您需要在游戏中编写复杂计算时,可以考虑使用并行计算来加速代码。例如,您可以把复杂的纯数学计算逻辑(如自定义AI、战场模拟、寻路等)安排在独立的线程,避免其占用宝贵的游戏逻辑帧时间。并行计算是一个难度较高的技术,您需要对操作系统和Lua虚拟机有一定的了解。
在蛋仔中,您可以设置1~3个辅助计算线程:
LuaAPI.dispatch_init(2) -- 设置并行计算辅助线程数为2dispatch_unit函数只能调用一次,请在游戏开始时就调用它。通常情况下,您可以将上述代码放置于main.lua的开始处。
每个辅助线程上都拥有一个Lua虚拟机(以下简称 从虚拟机 ),默认情况下它们与游戏中默认的Lua虚拟机(以下简称 主虚拟机 )是独立的,不共享任何数据和状态。
从虚拟机限制
从虚拟机在功能上有一些限制,您需要注意以下几点:
| 操作 | 主虚拟机 | 从虚拟机 |
|---|---|---|
| 使用内置库和数学库 | 可使用 | 可使用 |
| 调用游戏中的API | 可使用 | 不可使用 |
| 开发者模式 | 可使用 | 不可使用 |
| 是否唯一 | 唯一 | 可存在多个 |
| 数据传递限制 | 主->从或者从->主传递时,只支持传递表、字符串、数字及其复合结构,不支持传递单位 |
发起异步调用
您需要通过异步的方式来调用辅助计算线程上的函数。例如,当需要调第1个虚拟机上的函数时,您可以使用dispatch_queue()方法派发异步任务:
LuaAPI.dispatch_queue(0, "foo", { "param1", "param2", "param3" }) -- 虚拟机索引从0开始,所以第1个虚拟机应该传0第一个参数为虚拟机编号,第二个参数为被调用函数的名称,第三个参数为传递给被调用函数的参数。注意这个API不支持向被调用函数传递可变数量的参数——请像上面那样,将param1, param2, param3打包成一个表,然后再传递给dispatch_queue()。
加载代码
由于从虚拟机的环境是独立的,并不能直接使用主虚拟机中的代码。因此,我们需要通过异步调用require函数来手动为从虚拟机加载代码。
LuaAPI.dispatch_queue(0, "require", { "compute" })它等价于在从虚拟机中执行了如下代码:
require("compute")作为示例,我们编写一段简单的代码,保存为compute.lua:
function foo(param1, param2, param3)
return "return " .. param1 .. param2 .. param3
end被调用的函数将自动将dispatch_queue()内提供的参数展开为参数列表。
执行所有异步调用
LuaAPI.dispatch_queue()方法仅将您请求的函数放到队列中,并不会立即执行。您可以显式调用dispatch_flush()来号令所有的从虚拟机开始运行异步调用。
LuaAPI.dispatch_flush()通常情况下,当您需要发起多次异步调用时,建议使用先dispatch_queue派发所有异步调用,然后统一使用一次dispatch_flush()来让它们全部执行。
如果您不手动调用dispatch_flush(),那么在下帧开始前,游戏会默认为您调用dispatch_flush()。
获取异步调用结果
您可以在合适的时机,使用dispatch_sync()函数来获取所有异步调用的结果(返回值):
local results = LuaAPI.dispatch_sync()
for i, v in ipairs(results) do
local result = results[i]
if result[1] then
print(result[2]) -- return param1param2param3
else
error("Error occured! Message: " .. result[2])
end
end这个API会等待所有已发起的异步调用完成,并一次性返回所有异步调用的结果列表。结果列表中每个元素也是一个表,其第一个值为true或false,表示是否成功执行了该函数。如果成功,第二个值就是返回值,如果失败,第二个值就是错误信息。
注意,dispatch_sync()将严格按照调用dispatch_queue时的顺序返回结果,即第N次调用的结果存放于result[N]中。
封装回调函数(可选)
如果您需要同时多种异步调用,为了避免返回的结果混淆,可以自行将queue/sync过程封装为回调函数的形式。下面是一个封装示例:
local Engine = {}
local dispatchCallbacks = {}
function Engine.Setup(asyncCount)
LuaAPI.dispatch_init(asyncCount)
end
function Engine.Dispatch(asyncIndex, functionName, params, callback)
table.insert(dispatchCallbacks, callback or false)
LuaAPI.dispatch_queue(asyncIndex, functionName, params)
end
function Engine.Flush()
LuaAPI.dispatch_flush()
end
function Engine.Tick() -- 请在需要异步结果的时候调用此函数,例如可在帧回调开始时调用
local result = LuaAPI.dispatch_sync()
for i, v in ipairs(result) do
local callback = dispatchCallbacks[i]
if callback then
callback(v[1], v[2])
else
error("Error occured in Engine.Tick() " .. v[2])
end
end
dispatchCallbacks = {}
end
return Engine在这段代码中,Dispatch()等价于LuaAPI.dispatch_queue(),但是它在调用时会自动将一个参数视为回调函数,并添加到dispatchCallbacks中。接着在Engine.Tick()函数中,会找到并调用dispatchCallbacks中对应的回调函数,并将异步结果作为参数传递给回调函数。
示例代码
这里给出一个简单的功能示例,请将如下的代码保存为search.lua。
在这个示例中,我们构建了一个虚拟的2D世界网格,每个格点上通过不同的数值表示此网格内的物体类型;接着提供一个函数Query(x, y, radius, id),用来查询指定坐标(x, y),范围半径radius内,类型为typeid的物体,并返回所有满足条件的坐标。
local mapData = {}
local mapWidth = 0
local mapHeight = 0
function Init(data, width, height)
assert(#data == width * height)
mapData = data
mapWidth = width
mapHeight = height
end
function Query(x, y, radius, typeid)
-- query objects with id in (x, y) with radius
local results = {}
local idx = 1
for j = math.max(y - radius, 1), math.min(y + radius, mapHeight) do
local base = (j - 1) * mapWidth
for i = math.max(x - radius, 1), math.min(x + radius, mapWidth) do
if mapData[base + i] == typeid then
results[idx] = i
results[idx + 1] = j
idx = idx + 2
end
end
end
return results
end简明起见,这里没有使用复杂的加速数据结构(如R-Tree, Kd-Tree等)。
接下来编写main.lua:
local Engine = require("engine") -- 使用上文提供的封装接口
local threadCount = 3
Engine.Setup(threadCount)
local map = {
0, 0, 1, 0, 0, 1,
0, 0, 0, 1, 1, 2,
1, 0, 1, 0, 0, 3,
0, 2, 1, 3, 6, 4,
1, 1, 0, 0, 2, 5
}
for i = 1, threadCount do
-- 每个线程上的虚拟机都需要加载search.lua
Engine.Dispatch(i, "require", { "search" }, function (ok, result)
if not ok then
error("Error occured on loading search.lua: " .. result)
end
end)
-- 初始化地图
Engine.Dispatch(i, "Init", { map, 6, 5 }, function (ok, result)
if not ok then
error("Error occured on Init data: " .. result)
end
end)
end
function PreFrame()
-- 每帧前都先同步所有异步调用
Engine.Tick()
end
-- 查询的信息,四个一组,每组为 x, y, radius, typeid
local queries = {
1, 2, 2, 1,
3, 3, 1, 2,
2, 1, 2, 3,
4, 3, 2, 4,
1, 3, 2, 1
}
local frameIndex = 0
function PostFrame()
-- 每帧结束时发起异步查询
-- 为了避免被输出刷屏,每隔60帧(2秒)发起一轮
if frameIndex % 60 == 0 then
for i = 1, #queries // 4 do
local x, y, r, t = queries[i * 4 - 3], queries[i * 4 - 2], queries[i * 4 - 1], queries[i * 4]
Engine.Dispatch(i % threadCount, "Query", { x, y, r, t }, function (ok, result)
-- 每个回调对应一次异步调用
if ok then
print("Query result of Task " .. tostring(i) .. " (" .. tostring(t) .. ")= " .. table.concat(result, ","))
else
error("Error occured in Task " .. tostring(i) .. ": " .. result)
end
end)
end
end
frameIndex = frameIndex + 1
Engine.Flush()
end
LuaAPI.set_tick_handler(PreFrame, PostFrame)main.lua中展示了一种常用的并行计算的结构:即在每帧结束时发起多个异步调用,然后在下一帧开始的时候同步这些调用,这样就可以充分利用帧间的时间来做计算。
使用技巧
并行计算是一个强大的功能,但同时也需要注意以下几点:
- 不要在参数中频繁传递体积大且复杂的数据,否则会导致性能下降。
- 尽量将并行计算的任务均匀地分散到多个辅助线程中,以避免单个辅助线程的性能下降。
- 切分并行计算的任务时,应避免切得太细,每帧建议控制在20个左右。
