edit | blame | history | raw

NPC系统与行为树

npc_manager.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秒
-- 互动点检测(在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+状态机替代完整行为树**,降低复杂度:

-- 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