MicroCity笔记MicroCity笔记
笔记
  • Microcity Desktop 文档
  • Microcity Web 文档
  • 其它

    • 仿真框架(港口)说明文档
    • 模型/库资源
  • GitHub

    • MicroCity Desktop 仓库
    • MicroCity Web 仓库
    • MicroCity Web 在线环境
  • Gitee

    • MicroCity Desktop 仓库
    • MicroCity Web 仓库
  • zhhuu.top (自建修改)

    • MicroCity Web (fork) 仓库
    • MicroCity Web (fork) 在线环境
  • 简体中文
  • English
笔记
  • Microcity Desktop 文档
  • Microcity Web 文档
  • 其它

    • 仿真框架(港口)说明文档
    • 模型/库资源
  • GitHub

    • MicroCity Desktop 仓库
    • MicroCity Web 仓库
    • MicroCity Web 在线环境
  • Gitee

    • MicroCity Desktop 仓库
    • MicroCity Web 仓库
  • zhhuu.top (自建修改)

    • MicroCity Web (fork) 仓库
    • MicroCity Web (fork) 在线环境
  • 简体中文
  • English
  • 目录
  • 通用知识

    • Lua语言快速上手
    • MicroCity的版本
    • 仿真时间推进
    • 面向对象编程
    • 有关工具
  • MicroCity

    • 结果可视化
    • 操作网络
    • 模型求解
  • MicroCityWeb

    • 用户界面简介
    • 3D 场景
    • 3D 对象
    • 离散事件仿真和程序控制
    • 混合整数规划
    • 调试相关
    • 图表绘制功能
  • 思路

    • 自动化仓库仿真思路
    • 通用绘图代码
    • 港口AGV服务流程三维仿真思路
  • Gallery

    • 绘制一个时钟
    • 构建电梯仿真模型
    • 指数拓展的二分搜索
    • 计算复杂度分析

构建电梯仿真模型

build a clock thumbnail

目标

本文的目标是利用仿真框架创建一个电梯情景的仿真模型。主要内容包括队列的创建、人物的创建、电梯的创建、仿真逻辑的实现。

创建 Queue

当等待电梯的人比较多时,需要排队。所以这里我们首先需要创建队列。这里创建一个队列类,方便后续复用。队列类实现了队列的基本功能,包括进入(add)、退出(pull),并在对应的操作中刷新队列元素位置(refreshpos)。

创建 Queue 类的代码如下

