Skip to content

动态搭建

你可以在PC编辑器中新建地图,选择Lua模板图中的动态搭建,查看本示例的完整工程

游戏简介

玩家可以将砖块随意与其他组件进行焊接,并且有焊接预览功能。

技术要点

  • Lua 将焊接函数注册到帧更新函数中
  • Lua 创建焊接预览预制体,并根据玩家的位置和旋转进行变换
  • Lua 监听角色的举起事件和放下事件
  • Lua 给UI控件注册事件
  • Lua 通过射线检测进行检测焊接点的有效性
  • Lua 使用关节尝试焊接物体
  • Lua 创建StickController类,用于焊接管理

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

技术要点分析

将焊接函数注册到帧更新函数中

lua
-- 导入StickController类
local StickController = require("StickController")
-- 注册全局触发器
LuaAPI.global_register_trigger_event({ EVENT.GAME_INIT }, function()
	-- 创建StickController对象
	local stickController = StickController.new()
	-- 注册帧更新函数
	local function onPreTick()
		stickController:update()
	end
	-- 设置帧更新处理函数
	LuaAPI.set_tick_handler(onPreTick, nil)
end)

注:代码位置:main.lua

创建焊接预览预制体,并根据玩家的位置和旋转进行变换

  • 创建焊接预览预制体
lua
-- 对每个玩家做操作
for _, role in ipairs(GameAPI.get_all_valid_roles()) do
	-- 获取角色控制的单位
	local roleId = role.get_roleid()

	-- 创建用来预览焊接位置的指示器
	self.previewUnits[roleId] = GameAPI.create_obstacle(
		UnitPrefab["焊接预览"],
		math.Vector3(0, 0, 0),
		math.Quaternion(0, 0, 0),
		math.Vector3(scale, scale, scale),
		role
	)

	self.charLiftInfos[roleId] = {}

-- 初始化时需要将预览的指示器需要对所有人都默认隐藏
for _, role in ipairs(GameAPI.get_all_valid_roles()) do
	for _, unit in pairs(self.previewUnits) do
		role.set_unit_visible(unit, false)
	end
end

注:代码位置:StickController.lua

  • 根据玩家的位置和旋转进行变换
lua
-- 此函数已经注册到帧更新函数,每帧会调用一次
function StickController:update()
	-- 需要一直检测能否焊接,如果能焊接则显示焊接预览指示器和焊接按钮
	for roleid, liftInfo in pairs(self.charLiftInfos) do
		-- 获取角色控制的单位
		local role = GameAPI.get_role(roleid)
		-- 获取角色举起物体的信息
		local previewUnit = self.previewUnits[role.get_roleid()]
		-- 检查是否能焊接
		local canStick = self:checkRoleCanStick(role, liftInfo)
		-- 设置焊接按钮和焊接预览的可见性
		role.set_node_visible(UINodes["焊接按钮"], canStick)
		role.set_unit_visible(previewUnit, canStick)
		-- 如果能焊接,将焊接预览的位置和旋转设置到焊接预览指示器上
		if canStick then
			local rot = role.get_ctrl_unit().get_orientation()
			local pos = liftInfo.stickPos
			previewUnit.set_position(pos)
			previewUnit.set_orientation(rot)
		end
	end
end

注:代码位置:StickController.lua

监听角色的举起事件和放下事件

lua
-- 监听角色举起物体的事件
LuaAPI.unit_register_trigger_event(character, { EVENT.SPEC_LIFEENTITY_LIFT_BEGAN }, function(_, _, data)
	-- data 是举起物体
	local lifted = data.lifted_unit
	-- 此处做的操作是获取物体所有的关节,并将动态焊接的关节全部移除
	self:tryRemoveStickJoints(lifted)

	-- 举起的物体变半透明,同时关闭物理
	lifted.enable_expr_device_by_name("修改透明度")
	lifted.set_physics_active(false)
	-- 将玩家举起物体信息按玩家roleid存储起来
	self.charLiftInfos[roleId] = { unit = lifted }

	-- 举起后打开相机朝向的更新
	role.set_camera_rotation_sync_enabled(true)
end)


