Skip to content

生存割草玩法

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

游戏简介

玩家通过击杀怪物获取经验,提升等级、血量、攻击力等属性。击杀本轮所有怪物后方可进入下一波攻势,新一波攻势也会更加猛烈。

技术要点

  • Lua 定义类和实例化对象

  • Lua 实现不同需求的随机数生成器

  • Lua 实现预制体工厂类,根据不同需求创建预制体

  • Lua 实现双端队列,允许两端进行插入和删除操作

  • Lua 实现分帧加载逻辑,实现性能优化

  • Lua 怪物管理类,管理所有怪物

  • Lua 实现怪物的AI逻辑,提升游戏体验

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

技术要点分析

定义类和实例化对象

本示例中多处使用(如:Hero、Monster、MonsterManager、HeroManager等)

注:面向对象编程思想请参考以往章节

类的属性和方法

遍历获取所有角色,创建英雄对象

lua
---构造函数,初始化英雄
---@param character Character 角色对象
function Hero:ctor(character)
	self.character = character -- 设置英雄的角色
	self.level = 1 -- 初始等级为1
	self.exp = 0 -- 初始经验值为0
	local role = GameAPI.get_role(self.character.get_role_id()) -- 获取角色对象
	self.setLevel(self.level) -- 设置初始等级
	role.set_progressbar_current(UINodes["经验值"], self.exp) -- 设置经验值进度条当前值
	role.set_progressbar_max(UINodes["经验值"], self:getLevelUpExp()) -- 设置经验值进度条最大值
end
-- 获取升到下一级级所需经验值
function Hero:getLevelUpExp()
	-- 代码省略 -- 
end
-- 设置英雄等级
function Hero:setLevel()
	-- 代码省略 -- 
end
-- 增加经验值
function Hero:addExp()
	-- 代码省略 -- 
end

注:代码位置:Hero.lua

lua
function HeroManager:ctor()
	-- 初始化英雄列表
	self.heroes = {}
	-- 遍历所有有效角色
	for _, role in ipairs(GameAPI.get_all_valid_roles()) do
		-- 获取角色控制的单位
		local character = role.get_ctrl_unit()
		-- 创建新的英雄对象
		local hero = Hero.new(character)
		-- 将英雄对象添加到列表中,以角色ID为键
		self.heroes[role.get_roleid()] = hero
	end
end

注:代码位置:HeroManager.lua

实现不同需求的随机数生成器

随机数生成器

获取圆环内的随机点

lua
function MathUtils.randCirclePoint(center, minRange, maxRange)
	-- LuaAPI.rand() 返回0~1之间的随机数
	-- LuaAPI.rand() * 2 * math.pi 返回0~2pi之间的随机数
	local radian = LuaAPI.rand() * math.pi * 2
	-- 最小范围 + 随机数 * 范围区间 返回区间内的随机点[minRange, maxRange]
	local distance = minRange + math.sqrt(LuaAPI.rand()) * (maxRange - minRange)
	-- 根据距离和弧度计算出随机点
	local offset = math.Vector3(math.cos(radian) * distance, 2, math.sin(radian) * distance)
	-- 将偏移量加上中心点,返回随机点
	return center + offset
end

注:代码位置:Utils/MathUtils.lua

根据权重获取对应的item

lua
function MathUtils.weightedRandomChoice(items, weights)
	-- 计算权重总和
	local totalWeight = 0
	-- 遍历权重列表,累加权重
	for _, weight in ipairs(weights) do
		totalWeight = totalWeight + weight
	end

	-- 生成随机数,随机数区间[0, totalWeight]
	local randomValue = LuaAPI.rand() * totalWeight

	-- 累加权重,判断随机数落在以上哪个区间就返回
	-- 0———————————————————10———————12—————————16
	-- |_____item_1________|_item_2_|__item_3__|
	local currentWeight = 0
	for i, item in ipairs(items) do
		-- 累加权重
		currentWeight = currentWeight + weights[i]
		-- 判断是否大于随机数
		if randomValue <= currentWeight then
			-- 返回当前item
			return i, item
		end
	end
	-- 理论上不会到达这里
	return #items, items[#items]
end

注:代码位置:Utils/MathUtils.lua

实现预制体工厂类,根据不同需求创建预制体

  • 预制体工厂类负责创建不同种类的预制体,如:怪物、英雄等
  • 创建预制体函数按照是否需要回调函数可以区分两种,回调函数调用实现通过分帧加载器实现
  • 按照预制体类型调用不同函数,如果传入回调函数则创建预制体后会调用回调函数

