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到达后行为 |
|---|---|---|
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秒 |
-- 互动点检测(在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
Y3的NPBehave通过Blackboard驱动。对于本项目,由于NPC行为相对简单,**推荐使用Timer+状态机替代完整行为树**,降低复杂度:
-- 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