生存割草玩法
你可以在PC编辑器中新建地图,选择Lua模板图中的生存割草玩法,查看本示例的完整工程
游戏简介
玩家通过击杀怪物获取经验,提升等级、血量、攻击力等属性。击杀本轮所有怪物后方可进入下一波攻势,新一波攻势也会更加猛烈。
技术要点
Lua 定义类和实例化对象
Lua 实现不同需求的随机数生成器
Lua 实现预制体工厂类,根据不同需求创建预制体
Lua 实现双端队列,允许两端进行插入和删除操作
Lua 实现分帧加载逻辑,实现性能优化
Lua 怪物管理类,管理所有怪物
Lua 实现怪物的AI逻辑,提升游戏体验
注:本示例可在编辑器模板图中下载。
技术要点分析
定义类和实例化对象
本示例中多处使用(如:Hero、Monster、MonsterManager、HeroManager等)
注:面向对象编程思想请参考以往章节
遍历获取所有角色,创建英雄对象
---构造函数,初始化英雄
---@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
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
实现不同需求的随机数生成器
获取圆环内的随机点
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
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为例
G.prefabFactory:createPrefabWithCb(
-- 预设类型
PrefabType.UNIT_CREATURE,
-- 预设ID
monsterConf.prefabID,
-- 创建位置
position,
-- 创建旋转
rotation,
-- 创建缩放
scale,
-- 创建成功后的回调
function(unit)
self.unit = unit
self:onCreatureLoaded()
end
)
注:以上代码简化,详细代码位置:Monster.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,如果缓存中存在,则可复用直接返回;否则通过预制体类型调用不同的函数创建预制体。
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实现双端队列,允许两端进行插入和删除操作
- 双端队列的特点是可以从两端进行插入和删除操作,并且在队列中保持元素的顺序。
- 分帧加载器会使用双端队列来实现分帧加载。使用尾部插入头部删除的方式,实现先进先加载,后进后加载。
-- 创建新双端队列
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函数,此方法每帧调用一次更新分帧加载器的状态。
-- 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
-- 游戏初始化时添加到可更新列表中
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 怪物管理,管理所有怪物
-- 开始生成怪物
G.monsterManager:startSpawn()
代码位置:main.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
-- 开始下一波怪物生成
function MonsterManager:nextSpawnWave()
self:startSpawnWave(self.waveCount + 1) -- waveCount初始化时为1
end
代码位置:MonsterManager.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
-- 开始生成怪物波次
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状态:移动、攻击、死亡。
-- 更新怪物状态
-- 因为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
-- 怪物创建完成后的回调函数
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
-- 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
-- 移动并攻击目标
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注册游戏逻辑
LuaAPI.global_register_trigger_event({ EVENT.GAME_INIT }, function()
-- 整个游戏初始化逻辑
end
)
- 为所有角色设置状态和重生属性
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
- 初始化各种管理器
-- 初始化各种管理器
G.prefabFactory = PrefabFactory.new() -- 预设加载工厂类
G.frameLoader = FrameLoader.new(1, 1) -- 分帧加载器
G.monsterManager = MonsterManager.new() -- 怪物管理器
G.heroManager = HeroManager.new() -- 英雄管理器
- 处理可更新对象函数
-- 设置可更新对象列表
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)
- 开始生成怪物
-- 初始化怪物表
G.monsters = {}
-- 开始生成怪物
G.monsterManager:startSpawn()
注:以上代码简化,详细代码位置:main.lua
编辑器配置
UI界面
- 搭建经验值、等级、经验进度条等UI
创建武器预设及技能自定义
- 将现有武器预设另存为新预设,修改预设属性进行自定义
新的武器预设与原来武器预设配置相同;修改新武器预设配置且不会影响原来武器预设,可以实现武器的自定义。
- 武器主动技能自定义,通过更改主动技能实现
- 将现有技能预设另存为新预设,修改预设属性进行自定义
新的技能预设与原来技能预设配置相同;修改新技能预设配置不会影响原来技能预设,可以实现技能的自定义。
新UGC技能勾选后,即可显示锚点编辑器,可以实现增加锚点实现技能自定义。
注:添加锚点->自定义选项时,需要在触发器编辑器中处理锚点开始时逻辑
- 自定义锚点,打开触发器编写逻辑
打开触发器编辑器,找到自定义锚点名称下的触发器编写逻辑;当锚点开始时,触发器会被触发。
创建怪物预设及技能自定义
- 将现有怪物预设另存为新预设,修改预设属性进行自定义
- 修改怪物技能,实现怪物技能的自定义
- 将现有技能预设另存为新预设,修改预设属性进行自定义
注:怪物技能自定义与武器技能自定义配置流程相同
新预设同步到Lua编程中使用
编辑器侧:蛋仔开发助手已连接状态下且修改已经保存。
VSCode侧:蛋仔开发助手已连接状态下,需勾选预设标号、物品预设后点击导出数据即可在Data目录下找到相应数据文件。