Skip to content

跑商玩法

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

游戏简介

玩家用已有的金币在商店中购买物品,再通过贸易中心售卖商品获得更多的金币。

技术要点

  • Lua 定义TriggerAreaHandler类,用于处理商店的触发器
  • Lua 定义UIHandler类,用于处理玩家界面UI显示
  • Lua 定义ItemMangaer类,用于处理整个游戏的物品管理
  • Lua 初始化商店并实现道具刷新和购买
  • Lua 初始化贸易中心并实现更新公告和物品售卖

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

技术要点分析

Lua 定义TriggerAreaHandler类,用于处理商店的触发器

lua
-- 构造函数:初始化触发区域处理器
-- @param triggerArea 触发区域
-- @param enterCallback 进入区域时的回调函数
-- @param leaveCallback 离开区域时的回调函数
function TriggerAreaHandler:ctor(triggerArea, enterCallback, leaveCallback)
	-- 保存触发区域引用
	self.triggerArea = triggerArea
	-- 获取触发区域的单位ID
	local triggerAreaUnitId = LuaAPI.get_unit_id(triggerArea)

	-- 注册玩家进入触发区域的事件
	LuaAPI.global_register_trigger_event(
		{ EVENT.ANY_LIFEENTITY_TRIGGER_SPACE, Enums.TriggerSpaceEventType.ENTER, triggerAreaUnitId },
		function(_, _, data)
			-- 获取进入区域的角色
			local character = data.event_unit
			local role = character.get_role()
			-- 调用进入回调函数,并将进入区域的角色信息传入
			enterCallback(role)
		end
	)

	-- 注册玩家离开触发区域的事件
	LuaAPI.global_register_trigger_event(
		{ EVENT.ANY_LIFEENTITY_TRIGGER_SPACE, Enums.TriggerSpaceEventType.LEAVE, triggerAreaUnitId },
		function(_, _, data)
			-- 获取离开区域的角色
			local character = data.event_unit
			local role = character.get_role()
			-- 调用离开回调函数,并将离开区域的角色信息传入
			leaveCallback(role)
		end
	)
end

注:代码位置:TriggerAreaHandler.lua

Lua 定义UIHandler类,处理玩家界面UI

  • 定义UIHandler类,初始化玩家界面UI显示
lua
-- 构造函数
function UIHandler:ctor()
	-- 初始化按钮处理器映射
	self._role2BtnHandlers = {}

	-- 遍历所有有效角色
	for _, role in ipairs(GameAPI.get_all_valid_roles()) do
		-- 隐藏商品操作按钮
		role.set_node_visible(UINodes["商品操作按钮"], false)
		-- 设置初始金币数量
		role.set_kv_by_type(Enums.ValueType.Int, "money", 200)
		-- 更新角色金币UI显示
		self:updateRoleMoneyUI(role)
	end

	-- 注册商品操作按钮点击事件
	LuaAPI.global_register_custom_event("点击商品操作按钮", function(_, _, data)
		local role_id = data.role_id
		local handler = self._role2BtnHandlers[role_id]
		if handler then
			handler()
		end
	end)
end

注:代码位置:UIHandler.lua

  • 当购买或出售商品时,更新玩家金币显示
lua
-- 更新玩家金币显示
function UIHandler:updateRoleMoneyUI(role)
	-- 获取角色当前金币数量
	local money = role.get_kv_by_type(Enums.ValueType.Int, "money")
	-- 更新金币数量显示
	role.set_label_text(UINodes["金币数量"], tostring(money))
end

注:代码位置:UIHandler.lua

  • 商品出售和购买操作按钮使用同一个UI控件,通过使用UIHandler类中的setItemOpHandler方法,将按钮文本和点击回调绑定到控件上
lua
-- 更新操作按钮文本和点击回调
function UIHandler:setItemOpHandler(role, text, handler)
	local btn = UINodes["商品操作按钮"]
	-- 根据文本是否为空来设置按钮可见性
	role.set_node_visible(btn, text ~= "")
	-- 设置按钮文本
	role.set_button_text(btn, text)
	-- 存储角色对应的按钮处理函数
	self._role2BtnHandlers[role.get_roleid()] = handler
end

注:代码位置:UIHandler.lua

Lua 定义ItemMangaer类,用于处理整个游戏的物品管理

  • 初始化商品管理器,读取ItemData数据,建立ID到数据的映射
