# NPC系统与行为树 ## npc_manager.lua ```lua local CONST = require 'config.const' local M = {} local npc_list = {} ----------------------------------------------------------- -- 初始化所有NPC ----------------------------------------------------------- function M.init() local model_pool = CONST.NPC_MODEL_POOL local spawn_areas = y3.area.get_circle_areas_by_tag('npc_spawn') local roads = M.collect_roads() for i = 1, CONST.NPC_COUNT do local npc_type = M.pick_npc_type() local spawn = spawn_areas[math.random(#spawn_areas)]:random_point() local model = model_pool[math.random(#model_pool)] local npc = y3.unit.create_unit( CONST.NPC_PLAYER, CONST.NPC_UNIT_KEYS[npc_type], spawn, math.random(0, 360) ) npc:replace_model(model) npc:set_attr('最大生命', 1, '基础') npc:set_hp(1) npc:add_tag('npc') npc:add_tag('npc_' .. npc_type) npc:storage_set('npc_type', npc_type) npc:storage_set('model_key', model) npc:storage_set('is_panicked', false) -- 分配路径 if npc_type == 'patrol' or npc_type == 'civilian' then local road = roads[math.random(#roads)] npc:move_along_road(road, 1, false, true, true) npc:storage_set('assigned_road', road) end -- 商贩型固定不动 if npc_type == 'vendor' then npc:stop() end -- 闲逛型区域漫游 if npc_type == 'wanderer' then M.start_wander(npc, spawn) end table.insert(npc_list, npc) end end ----------------------------------------------------------- -- NPC类型权重 ----------------------------------------------------------- function M.pick_npc_type() local r = math.random(100) if r <= 50 then return 'civilian' elseif r <= 70 then return 'patrol' elseif r <= 85 then return 'vendor' else return 'wanderer' end end ----------------------------------------------------------- -- 收集地图中的Road ----------------------------------------------------------- function M.collect_roads() -- 在编辑器中预设Road并通过tag获取 -- 此处需根据实际地图配置 return CONST.ROAD_LIST end ----------------------------------------------------------- -- 闲逛AI:在区域内随机移动 ----------------------------------------------------------- function M.start_wander(npc, center_point) local function wander_step() if not npc:is_alive() then return end if npc:storage_get('is_panicked') then return end local wander_area = y3.area.create_circle_area(center_point, 15) local target = wander_area:random_point() wander_area:remove() npc:move_to_pos(target) -- 到达后随机等待再移动 y3.timer.wait(math.random(5, 12), function() wander_step() end) end wander_step() end ----------------------------------------------------------- -- 恐慌触发 ----------------------------------------------------------- function M.trigger_panic(source_point, radius) local panic_area = y3.area.create_circle_area(source_point, radius) local units_in = panic_area:get_all_unit_in_area() panic_area:remove() for _, u in ipairs(units_in) do if u:has_tag('npc') and u:is_alive() then u:storage_set('is_panicked', true) -- 朝远离source的方向跑 local npc_pos = u:get_point() local flee_point = M.calc_flee_point(npc_pos, source_point, 20) u:move_to_pos(flee_point) -- 恢复 local recover_time = math.random(8, 12) y3.timer.wait(recover_time, function() if not u:is_alive() then return end u:storage_set('is_panicked', false) -- 恢复正常行为 local road = u:storage_get('assigned_road') if road then u:move_along_road(road, 1, false, true, true) else local npc_type = u:storage_get('npc_type') if npc_type == 'vendor' then u:stop() end end end) end end end function M.calc_flee_point(npc_pos, danger_pos, distance) -- 简化:取反方向的点 local dx = npc_pos:get_x() - danger_pos:get_x() local dy = npc_pos:get_y() - danger_pos:get_y() local len = math.sqrt(dx*dx + dy*dy) if len < 0.01 then dx, dy = 1, 0; len = 1 end local nx, ny = dx/len * distance, dy/len * distance return y3.point.create(npc_pos:get_x() + nx, npc_pos:get_y() + ny) end ----------------------------------------------------------- -- 缩减NPC数量(紧迫期) ----------------------------------------------------------- function M.reduce_npc_count(ratio) local remove_count = math.floor(#npc_list * ratio) for i = 1, remove_count do local idx = math.random(#npc_list) local npc = npc_list[idx] if npc:is_alive() then npc:remove() end table.remove(npc_list, idx) end end return M ``` --- ## NPC互动点 互动点在编辑器中以圆形区域 + 标签实现: | 编辑器标签 | 互动类型 | NPC到达后行为 | |-----------|---------|-------------| | `interact_bench` | 长椅 | `play_animation('sit')` 8~15秒 | | `interact_board` | 告示栏 | `play_animation('look')` 5~10秒 | | `interact_well` | 水井 | `play_animation('wash')` 5~8秒 | | `interact_vendor` | 摊位 | `play_animation('trade')` 6~12秒 | ```lua -- 互动点检测(在civilian_ai中每次到达路径终点时检查) local function try_interact(npc) local interact_areas = y3.area.get_circle_areas_by_tag('interact_bench') -- 合并其他类型... for _, area in ipairs(interact_areas) do if area:is_point_in_area(npc:get_point()) then npc:stop() npc:play_animation('sit', 1.0, 0, -1, false, true) local wait = math.random(8, 15) y3.timer.wait(wait, function() npc:stop_cur_animation() -- 继续行走 local road = npc:storage_get('assigned_road') if road then npc:move_along_road(road, 1, false, true, true) end end) return true end end return false end ``` --- ## NPC行为树结构(概念) Y3的NPBehave通过Blackboard驱动。对于本项目,由于NPC行为相对简单,**推荐使用Timer+状态机替代完整行为树**,降低复杂度: ```lua -- NPC状态: idle / moving / interacting / panicked local function npc_tick(npc) local state = npc:storage_get('ai_state') or 'idle' if npc:storage_get('is_panicked') then return -- 恐慌期间由panic逻辑接管 end if state == 'idle' then -- 随机决定下一步 if math.random(100) <= 30 then try_interact(npc) npc:storage_set('ai_state', 'interacting') else npc:storage_set('ai_state', 'moving') end elseif state == 'moving' then if not npc:is_moving() then npc:storage_set('ai_state', 'idle') end elseif state == 'interacting' then -- 由互动计时器回调切回idle end end ```