Skip to content

无限大世界

你可以在PC编辑器中新建地图,选择Lua模板图中的无限大世界,查看本示例的完整工程

游戏简介

在玩家身边小范围内存在地板方块,随着玩家移动脚下的地板方块不断创建销毁。

技术要点

  • Lua 添加Tick回调函数
  • Lua 自定义哈希函数实现返回唯一值
  • Lua 使用基础类型进行数据管理
  • Lua 计算玩家当前地块信息
  • Lua 记录加载和销毁的地块信息
  • Lua 加载和销毁地块

注:本示例可在编辑器模板图中下载。

技术要点分析

Lua 添加Tick回调函数

lua
local function onPreTick(_)
		updateTiles()
	end
---设置Tick回调函数 
--- 第一个参数帧前回调,第二个参数帧后回调
LuaAPI.set_tick_handler(onPreTick, nil)

Lua 自定义哈希函数实现返回唯一值

lua
-- 自定义哈希函数
local function getKey(x, z)
	-- math.tointeger(x) 返回整数
	-- +4096 防止返回负数
	-- 65536=2^16 保证唯一性
	-- 假设 x 和 z 的范围是 [-4096, 4096),那么结果将始终是非负的
	return (math.tointeger(x) + 4096) * 65536 + math.tointeger(z) + 4096
end

注:代码位置:main.lua

Lua 使用基础类型进行数据管理

lua
local usedTiles = {} -- 记录已使用的地块{key:tileUnit},使用哈希函数计算唯一值作为key
local freeTiles = { tile1 = {}, tile2 = {} } -- 分开存储两种类型的地块
local tileUnit1 = LuaAPI.query_unit("地块1")
local tileUnit2 = LuaAPI.query_unit("地块2")
local tilePrefabId1 = tileUnit1.get_key()
local tilePrefabId2 = tileUnit2.get_key()
local groundSize = math.Vector3(4, 1, 4)

-- 定义加载和卸载半径
local LOAD_RADIUS = 1 -- 加载周围1格地块(3x3)
local UNLOAD_RADIUS = 3 -- 只有超出3格距离才卸载(7x7)

注:代码位置:main.lua

Lua 计算玩家当前地块信息

lua
-- 计算一下当前地块格子的坐标,将世界中的玩家坐标转换为地块索引坐标
-- position 玩家位置
-- groundSize 地块大小
-- x 和 z 是地块坐标
local divisorZ = 1.0 / math.toreal(groundSize.z)
local z = math.tointeger(math.floor((position.z + groundSize.z * 0.5) * divisorZ))
local divisorX = 1.0 / math.toreal(groundSize.x)
local x = math.tointeger(math.floor((position.x + groundSize.x * 0.5) * divisorX))

注:代码位置:main.lua

Lua 记录加载和销毁的地块信息

lua
-- local LOAD_RADIUS = 1 -- 加载周围1格地块(3x3)
-- local UNLOAD_RADIUS = 3 -- 只有超出3格距离才卸载(7x7)
-- 记录加载范围内的地块
-- 双层循环遍历加载范围内的地块
for i = x - LOAD_RADIUS, x + LOAD_RADIUS do
	for j = z - LOAD_RADIUS, z + LOAD_RADIUS do
		-- 地块索引坐标转世界坐标的位置
		local posX = i * groundSize.x
		local posZ = j * groundSize.z
		-- 计算地块索引坐标的哈希值
		local gridKey = getKey(posX, posZ)
		-- 存入inLoadRangeTiles
		inLoadRangeTiles[gridKey] = true
	end
end

-- 记录卸载范围内的地块(注:后面在卸载时如果不在卸载范围内,则进行销毁)
-- 双层循环遍历销毁范围内的地块
for i = x - UNLOAD_RADIUS, x + UNLOAD_RADIUS do
	for j = z - UNLOAD_RADIUS, z + UNLOAD_RADIUS do
		-- 地块索引坐标转世界坐标的位置
		local posX = i * groundSize.x
		local posZ = j * groundSize.z
		-- 计算地块索引坐标的哈希值
		local gridKey = getKey(posX, posZ)
		-- 存入inUnloadRangeTiles
		inUnloadRangeTiles[gridKey] = true
	end
