宁波网站建设小程序开发,珠海品牌网站设计,dede学校网站,wordpress小型店商城手把手教你用C#做疫情传播仿真在上篇文章中#xff0c;我介绍了用 C#做的疫情传播仿真程序的使用和配置#xff0c;演示了其运行效果#xff0c;但没有着重讲其中的代码。今天我将抽丝剥茧#xff0c;手把手分析程序的架构#xff0c;以及妙趣横生的细节。首先来回顾一下运… 手把手教你用C#做疫情传播仿真在上篇文章中我介绍了用 C#做的疫情传播仿真程序的使用和配置演示了其运行效果但没有着重讲其中的代码。今天我将抽丝剥茧手把手分析程序的架构以及妙趣横生的细节。首先来回顾一下运行效果 注意看程序中的信息包含信息统计、城市居民展示和医院展示三个部分其中居民按状态的不同显示为不同的颜色。本文将先从程序员的角度说说程序中的实现细节细节中会聊一聊与与 Java版的不同最后进行总结。细节介绍细节介绍一 · 从“人”说起居民类如下所示struct Person
{public PersonStatus Status;public Vector2 Position;public float EstimateDays;public float Direction;public static Person Create(float citySize){// ...}public void Draw(DeviceContext ctx, XResource x){// ...}public void MoveAroundInCity(float dt, float citySize){// ...}
}
enum PersonStatus
{Healthy, // 健康InfectedInShadow, // 被感染处于潜伏期Illness, // 发病InHospital, // 发病并进入医院Cured, // 治愈Dead, //死亡
}
一个城市将会模拟 5000个居民因此在设计这个类的时候应该尽可能地考虑性能、节约内存。所以状态最好越少越好在设计这个类的时候我谨慎地保留了状态 Status、当前位置 Position、用于做状态机的 EstimateDays和移动方向 Direction这四个状态。细节介绍二 - 居民的状态变更流居民状态扭转过程如下所示 (有传染性传染给健康人)???? ⬆ ⬆???? ⬆ ⬆
健康 ➡ 潜伏期 ➡ 发病 ➡ 入院隔离 ➡ 治愈↘ ↙↘ ↙死亡
其中 健康到 被感染的验证除了状态检测外还要由居民之间的距离决定。而是否戴口罩又会影响其判断距离这些逻辑用代码表示如下const float InffectRate 0.8f; // 靠得够近时被携带者感染的机率
static bool WearMask false; // 是否戴口罩
// 要靠多近才会触发感染验证
static float SafeDistance() WearMask ? 1.5f : 3.5f;
void StepDay()
{// ...// healthy - infectedListint newlyInffectedIds new Listint();newlyInffectedIds healthyIds.AsParallel().Where(x {foreach (var infectorId in infectorIds){if (Vector2.DistanceSquared(Persons[x].Position, Persons[infectorId].Position) SafeDistance() * SafeDistance())return true;}return false;}).ToList();foreach (int personId in newlyInffectedIds){Infect(personId);}
}
EstimateDays字段用于控制潜伏期、发病到去医院的等待时间、治愈时间这个字段用得较为巧妙。正常可能需要三个字段但这三种状态之间不存在状态共享因此可以使用一个共享的字段来代替。比如 infected-illness状态扭转的代码表述如下void StepDay()
{for (var i 0; i Persons.Length; i){// ... 其它代码// infected - illnessif (Persons[i].Status PersonStatus.InfectedInShadow){--Persons[i].EstimateDays;if (Persons[i].EstimateDays 0){Persons[i].Status PersonStatus.Illness;Persons[i].EstimateDays GenerateToHospitalDays();}continue;}}// ... 其它代码
}
注意代码中总会使用 EstimateDays来判断是否要进入下一个状态而进入下一个状态后便会重新指定新的 EstimateDays。通过这样的状态共享便可为 Person类节省许多状态。细节介绍3 - 性能优化注意上文中的代码它原本可能会是一个 5000x 5000的大循环而每帧的时间仅仅只有 1/6013.33ms。经过反复思考我使用了三种方法来优化。优化1 · 索引与缓存首先是在城市类 City中我使用了一个索引class City
{public Person[] Persons;private SortedSetint infectorIds new SortedSetint();private SortedSetint healthyIds new SortedSetint();// ... 其它代码
}
该索引维护了两个索引 infectorIds和 healthyIds保存好这两个索引后这个双层循环检测性能可以从 5000x 5000降低到 0- 2000x 2000最优情况是初期和未期数据规模趋近于 0最差情况在中期数据规模趋近于 2000x 2000总之会比简单的双层循环快很多。注意索引是有明显缺点的索引的本质是缓存缓存的本质是状态状态的属性之一就是 bug多一份索引就需要多加一处维护索引的位置就多加了一层“写 bug”的风险。另外索引过多可能会影响性能。我会尽我一切努力不给程序引入额外状态。除非我有一个无法拒绝的理由。优化2 · 多线程这算是 .NET的福利吧。如代码所示我使用了 PLINQ这是从 .NET4.0推出的新玩意只需一条简单的 AsParallel()就可以让代码几乎不变就能享受多核 CPU带来的性能红利我完全不需要处理同步等机制。优化3 · 使用值类型也如代码所示我特意为 Person类选择了值类型 struct它的优点在本程序中体现在两处一是在于创建时无需分配堆内存要知道内存分配需要请求操作系统就像浏览器请求服务器那样非常缓慢二是值类型数据的值在内存中是连续的。这对 CPU缓存是个天大的好消息。无论是否是现代 CPU对连续型的内存访问性能总是最高的在一性能测试中连续内存与非连续内存的 CPU访问速度差高达 50倍之大。注意 Java中没提供类似于 struct这样的关键字无法自定义值类型。但通过一定技巧如创建基元类型数组也能实现高性能的连续内存访问。我之前写过一篇文章《.NET中的值类型与引用类型》包含了详情说明包含缺点与优化、使用场景等和性能测试。细节介绍四 - 时间控制我尝试写过很多游戏和动态模拟器我认为时间控制的优劣最能体现出一个模拟器/游戏制作者的用心。一般程序员都喜欢将垂直同步事件当作游戏的心脏这样最简单用代码表述如下已简化void Render()
{float dt RenderTimer.LastFrameTimeInSecond;Update(dt);Draw(ctx);SwapChain.Present(1, 0);
}
这样的好处是逻辑可能比较简单可以在大脑中脑补每秒 60帧然后按 60帧设置参数想事情。这样一来更新逻辑 Update(dt)可能就会和垂直同步事件强绑定。要知道有些投影仪可能只有 50帧而某些显示器有 144帧然后就是它也和垂直同步选项强绑定一旦关闭垂直同步 Update逻辑可能就会过快而导致程序运行不正常。我的做法是将这些逻辑稍作封装代码中的配置只与真实世界中的时间相关而与垂直同步选项无关const float SecondsPerDay 0.3f; // 模拟器的秒数对应真实一天
class City
{float dayAccumulate 0;public void Update(float dt){// step movefor (var i 0; i Persons.Length; i){Persons[i].MoveAroundInCity(dt, CitySize);}// step statusdayAccumulate dt;day (dt / SecondsPerDay);while (dayAccumulate SecondsPerDay){StepDay();dayAccumulate - SecondsPerDay;}}
}
注意我使用了一个 SecondsPerDay来控制模拟器的运行速度将这个值调大或调小不影响运行的最终结果。我还使用了一个 dayAccumulate值用于做按“天”更新判断这样的话无论函数调用频率如何调用 StepDay()时都会确保相隔“一整天”。细节介绍五 - 缩放管理和时间管理一样我认为窗口大小与缩放控制也很重要否则程序只能以一种固定的分辨率、 DPI来运行。我使用的是我自己写的“准”游戏引擎 FlysEngine它基于 Direct2D可以通过矩阵变换轻松地管理好程序缩放protected override void OnDraw(DeviceContext ctx)
{ctx.Clear(Color.DarkGray);float minEdge Math.Min(ClientSize.Width / 2, ClientSize.Height / 2);float scale minEdge / 540; // relative coordinatectx.Transform Matrix3x2.Scaling(scale) *Matrix3x2.Translation(ClientSize.Width / 2, ClientSize.Height / 2);City.Draw(ctx, XResource);
}
注意我定义了一个“魔法值”—— 540它是 FHD1920x1080中短边 1080的一半。这样一来有两个好处。首先我程序后面所有代码都可以按照 1920x1080的“相对值”进行设计。无论客户的桌面分辨率是 4kUHD还是 1366x768都会以相同的比例做缩放。其次我还将坐标原点设为屏幕的正中心这样也更加简化了我的后续代码比如在控制 Person的出生点时我可以通过极坐标系直接生成struct Person
{public static Person Create(float citySize){float phi random.NextFloat(0, MathUtil.TwoPi);float r random.NextFloat(0, citySize);var p new Person { Status PersonStatus.Healthy };p.Position.X MathF.Sin(phi) * r;p.Position.Y -MathF.Cos(phi) * r;p.Direction random.NextFloat(0, MathF.PI * 2);return p;}// 其它代码
}
总结本文从五个细节聊了我的【.NET疫情传播程序】的代码其实这些代码不光应用在这个程序中也应用到了我写过的许多小游戏和模拟器都非常重要。所有这些代码都已经上传到我的 Githubhttps://github.com/sdcb/2019-ncp-simulation各位可以自由 star/ fork/提 issue/ PR。