-- 监听角色放下物体的事件
LuaAPI.unit_register_trigger_event(character, { EVENT.SPEC_LIFEENTITY_LIFT_ENDED }, function(_, _, data)
	-- data 是放下的物体
	local lifted = data.lifted_unit

	-- 举起的物体恢复透明度,同时恢复物理
	lifted.disable_expr_device_by_name("修改透明度")
	lifted.set_physics_active(true)
	-- 将玩家举起物体信息移除
	self.charLiftInfos[roleId] = {}

	-- 重置放下物体的位置
	local direction = character.get_orientation():apply(math.Vector3(0, 2, 0))
	lifted.set_position(character.get_position() + direction)
	lifted.set_orientation(character.get_orientation())

	-- 放下后关闭相机朝向的更新
	role.set_camera_rotation_sync_enabled(false)
end)

注:代码位置:StickController.lua

Lua 给UI控件注册事件

lua
-- 点击焊接按钮
LuaAPI.global_register_custom_event("点击焊接", function(_, _, data)
	local role = data.role
	-- 获取角色举起物体的信息
	local liftInfo = self.charLiftInfos[role.get_roleid()]
	-- 检查角色是否可以焊接
	if self:checkRoleCanStick(role, liftInfo) then
		-- 尝试焊接
		self:tryStickUnit(role, liftInfo)
	end
end)

注:代码位置:StickController.lua

Lua 通过射线检测进行检测焊接点的有效性

lua
function StickController:checkRoleCanStick(role, liftInfo)
	-- 传入玩家信息和举起物体信息
	if not liftInfo.unit then
		return false
	end

	-- 获取相机朝向
	local camRot = role.get_camera_rotation()
	if camRot.w == 0 then
		-- 需要剔除无效的相机朝向(刚开启相机方向监听的时候,相机朝向可能还未更新)
		return false
	end

	local character = role.get_ctrl_unit()
	-- 角色头顶处
	local startPos = character.get_position() + math.Vector3(0, 2.9, 0)
	-- 相机朝向
	local direction = camRot:apply(math.Vector3(0, 0, 1))
	local length = 15.0
	-- 给举起物体添加两个属性且设为nil
	liftInfo.stickTo = nil -- 焊接目标(指的是另一个可以焊接的组件)
	liftInfo.stickPos = nil -- 焊接点(坐标位置)

	-- 从角色头顶朝相机方向打射线(最远距离为 length),碰到的第一个物体和位置,作为焊接目标和焊接点
	GameAPI.raycast_unit(
		startPos,
		startPos + direction * length,
		{ Enums.UnitType.OBSTACLE },
		function(unit, point, normal)
			-- 只有射线检测成功才会更新焊接目标和焊接点
			liftInfo.stickTo = unit
			liftInfo.stickPos = point
		end
	)

	-- 只有找到焊接点才算成功,否则返回false
	return liftInfo.stickTo ~= nil
end

注:代码位置:StickController.lua

Lua 使用关节尝试焊接物体

lua
function StickController:tryStickUnit(role, liftInfo)
	-- 传入玩家信息和举起物体信息
	-- 先判断焊接目标和玩家举起的物体是否是同一个物体,如果不是才执行焊接
	if liftInfo.stickTo and liftInfo.stickTo ~= liftInfo.unit then
		-- 先把举起物体放下
		role.get_ctrl_unit().lift_unit(liftInfo.unit)

		-- 再恢复举起物体的物理
		liftInfo.unit.set_physics_active(true)
		-- 根据焊接点和焊接点旋转,将刚放下的物体调整位置
		liftInfo.unit.set_position(liftInfo.stickPos)
		liftInfo.unit.set_orientation(liftInfo.stick_rot or role.get_ctrl_unit().get_orientation())
		-- 给焊接物体添加一个关节,用于尝试焊接
		local joint = GameAPI.create_joint_assistant(Enums.JointAssistantKey.FIXED, liftInfo.unit, liftInfo.stickTo)

		-- 给关节标记为动态焊接,方便删除
		joint.set_kv_by_type(Enums.ValueType.Bool, "isDynamicStick", true)
	end
end

注:代码位置:StickController.lua

Lua 创建StickController类,用于焊接管理

lua
-- 导入ClassUtils.class函数
local class = require("Utils.ClassUtils").class

-- 创建StickController类
---@class StickController
---@field new fun(): StickController
local StickController = class("StickController")

