Unity游戏开发:基于SIR模型的传染病模拟器实现教程

Unity游戏开发:基于SIR模型的传染病模拟器实现教程
在游戏开发领域尤其是模拟经营或策略类游戏中疾病传播机制的设计是一个既经典又充满挑战的课题。它要求开发者不仅要理解基础的编程逻辑还要具备一定的数学建模和系统设计能力。本文将以一个经典的疾病传播模型为蓝本带领读者从零开始在 Unity 游戏引擎中实现一个简化但核心机制完整的“传染病模拟器”。这个过程将涵盖从核心概念理解、Unity 项目搭建、关键算法实现到可视化效果呈现和参数调试的完整流程。无论你是想深入了解游戏机制设计的在校学生还是希望为自己的独立游戏项目增添复杂系统的开发者这篇文章都将提供一条清晰的实践路径。我们将使用 C# 进行脚本编写并充分利用 Unity 的 GameObject 和 Component 系统来构建一个动态、可视化的模拟世界。1. 理解传染病模型的核心机制在开始编码之前必须清晰地定义我们要模拟的传染病模型。一个经典且实用的选择是 SIR 模型它虽然简化但足以体现疾病传播的动态特性。1.1 SIR 模型的基本原理SIR 模型将人群分为三个互斥的类别易感者 (Susceptible, S)尚未感染疾病但缺乏免疫力有被感染的风险。感染者 (Infected, I)当前携带病原体并具有传染性可以将疾病传播给易感者。康复者 (Recovered, R)已经从感染中恢复并假定获得了永久免疫力不再被感染也不再传播疾病。疾病传播的过程就是个体在这三个状态间的转移。其动力学通常用一组微分方程来描述但在离散时间的游戏模拟中我们可以用概率和循环来近似实现。1.2 从数学模型到游戏逻辑我们的游戏逻辑将围绕以下几个核心参数展开感染率 (Infection Rate)当一个易感者与一个感染者接触时单位时间内被感染的概率。这模拟了疾病的传染能力。恢复时间/恢复率 (Recovery Time/Rate)感染者从感染到康复所需的平均时间或单位时间内康复的概率。这模拟了个体免疫系统的响应或治疗的效果。初始感染人数模拟开始时感染者的数量是疫情的“火种”。在游戏中我们将创建大量代表个体的 Agent智能体每个 Agent 都有自己的 SIR 状态。游戏每一帧或每个固定时间步长我们将根据上述规则更新所有 Agent 的状态从而驱动整个疫情的演变。2. 搭建 Unity 项目与设计基础架构我们将使用 Unity 的面向数据思维方式来构建项目这有助于管理大量实体并保持代码清晰。2.1 创建新项目与基本设置首先打开 Unity Hub创建一个新的 3D 项目例如命名为“PandemicSimulator”。项目创建后进行初步设置规划项目文件夹结构在 Project 窗口的 Assets 文件夹下创建清晰的子文件夹如Scripts,Prefabs,Materials,Scenes。良好的结构是项目可维护性的基础。设置游戏视图在 Game 视图下拉菜单中将 Aspect Ratio 设置为一个固定的比例如 16:9以便于布局。2.2 设计核心数据结构Agent 状态我们创建一个 C# 脚本来定义单个 Agent 的行为和状态。在Scripts文件夹下右键创建新的 C# 脚本命名为AgentController。双击打开脚本我们先定义 Agent 的状态和必要属性using UnityEngine; public class AgentController : MonoBehaviour { // 定义 Agent 的三种状态 public enum AgentState { Susceptible, Infected, Recovered } [Header(Agent State)] public AgentState currentState AgentState.Susceptible; [Header(Disease Parameters)] public float infectionRadius 1.5f; // 感染范围 public float recoveryTime 5.0f; // 从感染到康复所需时间秒 private float infectionTimer 0f; // 感染计时器 // 用于可视化的Renderer组件 private Renderer agentRenderer; void Start() { // 获取Renderer组件用于改变颜色 agentRenderer GetComponentRenderer(); UpdateVisualAppearance(); // 如果初始状态就是感染者开始计时 if (currentState AgentState.Infected) { infectionTimer 0f; } } // 根据状态更新Agent的外观颜色 void UpdateVisualAppearance() { if (agentRenderer ! null) { switch (currentState) { case AgentState.Susceptible: agentRenderer.material.color Color.blue; // 易感者用蓝色 break; case AgentState.Infected: agentRenderer.material.color Color.red; // 感染者用红色 break; case AgentState.Recovered: agentRenderer.material.color Color.green; // 康复者用绿色 break; } } } }2.3 创建 Agent 预制体在 Hierarchy 窗口右键 - 3D Object - Cube创建一个立方体。将其 Scale 设置为 (0.3, 0.3, 0.3)使其成为一个小的个体。将刚才编写的AgentController脚本拖拽到该立方体上。将这个立方体从 Hierarchy 拖到 Project 窗口的Prefabs文件夹中创建一个预制体。创建成功后可以删除 Hierarchy 中的原始立方体。3. 实现疾病传播与状态转换逻辑现在我们需要为 Agent 添加核心行为移动、检测周围感染者、被感染、以及康复。3.1 为 Agent 添加移动和感染检测逻辑修改AgentController脚本添加移动和感染检测功能。public class AgentController : MonoBehaviour { // ... 之前已有的变量和Start方法 ... [Header(Movement)] public float moveSpeed 2.0f; private Vector3 randomDirection; private float directionChangeInterval 2.0f; private float directionChangeTimer 0f; [Header(Infection Parameters)] public float infectionRate 0.3f; // 每次接触的感染概率 void Update() { // 处理移动 HandleMovement(); // 处理状态逻辑 HandleStateLogic(); } void HandleMovement() { directionChangeTimer Time.deltaTime; if (directionChangeTimer directionChangeInterval) { // 随机生成一个新方向 randomDirection new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f)).normalized; directionChangeTimer 0f; directionChangeInterval Random.Range(1f, 3f); // 随机改变方向的间隔 } // 移动Agent transform.Translate(randomDirection * moveSpeed * Time.deltaTime, Space.World); // 简单的边界检查让Agent在区域内移动 Vector3 pos transform.position; if (Mathf.Abs(pos.x) 9f) randomDirection.x -Mathf.Sign(pos.x); if (Mathf.Abs(pos.z) 9f) randomDirection.z -Mathf.Sign(pos.z); transform.position new Vector3( Mathf.Clamp(pos.x, -9f, 9f), pos.y, Mathf.Clamp(pos.z, -9f, 9f) ); } void HandleStateLogic() { switch (currentState) { case AgentState.Susceptible: CheckForInfection(); break; case AgentState.Infected: UpdateInfection(); break; // Recovered 状态不需要做任何事 } } void CheckForInfection() { // 使用OverlapSphere检测周围的感染者 Collider[] hitColliders Physics.OverlapSphere(transform.position, infectionRadius); foreach (var hitCollider in hitColliders) { AgentController otherAgent hitCollider.GetComponentAgentController(); // 如果对方是感染者并且随机数小于感染率则被感染 if (otherAgent ! null otherAgent.currentState AgentState.Infected) { if (Random.value infectionRate * Time.deltaTime) // 乘以Time.deltaTime使概率与帧率无关 { BecomeInfected(); break; // 被感染后即可退出循环 } } } } void BecomeInfected() { currentState AgentState.Infected; infectionTimer 0f; UpdateVisualAppearance(); } void UpdateInfection() { infectionTimer Time.deltaTime; if (infectionTimer recoveryTime) { currentState AgentState.Recovered; UpdateVisualAppearance(); } } // 在Scene视图中绘制感染范围便于调试 void OnDrawGizmosSelected() { Gizmos.color currentState AgentState.Infected ? Color.red : Color.cyan; Gizmos.DrawWireSphere(transform.position, infectionRadius); } // ... UpdateVisualAppearance 方法保持不变 ... }3.2 创建游戏管理器并生成群体我们需要一个中心控制器来生成大量 Agent 并管理模拟参数。创建另一个 C# 脚本命名为GameManager。using UnityEngine; public class GameManager : MonoBehaviour { [Header(Simulation Parameters)] public GameObject agentPrefab; // 拖入之前创建的Agent预制体 public int totalAgents 100; public int initialInfected 3; public float simulationAreaSize 18f; // 模拟区域的大小 [Header(UI References)] // 稍后用于连接UI文本显示统计信息 public UnityEngine.UI.Text susceptibleText; public UnityEngine.UI.Text infectedText; public UnityEngine.UI.Text recoveredText; // 用于记录各状态人数 private int susceptibleCount; private int infectedCount; private int recoveredCount; void Start() { InitializeSimulation(); } void InitializeSimulation() { // 在指定区域内随机生成Agent for (int i 0; i totalAgents; i) { Vector3 randomPos new Vector3( Random.Range(-simulationAreaSize/2, simulationAreaSize/2), 0, Random.Range(-simulationAreaSize/2, simulationAreaSize/2) ); GameObject newAgent Instantiate(agentPrefab, randomPos, Quaternion.identity, transform); // 设置前initialInfected个Agent为感染者 AgentController agentCtrl newAgent.GetComponentAgentController(); if (i initialInfected) { agentCtrl.currentState AgentController.AgentState.Infected; } } UpdateCounts(); } void Update() { // 每帧更新统计信息对于大量Agent可以优化为每N帧更新一次 UpdateCounts(); UpdateUI(); } void UpdateCounts() { susceptibleCount 0; infectedCount 0; recoveredCount 0; foreach (Transform child in transform) { AgentController agent child.GetComponentAgentController(); if (agent ! null) { switch (agent.currentState) { case AgentController.AgentState.Susceptible: susceptibleCount; break; case AgentController.AgentState.Infected: infectedCount; break; case AgentController.AgentState.Recovered: recoveredCount; break; } } } } void UpdateUI() { if (susceptibleText ! null) susceptibleText.text S: susceptibleCount; if (infectedText ! null) infectedText.text I: infectedCount; if (recoveredText ! null) recoveredText.text R: recoveredCount; } // 提供一个重置模拟的方法可用于UI按钮 public void ResetSimulation() { // 销毁所有已存在的Agent foreach (Transform child in transform) { Destroy(child.gameObject); } // 重新初始化 InitializeSimulation(); } }4. 构建用户界面与运行验证为了让模拟过程更直观我们需要添加简单的用户界面UI来显示数据和提供控制。4.1 创建基础 UI在 Hierarchy 窗口右键 - UI - Canvas。这将创建一个画布用于放置UI元素。在 Canvas 下右键 - UI - Panel作为文本的背景。调整其锚点Anchors到左上角位置Pos X/Y设为 (10, 10)。在 Panel 下右键 - UI - Text创建三个文本元素分别命名为Text_Susceptible,Text_Infected,Text_Recovered。排列好它们的位置。再创建一个 Button命名为Button_Reset将其文本改为“重置模拟”。4.2 连接 UI 与 GameManager在 Hierarchy 顶层创建一个空 GameObject命名为_GameManager。将GameManager脚本附加到_GameManager对象上。将 Project 窗口中的 Agent 预制体拖拽到GameManager组件的Agent Prefab字段。将 Hierarchy 中三个 Text 对象分别拖拽到GameManager脚本的Susceptible Text,Infected Text,Recovered Text字段。选中Button_Reset对象在 Inspector 窗口的 Button 组件下方点击 On Click() 事件列表的 “” 号。将_GameManager对象拖拽到事件槽中然后在函数选择下拉菜单中找到GameManager.ResetSimulation。4.3 运行测试与初步参数调整点击 Unity 编辑器上的播放按钮运行模拟。你应该能看到大量蓝色易感者和少量红色初始感染者的小方块在场景中随机移动。红色方块附近的蓝色方块有一定概率会变成红色。红色方块在经过一段时候后默认5秒会变成绿色康复者。UI 上的 S, I, R 数字会动态变化。尝试调整GameManager和AgentController上的参数观察模拟结果的变化提高Infection Rate疫情传播速度会显著加快。缩短Recovery Time感染者更快康复可能抑制疫情峰值。增大Infection Radius更容易发生接触感染。5. 常见问题排查与模拟优化在实现过程中你可能会遇到一些典型问题。以下是排查思路和优化建议。5.1 性能与逻辑问题排查问题现象可能原因检查与解决方式帧率非常低游戏卡顿Agent数量过多如超过1000且每帧都在进行昂贵的物理检测OverlapSphere。1. 减少totalAgents数量。 2. 优化检测逻辑例如使用空间分区如网格系统只检测相邻网格的Agent。 3. 降低状态检测的频率如每0.1秒检测一次而非每帧。疾病完全无法传播Infection Rate设置过低如0.001或Infection Radius太小或Recovery Time太短。1. 逐步调高Infection Rate至 0.1 左右再测试。 2. 在Scene视图中选中一个感染者确认Gizmos显示的感染范围是否合理。 3. 确保CheckForInfection方法被正确调用。所有Agent瞬间全部感染Infection Rate设置过高如1.0且乘以Time.deltaTime的逻辑有误导致概率失真。1. 检查公式infectionRate * Time.deltaTime。在60FPS下一帧的Time.deltaTime约为0.016s。如果infectionRate为1则每帧概率为1.6%仍然很高。建议将infectionRate设置在0.01~0.5范围内。 2. 考虑使用固定时间步长进行感染检测而不是依赖帧率。Agent堆叠在角落不动移动逻辑中的边界检测可能过于严格导致方向不断被反转陷入死循环。检查HandleMovement方法中的边界反弹逻辑。可以尝试加入一个随机角度偏移或者让Agent在碰到边界后随机选择一个朝向区域内部的新方向。5.2 代码优化与功能增强建议优化性能使用协程进行间隔检测将CheckForInfection从Update中移出放入一个协程每0.1-0.2秒执行一次大幅减少计算量。// 在AgentController的Start方法中启动协程 StartCoroutine(PeriodicInfectionCheck()); IEnumerator PeriodicInfectionCheck() { while (true) { if (currentState AgentState.Susceptible) { CheckForInfection(); } yield return new WaitForSeconds(0.1f); // 每0.1秒检测一次 } }对象池如果需要频繁重置模拟使用对象池来复用Agent对象而不是反复Instantiate和Destroy。增强模型真实性潜伏期增加一个Exposed状态个体被感染后先进入潜伏期此时不传染过一段时间才变为有传染性的Infected状态SEIR模型。免疫力丧失为Recovered状态增加一个计时器一段时间后有一定概率变回SusceptibleSIRS模型。死亡引入一个Fatality Rate病死率感染者在康复前有概率“死亡”状态变为静止、颜色为灰色等。改善可视化与交互数据图表使用 Unity 的 UI 系统或 Asset Store 中的图表插件实时绘制 S、I、R 数量随时间变化的曲线。实时参数调整添加 UI Slider 控件允许在运行时动态调整感染率、恢复时间等参数立即观察对模拟的影响。6. 从模拟到游戏化设计的最佳实践一个可靠的模拟器是基础但要使其成为一个有趣的“游戏”还需要引入游戏设计元素。6.1 引入玩家干预机制游戏的核心是玩家的选择能影响结果。可以设计如下干预手段隔离玩家可以划定区域将该区域内的感染者“隔离”使其停止移动切断传播链。这会消耗“资源”并可能引起“民众不满”。研发疫苗/解药玩家可以投入资源加速“康复者”的产生或直接让一部分“易感者”获得免疫力变为康复者。研发需要时间且成功率可能不是100%。公共宣传降低全局的Infection Rate模拟戴口罩、勤洗手的效果但效果会随时间衰减。6.2 设计明确的目标与挑战目标不是消灭病毒这可能极难而是在资源耗尽前将感染人数控制在医疗系统可承受的阈值以下并最终研发出解药。失败条件感染人数超过临界值导致医疗挤兑或资源如经济、民众支持度归零。难度曲线病毒会变异参数自动变化或在特定时间点出现超级传播事件增加挑战性。6.3 生产环境下的考量如果这是一个严肃的科研或教学工具而非游戏则需要参数可配置化通过配置文件如JSON输入所有模型参数无需重新编译即可进行不同场景的模拟。数据导出将每一时间步的 S/I/R 数据记录到文件如CSV便于后续用专业工具如Python, R进行数据分析与可视化。模型验证与经典的微分方程数值解进行对比确保离散模拟的准确性。通过以上步骤我们不仅实现了一个技术上的传染病模拟器更探讨了如何将其提升为一个具有深度和可玩性的游戏项目。最关键的是理解参数之间的相互影响并设计出让玩家感到有意义的选择。

最新新闻

日新闻

周新闻

月新闻