function Queue(config)
    local queue = {}

    if type(config) == "nil" then
        config = {}
    end

    -- 设置属性
    queue.type = 'queue'
    queue.len = config.len or 20 -- 队列长度
    queue.origin = config.origin or {0, 0, 0} -- 原点
    queue.vec = config.vec or {2, 0, 0} -- 增长方向

    -- 计算
    queue.pos = {}
    for i = 1, queue.len do
        queue.pos[i] = {queue.origin[1] + queue.vec[1] * (i - 1), queue.origin[2] + queue.vec[2] * (i - 1),
                        queue.origin[3] + queue.vec[3] * (i - 1)}
        local pt = scene.addobj('points', {
            vertices = queue.pos[i],
            size = 5
        })
    end

    -- 任务相关函数
    function queue:refreshpos()
        for i = 1, queue.len do
            if type(queue[i]) == 'table' then
                queue[i]:addtask('move2', {table.unpack(queue.pos[i])})
            end
        end
    end

    function queue:pull()
        local pullItem = table.remove(queue, 1)
        queue:refreshpos(true)

        return pullItem
    end

    function queue:add(obj)
        if #queue == queue.len then
            print('[' .. queue.type .. queue.id .. '] 队列已满,加入失败')
            return
        end

        table.insert(queue, obj)
        obj:setpos(table.unpack(queue.pos[#queue]))
        obj.pos = {table.unpack(queue.pos[#queue])} -- 复制
        queue:refreshpos()
    end

    return queue
end

创建 Person

然后创建一个人物模型,用于测试队列的功能。由于每个人有不同的目标,且能够移动,因此可以将其看作能够执行不同任务的 Agent。因此,Person 类需要继承自 Agent 类以实现任务相关函数,如任务添加、任务执行、任务删除等操作。

由于 Person 类定义了模型,因此需要重新实现setrot()方法,以便在设置旋转时同时设置模型的旋转;而 Agent 类中已经实现了对于单个模型的setpos()方法,因此不需要重新实现,只用设置person.model即可。

Person 类中主要新定义了 stay 任务和 waitelevator 任务。

  • stay 任务指的是人物在电梯内原地不动,等待电梯到达其指定楼层;
  • waitelevator 任务指的是人物在 Queue 中等待电梯到达。

创建 Person 类的代码如下

function Person(config)
    local person = Agent(config)
    person.model = config.model or 'https://www.zhhuu.top/ModelResource/models/mc/steve.glb'
    person.model = scene.addobj(person.model) -- 添加模型
    person.type = 'person'
    person.id = person.model.id
    person.targetFloor = config.targetFloor or 2 -- 目标楼层

    function person:setrot(x, y, z)
        person.model:setrot(x, y, z)
    end

    -- 等待电梯到达目标楼层
    -- {'stay', {elevator=}}
    person.tasks.stay = {
        init = function(params)
            assert(type(params.elevator) == 'table' and params.elevator.type == 'elevator',
                "输入的电梯类型有误")

            params.dt = nil
            params.init = true
            -- 没有dt,等待elevator调用execute唤醒
        end,
        execute = function(dt, params)
            if params.elevator.floor == person.targetFloor then
                person:deltask()
                coroutine.queue(0, person.execute, person) -- 立刻执行下一个任务
            end
        end
    }

    -- 在队列中等待电梯到达
    -- {'waitelevator'}
    person.tasks.waitelevator = {
        init = function(params)
            params.dt = nil
            params.init = true
            -- 没有dt,等待elevator调用execute唤醒
        end,
        execute = function(dt, params)
            if person.elevator ~= nil then
                person:deltask()
                coroutine.queue(0, person.execute, person) -- 立刻执行下一个任务
            end
        end
    }

    return person
end

创建 Elevator

电梯(Elevator)是该仿真中任务较为复杂的一个 Agent,其中存储了电梯的信息(如层数、层高度)、工作状态、队列信息、当前楼层等信息。

电梯的任务主要包括:

  • pull:从队列中拉人
  • push:从队列中卸人
  • waitagents:等待 Agent 操作,如等待 Agent 到位

在电梯内的人物是一个队列,因此 Elevator 中定义了一个队列Elevator.queue,用于存储电梯内的人物;还通过Elevator.places定义了其中每个队列位置的具体位置坐标。

创建 Elevator 类的代码如下

function Elevator(config)
    if type(config) == 'nil' then
        config = {}
    end

    local elevator = Agent()
    elevator.model = scene.addobj('box', {
        length = 4,
        width = 4,
        height = 0.1,
        color = '#eee'
    })
    elevator.type = 'elevator'
    elevator.id = elevator.model.id
    elevator.origin = config.origin or {0, 0, 0}
    elevator.doorRadian = config.doorRadian or math.pi / 2 -- 出门的方向
    elevator.places = config.places or {{-1, 0, 1}, {-1, 0, -1}, {1, 0, -1}, {1, 0, 1}}
    elevator.queue = {} -- 物品队列

    elevator.floorHeights = config.floors or {0, 5} -- 默认两层楼,其高度对应为0和5
    elevator.floor = 1

    function elevator:move2y(y)
        local ox, _, oz = table.unpack(elevator.origin)
        elevator.model:setpos(ox, y, oz)

        for i = 1, #elevator.places do
            local p = elevator.queue[i]
            if type(p) == 'table' then
                p:setpos(elevator.places[i][1], y, elevator.places[i][3])
                p.pos = {elevator.places[i][1], y, elevator.places[i][3]}
            end
        end
    end

    function elevator:isFull()
        for i = 1, #elevator.places do
            if elevator.queue[i] == nil then
                return false
            end
        end
        return true
    end

    function elevator:minFloor(exceptFirst)
        local fmin = 1 / 0

        for i = 1, #elevator.places do
            if type(elevator.queue[i]) == 'table' and elevator.queue[i].targetFloor < fmin then
                -- 除了最低楼层为1楼、并且除去1楼的情况
                if not (elevator.queue[i].targetFloor == 1 and exceptFirst) then
                    fmin = elevator.queue[i].targetFloor
                end
            end
        end

        if fmin == 1 / 0 then
            return nil
        end

        return fmin
    end

    function elevator:getplace(i)
        local _, y, _ = elevator.model:getpos()
        return {elevator.places[i][1], y, elevator.places[i][3]}
    end

    function elevator:setpos(x, y, z)
        elevator:move2y(y)
    end

    function elevator:toFloor(f, floorList, doPull)
        elevator:addtask('fn', {
            f = function()
                print('电梯去', f, '楼,taking:')
                for i = 1, #elevator.places do
                    if type(elevator.queue[i]) == 'table' then
                        print(elevator.queue[i].type, elevator.queue[i].id, '->', elevator.queue[i].targetFloor)
                    end
                end
            end,
            args = {}
        })
        elevator:addtask('move2', {elevator.origin[1], elevator.floorHeights[f], elevator.origin[3]})
        elevator:addtask('setvalue', {
            key = 'floor',
            value = f
        })

        -- push
        elevator:addtask('push', {f}) -- 弹出到对应楼层的人

        -- pull
        if doPull then
            elevator:addtask('pull', {
                floor = elevator.floor,
                queue = floorList[f].queue
            })
        end
        elevator:addtask('waitagents') -- 从队列中pull到的agent
    end

    -- 从楼层队列中拉取person
    -- {'pull', {floor=,queue=}}
    elevator.tasks.pull = {
        init = function(params)
            assert(type(params.floor) == 'number',
                elevator.type .. elevator.id .. '的pull任务floor参数不为number')
            assert(type(params.queue) == 'table', elevator.type .. elevator.id .. '的pull任务queue参数不为table')

            params.dt = nil
            params.init = true
        end,
        execute = function(dt, params)
            -- 检查剩余位置
            for i = 1, #elevator.places do
                if elevator.queue[i] == nil then
                    -- pull到剩余位置中
                    local p = params.queue:pull()
                    if type(p) == "nil" then -- pull不到了
                        break
                    end

                    p.elevator = elevator
                    elevator.queue[i] = p
                    p:execute() -- 唤醒
                end
            end

            -- 删除任务
            elevator:deltask()
            coroutine.queue(0, elevator.execute, elevator) -- 立刻执行下一个任务
        end
    }

    -- 从电梯中弹出person
    -- {'push', {floor}}
    elevator.tasks.push = {
        init = function(params)
            assert(type(params[1]) == 'number', elevator.type .. elevator.id .. '的push任务参数非number')

            -- 弹出目标为对应楼层的人
            for i, people in ipairs(elevator.queue) do
                if people.targetFloor == elevator.floor then
                    -- 唤醒agent(弹出)
                    people:execute()
                    people.arrived = false -- 设置到达sink的状态
                end
            end

            params.init = true
        end,
        execute = function(dt, params)
            -- 检查离开状态(需要people唤醒)
            local arrived = true

            for i = 1, #elevator.places do
                local p = elevator.queue[i]
                if type(p) == 'table' and p.targetFloor == elevator.floor then
                    if not p.arrived then
                        arrived = false
                    else
                        -- 删除位置
                        elevator.queue[i] = nil
                    end
                end
            end

            -- 删除任务
            if arrived then
                elevator:deltask()
                coroutine.queue(0, elevator.execute, elevator) -- 立刻执行下一个任务
            end
        end
    }

    -- 检测agent是否到位
    -- {'waitagents'}
    elevator.tasks.waitagents = {
        init = function(params)
            params.dt = nil
            params.init = true
            -- 没有结束时间,等待person调用execute唤醒
        end,
        execute = function(dt, params)
            local all_arrived = true

            -- 判断是否全部wait的agent都到达
            for i = 1, #elevator.queue do
                if type(elevator.queue[i]) == 'table' and not elevator.queue[i].arrived then
                    all_arrived = false
                    break
                end
            end

            -- 全部到达,删除任务
            if all_arrived then
                elevator:deltask()
                coroutine.queue(0, elevator.execute, elevator) -- 立刻执行下一个任务
            end
        end
    }

    return elevator
end

仿真逻辑

环境创建

首先创建场景。创建一个电梯,给定层高和层数。然后在每层创建一个队列,用于存放等待电梯的人;创建一个离开点,令到达目标楼层的人从这个位置离开。其中,队列点位颜色为灰色,离开点颜色为红色。

-- 创建环境
local floorNum = 5

local floorHeights = {}
for i = 1, floorNum do
    floorHeights[i] = (i - 1) * 5
end

local elevator = Elevator({
    floors = floorHeights
})
local floors = {} -- {queue, sinkPos}

for f, height in ipairs(elevator.floorHeights) do
    floors[f] = {}
    floors[f].queue = Queue({
        len = 10,
        origin = {2, height, 0}
    })
    floors[f].queue.id = f
    floors[f].sinkPos = {2, height, 1}
    scene.addobj('points', {
        vertices = floors[f].sinkPos,
        size = 5,
        color = 'red'
    })
end

人物创建流程

人物生成具体操作使用 genPeople() 函数实现,主要设置了人物的流程:

人物定时生成使用 randomPeopleSummoner() 函数实现,主要设置了人物的生成时间间隔和根据 Elevator 的状态激活 elevatorNextFloor() 函数。人物生成时间随机生成,遵循负指数分布。

电梯运行逻辑

电梯通过 elevatorNextFloor() 来控制电梯到达的下一个楼层。

对应流程图如下:

虽然当前任务逻辑看上去已经比较复杂,但是仍有改进的空间

最后通过 elevatorNextFloor(elevator) 令电梯进入楼层检查流程。

人物生成流程

通过设置随机数种子控制人物生成的规律。此处设置如下

-- 创建泊松分布的随机数种子
local seed = math.randomseed(0, {
    distribution = "exponential",
    mu = "10"
}) -- 负指数分布,均值为5
local summonCount = 50

然后通过 randomPeopleSummoner() 函数串联人物生成流程和电梯唤醒。

function randomPeopleSummoner()
    local f = math.random(1, floorNum)
    local people = genPeople(f)
    table.insert(ActionObjs, people)
    summonCount = summonCount - 1

    if elevator.status ~= 'busy' then
        elevatorNextFloor(elevator)
    end

    if summonCount > 0 then
        coroutine.queue(seed:random(), randomPeopleSummoner)
    end
end

randomPeopleSummoner()

仿真控制

仿真控制相关代码。

-- 仿真控制
local ActionObjs = {elevator}
local simv = 4  -- 仿真速度
local watchdog = WatchDog(simv, ActionObjs, {
    isImmediateStop = false, -- 无任务不立刻停止仿真
    recycleType = {'person'} -- 指定回收Agent类型
})
watchdog:refresh()

总结

本案例中包含:

  • 如何创建可用的逻辑组件,对应本文中的 Queue;
  • 如何根据需求创建对应的 Agent(本文中的 Person 和 Elevator),包括如何使用 Agent 基类内置的任务(如 move2、fn、setvalue)、向其中创建指定任务以完成相应的业务逻辑(如 person.tasks.stay、elevator.tasks.pull 等);
  • 如何在流程中,包括 coroutine 唤醒的函数流程中添加任务到 Agent 中;
  • 仿真框架实现的其他所需设置,如创建 ActionObjs、WatchDog 等。

其他

需要注意的问题:

  • 需要注意添加任务的顺序,对应的操作需要使用任务完成还是直接完成。
  • 由于 Person 在排队时受等待任务阻塞,因此当前 Person 在结束阻塞之前都不会前进补齐空位。这是一个可能的改进方向。

示例

🔗 在 MicroCityWeb 中打开

完整代码

-- 1.下载函数库到虚拟磁盘
print('正在下载依赖库到虚拟磁盘...')
os.upload('https://www.zhhuu.top/ModelResource/libs/ct/agent.lua')
os.upload('https://www.zhhuu.top/ModelResource/libs/ct/watchdog.lua')
os.upload('https://www.zhhuu.top/ModelResource/libs/tablestr.lua')
print('下载完成')
-- 2.引用库
require('agent')
require('watchdog')
require('tablestr')

scene.setenv({
    grid = 'plane'
})
print()

function Queue(config)
    local queue = {}

    if type(config) == "nil" then
        config = {}
    end

    -- 设置属性
    queue.type = 'queue'
    queue.len = config.len or 20 -- 队列长度
    queue.origin = config.origin or {0, 0, 0} -- 原点
    queue.vec = config.vec or {2, 0, 0} -- 增长方向

    -- 计算
    queue.pos = {}
    for i = 1, queue.len do
        queue.pos[i] = {queue.origin[1] + queue.vec[1] * (i - 1), queue.origin[2] + queue.vec[2] * (i - 1),
                        queue.origin[3] + queue.vec[3] * (i - 1)}
        local pt = scene.addobj('points', {
            vertices = queue.pos[i],
            size = 5
        })
    end

    -- 任务相关函数
    function queue:refreshpos()
        for i = 1, queue.len do
            if type(queue[i]) == 'table' then
                queue[i]:addtask('move2', {table.unpack(queue.pos[i])})
            end
        end
    end

    function queue:pull()
        local pullItem = table.remove(queue, 1)
        queue:refreshpos(true)

        return pullItem
    end

    function queue:add(obj)
        if #queue == queue.len then
            print('[' .. queue.type .. queue.id .. '] 队列已满,加入失败')
            return
        end

        table.insert(queue, obj)
        obj:setpos(table.unpack(queue.pos[#queue]))
        obj.pos = {table.unpack(queue.pos[#queue])} -- 复制
        queue:refreshpos()
    end

    return queue
end

-- Person
function Person(config)
    local person = Agent(config)
    person.model = config.model or 'https://www.zhhuu.top/ModelResource/models/mc/steve.glb'
    person.model = scene.addobj(person.model) -- 添加模型
    person.type = 'person'
    person.id = person.model.id
    person.targetFloor = config.targetFloor or 2 -- 目标楼层

    function person:setrot(x, y, z)
        person.model:setrot(x, y, z)
    end

    -- 等待电梯到达目标楼层
    -- {'stay', {elevator=}}
    person.tasks.stay = {
        init = function(params)
            assert(type(params.elevator) == 'table' and params.elevator.type == 'elevator',
                "输入的电梯类型有误")

            params.dt = nil
            params.init = true
            -- 没有dt,等待elevator调用execute唤醒
        end,
        execute = function(dt, params)
            if params.elevator.floor == person.targetFloor then
                person:deltask()
                coroutine.queue(0, person.execute, person) -- 立刻执行下一个任务
            end
        end
    }

    -- 在队列中等待电梯到达
    -- {'waitelevator'}
    person.tasks.waitelevator = {
        init = function(params)
            params.dt = nil
            params.init = true
            -- 没有dt,等待elevator调用execute唤醒
        end,
        execute = function(dt, params)
            if person.elevator ~= nil then
                person:deltask()
                coroutine.queue(0, person.execute, person) -- 立刻执行下一个任务
            end
        end
    }

    return person
end

-- Elevator
function Elevator(config)
    if type(config) == 'nil' then
        config = {}
    end

    local elevator = Agent()
    elevator.model = scene.addobj('box', {
        length = 4,
        width = 4,
        height = 0.1,
        color = '#eee'
    })
    elevator.type = 'elevator'
    elevator.id = elevator.model.id
    elevator.origin = config.origin or {0, 0, 0}
    elevator.doorRadian = config.doorRadian or math.pi / 2 -- 出门的方向
    elevator.places = config.places or {{-1, 0, 1}, {-1, 0, -1}, {1, 0, -1}, {1, 0, 1}}
    elevator.queue = {} -- 物品队列

    elevator.floorHeights = config.floors or {0, 5} -- 默认两层楼,其高度对应为0和5
    elevator.floor = 1

    function elevator:move2y(y)
        local ox, _, oz = table.unpack(elevator.origin)
        elevator.model:setpos(ox, y, oz)

        for i = 1, #elevator.places do
            local p = elevator.queue[i]
            if type(p) == 'table' then
                p:setpos(elevator.places[i][1], y, elevator.places[i][3])
                p.pos = {elevator.places[i][1], y, elevator.places[i][3]}
            end
        end
    end

    function elevator:isFull()
        for i = 1, #elevator.places do
            if elevator.queue[i] == nil then
                return false
            end
        end
        return true
    end

    function elevator:minFloor(exceptFirst)
        local fmin = 1 / 0

        for i = 1, #elevator.places do
            if type(elevator.queue[i]) == 'table' and elevator.queue[i].targetFloor < fmin then
                -- 除了最低楼层为1楼、并且除去1楼的情况
                if not (elevator.queue[i].targetFloor == 1 and exceptFirst) then
                    fmin = elevator.queue[i].targetFloor
                end
            end
        end

        if fmin == 1 / 0 then
            return nil
        end

        return fmin
    end

    function elevator:getplace(i)
        local _, y, _ = elevator.model:getpos()
        return {elevator.places[i][1], y, elevator.places[i][3]}
    end

    function elevator:setpos(x, y, z)
        elevator:move2y(y)
    end

    function elevator:toFloor(f, floorList, doPull)
        elevator:addtask('fn', {
            f = function()
                print('电梯去', f, '楼,taking:')
                for i = 1, #elevator.places do
                    if type(elevator.queue[i]) == 'table' then
                        print(elevator.queue[i].type, elevator.queue[i].id, '->', elevator.queue[i].targetFloor)
                    end
                end
            end,
            args = {}
        })
        elevator:addtask('move2', {elevator.origin[1], elevator.floorHeights[f], elevator.origin[3]})
        elevator:addtask('setvalue', {
            key = 'floor',
            value = f
        })

        -- push
        elevator:addtask('push', {f}) -- 弹出到对应楼层的人

        -- pull
        if doPull then
            elevator:addtask('pull', {
                floor = elevator.floor,
                queue = floorList[f].queue
            })
        end
        elevator:addtask('waitagents') -- 从队列中pull到的agent
    end

    -- 从楼层队列中拉取person
    -- {'pull', {floor=,queue=}}
    elevator.tasks.pull = {
        init = function(params)
            assert(type(params.floor) == 'number',
                elevator.type .. elevator.id .. '的pull任务floor参数不为number')
            assert(type(params.queue) == 'table', elevator.type .. elevator.id .. '的pull任务queue参数不为table')

            params.dt = nil
            params.init = true
        end,
        execute = function(dt, params)
            -- 检查剩余位置
            for i = 1, #elevator.places do
                if elevator.queue[i] == nil then
                    -- pull到剩余位置中
                    local p = params.queue:pull()
                    if type(p) == "nil" then -- pull不到了
                        break
                    end

                    p.elevator = elevator
                    elevator.queue[i] = p
                    p:execute() -- 唤醒
                end
            end

            -- 删除任务
            elevator:deltask()
            coroutine.queue(0, elevator.execute, elevator) -- 立刻执行下一个任务
        end
    }

    -- 从电梯中弹出person
    -- {'push', {floor}}
    elevator.tasks.push = {
        init = function(params)
            assert(type(params[1]) == 'number', elevator.type .. elevator.id .. '的push任务参数非number')

            -- 弹出目标为对应楼层的人
            for i, people in ipairs(elevator.queue) do
                if people.targetFloor == elevator.floor then
                    -- 唤醒agent(弹出)
                    people:execute()
                    people.arrived = false -- 设置到达sink的状态
                end
            end

            params.init = true
        end,
        execute = function(dt, params)
            -- 检查离开状态(需要people唤醒)
            local arrived = true

            for i = 1, #elevator.places do
                local p = elevator.queue[i]
                if type(p) == 'table' and p.targetFloor == elevator.floor then
                    if not p.arrived then
                        arrived = false
                    else
                        -- 删除位置
                        elevator.queue[i] = nil
                    end
                end
            end

            -- 删除任务
            if arrived then
                elevator:deltask()
                coroutine.queue(0, elevator.execute, elevator) -- 立刻执行下一个任务
            end
        end
    }

    -- 检测agent是否到位
    -- {'waitagents'}
    elevator.tasks.waitagents = {
        init = function(params)
            params.dt = nil
            params.init = true
            -- 没有结束时间,等待person调用execute唤醒
        end,
        execute = function(dt, params)
            local all_arrived = true

            -- 判断是否全部wait的agent都到达
            for i = 1, #elevator.queue do
                if type(elevator.queue[i]) == 'table' and not elevator.queue[i].arrived then
                    all_arrived = false
                    break
                end
            end

            -- 全部到达,删除任务
            if all_arrived then
                elevator:deltask()
                coroutine.queue(0, elevator.execute, elevator) -- 立刻执行下一个任务
            end
        end
    }

    return elevator
end

-- 创建环境
local floorNum = 5

local floorHeights = {}
for i = 1, floorNum do
    floorHeights[i] = (i - 1) * 5
end

local elevator = Elevator({
    floors = floorHeights
})
local floors = {} -- {queue, sinkPos}

for f, height in ipairs(elevator.floorHeights) do
    floors[f] = {}
    floors[f].queue = Queue({
        len = 10,
        origin = {2, height, 0}
    })
    floors[f].queue.id = f
    floors[f].sinkPos = {2, height, 1}
    scene.addobj('points', {
        vertices = floors[f].sinkPos,
        size = 5,
        color = 'red'
    })
end

-- 仿真逻辑
function genPeople(initFloor)
    -- 生成目标楼层
    local targetF = 1
    if initFloor == 1 then
        targetF = math.random(2, floorNum)
    end

    local person = Person({
        targetFloor = targetF,
        model = math.random() > 0.5 and 'https://www.zhhuu.top/ModelResource/models/mc/steve.glb' or
            'https://www.zhhuu.top/ModelResource/models/mc/villager.glb'
    })
    person:setrot(0, elevator.doorRadian - math.pi, 0)

    floors[initFloor].queue:add(person) -- generator
    -- steve:setrot(0, elevator.doorRadian, 0)
    person:addtask('waitelevator')
    person:addtask('fn', {
        f = function()
            local elevator = person.elevator

            local i = 0
            -- 检查pull到哪个位置
            for k, v in ipairs(elevator.queue) do
                if person == v then
                    i = k
                    break
                end
            end

            person:addtask('move2', {elevator.places[i][1], elevator.floorHeights[initFloor], elevator.places[i][3]}) -- 移动到电梯内
            person:addtask('fn', {
                f = function()
                    person:setrot(0, elevator.doorRadian, 0)
                end,
                args = {}
            })
            person:addtask('setvalue', {
                key = 'arrived',
                value = true
            }) -- 设置到达状态
            person:addtask('fn', {
                f = function()
                    elevator:execute() -- 通知电梯
                end,
                args = {}
            })
            person:addtask('stay', {
                elevator = elevator
            }) -- 在电梯内等待

            person:addtask('move2', {table.unpack(floors[person.targetFloor].sinkPos)}) -- 离开电梯
            person:addtask('setvalue', {
                key = 'arrived',
                value = true
            }) -- 设置到达(sink)状态
            person:addtask('fn', {
                f = function()
                    elevator:execute() -- 通知电梯
                end,
                args = {}
            })
        end,
        args = {}
    })

    return person
end

function getMaxQueueFloor()
    local maxQueueLength = 0
    local maxQueueFloor = 1

    for f, floorItem in ipairs(floors) do
        if #floorItem.queue > maxQueueLength then
            maxQueueLength = #floorItem.queue
            maxQueueFloor = f
        end
    end

    return maxQueueFloor, maxQueueLength
end

-- 逻辑流程
function elevatorNextFloor(elevator)
    local minFloor = elevator:minFloor()

    -- 没人
    if minFloor == nil then
        print('elevator检测到没人,去人最多的楼')
        -- elevator:toFloor(1, floors, true)
        local maxFloor, maxQueueLength = getMaxQueueFloor()
        elevator.status = 'busy'
        elevator:toFloor(maxFloor, floors, true)

        if maxQueueLength > 0 then
            elevator:addtask('fn', {
                f = elevatorNextFloor,
                args = {elevator}
            })
        else
            print('elevator检测到没人,设置状态为idle')
            elevator.status = 'idle'
        end

        return
    end

    -- 有人
    elevator.status = 'busy'
    if minFloor == 1 then
        -- 最小目标为1楼
        if elevator:isFull() then
            -- 满员,直接去1楼
            print('elevator满员,直接去1楼')
            elevator:toFloor(1, floors, true)
        else
            -- 如果没满,去有人的下一层拉人
            -- 检查下一层是否有人
            print('elevator去有人的下一层拉人')
            local nextFloor = elevator.floor - 1

            while nextFloor > 1 do
                print('检查楼层', nextFloor)
                if #floors[nextFloor].queue > 0 then
                    print('elevaor检查到', nextFloor, '层有人,去拉人')
                    elevator:toFloor(nextFloor, floors, true)
                    break
                end
                nextFloor = nextFloor - 1
            end

            if nextFloor == 1 then
                print('elevator检测到没有人,去1楼')
                elevator:toFloor(1, floors, true)
            end
        end
    else
        -- 不是1楼,直接去minFloor,不拉人
        if #floors == minFloor then
            elevator:toFloor(minFloor, floors, true) -- 已经到了最顶层,拉人
        else
            elevator:toFloor(minFloor, floors, false) -- 没到最顶层,不拉人
        end
    end

    elevator:addtask('fn', {
        f = elevatorNextFloor,
        args = {elevator}
    }) -- queue next move
end

elevatorNextFloor(elevator)
scene.render()

-- 创建泊松分布的随机数种子
local seed = math.randomseed(0, {
    distribution = "exponential",
    mu = "10"
}) -- 负指数分布,均值为5
local summonCount = 50

-- 仿真控制
local ActionObjs = {elevator}
local simv = 4
local watchdog = WatchDog(simv, ActionObjs, {
    isImmediateStop = false,
    recycleType = {'person'}
})
watchdog:refresh()

function randomPeopleSummoner()
    local f = math.random(1, floorNum)
    local people = genPeople(f)
    table.insert(ActionObjs, people)
    summonCount = summonCount - 1

    if elevator.status ~= 'busy' then
        elevatorNextFloor(elevator)
    end

    if summonCount > 0 then
        coroutine.queue(seed:random(), randomPeopleSummoner)
    end
end

randomPeopleSummoner()
Last Updated:
Contributors: huuhghhgyg
Prev
绘制一个时钟
Next
指数拓展的二分搜索