lua
function ItemManager:ctor()
	-- 初始化角色举起物品的映射表
	self._char2lift = {}
	-- 初始化物品ID到物品数据的映射表
	self._id2ItemData = {}

	-- 遍历所有物品数据,建立ID到数据的映射
	for _, itemData in pairs(ItemData) do
		self._id2ItemData[itemData.prefabID] = itemData
	end

	-- 获取所有有效角色
	local allRoles = GameAPI.get_all_valid_roles()
	-- 为每个角色注册事件
	for _, role in ipairs(allRoles) do
		local character = role.get_ctrl_unit()
		self:registerEvents(character)
	end
end

注:代码位置:ItemManager.lua

  • 注册事件,当角色举起物品时,记录该物品信息;当角色放下物品时,销毁该物品
lua
function ItemManager:registerEvents(character)
	-- 注册角色举起物品的事件
	LuaAPI.unit_register_trigger_event(character, { EVENT.SPEC_LIFEENTITY_LIFT_BEGAN }, function(_, _, data)
		local itemObj = data.lifted_unit
		-- 禁用物品的物理效果
		itemObj.set_physics_active(false)
		-- 记录角色举起的物品信息
		self._char2lift[character.get_role_id()] =
			{ itemObj = itemObj.get_bound_equipment(), itemData = self._id2ItemData[itemObj.get_key()] }
	end)

	-- 注册角色放下物品的事件
	LuaAPI.unit_register_trigger_event(character, { EVENT.SPEC_LIFEENTITY_LIFT_ENDED }, function(_, _, data)
		local unit = data.lift_unit
		local itemObj = data.lifted_unit
		-- 重新启用物品的物理效果
		itemObj.set_physics_active(true)
		-- 计算放下物品的位置
		local direction = unit.get_orientation():apply(math.Vector3(0, 0, 1))
		itemObj.set_position(unit.get_position() + direction)
		-- 清除角色举起物品的记录
		self._char2lift[character.get_role_id()] = nil
	end)
end

注:代码位置:ItemManager.lua

  • 通过传入itemKey和目标位置旋转,创建商品
lua

-- 创建商品
function ItemManager:createItem(itemKey, targetPos, targetRot)
	local itemData = ItemData[itemKey]
	local prefabId = itemData.prefabID
	-- 在指定位置创建物品
	local itemObj = GameAPI.create_equipment(prefabId, targetPos)
	local unit = itemObj.get_unit()
	-- 设置物品朝向
	unit.set_orientation(targetRot or math.Quaternion(0, 0, 0))
	local item = { name = itemKey, itemData = itemData, itemObj = itemObj }
	-- 设置物品可被举起
	unit.set_lifted_enabled(true)
	-- 禁用物品交互
	unit.set_interact_enabled(false)
	-- 启用自定义投掷力度
	unit.set_custom_thrown_force_enabled(true)
	-- 设置投掷力度为0
	unit.set_custom_thrown_force(math.toreal(0))
	return item
end

注:代码位置:ItemManager.lua

  • 销毁商品
lua

-- 销毁商品
function ItemManager:destroyItem(item)
	item.itemObj.destroy_equipment()
end

注:代码位置:ItemManager.lua

  • 购买商品,检查金钱是否足够,扣除金钱,更新UI显示,创建商品
lua
-- 购买商品
function ItemManager:buyItem(role, item)
	-- 获取角色当前金钱
	local money = role.get_kv_by_type(Enums.ValueType.Int, "money")
	local buyPrice = item.itemData.buyPrice

	-- 检查金钱是否足够
	if money < buyPrice then
		role.show_tips("金币不足")
		return false
	end

	-- 扣除金钱
	money = money - buyPrice
	role.set_kv_by_type(Enums.ValueType.Int, "money", money)
	-- 更新UI显示
	G.uiHandler:updateRoleMoneyUI(role)

	-- 创建并给予商品
	local character = role.get_ctrl_unit()
	self:createItem(item.name, character.get_position())
	return true
end

注:代码位置:ItemManager.lua

  • 出售商品,扣除金钱,更新UI显示,销毁商品
lua

-- 出售商品
function ItemManager:sellItem(role, item, sellPrice)
	-- 获取角色当前金钱
	local money = role.get_kv_by_type(Enums.ValueType.Int, "money")
	-- 增加金钱
	money = money + sellPrice
	role.set_kv_by_type(Enums.ValueType.Int, "money", money)
	-- 更新UI显示
	G.uiHandler:updateRoleMoneyUI(role)
	-- 销毁商品
	self:destroyItem(item)
