手把手教你用C#做疫情传播仿真

手把手教你用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 -> infectedList<int> newlyInffectedIds = new List<int>();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 - 性能优化

注意上文中的代码,它原本可能会是一个 50005000的大循环,而每帧的时间仅仅只有 1/60=13.33ms

经过反复思考,我使用了三种方法来优化。

优化1 · 索引与缓存

首先是在城市类 City中,我使用了一个索引:

class City
{public Person[] Persons;private SortedSet<int> infectorIds = new SortedSet<int>();private SortedSet<int> healthyIds = new SortedSet<int>();// ... 其它代码
}

该索引维护了两个索引 infectorIds和 healthyIds,保存好这两个索引后,这个双层循环检测性能可以从 50005000降低到 020002000,最优情况是初期和未期,数据规模趋近于 0,最差情况在中期,数据规模趋近于 20002000,总之会比简单的双层循环快很多。

注意:索引是有明显缺点的,索引的本质是缓存,缓存的本质是状态,状态的属性之一,就是 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疫情传播程序】的代码,其实这些代码不光应用在这个程序中,也应用到了我写过的许多小游戏和模拟器,都非常重要。

所有这些代码都已经上传到我的 Github:https://github.com/sdcb/2019-ncp-simulation,各位可以自由 starfork/提 issuePR

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/311906.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

word List 10

word List 10 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

[蓝桥杯]回形取数-方向向量+模拟

题目描述 回形取数就是沿矩阵的边取数&#xff0c;若当前方向上无数可取或已经取过&#xff0c;则左转90度。一开始位于矩阵左上角&#xff0c;方向向下。 输入 输入第一行是两个不超过200的正整数m, n&#xff0c;表示矩阵的行和列。接下来m行每行n个整数&#xff0c;表示这个…

java程序员选择多个offer时需要看重哪些?_对不起,我们公司不要本科以下的大学生,学历对于程序员重不重要...

来自普通大学的学生&#xff0c;总会觉得逆袭如梦似幻&#xff0c;机会在哪里&#xff1f;但放长远来看&#xff0c;更重要的是要看到&#xff1a;环境只能影响&#xff0c;却不能决定我们的人生。人生路漫长&#xff0c;最终会怎样&#xff0c;还不一定呢&#xff01;在某综艺…

中小企业团队敏捷产品开发流程最佳实践

近期因为疫情的影响&#xff0c;不少互联网公司开始尝试远程工作。也出不了少如何做好远程工作的方法&#xff0c;我认为不管是场地办公还是远程办公都依赖于原来的产品开发流程。我曾经遵循CMMI5的流程管理过15人左右的跨国/语言/文化团队&#xff0c;也遵循敏捷Scrum管理过9人…

数据结构---二叉线索树

数据结构—二叉线索树 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> typedef struct bmTree {int data;struct bmTree* lchild, *rchild;int ltag, rtag; }bmTree; bmTree * preNULL;//中序遍历的前驱指针 void cr…

[蓝桥杯2019初赛]不同子串-substr,模拟

题目描述 一个字符串的非空子串是指字符串中长度至少为1 的连续的一段字符组成的串。 例如&#xff0c;字符串aaab 有非空子串a, b, aa, ab, aaa, aab, aaab&#xff0c;一共7 个。 注意在计算时&#xff0c;只算本质不同的串的个数。 请问&#xff0c;字符串0100110001010001 …

revit如何根据坐标进行画线_铭成教你如何根据同步带的齿形进行选型

1、同步带齿形分类梯型齿同步带分为&#xff1a;最轻型MXL、超轻型XXL、特轻型XL、轻型L、重型H、特重型XH、超重型XXH&#xff0c;共七种&#xff0c;这几种是目前市场用的最广泛的。特殊齿型的同步带又分为&#xff1a;最轻型T2.5、轻型T5、重型T10、最重型T20&#xff0c;现…

在VS Code里逛知乎、发文章?知乎 on VS Code来啦!重新定义内容创作!

本文为 牛岱 的原创文章在2020年2月10日首发于“玩转VS Code”知乎专栏你是否已经厌倦了知乎 Web 端文本编辑器糟糕的使用体验和时而出现的奇怪 Bug&#xff1f;身为程序员的你是否想用你最熟悉的 Markdown 语法写答案&#xff0c;并且获得最佳的代码块语法高亮&#xff1f;攥写…

