资讯动态

C#十字路口红绿灯模拟:状态机实现与配时参数配置

发布时间:2026/10/18 4:01:40 来源:尧图企业网站定制
简介这是一份面向C#初学者与进阶开发者的十字路口红绿灯模拟项目源码围绕多线程、计时器控制、状态管理与WinForms界面设计展开适合用来练习事件驱动编程与线程同步等核心技能。压缩包共29个文件约1.52MB包含6个cs源码文件、4个png红绿灯素材图、2个resx资源文件、1个sln解决方案与1个csproj工程文件以及exe、pdb、cache等编译与缓存产物结构完整可直接打开运行。项目通过Timer控件周期性触发状态切换用枚举定义红、黄、绿三种灯态并借助Thread或Task让四个方向独立运行配合Label或PictureBox实时刷新界面颜色同时涉及Monitor、Mutex等同步原语与异常处理机制。代码采用面向对象方式封装TrafficLight类便于后续扩展人行横道信号与自适应流量控制。目前已有1632人学习下载可作为课程设计、毕业设计或C#多线程练手的参考范例。1. 十字路口红绿灯模拟从需求到可运行方案的完整拆解做交通流仿真的人常遇到一个尴尬用专业工具搭个十字路口模型光授权费就够买一台新电脑而需求只是验证一个四相位配时逻辑。C# 十字路口红绿灯模拟这套方案就是冲着这个场景来的——用 WinForms 或 WPF 加一个定时器把东西南北四个方向、直行左转右转的相位关系、黄灯过渡、行人过街请求全部用代码描述清楚。它适合两类人正在做交通控制课设或毕设的学生需要一套能跑起来、能改参数、能截图放进论文的系统以及刚接触状态机与并发编程的开发者想找一个有真实约束的练手项目。整套方案不依赖任何商业仿真库纯 C# 实现核心逻辑在 800 行以内但覆盖了实际路口控制里最容易被忽略的细节。2. 需求分析与技术选型为什么用状态机而不是定时器堆叠2.1 路口场景的硬约束拆解先把一个标准十字路口的需求摊开。东西南北四个进口每个进口有直行、左转、右转三个流向加上行人过街。如果按最细粒度控制理论上有 12 个机动车流向加 4 个行人相位。但实际路口不会让所有流向同时放行因为存在冲突点东进口直行和西进口左转会在路口中心打架南进口左转和北进口直行也是冲突对。所以需求的核心不是“让灯亮起来”而是“在冲突矩阵约束下找到一组互不冲突的流向组合按顺序轮换”。我一般会把需求拆成三层。第一层是相位定义每个相位是一组可以同时放行的流向集合比如“东西直行”“东西左转”“南北直行”“南北左转”就是经典四相位。第二层是相位顺序与过渡相位之间不能硬切绿灯结束要有黄灯黄灯时长通常 3 秒黄灯期间已进入路口的车辆要清空。第三层是配时参数每个相位的绿灯时长、黄灯时长、全红清空时间这些参数要能配置不能写死在代码里。还有一个容易被漏掉的需求行人过街请求。行人按钮按下后系统要记录请求在当前相位结束后插入行人相位而不是立即打断车流。这个“请求排队”的逻辑是区分玩具代码和可用方案的分水岭。2.2 状态机模型 vs 定时器堆叠很多初学者第一反应是开四个定时器每个方向一个各自倒计时。这个做法在简单场景能跑但一旦加入黄灯过渡和行人请求就会翻车。原因很简单定时器之间没有共享状态东方向绿灯还剩 2 秒时西方向定时器可能已经触发绿灯两个方向同时放行冲突就出现了。正确的做法是把整个路口控制建模成一个有限状态机。状态就是相位编号比如 S0 表示东西直行绿灯S1 表示东西直行黄灯S2 表示南北直行绿灯以此类推。状态迁移由两个条件触发当前状态倒计时归零或者行人请求在当前相位允许结束时被消费。所有灯色输出都是当前状态的函数不存在多个定时器各自为政的情况。// 相位状态枚举每个状态对应一组确定的灯色输出 public enum PhaseState { EW_Straight_Green, // 东西直行绿灯 EW_Straight_Yellow, // 东西直行黄灯 EW_Left_Green, // 东西左转绿灯 EW_Left_Yellow, // 东西左转黄灯 NS_Straight_Green, // 南北直行绿灯 NS_Straight_Yellow, // 南北直行黄灯 NS_Left_Green, // 南北左转绿灯 NS_Left_Yellow, // 南北左转黄灯 Pedestrian_Cross, // 行人过街相位 All_Red // 全红清空 }这个枚举看起来长但它把“什么时候哪些灯亮”这件事完全确定下来了。每个状态对应一个灯色映射表比如EW_Straight_Green状态下东进口直行绿灯亮、西进口直行绿灯亮、南北方向全红、行人灯红。映射表用字典或 switch 表达式实现改灯色逻辑只需要改这一处。2.3 技术栈选择与项目结构方案用 C# WinForms 做界面原因是课设和毕设场景下 WinForms 的绘图 API 足够画路口示意图而且不需要额外学习 XAML。如果要做更现代的界面WPF 也可以核心状态机代码完全复用。定时器用System.Windows.Forms.Timer间隔设为 100 毫秒每次 Tick 减少剩余时间归零时触发状态迁移。不用System.Timers.Timer是因为 WinForms 的 Timer 在 UI 线程执行避免跨线程更新控件的麻烦。项目结构分四层Models放相位枚举和配时参数类Logic放状态机核心UI放窗体和绘图Config放 JSON 配置文件。配时参数不写死在代码里用 JSON 读写这样改绿灯时长不用重新编译。// 配时参数类所有时间单位统一为秒 public class TimingConfig { public int StraightGreen { get; set; } 30; // 直行绿灯时长 public int LeftGreen { get; set; } 15; // 左转绿灯时长 public int Yellow { get; set; } 3; // 黄灯时长 public int AllRed { get; set; } 2; // 全红清空时长 public int PedestrianCross { get; set; } 20; // 行人过街时长 public bool EnableLeftPhase { get; set; } true; // 是否启用左转专用相位 }参数说明StraightGreen默认 30 秒是城市路口常见值左转 15 秒是因为左转车流通常小于直行。AllRed设为 2 秒给黄灯结束时还在路口的车辆留清空时间。EnableLeftPhase为 false 时左转和直行合并放行适合左转车流很小的路口。这些默认值不是拍脑袋是参考了常规配时手册里的典型值实际使用时按路口流量调整。3. 总体架构设计四层结构与状态迁移引擎3.1 分层职责与数据流整个系统分四层数据单向流动。最底层是TimingConfig只负责存参数不参与逻辑。往上是PhaseStateMachine它持有当前状态、剩余时间、行人请求队列对外暴露Tick()和RequestPedestrian()两个方法。再往上是LightController它把状态机的状态翻译成每个方向每个灯的具体颜色供 UI 读取。最上层是MainForm负责绘图和按钮事件。数据流是这样的定时器每 100 毫秒调用一次PhaseStateMachine.Tick()Tick 内部减少剩余时间归零时执行状态迁移迁移后更新当前状态。UI 每次重绘时向LightController查询当前灯色画到界面上。行人按钮点击时调用RequestPedestrian()把请求加入队列状态机在合适的迁移点消费这个请求。这个分层的好处是状态机完全不依赖 UI可以单独写单元测试。比如测试“东西直行绿灯 30 秒后是否进入黄灯”只需要构造状态机、连续调用 Tick 300 次、断言状态变为EW_Straight_Yellow。UI 换掉不影响逻辑逻辑改掉不影响 UI。3.2 状态迁移表与冲突矩阵状态迁移不是随便跳的要按冲突矩阵来。冲突矩阵是一个二维表行和列都是流向交叉点表示两个流向是否冲突。比如东直行和西直行不冲突可以同时放行东直行和西左转冲突不能同时放行。状态机设计时每个相位对应的流向集合必须满足“集合内任意两个流向都不冲突”。四相位方案的迁移顺序是东西直行绿灯 → 东西直行黄灯 → 全红 → 东西左转绿灯 → 东西左转黄灯 → 全红 → 南北直行绿灯 → 南北直行黄灯 → 全红 → 南北左转绿灯 → 南北左转黄灯 → 全红 → 回到东西直行。如果启用行人相位在某个全红之后插入行人过街行人过街结束后再进入下一个机动车相位。// 状态迁移核心逻辑根据当前状态和剩余时间决定下一个状态 private void AdvanceState() { switch (currentState) { case PhaseState.EW_Straight_Green: // 绿灯结束检查是否有行人请求 if (pedestrianQueue.Count 0 !leftPhaseActive) { currentState PhaseState.EW_Straight_Yellow; } else { currentState PhaseState.EW_Straight_Yellow; } remainingSeconds config.Yellow; break; case PhaseState.EW_Straight_Yellow: currentState PhaseState.All_Red; remainingSeconds config.AllRed; break; case PhaseState.All_Red: // 全红结束后根据配置决定进入左转还是对向直行 if (config.EnableLeftPhase !leftPhaseDone) { currentState PhaseState.EW_Left_Green; remainingSeconds config.LeftGreen; leftPhaseDone true; } else { currentState PhaseState.NS_Straight_Green; remainingSeconds config.StraightGreen; leftPhaseDone false; } break; // 其余状态迁移逻辑类似按迁移表逐项实现 } }这段代码的关键点是每次迁移只改变currentState和remainingSeconds两个变量灯色输出由LightController根据新状态重新计算。leftPhaseDone是一个布尔标记用来控制东西左转和南北左转的轮换顺序避免左转相位连续执行两次。行人请求队列在绿灯结束时检查如果有请求且当前不是左转相位就在黄灯和全红之后插入行人相位。3.3 灯色映射与界面刷新灯色映射用一个字典实现键是(PhaseState, Direction, Movement)三元组值是LightColor枚举。比如(EW_Straight_Green, East, Straight)映射到Green(EW_Straight_Green, North, Straight)映射到Red。这样 UI 只需要遍历所有方向所有流向查字典拿颜色画圆或画箭头。界面刷新用Invalidate()触发重绘在OnPaint里画路口示意图。路口画成一个十字四个进口各画三个箭头直行、左转、右转箭头颜色就是当前灯色。行人过街画成斑马线两端的红绿小人。倒计时数字画在每个进口旁边显示当前相位剩余秒数。这些绘图代码不涉及复杂逻辑用 GDI 的Graphics.DrawLine和FillEllipse就能完成。提示绘图时把路口坐标和箭头偏移量定义成常量不要散落在代码里。改界面尺寸时只改常量不用逐行找坐标。4. 核心模块实现从状态机到行人请求队列4.1 状态机类的完整实现状态机类是整个方案的心脏它需要处理三件事倒计时递减、状态迁移、行人请求入队与消费。倒计时递减在Tick()里做每次减 0.1 秒因为定时器间隔 100 毫秒减到 0 时调用AdvanceState()。行人请求入队用RequestPedestrian()它把请求加入Queuebool队列长度限制为 1避免重复请求堆积。public class PhaseStateMachine { private PhaseState currentState; private double remainingSeconds; private readonly TimingConfig config; private readonly Queuebool pedestrianQueue new Queuebool(); private bool leftPhaseDone false; public PhaseState CurrentState currentState; public double RemainingSeconds remainingSeconds; public PhaseStateMachine(TimingConfig config) { this.config config; currentState PhaseState.EW_Straight_Green; remainingSeconds config.StraightGreen; } // 定时器每100毫秒调用一次 public void Tick() { remainingSeconds - 0.1; if (remainingSeconds 0) { AdvanceState(); } } // 行人按钮点击时调用 public void RequestPedestrian() { if (pedestrianQueue.Count 0) { pedestrianQueue.Enqueue(true); } } private void AdvanceState() { // 迁移逻辑见上一节此处省略重复代码 // 关键点每次迁移后重置 remainingSeconds } }参数说明remainingSeconds用double而不是int因为定时器间隔是 100 毫秒用整数会累积误差。pedestrianQueue用Queuebool而不是List因为行人请求是先进先出而且只需要知道“有没有请求”不需要知道请求了几次。leftPhaseDone标记在每次进入南北直行时重置保证下一轮东西方向先执行左转。4.2 行人请求的排队与插入逻辑行人请求最容易翻车的地方是“按下按钮后灯立刻变”。实际路口不会这样因为突然变灯会导致正在通过的车辆急刹。正确做法是行人请求入队后状态机在当前机动车相位绿灯结束时检查队列如果有请求就在黄灯和全红之后插入行人相位而不是立即切换。插入逻辑在AdvanceState()里实现。当EW_Straight_Green或NS_Straight_Green结束时如果pedestrianQueue.Count 0下一个状态设为EW_Straight_Yellow黄灯结束后进入All_Red全红结束后不进入左转或对向直行而是进入Pedestrian_Cross。行人相位结束后再回到正常的机动车相位轮换。// 在 All_Red 状态结束时检查行人请求 case PhaseState.All_Red: if (pedestrianQueue.Count 0) { pedestrianQueue.Dequeue(); currentState PhaseState.Pedestrian_Cross; remainingSeconds config.PedestrianCross; } else if (config.EnableLeftPhase !leftPhaseDone) { currentState PhaseState.EW_Left_Green; remainingSeconds config.LeftGreen; leftPhaseDone true; } else { currentState PhaseState.NS_Straight_Green; remainingSeconds config.StraightGreen; leftPhaseDone false; } break;这段逻辑保证了行人请求不会打断当前绿灯而是在绿灯自然结束后插入。行人相位时长由config.PedestrianCross控制默认 20 秒足够行人通过双向六车道。如果路口更宽这个值要相应增加通常按“车道数 × 3 秒”估算。4.3 配时参数的 JSON 读写配时参数不写死在代码里用 JSON 文件读写。项目启动时从timing.json读取如果文件不存在就用默认值并生成一份。界面上提供“保存配置”按钮把当前参数写回 JSON。这样改绿灯时长不用重新编译也方便把不同路口的配时方案存成不同文件。{ StraightGreen: 30, LeftGreen: 15, Yellow: 3, AllRed: 2, PedestrianCross: 20, EnableLeftPhase: true }读写用System.Text.Json不需要额外 NuGet 包。反序列化时用JsonSerializer.DeserializeTimingConfig(json)序列化时用JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented true })。WriteIndented让生成的 JSON 带缩进方便手动编辑。注意JSON 文件路径用AppDomain.CurrentDomain.BaseDirectory拼接不要用相对路径。相对路径在 Visual Studio 调试和发布后运行时的基准目录不同容易读不到文件。5. 避坑与常见问题状态机项目里最容易翻车的五个点5.1 定时器精度导致倒计时跳秒现象界面上倒计时从 3 秒直接跳到 1 秒中间少了 2 秒。原因System.Windows.Forms.Timer的间隔不精确系统繁忙时可能延迟到 150 毫秒才触发一次如果每次固定减 0.1 秒实际时间就比显示时间快。解决不要假设每次 Tick 间隔正好 100 毫秒用Stopwatch记录真实经过时间每次 Tick 按实际时间差减少remainingSeconds。private Stopwatch stopwatch Stopwatch.StartNew(); private double lastElapsed 0; public void Tick() { double currentElapsed stopwatch.Elapsed.TotalSeconds; double delta currentElapsed - lastElapsed; lastElapsed currentElapsed; remainingSeconds - delta; if (remainingSeconds 0) { AdvanceState(); } }这样即使定时器延迟倒计时也按真实时间走不会跳秒。5.2 黄灯期间车辆未清空就放行冲突流向现象东西直行黄灯刚结束南北直行绿灯就亮了但路口里还有东西方向的车辆没出去导致碰撞。原因黄灯时长设得太短或者全红时间被省略。解决黄灯默认 3 秒全红默认 2 秒两者相加 5 秒清空时间。如果路口较大比如双向八车道全红要加到 3 到 4 秒。这个值不能省省了就是拿安全换效率。5.3 行人请求队列无限增长现象行人按钮被连续按了十几次队列里堆了十几个请求导致后续每个相位结束后都插入行人相位机动车道长时间绿灯不亮。原因RequestPedestrian()没有限制队列长度。解决队列长度限制为 1已经有请求时再按按钮不重复入队。代码里用if (pedestrianQueue.Count 0)判断简单有效。5.4 状态迁移遗漏导致死锁现象程序运行一段时间后卡在某个状态不动倒计时一直是 0。原因AdvanceState()的 switch 语句漏了某个状态的 case或者某个 case 里没有重置remainingSeconds。解决在AdvanceState()末尾加一个断言确保remainingSeconds 0否则抛出异常并记录当前状态。开发阶段用Debug.Assert发布后写日志。5.5 界面重绘闪烁现象倒计时数字刷新时界面闪烁箭头颜色变化时整块区域闪一下。原因OnPaint里没有启用双缓冲每次重绘都先擦背景再画前景。解决在窗体构造函数里设置DoubleBuffered true或者重写CreateParams加上WS_EX_COMPOSITED样式。WinForms 的双缓冲对 GDI 绘图有效设置后闪烁基本消失。6. 验收与答辩怎么证明这套模拟真的能跑验收这套方案不是看界面画得多漂亮而是看状态迁移是否符合冲突矩阵、配时参数是否可配置、行人请求是否按规则插入。我一般会准备三组测试用例答辩时现场跑。第一组是基础相位轮换测试。把直行绿灯设为 10 秒、左转 5 秒、黄灯 3 秒、全红 2 秒启动后观察一个完整周期。预期是东西直行 10 秒 → 黄灯 3 秒 → 全红 2 秒 → 东西左转 5 秒 → 黄灯 3 秒 → 全红 2 秒 → 南北直行 10 秒 → 黄灯 3 秒 → 全红 2 秒 → 南北左转 5 秒 → 黄灯 3 秒 → 全红 2 秒 → 回到东西直行。总周期 50 秒。用秒表掐一下误差在 1 秒以内算合格。第二组是行人请求测试。在东西直行绿灯第 5 秒时点击行人按钮预期是东西直行继续到 10 秒结束 → 黄灯 3 秒 → 全红 2 秒 → 行人过街 20 秒 → 回到机动车相位轮换。行人相位不会在绿灯中途插入也不会被忽略。如果点击按钮后绿灯立刻变红说明插入逻辑写错了。第三组是参数边界测试。把直行绿灯设为 1 秒、黄灯设为 0 秒、全红设为 0 秒看程序是否崩溃。合格的表现是程序不崩溃但界面提示参数不合法或者自动修正为最小值比如黄灯最少 1 秒。这个测试考察的是参数校验答辩时能体现你对异常输入的考虑。测试项输入预期输出判定标准基础轮换直行10s 左转5s 黄3s 全红2s周期50s顺序正确秒表误差≤1s行人请求绿灯第5s按按钮绿灯结束后插入行人相位不打断当前绿灯参数边界黄灯0s 全红0s不崩溃提示或修正程序正常运行答辩时还有一个加分项把状态迁移过程用日志打出来。每次AdvanceState()被调用时记录时间戳、旧状态、新状态、剩余时间。答辩现场打开日志文件指着时间戳说“你看东西直行绿灯从第 0 秒到第 10 秒第 10 秒准时进入黄灯”比口头解释有说服力得多。// 状态迁移日志记录每次迁移的详细信息 private void LogTransition(PhaseState oldState, PhaseState newState) { string logLine ${DateTime.Now:HH:mm:ss.fff} | {oldState} - {newState} | remaining{remainingSeconds:F1}s; File.AppendAllText(transition.log, logLine Environment.NewLine); }这个日志在调试阶段帮我抓过好几次迁移遗漏的 bug。有一次南北左转黄灯结束后没有进入全红直接跳到了东西直行绿灯日志显示NS_Left_Yellow - EW_Straight_Green中间少了All_Red。查代码发现是 switch 里漏了一个 case。从那以后我每次写完状态迁移逻辑都强制走一遍日志检查确认每个状态的出口都指向正确的下一个状态。希望这套方案能帮你把十字路口红绿灯模拟从“能亮灯”做到“能验收”。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