end

注:代码位置:ItemManager.lua

  • 获取指定角色当前举起的商品信息
lua
-- 获取指定角色当前举起的商品信息
function ItemManager:getRoleLiftedItem(role)
	return self._char2lift[role.get_roleid()]
end

注:代码位置:ItemManager.lua

Lua 初始化商店并实现道具刷新和购买

  • 初始化商店,创建触发器处理器,并注册进入和离开出售区域的处理器
lua
for i = 1, 2 do
	-- 查询商品桌子单位
	local desk = LuaAPI.query_unit("商品桌子" .. i)
	-- 随机添加商品到桌子上
	self:randomAddItem(desk)

	-- 获取商品触发器
	local triggerArea = desk.get_child_by_name("商品触发器")
	-- 创建触发器处理器
	TriggerAreaHandler.new(triggerArea, function(role)
		-- 进入了出售区域, 显示玩家购买按钮
		G.uiHandler:setItemOpHandler(role, "购买", function()
			local deskUnitId = LuaAPI.get_unit_id(desk)
			local item = self.deskItems[deskUnitId]
			-- 尝试购买商品
			-- 购买商品
			if item and G.itemManager:buyItem(role, item) then
				-- 购买成功后,进入刷新状态
				self:enterRefreshState(desk)
				-- 销毁已购买的商品
				G.itemManager:destroyItem(item)
				-- 从桌子上移除商品
				self.deskItems[deskUnitId] = nil
			end
		end)
	end, function(role)
		-- 角色离开出售区域时,移除购买按钮
		G.uiHandler:setItemOpHandler(role, "", nil)
	end)
end

注:代码位置:BuyShop.lua

  • 实现道具刷新,并添加到桌子上
lua
function BuyShop:enterRefreshState(deskObj)
	-- 获取商品公告牌
	local billboard = deskObj.get_child_by_name("商品公告牌")
	-- 获取桌子的单位ID
	local unitId = LuaAPI.get_unit_id(deskObj)
	-- 定义更新倒计时的函数
	local function updateCountDown()
		local second = self.shopCountDowns[unitId].countDown
		second = second - 1
		self.shopCountDowns[unitId].countDown = second
		-- 更新公告牌显示的倒计时
		billboard.set_billboard_content("刷新倒计时#n #n" .. second)
		-- 倒计时结束时,随机添加新商品并取消定时器
		if second < 1 then
			self:randomAddItem(deskObj)
			LuaAPI.global_unregister_trigger_event(self.shopCountDowns[unitId].timerId)
		end
	end
	-- 注册定时器,每秒更新一次倒计时
	local timerId = LuaAPI.global_register_trigger_event({ EVENT.REPEAT_TIMEOUT, 1.0 }, function()
		updateCountDown()
	end)
	-- 初始化倒计时数据
	self.shopCountDowns[unitId] = { timerId = timerId, countDown = 4 }
	-- 立即更新一次倒计时
	updateCountDown()
end

注:代码位置:BuyShop.lua

  • 随机添加道具
lua
-- 根据概率随机选择一个道具上架出售
function BuyShop:randomAddItem(deskObj)
	local items = {}
	local totalWeight = 0
	-- 遍历所有商品数据,准备随机选择
	for itemKey, itemData in pairs(ItemData) do
		-- 将商品数据添加到items表中
		table.insert(items, itemKey)
		totalWeight = totalWeight + itemData.probability
	end
	-- 生成随机数
	local randomValue = LuaAPI.rand() * totalWeight
	-- 选择项目
	local currentWeight = 0
	for _, itemKey in ipairs(items) do
		currentWeight = currentWeight + ItemData[itemKey].probability
		if randomValue <= currentWeight then
			-- 将选中的商品绑定到指定的桌子上
			self:bindShopItem(itemKey, deskObj)
			return
		end
	end
end

注:代码位置:BuyShop.lua

  • 绑定商品到桌子上