以下为预制体工厂类,以Monster为例

lua
G.prefabFactory:createPrefabWithCb(
		-- 预设类型
		PrefabType.UNIT_CREATURE,
		-- 预设ID
		monsterConf.prefabID,
		-- 创建位置
		position,
		-- 创建旋转
		rotation,
		-- 创建缩放
		scale,
		-- 创建成功后的回调
		function(unit)
			self.unit = unit
			self:onCreatureLoaded()
		end
)

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

lua
---从缓存中返回对象,非阻塞
---@param prefabType string 预设类型(PrefabFactory.PrefabType)
---@param prefabID integer 预设ID,可从编辑器中查看
---@param pos Vector3 创建位置
---@param rot Quaternion 创建旋转
---@param scale Vector3|nil 创建缩放
---@param callback fun(obj: Unit|Obstacle|Creature|UnitGroup|Equipment|any)|nil 创建成功后的回调
---@return integer id
function PrefabFactory:createPrefabWithCb(prefabType, prefabID, pos, rot, scale, callback)
	-- 因以Monster为例,预设类型为PrefabType.UNIT_CREATURE
	-- 以prefabID为键,取对应的key缓存的数据
	local objs = self._recyleObjs[prefabID]
	local obj = nil
	-- 判断缓存中是否存在数据,若存在,则取出数据
	if objs and #objs > 0 then
		-- 取出数据
		obj = table.remove(objs)
		-- 设置模型的可见性
		obj.set_model_visible(true)
		-- 判断类型来决定是否开启物理
		if prefabType == self.PrefabType.UNIT_CREATURE then
			obj.set_physic_enable(true)
		end
	end
	-- 按照预设类型调用不同的函数,对应函数的返回值是,创建好的组件
	local func = self["_create" .. prefabType .. "WithCb"]
	-- 此处做了两个操作,一个是调用创建函数,一个是返回[创建函数返回的值] 注:创建函数的返回值接着被当前createPrefabWithCb函数返回
	-- 可分写两步
	-- 1. loacl target = fun(...)
	-- 2. return target
	return func(self, obj, prefabID, pos, rot, scale, 
		-- 以下内容为回调函数 注:此处的回调函数在创建函数中调用
		function(target)
		-- 为target组件设置自定义值
		target.set_kv_by_type(Enums.ValueType.Str, "type", prefabType)
		target.set_kv_by_type(Enums.ValueType.Int, "prefabID", prefabID)
		target.set_kv_by_type(Enums.ValueType.Vector3, "pos", pos)
		target.set_kv_by_type(Enums.ValueType.Quaternion, "rot", rot)
		-- 判断是否有回调函数,则执行回调函数
		if callback ~= nil then
			-- 调用回调函数时传入组件
			callback(target)
		end
	end)
end

注:代码位置:Utils/PrefabFactory.lua

根据传入的预制体类型和预制体ID,如果缓存中存在,则可复用直接返回;否则通过预制体类型调用不同的函数创建预制体。

lua
function PrefabFactory:_createUnitGroupWithCb(obj, prefabID, pos, rot, scale, callback)
	-- obj 为缓存中取到的数据
	--- prefabID 预设ID
	local phyPos = pos
	-- 会判断是否在缓存中取到数据,若为空,则创建预制体;若不为空,只需要设置位置和旋转
	if obj then
		obj.set_position(phyPos)
		obj.set_orientation(rot)
		if callback ~= nil then
			callback(obj)
		end
		return -1
	else
	-- 否则通过分帧加载器创建预制体
		return G.frameLoader:load(GameAPI.create_unit_group, function(target)
			if callback ~= nil then
				callback(target)
			end
		end, prefabID, phyPos, rot, nil)
	end
end

注:代码位置:Utils/PrefabFactory.lua

Lua实现双端队列,允许两端进行插入和删除操作

  • 双端队列的特点是可以从两端进行插入和删除操作,并且在队列中保持元素的顺序。
  • 分帧加载器会使用双端队列来实现分帧加载。使用尾部插入头部删除的方式,实现先进先加载,后进后加载。
