Skip to content

高级特性

这部分内容主要面向高级开发者,如果您需要进一步深挖地图的表现力,或者提升游戏运行性能,可以参考本章节。

并行计算

辅助计算线程

当您需要在游戏中编写复杂计算时,可以考虑使用并行计算来加速代码。例如,您可以把复杂的纯数学计算逻辑(如自定义AI、战场模拟、寻路等)安排在独立的线程,避免其占用宝贵的游戏逻辑帧时间。并行计算是一个难度较高的技术,您需要对操作系统和Lua虚拟机有一定的了解。

在蛋仔中,您可以设置1~3个辅助计算线程:

lua
LuaAPI.dispatch_init(2) -- 设置并行计算辅助线程数为2

dispatch_unit函数只能调用一次,请在游戏开始时就调用它。通常情况下,您可以将上述代码放置于main.lua的开始处。

每个辅助线程上都拥有一个Lua虚拟机(以下简称 从虚拟机 ),默认情况下它们与游戏中默认的Lua虚拟机(以下简称 主虚拟机 )是独立的,不共享任何数据和状态。

从虚拟机限制

从虚拟机在功能上有一些限制,您需要注意以下几点:

操作主虚拟机从虚拟机
使用内置库和数学库可使用可使用
调用游戏中的API可使用不可使用
开发者模式可使用不可使用
是否唯一唯一可存在多个
数据传递限制主->从或者从->主传递时,只支持传递表、字符串、数字及其复合结构,不支持传递单位

发起异步调用

您需要通过异步的方式来调用辅助计算线程上的函数。例如,当需要调第1个虚拟机上的函数时,您可以使用dispatch_queue()方法派发异步任务:

lua
LuaAPI.dispatch_queue(0, "foo", { "param1", "param2", "param3" }) -- 虚拟机索引从0开始,所以第1个虚拟机应该传0

第一个参数为虚拟机编号,第二个参数为被调用函数的名称,第三个参数为传递给被调用函数的参数。注意这个API不支持向被调用函数传递可变数量的参数——请像上面那样,将param1, param2, param3打包成一个表,然后再传递给dispatch_queue()。

加载代码

由于从虚拟机的环境是独立的,并不能直接使用主虚拟机中的代码。因此,我们需要通过异步调用require函数来手动为从虚拟机加载代码。

lua
LuaAPI.dispatch_queue(0, "require", { "compute" })

它等价于在从虚拟机中执行了如下代码:

lua
require("compute")

作为示例,我们编写一段简单的代码,保存为compute.lua:

lua
function foo(param1, param2, param3)
	return "return " .. param1 .. param2 .. param3
end

被调用的函数将自动将dispatch_queue()内提供的参数展开为参数列表。

执行所有异步调用

LuaAPI.dispatch_queue()方法仅将您请求的函数放到队列中,并不会立即执行。您可以显式调用dispatch_flush()来号令所有的从虚拟机开始运行异步调用。

lua
LuaAPI.dispatch_flush()

通常情况下,当您需要发起多次异步调用时,建议使用先dispatch_queue派发所有异步调用,然后统一使用一次dispatch_flush()来让它们全部执行。

如果您不手动调用dispatch_flush(),那么在下帧开始前,游戏会默认为您调用dispatch_flush()。

获取异步调用结果

您可以在合适的时机,使用dispatch_sync()函数来获取所有异步调用的结果(返回值):

lua
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过程封装为回调函数的形式。下面是一个封装示例:

lua
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的物体,并返回所有满足条件的坐标。

lua

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:

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个左右。