[蓝桥杯]带分数-dfs

题目描述 100 可以表示为带分数的形式&#xff1a;100 3 69258 / 714。 还可以表示为&#xff1a;100 82 3546 / 197。 注意特征&#xff1a;带分数中&#xff0c;数字1~9分别出现且只出现一次&#xff08;不包含0&#xff09;。 类似这样的带分数&#xff0c;100 有 11 种…

线性代数---向量问题的求解方法

线性代数—向量问题的求解方法 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

java 定时_Java线上定时任务不定期挂掉问题分析

作者&#xff1a;IKNOW本尊问题背景收到频繁的告警邮件&#xff0c;定时任务调度失败&#xff0c;查看xxl-job的执行器列表是空的&#xff0c;但是服务又显示健康&#xff0c;查看历史任务执行记录发现执行器是依次递减&#xff0c;由于是线上服务&#xff0c;只能先重启&#…

gRPC in ASP.NET Core 3.x -- Protocol Buffer, Go语言的例子(上)

前两篇文章半年前写的&#xff1a;gRPC in ASP.NET Core 3.0 -- Protocol Buffer&#xff08;1&#xff09;&#xff0c;gRPC in ASP.NET Core 3.0 -- Protocol Buffer&#xff08;2&#xff09;之前把protocol buffer的基础知识讲了一遍&#xff0c;今天使用Go语言做一些例子。…

word List 11

word List 11 如果存在什么问题&#xff0c;欢迎批评指正&#xff01;谢谢&#xff01;

[蓝桥杯]蚂蚁感冒

题目描述 长100厘米的细长直杆子上有n只蚂蚁。它们的头有的朝左&#xff0c;有的朝右。 每只蚂蚁都只能沿着杆子向前爬&#xff0c;速度是1厘米/秒。 当两只蚂蚁碰面时&#xff0c;它们会同时掉头往相反的方向爬行。 这些蚂蚁中&#xff0c;有1只蚂蚁感冒了。并且在和其它蚂蚁…

温故知新 .Net重定向深度分析

在早期的.NET Framework程序员心里&#xff0c;重定向Redirect其实分为两种&#xff1a;Response.Redirect&#xff1a;Response对象的Redirect方法提供了一种实现客户端重定向的方法Server.Transfer&#xff1a;Server对象的Transfer方法使用服务器执行重定向&#xff0c;并避…

笔记本电脑销量排名_网友总结京东笔记本销量,联想高居第一,华为表现很出色...

笔记本电脑&#xff0c;还是要看大品牌。网友总结近30天京东笔记本销量&#xff0c;结局让人出乎意料。网友直呼&#xff0c;原来华为笔记本这么强。智能手机的出现&#xff0c;对传统的PC行业造成了一定的影响&#xff0c;但它始终还是无法替代PC的功能。在这个行业中&#xf…

数据结构---二叉排序树

数据结构—二叉排序树 原理&#xff1a;参考趣学数据结构 代码&#xff1a; #include<stdio.h> #include<stdlib.h> typedef struct bstTree {int data;struct bstTree* lchild, *rchild; }bstTree; void createBSTTree(bstTree* & T,int data) {//创建二叉…

研发协同平台持续集成2.0架构演进

在上篇《研发协同平台持续集成实践》一文中我们分享了为什么要做持续集成&#xff0c;技术选型&#xff0c;工作原理以及实践落地。今天我们从架构上来分享一下架构层面的设计和演进。持续集成1.0在最开始设计的过程中&#xff0c;本着一切从需求出发&#xff0c;一切以实现业务…

口袋操作系统_全自动阀口袋包装机的发展

随着全球产业技术的不断发展&#xff0c;包装机行业也出现了产业结构调整的动向&#xff0c;因为全新的全自动阀口袋包装机和全新的包装产品技术问世&#xff0c;那些制作粗糙&#xff0c;能源消耗大&#xff0c;技术相对比较落后的阀口袋包装机也将会被市场一步步淘汰掉&#…

[蓝桥杯]最大子阵

题目描述 给定一个n*m的矩阵A&#xff0c;求A中的一个非空子矩阵&#xff0c;使这个子矩阵中的元素和最大。 其中&#xff0c;A的子矩阵指在A中行和列均连续的一块。 输入 输入的第一行包含两个整数n, m&#xff0c;分别表示矩阵A的行数和列数。 接下来n行&#xff0c;每行m个整…