-- 创建StickController类构造函数以及其他函数
function StickController:ctor()
	self.previewUnits = {} -- 预览焊接位置的指示器
	self.charLiftInfos = {} -- 角色举起物体的信息
	-- 代码省略 --
end

function StickController:checkRoleCanStick(role, liftInfo)
	-- 代码省略 --
end

function StickController:tryStickUnit(role, liftInfo)
	-- 代码省略 --
end

function StickController:tryRemoveStickJoints(unit)
	-- 代码省略 --
end

function StickController:update()
	-- 代码省略 --
end

return StickController

注:以上代码简化,详细代码位置:StickController.lua

完整代码

lua
local StickController = require("StickController")

LuaAPI.global_register_trigger_event({ EVENT.GAME_INIT }, function()
	local stickController = StickController.new()

	local function onPreTick()
		stickController:update()
	end

	LuaAPI.set_tick_handler(onPreTick, nil)
end)

注:代码位置:main.lua

lua
local UnitPrefab = require("Data.Prefab").unit
local UINodes = require("Data.UINodes")
local class = require("Utils.ClassUtils").class

---@class StickController
---@field new fun(): StickController
local StickController = class("StickController")

function StickController:ctor()
	self.previewUnits = {} -- 预览焊接位置的指示器
	self.charLiftInfos = {} -- 角色举起物体的信息

	local scale = 0.5
	for _, role in ipairs(GameAPI.get_all_valid_roles()) do
		local roleId = role.get_roleid()

		-- 创建用来预览焊接位置的指示器
		self.previewUnits[roleId] = GameAPI.create_obstacle(
			UnitPrefab["焊接预览"],
			math.Vector3(0, 0, 0),
			math.Quaternion(0, 0, 0),
			math.Vector3(scale, scale, scale),
			role
		)

		self.charLiftInfos[roleId] = {}
		local character = role.get_ctrl_unit()

		-- 监听角色举起物体的事件
		LuaAPI.unit_register_trigger_event(character, { EVENT.SPEC_LIFEENTITY_LIFT_BEGAN }, function(_, _, data)
			local lifted = data.lifted_unit
			self:tryRemoveStickJoints(lifted)

			-- 举起的物体变半透明,同时关闭物理
			lifted.enable_expr_device_by_name("修改透明度")
			lifted.set_physics_active(false)

			self.charLiftInfos[roleId] = { unit = lifted }

			-- 举起后打开相机朝向的更新
			role.set_camera_rotation_sync_enabled(true)
		end)

		-- 监听角色放下物体的事件
		LuaAPI.unit_register_trigger_event(character, { EVENT.SPEC_LIFEENTITY_LIFT_ENDED }, function(_, _, data)
			local lifted = data.lifted_unit

			-- 举起的物体恢复透明度,同时恢复物理
			lifted.disable_expr_device_by_name("修改透明度")
			lifted.set_physics_active(true)
			self.charLiftInfos[roleId] = {}

			-- 重置放下物体的位置
			local direction = character.get_orientation():apply(math.Vector3(0, 2, 0))
			lifted.set_position(character.get_position() + direction)
			lifted.set_orientation(character.get_orientation())

			-- 放下后关闭相机朝向的更新
			role.set_camera_rotation_sync_enabled(false)
		end)
	end

	-- 预览的指示器需要对所有人都默认隐藏
	for _, role in ipairs(GameAPI.get_all_valid_roles()) do
		for _, unit in pairs(self.previewUnits) do
			role.set_unit_visible(unit, false)
		end
	end

	-- 点击焊接按钮
	LuaAPI.global_register_custom_event("点击焊接", function(_, _, data)
		local role = data.role
		local liftInfo = self.charLiftInfos[role.get_roleid()]
		if self:checkRoleCanStick(role, liftInfo) then
			self:tryStickUnit(role, liftInfo)
		end
	end)
end

