接手这个模型纯属偶然。当时帮某公司评估一个四间办公室共享一条走廊的疏散方案,手头没有商业仿真软件,甲方给的期限又紧。我第一反应是:这东西用MATLAB就能做,而且元胞自动机(CA)的思路刚好契合——空间离散化以后,房间、走廊、门、出口全变成矩阵格子,剩下的就是一套更新规则。项目标题就叫“MATLAB四房间走廊疏散模型设计与实现”,过程踩了不少坑,最后跑出来的结果倒挺能说明问题。
这篇内容不是给你抄代码的,而是把从“需求拆解”到“模型设计”再到“调试出结果”完整捋一遍。如果你手头有类似的小型疏散评估、流程仿真,或者纯粹想用MATLAB做个可视化模型练手,这篇能帮你少走弯路。我把关键参数、规则设计、代码思路和常见坑都放在下面,你可以直接照着复现。
1. 项目需求与建模思路拆解
1.1 先搞清楚模型到底要回答什么问题
四房间+走廊的结构非常典型,医院诊区、教研楼办公室、小型企业工区都是这种格局。通常要回答三个问题:
- 总疏散时间是多少,是否满足安全要求(比如规范通常给2分钟这种指标)。
- 哪个部位是真正的瓶颈,房门、走廊还是出口。
- 如果调整门的宽度、走廊宽度,或增加出口,效果能改善多少。
这些问题看起来简单,但纯粹靠解析计算很难回答。排队论能算稳态流量,但算不出空间拥堵的动态过程;社会力模型精度高,但在MATLAB里调参和计算量大得让人头疼。元胞自动机恰好卡在中间:离散网格天然匹配建筑平面,规则简单,计算快,可视化直观,而且拥堵、排队、瓶颈这些现象,用简单的局部规则反复迭代就能涌现出来。
1.2 为什么最终选了元胞自动机而不是其他方法
我用一个表格对比过三种常见方案的适用性:
| 方案 | 空间表达 | 优点 | 缺点 |
|---|---|---|---|
| 排队论 | 网络节点 | 解析公式快 | 无法体现空间拥堵、门宽与走廊几何 |
| 社会力模型 | 连续坐标 | 行为逼真、可加心理力 | 参数多、MATLAB循环跑得慢、调参痛苦 |
| 元胞自动机 | 离散网格 | 实现简单、与建筑平面天然一致、可视化好 | 格子大小影响精度,个体行为较规则化 |
元胞自动机最核心的一点是:复杂行为不需要复杂规则来堆。你只需要五个方向(前、后、左、右、原地)的决策逻辑,配合一个全局导航场,人群的排队、绕行、拥堵就自然出现了。这很像蚁群或者交通流模拟——微观规则简单,宏观现象丰富。
1.3 模型的输入输出与基本假设
动手前必须把模型的边界写清楚,不然后面根本没法解释结果。
- 输入:建筑平面参数(四间房间尺寸、走廊长宽、门与出口的位置和宽度)、人员数量与初始分布、行为参数(移动概率、每步耗时)。
- 输出:总疏散时间、剩余人数-时间曲线、房间清空时序、瓶颈位置。
- 基本假设:
- 所有人员熟悉出口方向,不做随机乱窜。
- 移动速度均匀,没有老人小孩差异。
- 没有逆行人流,所有人朝出口方向推进。
- 忽略恐慌造成的推挤超越。
这些假设在真实场景里不可能完全成立,但对比较方案而言足够了。仿真模型的价值从来不是预测绝对时间,而是对比不同策略、定位系统短板。
2. 空间网格设计与建筑拓扑实现
2.1 网格分辨率与地图尺寸怎么定
网格分辨率是整个模型的第一个关键决策。格子太小,矩阵爆炸、循环慢;格子太大,门和走廊的宽度失真。我按0.4米一个格子来处理:一个成年人占据的平面投影大约0.5米见方,0.4米格能勉强模拟单格通行,又不会太细。
以四房间走廊为例,我的尺寸是这样定的:
- 每个房间:6米×5米,换算成15×12格。
- 走廊:长20米,宽1.4米,换算成50×4格。
- 门:宽0.8米,即2格宽。
- 出口:宽1.2米,即3格宽(基准方案)。
- 整体网格:长约21米,宽约11.2米,换算后是62×28格左右。
这里有个经验要记住:门至少留2格宽。如果门只有1格,模型就会产生一个人为瓶颈,哪怕实际情况中门能并行侧身通过两人,网格模型也模拟不出来。这个失真会让疏散时间虚高,导致错误的结论。
2.2 地图状态编码:墙、空地、门、出口
我不建议只用0和1标记“可走/不可走”。后面可视化要区分房间、走廊、门、出口,建议直接用枚举值:
| 数值 | 含义 |
|---|---|
| 0 | 空地(可通行) |
| 1 | 墙体(不可通行) |
| 2 | 门(可通行) |
| 3 | 出口格(疏散终点) |
建地图时先铺空地,再把墙体坐标填1,门和出口单独填2和3。MATLAB代码大概长这样:
% 初始化地图:row是y方向, col是x方向 gridMap = zeros(nRows, nCols); % 四房间区域 room1 = [2, 13; 2, 16]; % 用 [rowStart, rowEnd; colStart, colEnd] room2 = [2, 13; 34, 48]; room3 = [17, 28; 2, 16]; room4 = [17, 28; 34, 48]; % 走廊区域 corridor = [14, 17; 2, 58]; % 先全部置墙,再开洞 gridMap(:) = 1; gridMap(room1(1):room1(2), room1(3):room1(4)) = 0; % 走廊 gridMap(corridor(1):corridor(2), corridor(3):corridor(4)) = 0; % 门(2格宽) gridMap(14:15, 16:17) = 2; % 房间1门 % 出口(3格宽,走廊右端) gridMap(15:17, 61:63) = 3;骨架先打出来,后面再细调。矩阵的行列顺序在MATLAB里对应y和x,后画画的时候经常搞反,这个我在第6章会专门说。
2.3 距离场:让每个人都知道该往哪走
人群里每个个体的视线范围有限,但疏散者又要整体朝出口移动,不能瞎走。最简单有效的做法是预先计算一张“距离场”:每个可通行格点都有一个到出口的步数。每走一步,各人检查上下左右四个相邻格,选择距离场数值最小的那个格子就行。
这个距离场用广度优先搜索(BFS)生成。从出口格开始,每往外扩展一层,步数加1。墙不能穿,门可以过。核心逻辑是:
function distField = computeDistanceField(gridMap, exitIdx) [nRows, nCols] = size(gridMap); distField = inf(nRows, nCols); queue = exitIdx; distField(sub2ind([nRows, nCols], exitIdx(:,1), exitIdx(:,2))) = 0; dirs = [-1 0; 1 0; 0 -1; 0 1]; % 上下左右 head = 1; while head <= size(queue, 1) cur = queue(head, :); head = head + 1; for k = 1:4 nr = cur(1) + dirs(k,1); nc = cur(2) + dirs(k,2); if nr < 1 || nr > nRows || nc < 1 || nc > nCols continue; end if gridMap(nr, nc) == 1 continue; end if distField(nr, nc) > distField(cur(1), cur(2)) + 1 distField(nr, nc) = distField(cur(1), cur(2)) + 1; queue(end+1, :) = [nr, nc]; end end end end这张距离场就是所有人的“全局导航图”,相当于默认所有人知道离出口最近的路。走廊里距离场沿出口方向递减,房间里的人会先朝门走,再到走廊汇流。
3. 人员生成与移动规则实现
3.1 初始人员怎么分布
初始位置我建议均匀随机分布在房间内,而不是全堆在门口。这样更贴近常态。人数按功能需求来,比如某办公室最多容纳15人,四间一共60人;想评估极端情况就把人数调到120人。
生成方式是从每个房间的可走格子里随机取样:
roomCells = find(gridMap(r1:r2, c1:c2) == 0); idx = randperm(length(roomCells), 15); [rowSub, colSub] = ind2sub(size(gridMap), roomCells(idx)); peoplePos = [rowSub(:), colSub(:)];这里有个小建议:同一个格子不要放两个人。初始占据就重叠的话,后面移动规则会乱套。所以取样要用不重复的随机采样,人数也不要超过房间格数。
3.2 单步移动规则与冲突处理
每个时间步长模拟0.5秒。每个人按顺序做一次决策:
- 检查相邻四格的距离场值,挑出最小值所在格。
- 如果这个目标格是空的(没有人占),且该人格未在本步被占用,则移过去。
- 如果目标格已经被人占了,那就原地等待,或者以30%的概率选择次优邻格(模拟轻微绕行)。
- 一旦走到标记为出口的格子,该人员立即从模型中移除,记为“已疏散”。
冲突处理是CA模型最容易翻车的地方。最简单的做法是“两步走”:第一步所有人上报目标格,第二步检查同一个目标格是否被多人选中,如果冲突则随机保留一人,其他人原地不动。这个规则不一定能完全模拟现实的推挤,但胜在稳定性高、不穿模。
% 伪代码逻辑 targets = zeros(nPeople, 2); for i = 1:nPeople if isEvacuated(i), continue; end [bestR, bestC] = findBestNeighbor(peoplePos(i,:), distField); targets(i, :) = [bestR, bestC]; end for i = 1:nPeople if isEvacuated(i), continue; end conflict = false; for j = 1:nPeople if i ~= j && isequal(targets(i,:), targets(j,:)) conflict = true; end end if conflict && rand() > 0.5 % 留着原地 else peoplePos(i,:) = targets(i,:); end end移动概率也可以加。默认0.9说明每10步可能有人原地停顿一次,用来模拟反应迟缓。这个参数对疏散时间的影响很直接,我后面会在实验部分提到。
3.3 单格占用与速度标定
0.5秒走一格,相当于速度约0.8米/秒,属于比较保守的疏散速度。如果想让结果更乐观,可以改成0.4秒一格,也就是1米/秒。实际调试中我一般先从0.5秒开始,等模型稳定后再调速度参数。这样你能明确看到“时间参数”对结果的影响,而不是所有因素搅在一起。
4. 主程序流程与可视化实现
4.1 主循环的结构
整个模型就是一个大的while循环,直到所有人都疏散成功或步数超过上限:
maxSteps = 300; currentStep = 0; remaining = totalPeople; while remaining > 0 && currentStep < maxSteps currentStep = currentStep + 1; % 移动人员,处理冲突,检查是否到达出口 [peoplePos, remaining] = updatePeople(peoplePos, distField, gridMap); % 每10帧画一次图 if mod(currentStep, 10) == 0 drawEvacuationMap(gridMap, peoplePos, currentStep); end % 记录剩余人数 remainingCurve(currentStep) = remaining; end主循环本身不复杂,真正的复杂度全在“updatePeople”这个函数里。建议把“计算目标格”和“冲突处理”分开写成子函数,后面加规则或者调试时都方便。如果一开始就全部塞在主循环里,代码注定变成一团乱麻。
4.2 可视化技巧:别让动画拖死性能
我第一次跑的时候每步都画图,结果计算10秒,画图拖慢50秒。后来改成每5到10步才刷新一次画面,流畅度马上上来了。
画图的核心是把人数叠到地图上:
function drawEvacuationMap(gridMap, peoplePos, step) imagesc(gridMap); hold on; colormap([0.8 0.8 0.8; 0.2 0.2 0.2; 1 0.8 0; 0 1 0]); if ~isempty(peoplePos) plot(peoplePos(:,2), peoplePos(:,1), 'b.', 'MarkerSize', 10); end title(sprintf('Step %d', step)); hold off; drawnow; end注意plot的x坐标用了列号,y坐标用了行号,这样才和imagesc的显示一致。更精细的可以人用圆形patch,墙深色、门黄色、出口绿色,一眼扫过去就知道状态。
4.3 数据记录:疏散曲线才是核心输出
动画只是给人看的,真正拿去向别人说明问题的,是“剩余人数-时间”曲线。这条曲线有两个关键特征:
- 斜率最大处,代表人员排空速率最高的阶段,往往出现在前半段。
- 曲线尾部变平,说明瓶颈处还有零散人员在排队或等待。
把这条曲线画出来,比任何动画都有说服力。
5. 典型实验结果与瓶颈分析
5.1 基准方案:四房间60人
基准方案参数我设为:门宽2格,出口宽3格,总人数60人,移动概率0.9,每步0.5秒。
一次典型运行的结果:
- 总疏散时间:约38步,折合19秒。
- 房间清空顺序:离出口近的房间率先清空,离出口远的最后一个房间最后清空。
- 例如出口附近房间清空约10步完成,远端房间拖延到30步后才结束。
- 剩余人数曲线呈两段式:前期线性快速下降,尾部明显放缓。
尾部放缓就是瓶颈在起作用。观察动画时你会看到最后十几个人基本都堵在走廊靠出口处,门口没有多余容量,相当于一个漏斗。
5.2 变量对照:出口宽度是否真的越大越好
我做了三组对照,都保持60人不变:
| 出口宽度 | 总疏散时间 | 瓶颈位置 |
|---|---|---|
| 1格(0.4米) | 47步(23.5秒) | 出口单格,严重排队 |
| 2格(0.8米) | 41步(20.5秒) | 出口附近,排队稍缓 |
| 3格(1.2米) | 38步(19秒) | 走廊与出口交界处 |
看出来没有,出口从1格加到2格,时间缩短了约13%;从2格加到3格,时间只缩短了约7%。当出口做到3格以上,瓶颈就转移到走廊内部了,再继续加宽出口的意义非常有限。
这个结论在疏散评估里特别重要。它告诉我们:盲目加大某个环节的尺寸,不如先找到真正的卡点。如果走廊本身只有1.4米宽,出口再宽也没用,人都在走廊排队。
5.3 参数敏感性:人数翻倍、移动概率变化的影响
我把人数从60人翻到120人,其他条件不变,疏散时间从19秒左右变成约37秒,接近翻倍但略低于线性增长。翻倍并不是严格两倍,因为初始人数多了以后,走廊里更早进入“高密度排队”状态,出口能够始终保持饱和通行。排队论里的“流量守恒”在这个模型里看得清清楚楚。
移动概率从0.9降到0.7,总时间增加约25%。这说明人员反应速度对极端工况下疏散时间影响很大,也侧面反映了演练与应急预案的重要性。
还有一个发现:门宽2格变成门宽1格时,远端房间的清空时间拉长非常明显,说明门宽瓶颈对远端房间不公平。疏散设计里,不能只看整体时间,还要看每个房间的清空顺序是否合理,得避免某个房间的人要跨越整个走廊才能出去。
6. 常见问题与调试心得
6.1 新手最容易踩的四个坑
坑一:门画成1格宽。这是最隐蔽的错误。单格门天然只能单列通行,模型出来的疏散时间一定偏大。任何门至少两格宽起步,除非这个门真的窄到只能侧身。
坑二:坐标轴方向搞反。矩阵的第1维是行,对应y方向;第2维是列,对应x方向。用plot画人员位置时,如果把x和y颠倒,动画里所有人都在镜面世界里乱跑,而距离场却按原来的方向算,结果就是明明出口在右边,人却往左边冲。调试方法很简单:只放一个人,看他走的路对不对。
坑三:随机数种子不固定。做对照实验时,如果不固定rand的种子,每次人数分布、移动冲突都不一致,你根本分不清结果的差异是参数带来的还是随机波动带来的。开跑前加一句rng(2021),后面所有结果都可复现。
坑四:不画图裸跑,出问题完全不知道。我第一次跑完数据,发现疏散时间异常短,想破脑袋没找到原因。后来把动画打开,才发现所有人都从墙体里“穿”过去了。因为生成初始位置时,我把墙格也算进了可走格列表里。可视化是排查这种低级错误的最直接手段。
6.2 调试方法论:从单人测试开始
我的调试流程已经固定了:
- 先放一个人,从每个房间各测一次,确保路径最短且不穿墙。
- 再放10个人,近距离观察是否发生卡死或来回抖动。
- 最后放满60人,跑完整流程,记录疏散曲线。
- 分析曲线拐点,对照动画,确认瓶颈位置合理。
如果第2步就出现人员“原地抖动”,多半是两个格子的距离场值相等,人物在振荡。解决办法是给每个格子的距离场加一个非常小的随机扰动,打破对称性。也可以用“优选前方优先”规则:上>下>左>右,固定顺序,避免抖动。
6.3 规则可以往哪里扩展
这个四房间走廊模型只是骨架,扩展空间很大:
- 改成多出口场景,比较“最近出口”和“均匀分流”策略的差别。
- 加一个烟雾扩散场,距离场加上烟雾权重,模拟能见度降低时人员的路径选择。
- 把0.4米网格改成0.25米网格,更精细地模拟门的占用情况,但计算量会明显上升。
- 把走廊改成长廊+楼梯间,就能向多层建筑疏散延伸。
我自己的体会是,这个模型的价值不在“精确还原某一天某栋楼的疏散”,而在于它是一个低成本、可反复实验的决策工具。改一个参数、跑一遍、看曲线、找瓶颈,这套流程比任何口头判断都踏实。项目做到后期,我开始对“出口越宽越好”“人越多时间越长”这些直觉结论产生了新的理解——很多时候直觉方向是对的,但量的关系完全出乎意料。这大概就是仿真的意义所在。