end

注:代码位置:main.lua

Lua 加载和销毁地块

lua
-- 加载新的地块
for gridKey in pairs(inLoadRangeTiles) do
	-- 判断是否已经加载过
	if not usedTiles[gridKey] then
		-- 通过哈希值获取地块索引坐标,相当于哈希函数的逆运算
		local posX = math.floor(gridKey / 65536) - 4096
		local posZ = gridKey % 65536 - 4096

		-- 棋盘格判断:根据格子坐标和决定使用哪种地块
		local gridX = math.floor(posX / groundSize.x)
		local gridZ = math.floor(posZ / groundSize.z)
		 -- 判断是哪种类型的地块
		local isType1 = (gridX + gridZ) % 2 == 0

		local freeList, prefabId, model
		-- 判断是哪种类型的地块,并赋值地块
		if isType1 then
			freeList = freeTiles.tile1
			prefabId = tilePrefabId1
			model = tileUnit1
		else
			freeList = freeTiles.tile2
			prefabId = tilePrefabId2
			model = tileUnit2
		end
		-- 从 freeList 表中移除并返回最后一个元素
		local unit = table.remove(freeList)
		-- 判断是否有空闲地块,如果有则使用,如果没有则创建
		if unit then
			unit.set_position(math.Vector3(posX, 0.0, posZ))
			unit.set_model_visible(true)
			unit.set_physics_active(true)
		else
			unit = GameAPI.create_obstacle(prefabId, math.Vector3(posX, 0.0, posZ),
				math.Quaternion(0, 0, 0), model.get_scale())
		end
		-- 记录加载的地块
		usedTiles[gridKey] = unit
	end
end

-- 移除超出卸载范围的地块
for gridKey, unit in pairs(usedTiles) do
	-- 判断是否超出卸载范围的地块
	if not inUnloadRangeTiles[gridKey] then
		-- 关闭可见性并关闭物理
		unit.set_model_visible(false)
		unit.set_physics_active(false)

		-- 判断是哪种类型的地块并放入对应的回收池
		local unitId = unit.get_key()
		-- 判断是哪种类型的地块,并放入对应的回收池
		if unitId == tilePrefabId1 then
			table.insert(freeTiles.tile1, unit)
		else
			table.insert(freeTiles.tile2, unit)
		end
		-- 移除地块
		usedTiles[gridKey] = nil
	end
end

注:代码位置:main.lua

完整代码

lua
local UINodes = require("Data.UINodes")

