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

创建可变形组件

在蛋仔编辑器的基础组件中,可以创建可变形的组件。例如带倒角的方块,可定制边数和内外径的多边形环(可变形圆环),以及自定义曲线/曲面。这些组件通过参数化的配置,可以实现丰富的定制效果。 您可以将编辑好的可变形组件,保存为组件预设,接着在游戏运行时,通过GameAPI.create_obstacle()来创建。但是,这种方式创建的组件,其参数已经在编辑时确定,无法动态指定。

蛋仔在Lua中提供了更加强大的运行时创建可变形组件的API,您现在可以在游戏运行时,动态地创建可变形组件。例如,您可以根据玩家在游戏中的操作/轨迹来创建自定义曲面,实现更有趣的地图交互。

创建可变形组件分两个步骤:

  1. 注册可变形组件的几何体形状
  2. 创建可变形组件

第一步,调用GameAPI.register_geometry_*()系列API来注册可变形组件的几何体形状。这一步传入可变形组件的参数,并生成对应的几何体形状“预设”,并不实际创建组件。

以创建自定义曲面/曲线为例:

lua
	local Vector3 = math.Vector3
	local positions = {
		Vector3(0, 0, 0),
		Vector3(10, 0, 10),
		Vector3(15, 0, 5),
		Vector3(15, 0, -5),
		Vector3(12.5, 0, 3),
		Vector3(10, 0, 0),
		Vector3(12.5, 0, -3),
		Vector3(10, 0, -10),
	}

	local normals = {
		Vector3(0, 1, 0),
		Vector3(0, 0, -1),
		Vector3(-1, 0, 0),
		Vector3(0, 0, 1),
		Vector3(0, 0, 1),
		Vector3(-1, 0, 0),
		Vector3(-1, 1, 0),
		Vector3(0, 1, 0),
	}

	local radius = {
		1, 2, 5, 2, 2, 5, 1, 2
	}

	local path = GameAPI.register_geometry_spline(false, positions, normals, radius, 0.2, 0.05, 0.5, {})
	local path2 = GameAPI.register_geometry_spline(true, positions, normals, radius, 0.2, 0.05, 0.5, {})

GameAPI.register_geometry_spline接收七个参数,分别为类型(true为曲线,false为曲面)、位置列表、法线列表、半径列表、距离精度、角度精度、厚度(仅对于曲面)、附加参数(暂时无用)。

这个API返回一个字符串形式的ID,用来表示这个几何体。这里我们以同样的顶点配置,分别创建一个曲线,一个曲面。

第二步,调用GameAPI.create_obstacle_from_geometry()来用刚刚得到的几何体创建可变形的组件:

lua
	for i = 1, 2 do
		GameAPI.create_obstacle_from_geometry(1073819754, Vector3(-30 * i + 30, 15, 5), Quaternion(0.0, 1.57, 0.0), Vector3(1, 1, 1), nil, i % 2 == 0 and path or path2)
	end

GameAPI.create_obstacle_from_geometry()接收六个参数,分别为参考预设ID、位置、旋转、缩放、所有者和前一步得到的几何体ID。

由于几何体本身仅仅是一个形状描述,并没有指定皮肤、物理配置等属性,因此在创建组件时,需要传入一个参考预设ID,除了几何形状和物理形状以外,其他的属性都将从这个预设中继承。

上面的示例代码中,1073819754是演示用的预设ID,请替换为你自己的预设ID。

除了自定义曲面以外,目前蛋仔还支持了创建可变形圆环、圆台、方块,每种类型所支持的参数各不相同,请参照API文档。

lua
-- splines
do
	local Vector3 = math.Vector3
	local positions = {
		Vector3(0, 0, 0),
		Vector3(10, 0, 10),
		Vector3(15, 0, 5),
		Vector3(15, 0, -5),
		Vector3(12.5, 0, 3),
		Vector3(10, 0, 0),
		Vector3(12.5, 0, -3),
		Vector3(10, 0, -10),
	}

	local normals = {
		Vector3(0, 1, 0),
		Vector3(0, 0, -1),
		Vector3(-1, 0, 0),
		Vector3(0, 0, 1),
		Vector3(0, 0, 1),
		Vector3(-1, 0, 0),
		Vector3(-1, 1, 0),
		Vector3(0, 1, 0),
	}

	local radius = {
		1, 2, 5, 2, 2, 5, 1, 2
	}

	local path = GameAPI.register_geometry_spline(false, positions, normals, radius, 0.2, 0.05, 0.5, {})
	local path2 = GameAPI.register_geometry_spline(true, positions, normals, radius, 0.2, 0.05, 0.5, {})

	-- 注意,这里预设ID(第一个参数)请替换为你自己的预设ID,下同
	for i = 1, 2 do
		GameAPI.create_obstacle_from_geometry(i % 2 == 0 and 1073819754 or 1073815637, Vector3(-30 * i + 30, 15, 5), Quaternion(0.0, 1.57, 0.0), Vector3(1, 1, 1), nil, i % 2 == 0 and path or path2)
	end
end

-- ring

do
	local path = GameAPI.register_geometry_ring(2.0, 3.2, 4.0, 5, 8, 0.25, 270.0, {})	
	GameAPI.create_obstacle_from_geometry(1073811526, Vector3(-4, 20, 15), Quaternion(0.0, 1.57, 0.0), Vector3(1, 1, 1), nil, path)
end

-- frustum
do
	local path = GameAPI.register_geometry_frustum(6.0, 3.2, 4.0, 8, 10, 0.25, 225.0, 8, 0.25, {})	
	GameAPI.create_obstacle_from_geometry(1073811526, Vector3(-24, 20, 15), Quaternion(0.0, 1.57, 0.0), Vector3(1, 1, 1), nil, path)
end

-- box
do
	local path = GameAPI.register_geometry_box(3.5, 0.5, 2, false, {})
	GameAPI.create_obstacle_from_geometry(1073807375, Vector3(-14, 25, 15), Quaternion(0.0, 1.57, 0.0), Vector3(1, 1, 1), nil, path)
end

示例效果