lua
-- 在指定位置创建商品道具
function BuyShop:bindShopItem(itemKey, deskObj)
	local itemData = ItemData[itemKey]
	-- 计算商品的目标位置(在桌子上方2个单位)
	local targetPos = deskObj.get_position() + math.Vector3(0, 2, 0)
	-- 获取商品的旋转(如果没有指定,则使用默认值)
	local targetRot = itemData.rot or math.Quaternion(0, 0, 0)
	-- 创建商品
	local item = G.itemManager:createItem(itemKey, targetPos, targetRot)
	-- 获取桌子的单位ID
	local deskUnitId = LuaAPI.get_unit_id(deskObj)
	-- 将商品与桌子关联
	self.deskItems[deskUnitId] = item
	-- 设置商品不可被抓举和物理交互
	local unit = item.itemObj.get_unit()
	unit.set_lifted_enabled(false)
	unit.set_physics_active(false)
	-- 更新商品公告牌的内容
	local billboard = deskObj.get_child_by_name("商品公告牌")
	billboard.set_billboard_content(itemKey .. "#n #n$" .. itemData.buyPrice)
	return item
end

注:代码位置:BuyShop.lua

Lua 初始化贸易中心并实现更新公告和物品售卖

  • 初始化贸易中心,并注册进入和离开出售区域的处理器
lua
-- 添加商店的交互区
local triggerArea = LuaAPI.query_unit("出售区域触发器")
TriggerAreaHandler.new(triggerArea, function(role)
	-- 进入了出售区域, 显示玩家购买按钮
	G.uiHandler:setItemOpHandler(role, "出售", function()
		-- 按钮的事件
		self:onSellItem(role)
	end)
end, function(role)
	-- 角色离开出售区域时,移除购买按钮
	G.uiHandler:setItemOpHandler(role, "", nil)
end)

注:代码位置:TradeCenter.lua

  • 实现公告更新
lua
-- 注册一个定时器,实现定时刷新时间和出售倍率信息
LuaAPI.global_register_trigger_event({ EVENT.REPEAT_TIMEOUT, 1 }, function()
	self:refreshBillBoard()
end)

注:代码位置:TradeCenter.lua

lua
-- 更新出售商店公告, 时间, 折扣信息
function TradeShop:refreshBillBoard()
	-- 更新当前时间。这里使用分钟作为单位,每次调用方法时增加1分钟,并在达到24小时(1440分钟)时循环。
	self._curTime = (self._curTime + 1) % (24 * 60)
	-- 计算当前小时。使用整除运算 // 将分钟转换为小时。
	local hour = self._curTime // 60
	-- _discounts 是一个数组,用于存储出售倍率。
	self._curSellDiscount = self._discounts[hour]
	-- 找到出售公告牌
	local billboard = LuaAPI.query_unit("出售公告牌")
	-- 设置公告牌的文本
	billboard.set_billboard_text(
		string.format("%02d:%02d", self._curTime // 60, self._curTime % 60)
			.. "#n #n出售价格倍率:"
			.. string.format("%.1f", self._curSellDiscount)
	)
end

注:代码位置:TradeShop.lua

代码主逻辑

lua
-- 导入必要的模块
local SellShop = require("SellShop")
local BuyShop = require("BuyShop")
local ItemManager = require("ItemManager")
local UIHandler = require("UIHandler")

--- 唯一全局变量
G = {}

-- 游戏开始事件
LuaAPI.global_register_trigger_event({ EVENT.GAME_INIT }, function()
	-- 初始化物品管理器
	G.itemManager = ItemManager.new()
	-- 初始化购买商店
	G.buyShop = BuyShop.new()
	-- 初始化出售商店
	G.sellShop = SellShop.new()
	-- 初始化UI处理器
	G.uiHandler = UIHandler.new()
end)

注:代码位置:main.lua

编辑器配置

UI界面

  • 搭建金币显示以及商品操作按钮

UI界面

创建房子预设

  • 房子所有子组件合并,并存为预设

合并组件组

  • 出售公告牌、出售区域交互、出售区域触发器成为房子-出售商店子物体,方便组件管理

房子-出售商店

  • 使用蛋仔开发助手同步至VSCode侧,可在Data目录下找到相应数据文件

创建物品预设

  • 打开预设编辑器,在管理窗口下选择背包一栏的组件,将物品预设存为新预设

存为新预设

  • 新预设可在背包一栏下玩家自定义中查看

新预设位置

  • 选中当前新预设后,更多物品表现效果可以通过修改外观实现,点击外观预设弹出创生面板选择

修改表现效果

  • 使用蛋仔开发助手同步至VSCode侧,可在Data目录下找到相应数据文件