演示

简介

之前看一个用unity3d做的疫情模拟的视频感觉挺有意思的,而我正好也在学这个,眼看现在就要开学了,就想着按照我们学校做一个具体全面一点的疫情模拟。于是就开始了制作。下载地址在最下面。

制作流程

构思

首先第一步不是直接开始创建项目,而是写了一份大纲文档,构思制作的可行性,需要实现哪些功能,界面怎样布局,这一步花了不少时间,但有了它就可以节省开发过程中浪费的大量构思时间(深有体会)。
sp_2020-05-28_11-13-11.png
因为是做疫情模拟,数据量很大,所以其它方面要尽量抽象,突出重点也节省性能,所去找了张校园俯视图,然后绘制了一张抽象地图。
然后是功能,希望在开始模拟前,让一些参数给用户在相应范围内调整,例如学生总数,最初感染的人数,每次接触患者感染的几率,感染后多久后具备传染性,以及口罩类型,分为医用外科口罩、普通棉布口罩、医用防护口罩(N95),并查阅了这些口罩的防护效果,当计算是否感染时防护效果会用到。而有些参数需要用但不能给用户调整,例如碰撞传染检测频率、最小最大倍速、口罩减免效果、游戏时间与现实时间比例、管理行动的时间表等,给用户调整容易乱。还有游戏过程中需要能暂停和调整速度。
然后是UI,分为主界面、测试参数填写,游戏界面和暂停界面。UI和背景配色尽量简约,不是游戏整的太花就很怪,为了适应不同的手机屏幕,还需要给不同的UI设置相应的对齐方式。

开发

创建项目,然后绘制一张地图并用导航网格Back一下用于后面Ai寻路
sp_2020-05-24_19-39-30.png
创建一个Capsule作为学生,挂上NavMeshAgent(用于自动寻路)和rigidbody(用于检测感染)后保存为prefab,便于创建时复制。为了节省性能,关闭了物理效果,碰撞器改为触发器。
sp_2020-05-28_13-21-42.jpg
创建编写一些脚本:

  • GameController用于疫情模拟逻辑控制
  • StudentController控制学生行为和存储学生信息
  • EventScript用于UI事件控制
  • SwitchAnimr用于动画过渡
  • GameData保存各项参数
  • UtilFunction编写一些通用的静态方法便于调用
  • AudioManager管理各种音效
    主要的就这些,其他的就不举例了。
    实现学生作息自动管理
    sp_2020-05-28_13-24-56.png
    首先定义一个结构体Worktable,存储每个工作的开始时间和工作下标,工作分为上课(0)、回寝(1)、吃饭(2)、娱乐(3)。
public struct WorkTable
{
    public int startTime;       //单位:秒
    public int workIdx;
}

然后创建一个WorkTable数组,按顺序用于存储一天的作息,然后遍历
sp_2020-05-28_13-32-21.jpg

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,infectedProbability
2) * (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
download.png

最后修改:2022 年 09 月 16 日
如果觉得我的文章对你有用,请随意赞赏