function StickController:checkRoleCanStick(role, liftInfo)
	if not liftInfo.unit then
		return false
	end

	-- 获取相机朝向
	local camRot = role.get_camera_rotation()
	if camRot.w == 0 then
		-- 需要剔除无效的相机朝向(刚开启相机方向监听的时候,相机朝向可能还未更新)
		return false
	end

	local character = role.get_ctrl_unit()
	-- 角色头顶处
	local startPos = character.get_position() + math.Vector3(0, 2.9, 0)
	-- 相机朝向
	local direction = camRot:apply(math.Vector3(0, 0, 1))
	local length = 15.0
	liftInfo.stickTo = nil
	liftInfo.stickPos = nil

	-- 从角色头顶朝相机方向打射线(最远距离为 length),碰到的第一个物体和位置,作为焊接目标和焊接点
	GameAPI.raycast_unit(
		startPos,
		startPos + direction * length,
		{ Enums.UnitType.OBSTACLE },
		function(unit, point, normal)
			-- 只有射线检测成功才会更新焊接目标和焊接点
			liftInfo.stickTo = unit
			liftInfo.stickPos = point
		end
	)

	-- 只有找到焊接点才算成功
	return liftInfo.stickTo ~= nil
end

function StickController:tryStickUnit(role, liftInfo)
	if liftInfo.stickTo and liftInfo.stickTo ~= liftInfo.unit then
		-- 先把举起物体放下
		role.get_ctrl_unit().lift_unit(liftInfo.unit)

		-- 再恢复举起物体的物理
		liftInfo.unit.set_physics_active(true)
		liftInfo.unit.set_position(liftInfo.stickPos)
		liftInfo.unit.set_orientation(liftInfo.stick_rot or role.get_ctrl_unit().get_orientation())
		local joint = GameAPI.create_joint_assistant(Enums.JointAssistantKey.FIXED, liftInfo.unit, liftInfo.stickTo)

		-- 给关节标记为动态焊接,方便删除
		joint.set_kv_by_type(Enums.ValueType.Bool, "isDynamicStick", true)
	end
end

function StickController:tryRemoveStickJoints(unit)
	-- 只删除标记为动态焊接的关节
	for _, v in ipairs(GameAPI.get_joint_assistants(unit)) do
		if v.has_kv("isDynamicStick") then
			GameAPI.destroy_unit(v)
		end
	end
end

function StickController:update()
	-- 需要一直检测能否焊接,如果能焊接则显示焊接预览指示器和焊接按钮
	for roleid, liftInfo in pairs(self.charLiftInfos) do
		local role = GameAPI.get_role(roleid)
		local previewUnit = self.previewUnits[role.get_roleid()]
		local canStick = self:checkRoleCanStick(role, liftInfo)
		role.set_node_visible(UINodes["焊接按钮"], canStick)
		role.set_unit_visible(previewUnit, canStick)

		if canStick then
			local rot = role.get_ctrl_unit().get_orientation()
			local pos = liftInfo.stickPos
			previewUnit.set_position(pos)
			previewUnit.set_orientation(rot)
		end
	end
end

return StickController

注:代码位置:StickController.lua

编辑器配置

UI界面

  • UI界面添加焊接按钮,并设置焊接按钮的点击事件

焊接按钮

  • VSCode侧注册自定义事件,实现焊接功能
lua
-- 点击焊接按钮
LuaAPI.global_register_custom_event("点击焊接", function(_, _, data)
	local role = data.role
	local liftInfo = self.charLiftInfos[role.get_roleid()]
	if self:checkRoleCanStick(role, liftInfo) then
		self:tryStickUnit(role, liftInfo)
	end
end)

注:以上代码简化,详细代码位置:StickController.lua

创建焊接预览预设

  • 使用多种组件组件,合并成焊接预览并存为预设

合并组件

添加新预设

  • 将焊接预览组件组添加为新预设后,使用蛋仔开发助手同步至VSCode侧通过lua编程实现动态创建焊接预览

  • 游戏初始化时使用预设数据Prefab.lua配置文件获取焊接预览预设编号,为每个玩家创建焊接预览

lua
-- Prefab.lua中数据由编辑器自动生成
local UnitPrefab = require("Data.Prefab").unit

for _, role in ipairs(GameAPI.get_all_valid_roles()) do
	local roleId = role.get_roleid()

	-- 创建用来预览焊接位置的指示器
	self.previewUnits[roleId] = GameAPI.create_obstacle(
		UnitPrefab["焊接预览"],
		math.Vector3(0, 0, 0),
		math.Quaternion(0, 0, 0),
		math.Vector3(scale, scale, scale),
		role
	)
	---代码省略---
end

注:以上代码简化,详细代码位置:StickController.lua