本文主要记录在个人项目的游戏中对时间系统的实现。
尝试了 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 上,具体效果如下图所示:
原因分析
为什么会出现这样的情况呢?我们对 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."); }
|
加上了两句控制台输出,再次运行会发现:
只有 “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() { }
private void Update() { do { Thread = new Thread(new ThreadStart(GetTimeTxt)); Thread.Start(); } while (!Thread.IsAlive); }
|
效果如下:
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 类的使用经验让我对 Unity3D 的了解更深了一步,无论是引擎本身,还是多线程方面,或者协程方面。
同时也让我意识到我学习的深度还是太浅了,虽然有一定的 Unity3D 使用经验,但也仅仅是使用经验而已,如果要学习,那要的就不只是使用经验,而是了解更具体的实现。