赞
踩
之前看一个用unity3d做的疫情模拟的视频感觉挺有意思的,而我正好也在学这个,眼看现在就要开学了,就想着按照我们学校做一个具体全面一点的疫情模拟。于是就开始了制作。下载地址在最下面。
首先第一步不是直接开始创建项目,而是写了一份大纲文档,构思制作的可行性,需要实现哪些功能,界面怎样布局,这一步花了不少时间,但有了它就可以节省开发过程中浪费的大量构思时间(深有体会)。
因为是做疫情模拟,数据量很大,所以其它方面要尽量抽象,突出重点也节省性能,所去找了张校园俯视图,然后绘制了一张抽象地图。
然后是功能,希望在开始模拟前,让一些参数给用户在相应范围内调整,例如学生总数,最初感染的人数,每次接触患者感染的几率,感染后多久后具备传染性,以及口罩类型,分为医用外科口罩、普通棉布口罩、医用防护口罩(N95),并查阅了这些口罩的防护效果,当计算是否感染时防护效果会用到。而有些参数需要用但不能给用户调整,例如碰撞传染检测频率、最小最大倍速、口罩减免效果、游戏时间与现实时间比例、管理行动的时间表等,给用户调整容易乱。还有游戏过程中需要能暂停和调整速度。
然后是UI,分为主界面、测试参数填写,游戏界面和暂停界面。UI和背景配色尽量简约,不是游戏整的太花就很怪,为了适应不同的手机屏幕,还需要给不同的UI设置相应的对齐方式。
创建项目,然后绘制一张地图并用导航网格Back一下用于后面Ai寻路
创建一个Capsule作为学生,挂上NavMeshAgent(用于自动寻路)和rigidbody(用于检测感染)后保存为prefab,便于创建时复制。为了节省性能,关闭了物理效果,碰撞器改为触发器。
创建编写一些脚本:
主要的就这些,其他的就不举例了。
实现学生作息自动管理
首先定义一个结构体Worktable,存储每个工作的开始时间和工作下标,工作分为上课(0)、回寝(1)、吃饭(2)、娱乐(3)。
public struct WorkTable
{
public int startTime; //单位:秒
public int workIdx;
}
然后创建一个WorkTable数组,按顺序用于存储一天的作息,然后遍历
void Update(){ if(gameStart) { ... if (isFree==false&&isAutoWork && gameTime >= workTable[workTableIdx].startTime && (gameTime < workTable[workTable.Length - 1].startTime || (gameTime >= workTable[workTable.Length - 1].startTime && workTableIdx == workTable.Length - 1))) { //时间一到,所有学生进行该工作 ChangeAllGoal(workTable[workTableIdx].workIdx); workTableIdx = (workTableIdx + 1) % workTable.Length; } } } private void MakeDefWorkTable() { workTable = new WorkTable[6]; workTable[0].startTime = 7 * HOUR;workTable[0].workIdx = 1; workTable[1].startTime = (int)(11.5 * HOUR); workTable[1].workIdx = 3; workTable[2].startTime = (int)(13.5 * HOUR); workTable[2].workIdx = 4; workTable[3].startTime = (int)(14.5 * HOUR); workTable[3].workIdx = 1; workTable[4].startTime = (int)(17.5 * HOUR); workTable[4].workIdx = 3; workTable[5].startTime = (int)(19.5 * HOUR); workTable[5].workIdx = 2; }
如果用户想自己来控制学生作息,点击按钮后直接调用ChangeAllGoal(工作序号)方法即可。
实现学生自由行动
当点击自由行动后,在GameController中调用所有学生的FreeWork()方法,在该方法中,先会随机给学生一个工作,然后用Invoke,在一定时间后再次调用该方法。直到用户点击管理行动后,在GameController取消所有学生的Invoke该方法。
//随机进行一个工作,并在1.5~3.5小时(游戏时间)后切换下一个,以此往复
public void FreeWork()
{
int workIdx = Random.Range(1, 5);
GameObject goal = null;
switch (workIdx)
{
case 1:goal = teachingBuilding;break;
case 2:goal = dormitory;break;
case 3:goal = gc.canteens[Random.Range(0, gc.canteens.Length)];break;
case 4:goal = gc.sports[Random.Range(0, gc.sports.Length)];break;
}
nav.SetDestination(goal.transform.position);
Invoke(nameof(FreeWork), Random.Range(1.5f * 3600 / GameData.timeMultiple, 3.5f * 3600 / GameData.timeMultiple));
}
实现暂停和加速
暂停和加速只要修改Time.timeScale的值即可,但需要注意的是,iTween动画的速度也会随着时间速度的改变而改变,当Time.timeScale为0时,Invoke方法和iTween动画也暂停了,如果要让iTween动画不受时间速度所影响,可以在调用iTween动画时添加ignoretimescale参数并设为true即可。
实现视角移动
视角移动分为垂直移动和水平移动。
垂直移动:直接根据游戏界面右下角Handle移动的y值/可移动范围的一半,得出的比例乘以垂直移动速度,最后让相机坐标的y轴加上这个值即可。
水平移动:
在用户拖拽的每一帧,用该帧用户触碰到的点相对于上一帧触碰的点的偏移赋给一个Vector2变量moveVec,然后让相机坐标的z和x分别减去moveVec的y和x即可。优化:为了让不同的高度都保持同样的屏幕移动速度(避免出现相机拉近时屏幕移动飞快拉远移动缓慢),moveVec需要先乘以相机高度和一个移动系数,我实验得出的是0.00107f就刚好能让拖拽前点中的位置在拖拽过程中始终和地图上的点对应。
public void OnBeginDrag(PointerEventData eventData)
{
carmBeginPos = Camera.main.transform.position;
}
public void OnDrag(PointerEventData eventData)
{
//eventData.delta = 自上次更新以来的指针坐标增量变化。
moveVec = eventData.delta * Camera.main.transform.position.y * ms; //越高移动越快
Camera.main.transform.position -= Vector3.forward * moveVec.y + Vector3.right * moveVec.x;
}
感染判断
感染概率计算公式:
在没有口罩的情况下,每次接触,感染概率为 取一个[0,1)之间的随机数a,再取一个[0,感染概率*2]之间的随机数b,判断a是否小于b,小于就感染。以感染概率为5%为例,每次感染的平均概率就为5%。
而有口罩的话,b就还需要乘以1-口罩阻挡病毒比例。
Rand(0f,1f) < Rand(0f,infectedProbability2) (wearMask?1-maskMulProbability:1)
public void Infected(bool _isInfected = false)
{
if (isInfected) return;
//感染判断
if (_isInfected||Random.Range(0f,1f) < Random.Range(0f,GameData.infectedProbability*2)*(gc.wearMask?1-GameData.maskMulProbability:1))
{
isInfected = true;
material.color = Color.yellow;
Invoke(nameof(Contagion), GameData.ContagionTime / GameData.timeMultiple);
FindObjectOfType<GameController>().Normal2Infected();
}
}
大概就这些。
Windows: https://lanzouw.com/iMqHEd977ub
Android: https://lanzouw.com/id7q66d
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。