lua
-- 创建新双端队列
function Deque:ctor(capacity)
	local DEFAULT_CAPACITY = 8
	self._capacity = math.max(capacity or DEFAULT_CAPACITY, 4) -- 当前缓冲区容量
	self._data = {} -- 存储数据的数组
	self._head = 0 -- 虚拟头指针(总指向下一个可插入位置)
	self._tail = 1 -- 虚拟尾指针
	self._size = 0 -- 当前元素数量
end

-- 尾部插入
function Deque:pushBack(value)
	if self._size == self._capacity then
		self:_resize(self._capacity * 2) -- 容量满后,扩容一倍空间
	end
	self._data[self._tail] = value -- 插入数据尾部
	self._tail = (self._tail + 1) % self._capacity -- 计算尾部index
	self._size = self._size + 1 -- 数量+1
end

-- 头部删除
function Deque:popFront()
	if self._size == 0 then -- 数量=0 返回空
		return nil
	end
	self._head = (self._head + 1) % self._capacity -- 计算头部index
	local value = self._data[self._head] -- 取出数据
	self._data[self._head] = nil -- 删除数据
	self._size = self._size - 1 -- 数量-1
	-- 容量大于8且数量少于1/4时,缩容一半
	if self._capacity > 8 and self._size < self._capacity // 4 then
		self:_resize(math.max(8, self._capacity // 2))
	end

	return value
end

注:代码位置:Utils/Deque.lua

Lua实现分帧加载逻辑,实现性能优化

  • 实现不需要在同一帧内创建预制体,将创建预制体分摊多帧执行。

  • 分帧加载器主要负责实现分帧加载预制体,提供load,cancelLoad函数。

  • 分帧加载器中update函数,此方法每帧调用一次更新分帧加载器的状态。

Lua
-- self.frameInterval 帧间隔
-- self.frameCount 每次加载的数量
-- self.loadQueue 加载队列
function FrameLoader:update()
	-- 当帧间隔大于0时,减1;否则设置为帧间隔
	if self.frameTick > 0 then -- 帧tick大于0时-1;小于等于0时,将帧间隔赋值
		self.frameTick = self.frameTick - 1
		return
	else
		self.frameTick = self.frameInterval
	end
	-- 通过队列获取需要加载的数量
	local loadCount = self.loadQueue:size() -- 队列大小
	if loadCount == 0 then
		return
	end
	local frameCount = self.frameCount -- 每次加载的数量
	while loadCount ~= 0 and frameCount > 0 do -- 队列中需要加载的预制体不等于0,且每次加载的数量大于0,进入循环。
		local item = self.loadQueue:popFront() -- 队列头部取出
		if item ~= nil and not item.dumped then -- 取出的数据不为空,且没有被取消加载
			local ret = item.func(table.unpack(item.args)) -- 调用自身创建预制体函数
			if item.cb ~= nil then -- 回调不为空,则调用
				item.cb(ret)
			end
			frameCount = frameCount - 1 -- 成功加载一个预制体,所以每次加载的数量-1
		end
		loadCount = loadCount - 1 -- 成功加载一个预制体,所以需要加载的数量也要-1
	end
end

注:代码位置:Utils/FrameLoader.lua

lua
-- 游戏初始化时添加到可更新列表中
G.tickables = {
		G.frameLoader,
	}
-- 每帧遍历可更新列表,并调用update函数
local function onPreTick(_)
	for _, v in ipairs(G.tickables) do
		v:update()
	end
end
	-- 设置帧更新处理函数
	LuaAPI.set_tick_handler(onPreTick, onPostTick)

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

Lua 怪物管理,管理所有怪物

Lua
-- 开始生成怪物
G.monsterManager:startSpawn()

代码位置:main.lua

lua
function MonsterManager:startSpawn()
	-- 开启怪物生成倒计时 
	self:interWaveCountDown(10) 
end

> 代码位置:MonsterManager.lua

-- 波次间隔倒计时
function MonsterManager:interWaveCountDown(countdown)
	local triggerId = nil
	self:updateCountDownUI(countdown, true) -- 显示倒计时UI
	local function _timerFunc(eventName, actor, data)
		countdown = countdown - 1
		self:updateCountDownUI(countdown, true) -- 每秒更新UI

		if countdown <= 0 then 
			if triggerId ~= nil then
				-- 倒计时结束且有全局触发器返回的id,就取消全局触发器,关闭UI,开始怪物生成一波
				LuaAPI.global_unregister_trigger_event(triggerId)
				self:updateCountDownUI("", false)
				self:nextSpawnWave()
			end
		end
	end
	-- 将_timerFunc注册到全局触发器中
	triggerId = LuaAPI.global_register_trigger_event({ EVENT.REPEAT_TIMEOUT, 1.0 }, _timerFunc)
end

代码位置:MonsterManager.lua

Lua
-- 开始下一波怪物生成
function MonsterManager:nextSpawnWave()
	self:startSpawnWave(self.waveCount + 1) -- waveCount初始化时为1
end

代码位置:MonsterManager.lua

Lua
-- 开始指定波次的怪物生成
function MonsterManager:startSpawnWave(waveCount)
	self.waveCount = math.min(waveCount, #MonsterSpawnWaveData) -- 将当前波的数量做限制
	self.currWaveData = MonsterSpawnWaveData[self.waveCount] -- Lua文件中每波怪物的配置数据
	self.currWave = SpawnWave.new(self, self.currWaveData) -- new一个每波怪物的对象
	self.currWave:start() -- 开始生成怪物一波
end

代码位置:MonsterManager.lua

Lua
-- 开始生成怪物波次
function SpawnWave:start()
	self.startedTime = 0

	-- 定义定时器回调函数
	local function _timerFunc(eventName, actor, data)
		local numMonster = #self.owner.monsters -- 此处的owner,代表的是MonsterManager类的对象
		-- 检查是否达到最大怪物数量或怪物池是否为空
		if numMonster >= self.maxNum or #self.monsters <= 0 then
			return
		end

		-- 计算权重并选择怪物类型
		local weights = {}
		for _, item in ipairs(self.monsters) do
			table.insert(weights, item.data.weight)
		end
		local idx, monsterSpawnData = MathUtils.weightedRandomChoice(self.monsters, weights)
		local range = monsterSpawnData.data.range
		-- 计算本次生成的怪物数量
		-- (最大数量- 当前怪物数量 = 剩余需要生成怪物数量), maxNumOnce =  单次生成最大数量
		local spawnNum = math.min(self.maxNum - numMonster, MathUtils.randint(1, self.maxNumOnce)) -- 将两个数量比较取最小的返回
		spawnNum = math.min(spawnNum, monsterSpawnData.count) -- 生成数量与每种怪物生成的数量比较,返回最小值
		-- 随机选择一个角色作为生成中心
		local roles = GameAPI.get_all_valid_roles()
		local role = MathUtils.randomChoice(roles)
		local character = role.get_ctrl_unit()
		-- 获取地图边界
		local boundaryXMin = Consts.MAP_BOUNDARY_X[1]
		local boundaryXMax = Consts.MAP_BOUNDARY_X[2]
		local boundaryZMin = Consts.MAP_BOUNDARY_Z[1]
		local boundaryZMax = Consts.MAP_BOUNDARY_Z[2]
		-- 生成怪物
		for _ = 1, spawnNum do
			-- 超出地图外需要重新随机位置
			local maxTryCount = 5
			local pos
			repeat
				pos = MathUtils.randCirclePoint(character.get_position(), range[1], range[2]) -- 在圆环中随机生成随机点
				maxTryCount = maxTryCount - 1
				-- 没有找到合法位置的情况
				if maxTryCount == 0 then -- 尝试五次,都不在合理位置区域内,就退出
					break
				end
			until pos.x > boundaryXMin and pos.x < boundaryXMax and pos.z > boundaryZMin and pos.z < boundaryZMax
			if maxTryCount ~= 0 then -- 尝试机会没有用尽,进入执行
				-- 生成怪物并设置随机朝向
				local yaw = LuaAPI.rand() * math.pi * 2
				self.owner:createMonster( -- 调用MonsterManager类的创建怪物寒素
					monsterSpawnData.data.key, --生成怪物的key
					pos,
					math.Quaternion(0, yaw, 0),
					function(monster, dmgSrc) -- 传入死亡回调函数
						self:onMonsterDie(monster, dmgSrc) -- 处理英雄加经验,从怪物列表中移除,检查是否所有怪物都死亡,是否开始下一波怪物攻击。
					end
				)

				-- 更新怪物数量
				monsterSpawnData.count = monsterSpawnData.count - spawnNum
				if monsterSpawnData.count <= 0 then
					table.remove(self.monsters, idx) -- 该种怪物全部生成后,就移除怪物队列
				end
			end
		end
		self.startedTime = self.startedTime + self.spawnInterval
	end

	-- 先立刻触发一次
	_timerFunc()
	-- 注册定时器事件
	self._spawnTimerId = LuaAPI.global_register_trigger_event({ EVENT.REPEAT_TIMEOUT, self.spawnInterval }, _timerFunc)
end

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

Lua实现怪物的AI逻辑,提升游戏体验

  • 怪物创建后的回调函数中注册定期执行AI逻辑的事件;添加AI逻辑到Monster.globalTriggerEvents中,怪物销毁时会自动移除AI逻辑。
  • 使用LuaAPI.global_register_trigger_event注册AI逻辑,调用间隔为thinkInterval(AI思考间隔)=0.2。
  • 怪物AI状态:移动、攻击、死亡。
lua
-- 更新怪物状态
-- 因为onCreatureLoaded函数会将自身添加到可更新对象列表中,所以update每帧调用
function Monster:update()
	-- 如果卡住超过3次,尝试解除卡住状态
	if self.isStucked >= 3 then
		local unitPos = self.unit.get_position()
		if self.target then
			-- 如果有目标,稍微上移位置
			self.unit.set_position(unitPos + math.Vector3(0, 0.3, 0))
		else
			-- 如果没有目标,重新获取巡逻位置
			self.patrolPos = self:getPatrolPos(unitPos)
		end
	end
end

注:代码位置:Monster.lua

lua
-- 怪物创建完成后的回调函数
function Monster:onCreatureLoaded()
	-- 将自身添加到可更新对象列表中
	G.addTickable(self) -- 此处对应main.py中 function G.addTickable(obj)

	-- 初始化全局触发事件列表
	self.globalTriggerEvents = {} -- 存放当前怪物的全局触发器,在当前怪物销毁时同一将全局触发器取消掉

	-- 延迟随机时间后注册AI更新事件
	LuaAPI.call_delay_time(LuaAPI.rand(), function()
		if not self.unit then
			return
		end

		-- 注册定期执行AI逻辑的事件
		table.insert( -- 将当前全局触发器添加到全局触发器事件列表
			self.globalTriggerEvents,
			LuaAPI.global_register_trigger_event({ EVENT.REPEAT_TIMEOUT, self.thinkInterval }, function()
				if self.aiEnable then
					self:tickAI() -- 循环调用,调用间隔为thinkInterval
				end
			end)
		)
	end)

	-- 注册怪物死亡事件
	self.deadDestroyHandle = LuaAPI.unit_register_trigger_event(
		self.unit,
		{ EVENT.SPEC_LIFEENTITY_DIE }, -- 指定生命体被击败
		function(_, _, data)
			self.dead = true
			if self.deadDestroyCb then
				self.deadDestroyCb(self, data.dmg_unit) -- 调用销毁的回调函数
			end
			self:onDeadDestroy() -- 调用怪物的销毁函数
		end
	)

	-- 设置AI启用状态
	self:setAIEnable(self.aiEnable)
end

注:代码位置:Monster.lua

lua
-- AI逻辑更新
function Monster:tickAI()
	-- 检查当前目标是否有效
	if self.target then
		local unitPos = self.unit.get_position() -- 得到当前怪物的位置
		if not self:validateTarget(self.target, unitPos, self.aiConf.targetGiveupDist) then -- 判断目标和当前怪物是否是有效目标
			self.target = nil

			-- 目标失效加快下次索敌时机
			self.targetSearchCD = self.targetSearchCD - (SEARCH_CD_IF_TARGET - SEARCH_CD_IF_NOTARGET)
		end
	end

	-- 更新目标搜索冷却时间
	self.targetSearchCD = self.targetSearchCD - self.thinkInterval
	if self.targetSearchCD <= 0 then
		self.target = self:searchClosestTarget() -- 寻找最近的一个目标返回
		if self.target then
			self.targetSearchCD = SEARCH_CD_IF_TARGET -- 有目标时,cd用有目标的
		else
			self.targetSearchCD = SEARCH_CD_IF_NOTARGET -- 无目标时,cd用无目标的,无目标会更快的进入索敌
		end
	end

	local isStucked = false
	local newPos = nil
	-- 如果有目标,移动并攻击
	if self.target then
		self.state = self:moveToAttack(self.target) -- 移动并攻击目标
		if self.state == "move" then
			local targetPos = self.target.get_position()
			newPos = self.unit.get_position()
			-- 检查是否卡住
			if targetPos.y - newPos.y > (self.isStucked >= 3 and -1000 or 0.2) then -- 三元运算符的简化形式 
				-- 如果 self.isStucked >= 3 为真,则结果为 -1000
				-- 如果 self.isStucked >= 3 为假,则结果为 0.2
				-- 再比较t argetPos.y - newPos.y > 真假的返回值
				local oldPos = self.lastMovePos
				if oldPos then
					local moved = newPos - oldPos -- 计算从上一位置到新位置的移动向量
					moved = moved - math.Vector3(0, moved.y, 0) -- 移除垂直方向(y轴)的移动,只关注水平面的移动。
					-- 计算移动向量与目标方向;并获取单位向量后计算点积的大小,点击点积小于 0.05 表示移动方向与目标方向几乎垂直或相反,认为可能卡住
					isStucked = moved:dot((targetPos - newPos):getUnit()) < 0.05
				end
				self.lastMovePos = newPos -- 更新移动位置
			end
		end
	else
		-- 如果没有目标,进行巡逻
		self.state = self:patrol() -- 此函数功能是执行训练行为
		if self.state == "move" then
			newPos = self.unit.get_position()
			local oldPos = self.lastMovePos
			if oldPos then
				local moved = newPos - oldPos
				moved = moved - math.Vector3(0, moved.y, 0)
				-- 如果单位在正常移动,它应该在每次更新之间移动一定的距离
				-- 如果单位卡住了,它的位置几乎不会改变,因此两个时间点之间的移动距离会非常小
				isStucked = moved:length() < 0.05
			end
		end
	end

	-- 更新卡住状态
	if self.state == "move" then
		assert(newPos)
		self.isStucked = math.min(math.max(0, self.isStucked + (isStucked and 1 or -1)), 3) -- 最多卡住三次
		self.lastMovePos = newPos
	else
		self.isStucked = 0
		self.lastMovePos = nil
	end
	assert(self.state ~= nil)
end

注:代码位置:Monster.lua

lua
-- 移动并攻击目标
function Monster:moveToAttack(target)
	local aiConf = self.aiConf -- 该怪物的AI配置 可以查看MonsterData.lua文件中每个怪物的ai
	local unitPos = self.unit.get_position()
	local targetPos = target.get_position()
	local direction = targetPos - unitPos -- 得到目标和当前怪物的方向
	local yDistance = direction.y
	local roleHeight = 1.8
	direction = direction - math.Vector3(0, direction.y, 0) -- 去除y值得影响
	local dirLength = direction:length() -- 长度代表距离
	-- 检查位置、高度和方向是否有效
	local isPosValid = dirLength <= aiConf.attackDist -- 是否攻击距离
	local isNotHigher = yDistance <= aiConf.attackHeight -- 是否是攻击高度
	local isNotLower = yDistance >= -roleHeight -- 是否是攻击高度
	local isHeightValid = isNotHigher and isNotLower --高度检查是否有效
	local isDirValid = self.unit.get_direction():dot(direction) / dirLength > aiConf.attackCosMin -- 方向检查是否有效

	if isPosValid and isHeightValid and isDirValid then
	 	-- 都有效才能执行下面语句
		local attackPos = unitPos + math.Vector3(0, 1.2, 0)
		if aiConf.attack_offset then
			attackPos = unitPos + self.unit.get_orientation():apply(aiConf.attack_offset) -- 计算攻击位置
		end

		local attackDir = targetPos + math.Vector3(0, 1.2, 0) - attackPos -- 计算攻击方向
		-- 如果是远程攻击,检查是否有障碍物
		if aiConf.isShoot then
			local shootObstacle = nil
			GameAPI.raycast_unit( -- 射线检测判断是否有障碍物
				attackPos,
				attackPos + attackDir,
				{ Enums.UnitType.OBSTACLE },
				function(unit, point, normal)
					shootObstacle = { unit, point, normal }
				end
			)
			if shootObstacle then
				isPosValid = false -- 有障碍物就是位置无效
			end
		end

		if isPosValid then
			-- 如果有射击散布,计算散布后的攻击方向
			if aiConf.shootScatter then
				local normDir = attackDir:clone()
				normDir:normalize()
				local scatter = attackDir:length() * aiConf.shootScatter -- 散布量与攻击距离和配置散步得系数成正比
				local left = math.Vector3(0, 1, 0):cross(normDir) -- 叉乘可以得到垂直于normDir和向上方向得一个向量
				attackDir = attackDir + left * (LuaAPI.rand() - 0.5) * 2 * scatter -- 将随机偏移应用到原攻击方向上
			end

			self.unit.force_stop_move() -- 停止移动
			self.unit.cast_ability_by_ability_slot_and_direction(attackDir, 5, 0.0) ---控制角色对目标方向释放指定槽位技能
			return "skill" -- 释放技能状态
		end
	end

	-- 如果目标位置有效但高度不够,尝试跳跃
	if isPosValid and not isNotHigher and isDirValid then
		self.unit.jump() -- 只有高度不合适,就跳跃
		return "move"
	end

	-- 如果无法攻击,继续移动
	self.unit.force_start_move(direction, 1.0) -- 开始移动
	return "move"
end

注:代码位置:Monster.lua

代码主逻辑

  • 使用LuaAPI.global_register_trigger_event注册游戏逻辑
lua
LuaAPI.global_register_trigger_event({ EVENT.GAME_INIT }, function()
	-- 整个游戏初始化逻辑
end
)
  • 为所有角色设置状态和重生属性
lua
for _, role in ipairs(GameAPI.get_all_valid_roles()) do
	local character = role.get_ctrl_unit()
	character.set_reborn_in_place(true, false)
	-- 创建并装备剑
	local sword = GameAPI.create_equipment(ItemData.Sword.prefabID, character.get_position())
	character.swap_equipment_slot(sword, Enums.EquipmentSlotType.EQUIPPED, 1)
	-- 创建并装备枪
	local gun = GameAPI.create_equipment(ItemData.Gun.prefabID, character.get_position())
	character.swap_equipment_slot(gun, Enums.EquipmentSlotType.EQUIPPED, 2)
	end
  • 初始化各种管理器
lua
-- 初始化各种管理器
G.prefabFactory = PrefabFactory.new() -- 预设加载工厂类
G.frameLoader = FrameLoader.new(1, 1) -- 分帧加载器
G.monsterManager = MonsterManager.new() -- 怪物管理器
G.heroManager = HeroManager.new() -- 英雄管理器
  • 处理可更新对象函数
lua
-- 设置可更新对象列表
	G.tickables = {
		G.frameLoader,
	}

	-- 添加可更新对象的函数
	function G.addTickable(obj)
		assert(obj.update)
		table.insert(G.tickables, obj)
	end

	-- 移除可更新对象的函数
	function G.removeTickable(obj)
		for i, v in ipairs(G.tickables) do
			if v == obj then
				table.remove(G.tickables, i)
				break
			end
		end
	end

	-- 定义每帧更新前的处理函数
	local function onPreTick(_)
		for _, v in ipairs(G.tickables) do
			v:update()
		end
	end

	-- 定义每帧更新后的处理函数(当前为空)
	local function onPostTick() end

	-- 设置帧更新处理器
	LuaAPI.set_tick_handler(onPreTick, onPostTick)
  • 开始生成怪物
lua
-- 初始化怪物表
G.monsters = {}
-- 开始生成怪物
G.monsterManager:startSpawn()

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

编辑器配置

UI界面

  • 搭建经验值、等级、经验进度条等UI

UI界面

创建武器预设及技能自定义

  • 将现有武器预设另存为新预设,修改预设属性进行自定义

武器新预设

新的武器预设与原来武器预设配置相同;修改新武器预设配置且不会影响原来武器预设,可以实现武器的自定义。

  • 武器主动技能自定义,通过更改主动技能实现

主动技能

  • 将现有技能预设另存为新预设,修改预设属性进行自定义

技能新预设

新的技能预设与原来技能预设配置相同;修改新技能预设配置不会影响原来技能预设,可以实现技能的自定义。

锚点编辑

添加锚点

新UGC技能勾选后,即可显示锚点编辑器,可以实现增加锚点实现技能自定义。

注:添加锚点->自定义选项时,需要在触发器编辑器中处理锚点开始时逻辑

  • 自定义锚点,打开触发器编写逻辑

点击武器触发器

触发器逻辑

打开触发器编辑器,找到自定义锚点名称下的触发器编写逻辑;当锚点开始时,触发器会被触发。

创建怪物预设及技能自定义

  • 将现有怪物预设另存为新预设,修改预设属性进行自定义

怪物新预设

  • 修改怪物技能,实现怪物技能的自定义

怪物技能

  • 将现有技能预设另存为新预设,修改预设属性进行自定义

注:怪物技能自定义与武器技能自定义配置流程相同

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

导出数据

Prefab.lua

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

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