0%

Unity3D 中对时间系统的两种实现方式的分析

本文主要记录在个人项目的游戏中对时间系统的实现。

尝试了 Timer 类和协程(Coroutine)两种实现方式,Timer 类存在一定的问题,因此最终采用了协程的方式。

需求

项目是一个生存类的游戏,玩家流落到荒岛上,需要在荒岛上进行生存,因此需要一个计时器来记录玩家在岛上生存的时间。

现实中的 0.5s 对应游戏中的 1min,计时器从 Day 0 - 06 : 00 开始,同时将当前记录的时间实时显示在 UI 上的 Text 组件中。

Timer 类实现

TimerData 类

该脚本用于存储 Timer 的数据。

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
public class TimerData
{
private static TimerData _instance;

private TimerData()
{
TotalMinutes = 360;

Timer = new Timer(500);
Timer.Elapsed += UpdateTimeByRealTime;
Timer.Start();
}

public static TimerData Instance => _instance ?? (_instance = new TimerData());

public Timer Timer { get; }

public int Days => TotalMinutes / 3600;

public int Hours => TotalMinutes % 3600 / 60;

public int Minutes => TotalMinutes % 60;

public int TotalMinutes { get; private set; }

private void UpdateTimeByRealTime(object sender, ElapsedEventArgs e)
{
TotalMinutes++;
}
}

TimerTest 类

将该脚本挂载在一个 Text 组件上,GetTimeTxt 方法用于实时更新 Text 组件的内容。

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

public class TimerTest : MonoBehaviour
{
public Text TimeTxt;

public TimerData Data => TimerData.Instance;

private void Awake() { TimeTxt = transform.GetComponent<Text>(); }

private void Start() { Data.Timer.Elapsed += GetTimeTxt; }

public void GetTimeTxt(object sender, ElapsedEventArgs e)
{
TimeTxt.text = $"Day {Data.Days} - {Data.Hours:D2} : {Data.Minutes:D2}";
}
}

最终效果

左边是 Text 组件,右边是对应的 Inspector 区域。

可以看到,Inspector 中 Text 组件的文本内容会发生改变,但是无法更新在 UI 上,具体效果如下图所示:

timer-test

原因分析

为什么会出现这样的情况呢?我们对 TimerTest 类的 GetTimeTxt 方法做一定的修改。

1
2
3
4
5
6
public void GetTimeTxt(object sender, ElapsedEventArgs e)
{
Debug.Log("Before Updating.");
TimeTxt.text = $"Day {Data.Days} - {Data.Hours:D2} : {Data.Minutes:D2}";
Debug.Log("After Updating.");
}

加上了两句控制台输出,再次运行会发现:

timer-test-debug

只有 “Before Updating.” 进行了输出,而 “After Updating.” 并没有输出。

原因是后续我在使用协程的时侯,才突然意识到的。

协程是同步的,但 Timer 类并不是。Timer 类是多线程的,而在 Unity3D 中,只允许主线程对组件进行访问。

验证

接下来进行验证,在 TimerTest 类中添加 Update 方法,并以 GetTimeTxt 方法为参数开启一个子线程。同时注释 Start 方法中的 Timer 事件添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Thread Thread { get; set; }

private void Start()
{
// Data.Timer.Elapsed += GetTimeTxt;
}

private void Update()
{
do {
Thread = new Thread(new ThreadStart(GetTimeTxt));
Thread.Start();
} while (!Thread.IsAlive);
}

效果如下:

timer-test-multi-thread

Text 组件的文本内容在更新,但是 UI 上只更新了最初的一次。

同时 Unity3D 也抛出了异常,停止后发现,Before 消息的次数 = Exception 消息的次数 + After 消息的次数。

但是为什么 UI 会更新,且只更新了最初的一次呢?为什么这次的多线程可以执行至 After 部分?

这个部分实在是没有头绪,网上查阅资料也无果,所以暂时把问题做一个记录,希望之后能够有机会解决。

协程实现

既然多线程实现存在一定的问题,就尝试一下单线程的实现。

事实上我还挺喜欢协程的,使用起来条理清晰,易实现,易 Debug。

TimerTestCoroutine 类

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
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class TimerTestCoroutine : MonoBehaviour
{
private static readonly WaitForSeconds DotFiveSeconds = new WaitForSeconds(0.5F);

private static TimerTestCoroutine _instance;

public Text TimeTxt;

public static TimerTestCoroutine Instance =>
_instance ?? (_instance = GameObject.Find("Canvas/Time").GetComponent<TimerTestCoroutine>());

public Coroutine GetTimeTxtCoroutine { get; set; }

public int Days => TotalMinutes / 3600;

public int Hours => TotalMinutes % 3600 / 60;

public int Minutes => TotalMinutes % 60;

public int TotalMinutes { get; private set; }

private void Awake() { TimeTxt = transform.GetComponent<Text>(); }

private void Start()
{
TotalMinutes = 360;
GetTimeTxtCoroutine = StartCoroutine(GetTimeTxt());
}

private IEnumerator GetTimeTxt()
{
while (true) {
TimeTxt.text = $"Day {Days} - {Hours:D2} : {Minutes:D2}";
yield return DotFiveSeconds;
TotalMinutes++;
}
}
}

最终效果

这次的效果就是目的效果了。

timer-test-coroutine

总结

这次 Timer 类的使用经验让我对 Unity3D 的了解更深了一步,无论是引擎本身,还是多线程方面,或者协程方面。

同时也让我意识到我学习的深度还是太浅了,虽然有一定的 Unity3D 使用经验,但也仅仅是使用经验而已,如果要学习,那要的就不只是使用经验,而是了解更具体的实现。