-- 游戏开始事件
LuaAPI.global_register_trigger_event({ EVENT.GAME_INIT }, function()
	local usedTiles = {}
	local freeTiles = { tile1 = {}, tile2 = {} } -- 分开存储两种类型的地块
	local tileUnit1 = LuaAPI.query_unit("地块1")
	local tileUnit2 = LuaAPI.query_unit("地块2")
	local tilePrefabId1 = tileUnit1.get_key()
	local tilePrefabId2 = tileUnit2.get_key()
	local groundSize = math.Vector3(4, 1, 4)

	-- 定义加载和卸载半径
	local LOAD_RADIUS = 1 -- 加载周围1格地块(3x3)
	local UNLOAD_RADIUS = 3 -- 只有超出3格距离才卸载(7x7)

	local function getKey(x, z)
		return (math.tointeger(x) + 4096) * 65536 + math.tointeger(z) + 4096
	end

	-- 初始两个地块
	usedTiles[getKey(0, 0)] = tileUnit1
	usedTiles[getKey(4, 0)] = tileUnit2

	-- 根据玩家位置更新地块
	local function updateTiles()
		local allRoles = GameAPI.get_all_valid_roles()
		local inLoadRangeTiles = {} -- 加载范围内的地块
		local inUnloadRangeTiles = {} -- 卸载范围内的地块

		for _, role in ipairs(allRoles) do
			local character = role.get_ctrl_unit()
			local position = character.get_position()
			role.set_label_text(UINodes["位置"],
				string.format("当前位置: (%d, %d, %d)", math.tointeger(position.x), math.tointeger(position.y),
					math.tointeger(position.z)))

			-- 计算一下当前地块格子的坐标
			local divisorZ = 1.0 / math.toreal(groundSize.z)
			local z = math.tointeger(math.floor((position.z + groundSize.z * 0.5) * divisorZ))
			local divisorX = 1.0 / math.toreal(groundSize.x)
			local x = math.tointeger(math.floor((position.x + groundSize.x * 0.5) * divisorX))

			-- 记录加载范围内的地块
			for i = x - LOAD_RADIUS, x + LOAD_RADIUS do
				for j = z - LOAD_RADIUS, z + LOAD_RADIUS do
					local posX = i * groundSize.x
					local posZ = j * groundSize.z
					local gridKey = getKey(posX, posZ)
					inLoadRangeTiles[gridKey] = true
				end
			end

			-- 记录卸载范围内的地块
			for i = x - UNLOAD_RADIUS, x + UNLOAD_RADIUS do
				for j = z - UNLOAD_RADIUS, z + UNLOAD_RADIUS do
					local posX = i * groundSize.x
					local posZ = j * groundSize.z
					local gridKey = getKey(posX, posZ)
					inUnloadRangeTiles[gridKey] = true
				end
			end
		end

		-- 加载新的地块
		for gridKey in pairs(inLoadRangeTiles) do
			if not usedTiles[gridKey] then
				local posX = math.floor(gridKey / 65536) - 4096
				local posZ = gridKey % 65536 - 4096

				-- 棋盘格判断:根据格子坐标和决定使用哪种地块
				local gridX = math.floor(posX / groundSize.x)
				local gridZ = math.floor(posZ / groundSize.z)
				local isType1 = (gridX + gridZ) % 2 == 0

				local freeList, prefabId, model
				if isType1 then
					freeList = freeTiles.tile1
					prefabId = tilePrefabId1
					model = tileUnit1
				else
					freeList = freeTiles.tile2
					prefabId = tilePrefabId2
					model = tileUnit2
				end

				local unit = table.remove(freeList)
				if unit then
					unit.set_position(math.Vector3(posX, 0.0, posZ))
					unit.set_model_visible(true)
					unit.set_physics_active(true)
				else
					unit = GameAPI.create_obstacle(prefabId, math.Vector3(posX, 0.0, posZ),
						math.Quaternion(0, 0, 0), model.get_scale())
				end
				usedTiles[gridKey] = unit
			end
		end

		-- 移除超出卸载范围的地块
		for gridKey, unit in pairs(usedTiles) do
			if not inUnloadRangeTiles[gridKey] then
				unit.set_model_visible(false)
				unit.set_physics_active(false)

				-- 判断是哪种类型的地块并放入对应的回收池
				local unitId = unit.get_key()
				if unitId == tilePrefabId1 then
					table.insert(freeTiles.tile1, unit)
				else
					table.insert(freeTiles.tile2, unit)
				end

				usedTiles[gridKey] = nil
			end
		end
	end

	local function onPreTick(_)
		updateTiles()
	end

	LuaAPI.set_tick_handler(onPreTick, nil)
end)

注:代码位置:main.lua

编辑器配置

创建地块预设

  • 将组件自定义属性后并存为预设,方便使用预设编号动态创建地块

场景管理界面

预设编辑器界面

新预设同步到Lua编程中使用

导出数据

编辑器侧:蛋仔开发助手已连接状态下且修改已经保存。

VSCode侧:蛋仔开发助手已连接状态下,需勾选预设标号、物品预设后点击导出数据即可在Data目录下找到相应数据文件。