这一节我们来实现一个简单的时间系统

ITimeSystem

时间系统肯定是在系统层的,它需要游戏一开始初始化时就能统计时间信息。

在Scripts——System文件夹下新建TimeSystem文件夹,并在其中新建ITimeSystem脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
using System;
using System.Collections.Generic;
using UnityEngine;
using FrameWorkDesign;

namespace ShootingEditor2D
{
interface ITimeSystem : ISystem
{
float CurrentSeconds { get; }
void AddTaskDelay(float seconds, Action onDelayFinish);
}

public class TimeSystem : AbstractSystem, ITimeSystem
{
public float CurrentSeconds { get; private set; }

private LinkedList<DelayTask> mDelayTasks = new LinkedList<DelayTask>();

public void AddTaskDelay(float seconds, Action onDelayFinish)
{
var delayTask = new DelayTask()
{
Seconds = seconds,
OnFinish = onDelayFinish,
State = DelayTaskState.NonStart
};

mDelayTasks.AddLast(delayTask);
}

protected override void OnInit()
{
CurrentSeconds = 0;
var timeSystemGameObj = new GameObject(nameof(TimeSystemUpdateBehaviour));
var updateBehaviour = timeSystemGameObj.AddComponent<TimeSystemUpdateBehaviour>();

updateBehaviour.OnUpdate += OnUpdate;
}

public class TimeSystemUpdateBehaviour : MonoBehaviour
{
public event Action OnUpdate;//为Action委托添加event标记

private void Update()
{
OnUpdate?.Invoke();
}
}

void OnUpdate()
{
CurrentSeconds += Time.deltaTime;

if (mDelayTasks.Count > 0)
{
var currentNode = mDelayTasks.First;

while (currentNode != null)
{
var nextNode = currentNode.Next;
var delayTask = currentNode.Value;

if(delayTask.State == DelayTaskState.NonStart)
{
delayTask.State = DelayTaskState.Started;
delayTask.StartSeconds = CurrentSeconds;
delayTask.FinishSeconds = CurrentSeconds + delayTask.Seconds;
}
else if (delayTask.State == DelayTaskState.Started)
{
if (CurrentSeconds >= delayTask.FinishSeconds)
{
delayTask.State = DelayTaskState.Finish;
delayTask.OnFinish.Invoke();

delayTask.OnFinish = null;

//链表可以删除delayTask,也可以删除currentNode(LinkedListNode<DelayTask>)
//使用后者性能更好,因为它是对链表指针进行的操作,删除后节点前后相邻的指针会合并
//使用前者删除的话会遍历链表的内容
mDelayTasks.Remove(currentNode);
}
}

currentNode = nextNode;
}
}
}
}

public class DelayTask
{
public float Seconds { get; set; }
public Action OnFinish { get; set; }
public float StartSeconds { get; set; }
public float FinishSeconds { get; set; }
public DelayTaskState State { get; set; }
}

public enum DelayTaskState
{
NonStart,
Started,
Finish
}
}

当前时间系统的设计图如下

时间系统设计图

注意:

  • 此时间系统初始化后一直存在,代码内并没有注销方式
  • 时间系统需要依赖Unity的Update,所以添加了TimeSystemUpdateBehaviour内部类,并在OnInit的时候挂载了这个类
  • TimeSystemUpdateBehaviour中可以看到,Unity每一帧都发布OnUpdate这个事件,这样TimeSystem内部的OnUpdate方法就能及时刷新,TimeSystemUpdateBehaviour其实算是个表现层,所以采用事件发布的这种形式来与TimeSystem分离,注意这种思路
  • 每调用一次AddTaskDelay方法,就会new一个DelayTask,我们用链表纪录这些DelayTask并用while遍历链表,选择链表是因为我们不确定DelayTask的多少,代码使用while遍历链表的方式比foreach效率要高,要注意这种方式
  • 一定要时刻记住引用该置空的时候就要置空,当一个DelayTask结束时,这个类就没有用了,它内部的引用(OnFinish)就要置空

然后在ShootingEditor2D脚本中注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using FrameWorkDesign;

namespace ShootingEditor2D
{
public class ShootingEditor2D : Architecture<ShootingEditor2D>
{
protected override void Init()
{
this.RegisterSystem<IStatSystem>(new StatSystem());
this.RegisterSystem<IGunSystem>(new GunSystem());
this.RegisterSystem<ITimeSystem>(new TimeSystem());//

this.RegisterModel<IPlayerModel>(new PlayerModel());
}
}
}

测试

每当我们要做一个新的系统,肯定需要场景来反复测试。

在Assets文件夹下新建Tests文件夹,并在此文件夹内新建TimeSystem文件夹,在其中新建脚本TimeSystemTest和场景TimeSystemTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;

namespace ShootingEditor2D.Tests
{
public class TimeSystemTest : MonoBehaviour
{

// Use this for initialization
void Start()
{
Debug.Log(Time.time);
ShootingEditor2D.Interface.GetSystem<ITimeSystem>()
.AddTaskDelay(3, () =>
{
Debug.Log(Time.time);
});
}

}
}

将此脚本挂载在场景任意对象上,开始游戏3秒后就会有Debug弹出。

这种写一个脚本挂到一个独立场景下试运行,是最简单的单元测试的方法,我们可以测试更复杂的TimeSystem功能,比如游戏暂停时,游戏继续时。当游戏项目特别复杂的时候,写一些单元测试来保证某个系统正常运转是非常重要的。

Test Runner

Unity内部已经提供了单元测试的框架,能够集成并记录所有的测试项目,再开发的后期能够多次进行测试,更加规范。

About Unity Test Framework | Test Framework | 1.1.33 (unity3d.com)

记住写单元测试的目的,是为了保证系统正常运转,后期运行已经积累的单元测试可以节省很多人力测试的工作量,除了单元测试还有集成测试,人力(黑盒)测试,但并不是要所有地方都要按照单元测试的方式写代码,不过单元测试的确可以提高设计能力。