Unity 离线建造系统

很多游戏,特别是养成类手游,都会有自己独特的建造系统,一个建造装置的状态循环或者说生命周期一般是这样的:

1.准备建造,设置各项资源的投入等

2.等待一段倒计时,正在建造中

3.建造结束,选择是否收取资源

 

大体上,可以将建造盒子分为以下三种状态,每一个状态的逻辑和显示的页面不同:

1 public enum BuildBoxState
2 {
3     Start,
4     Doing,
5     Complete
6 }
 1     private void ShiftState(BuildBoxState state)
 2     {
 3         switch (state)
 4         {
 5             case BuildBoxState.Start:
 6                 Start.SetActive(true);
 7                 Doing.SetActive(false);
 8                 Complete.SetActive(false);
 9 
10                 ResetResCount();
11                 break;
12             case BuildBoxState.Doing:
13                 Start.SetActive(false);
14                 Doing.SetActive(true);
15                 Complete.SetActive(false);
16 
17                 StartCoroutine(ShowBuildTime());
18                 break;
19             case BuildBoxState.Complete:
20                 Start.SetActive(false);
21                 Doing.SetActive(false);
22                 Complete.SetActive(true);
23 
24                 break;
25         }
26         CurState = state;
27     }

这里值得思考的并非是状态的切换或者基础的按钮侦听,视图资源更新等。

如何在离线一段时间后重新获取目前对应建造盒子所处的状态才是重点;并且如果处于建造中状态的话,还应该能正确的显示剩余时间的倒计时。

一个非常常见的想法是,在建造开始时记录一份开始建造的时间数据给服务器或存在本地离线数据中,当下一次再登录时读取当前系统的时间,并通过总共需要的建造时长来计算剩余时间。

但假如总共需要的建造时长与当时投入的资源类型和量都有关系,这时就需要至少额外记载一类数据来进行计算。那么,有没有方法仅通过一个数据得到剩余时长呢?

答案是,不记录开始建造的时刻,改为记录拟定建造完成的时刻。

如此一来,每次离线登录后,只需要干两件事既可以判断出所有状态视图:

1.是否存在该建造盒子ID对应的拟定建造完成时刻的数据,如果不存在,一定是处于准备状态,即Start状态。

2.如果存在,对比当前系统时刻与拟定建造完成时刻的数据大小,大于等于则处于完成状态,小于则依然在建造中,并按秒显示差值更新。

 

记录的时刻如下:

 1     public string BuildCompleteTime
 2     {
 3         get
 4         {
 5             if (PlayerPrefs.HasKey(ID.ToString()))
 6                 return PlayerPrefs.GetString(ID.ToString());
 7             return S_Null;
 8         }
 9         set
10         {
11             PlayerPrefs.SetString(ID.ToString(), value);
12             PlayerPrefs.Save();
13         }
14     }

 

每次开始时,只需要判断这个数据是否存在:

 1     protected override void InitState()
 2     {
 3         View = HudView as BuildBoxView;
 4         if (BuildCompleteTime == S_Null)
 5         {
 6             ShiftState(BuildBoxState.Start);
 7         }
 8         else
 9         {
10             ShiftState(BuildBoxState.Doing);
11         }
12     }

通过建造中的时刻关系自动判断是否完成:

 1     IEnumerator ShowBuildTime()
 2     {
 3         var ct = GetCompleteTime();
 4         if (CheckBuildCompleted(ct))
 5         {
 6             ShiftState(BuildBoxState.Complete);
 7             yield break;
 8         }
 9         else
10         {
11             for (; ; )
12             {
13                 View.SetTime(CalNeedTime(ct));
14                 yield return new WaitForSeconds(1);
15             }
16         }
17     }

当建造完成点击收取资源时,切换为准备状态的同时,自动清空拟定建造完成时刻的数据记录:

1     private void OnClickGet()
2     {
3         Canvas.SendEvent(new GetItemEvent());
4         ClearCompleteTime();
5         ShiftState(BuildBoxState.Start);
6     }

这里有一个问题是,为什么不在建造完成时就清理数据呢,因为有一种情况是,建造完成后,玩家还没来得及点击收取,就直接进入了离线状态,如果此时再次登录时数据已经清空,那他将做了一场无用功。

说不定直接垃圾游戏毁我青春败我前程了,为了避免这种情况发生,我们只有确保玩家真正收取到资源的那一刻才能清理数据。

 

到此,整个建造的基础逻辑已经梳理完毕。如果要实现快速建造的话,也只不过是将拟定的完成时间直接设置为此刻即可。如果之前记录的是开始建造的时刻,此时又会进行更多额外计算。

 

接下来,关于时间的坑这里也略提一二吧,一开始我以为记录时刻只需要记录时分秒即可,因为最多的建造时长也不超过10小时一般,游戏要保证玩家每天登陆,不可能动用海量的时间去建造一个资源。

如若如此,策划很可能会马上被抓出来祭天,并被玩家评论区冰冷的口水淹没。

但后来写着写着就发现了一个问题,那就是好多天没登录的玩家怎么办,只记录时分秒根本没办法判断时间的早晚,后来想一会还是把日期也记录下来吧。

1 public struct TimeData
2 {
3     public int DayOfYear;
4     public int Hour;
5     public int Minute;
6     public int Second;
7 }

要是你问,那一年以上没登录怎么办,那只能说,你建造的资源已经被时光的齿轮碾碎了(允悲...)。后来突然想起来如果是某一年的最后一天呢...emm,还是老实写全吧:

 1 public struct TimeData
 2 {
 3     public int Year;
 4     public int DayOfYear;
 5     public int Hour;
 6     public int Minute;
 7     public int Second;
 8 
 9     public TimeData(int year,int dayOfYear,int hour,int minute,int second)
10     {
11         Year = year;
12         DayOfYear = dayOfYear;
13         Hour = hour;
14         Minute = minute;
15         Second = second;
16     }
17 }

 

完整时间数据管理脚本:

  1 using System;
  2 
  3 public class TimeDataManager : Singleton<TimeDataManager>
  4 {
  5     const char S_Time = ':';
  6     public int GetYearDayCount(int year)
  7     {
  8         return year % 4 == 0 ? 366 : 365;
  9     }
 10 
 11     public string TimeToString(TimeData d)
 12     {
 13         return d.Year.ToString() + S_Time + d.DayOfYear.ToString() + S_Time + d.Hour.ToString() + S_Time + d.Minute.ToString() + S_Time + d.Second.ToString();
 14     }
 15 
 16     public TimeData StringToTime(string str)
 17     {
 18         var d = new TimeData();
 19         var s = str.Split(S_Time);
 20         d.Year = int.Parse(s[0]);
 21         d.DayOfYear = int.Parse(s[1]);
 22         d.Hour = int.Parse(s[2]);
 23         d.Minute = int.Parse(s[3]);
 24         d.Second = int.Parse(s[4]);
 25         return d;
 26     }
 27 
 28     public TimeData GetNowTime()
 29     {
 30         var d = new TimeData();
 31         var t = DateTime.Now;
 32         d.Year = t.Year;
 33         d.DayOfYear = t.DayOfYear;
 34         d.Hour = t.Hour;
 35         d.Minute = t.Minute;
 36         d.Second = t.Second;
 37         return d;
 38     }
 39 
 40     public bool CheckTimeBeforeNow(TimeData d)
 41     {
 42         var now = GetNowTime();
 43         if (now.Year < d.Year) { return false; }
 44         else if (now.Year > d.Year) { return true; }
 45         else if (now.DayOfYear < d.DayOfYear) { return false; }
 46         else if (now.DayOfYear > d.DayOfYear) { return true; }
 47         else if (now.Hour < d.Hour) { return false; }
 48         else if (now.Hour > d.Hour) { return true; }
 49         else if (now.Minute < d.Minute) { return false; }
 50         else if (now.Minute > d.Minute) { return true; }
 51         else if (now.Second < d.Second) { return false; }
 52         return true;
 53     }
 54 
 55     public TimeData Add(TimeData moment,TimeData time)
 56     {
 57         var y = moment.Year + time.Year;
 58         var d = moment.DayOfYear + time.DayOfYear;
 59         var h = moment.Hour + time.Hour;
 60         var m = moment.Minute + time.Minute;
 61         var s = moment.Second + time.Second;
 62 
 63         if (s > 59)
 64         {
 65             s -= 60;
 66             m++;
 67         }
 68 
 69         if (m > 59)
 70         {
 71             m -= 60;
 72             h++;
 73         }
 74 
 75         if (h > 23)
 76         {
 77             h -= 24;
 78             d++;
 79         }
 80 
 81         var ydc = GetYearDayCount(moment.Year);
 82         if (d > ydc)
 83         {
 84             d -= ydc;
 85             y++;
 86         }
 87 
 88         return new TimeData(y, d, h, m, s);
 89     }
 90 
 91     public TimeData Sub(TimeData afterTime,TimeData beforeTime)
 92     {
 93         var d = new TimeData();
 94         d.Second = afterTime.Second - beforeTime.Second;
 95         d.Minute = afterTime.Minute - beforeTime.Minute;
 96         d.Hour = afterTime.Hour - beforeTime.Hour;
 97         d.DayOfYear = afterTime.DayOfYear - beforeTime.DayOfYear;
 98         d.Year = afterTime.Year - beforeTime.Year;
 99 
100         if (d.Second < 0)
101         {
102             d.Second += 60;
103             d.Minute--;
104         }
105 
106         if (d.Minute < 0)
107         {
108             d.Minute += 60;
109             d.Hour--;
110         }
111 
112         if (d.Hour < 0)
113         {
114             d.Hour += 24;
115             d.DayOfYear--;
116         }
117 
118         var ydc = GetYearDayCount(beforeTime.Year);
119         if (d.DayOfYear < 0)
120         {
121             d.DayOfYear += ydc;
122             d.Year--;
123         }
124 
125         return d;
126     }
127 }
View Code

完整建造脚本:

  1 using System.Collections;
  2 using UnityEngine;
  3 
  4 public enum BuildBoxState
  5 {
  6     Start,
  7     Doing,
  8     Complete
  9 }
 10 
 11 public class BuildBoxCtrl : HudBase
 12 {
 13     private BuildBoxView View;
 14     public BuildBoxState CurState { get; set; }
 15 
 16     public GameObject Start;
 17     public GameObject Doing;
 18     public GameObject Complete;
 19 
 20     private const int ResDef = 160;
 21     private const int ResMax = 800;
 22     private const int ResMin = 100;
 23     private const int ResCha = 10;
 24     private int CurResCount;
 25 
 26     private const string S_Null = "";
 27 
 28     public int ID;
 29 
 30     public string BuildCompleteTime
 31     {
 32         get
 33         {
 34             if (PlayerPrefs.HasKey(ID.ToString()))
 35                 return PlayerPrefs.GetString(ID.ToString());
 36             return S_Null;
 37         }
 38         set
 39         {
 40             PlayerPrefs.SetString(ID.ToString(), value);
 41             PlayerPrefs.Save();
 42         }
 43     }
 44 
 45     protected override void InitState()
 46     {
 47         View = HudView as BuildBoxView;
 48         if (BuildCompleteTime == S_Null)
 49         {
 50             ShiftState(BuildBoxState.Start);
 51         }
 52         else
 53         {
 54             ShiftState(BuildBoxState.Doing);
 55         }
 56     }
 57 
 58     protected override void AddListeners()
 59     {
 60         View.AddRes.onClick.AddListener(() => SetResCount(ResCha));
 61         View.CutRes.onClick.AddListener(() => SetResCount(-ResCha));
 62         View.Build.onClick.AddListener(OnClickBuild);
 63         View.Get.onClick.AddListener(OnClickGet);
 64         View.Speed.onClick.AddListener(() => Canvas.SendEvent(new ShowConfirmWindowEvent(ID)));
 65         Canvas.AddListener<ConfirmCompleteEvent>(ConfirmCompleteHandler);
 66     }
 67 
 68     protected override void RemoveListeners()
 69     {
 70         View.AddRes.onClick.RemoveAllListeners();
 71         View.CutRes.onClick.RemoveAllListeners();
 72         View.Build.onClick.RemoveAllListeners();
 73         View.Get.onClick.RemoveAllListeners();
 74         View.Speed.onClick.RemoveAllListeners();
 75         Canvas.RemoveListener<ConfirmCompleteEvent>(ConfirmCompleteHandler);
 76     }
 77 
 78     private void ConfirmCompleteHandler(ConfirmCompleteEvent e)
 79     {
 80         if (e.bYes && e.ID == ID)
 81         {
 82             SetCompleteTimeAtNow();
 83             ShiftState(BuildBoxState.Complete);
 84         }
 85     }
 86 
 87     private void OnClickBuild()
 88     {
 89         var pd = GameData.Instance.PlayerData;
 90         if (pd.ResourcePoint < CurResCount)
 91             return;
 92 
 93         pd.ResourcePoint -= CurResCount;
 94         Canvas.SendEvent(new UpdateUpBoxEvent());
 95 
 96         SetCompleteTime();
 97         ShiftState(BuildBoxState.Doing);
 98     }
 99 
100     private void OnClickGet()
101     {
102         Canvas.SendEvent(new GetItemEvent());
103         ClearCompleteTime();
104         ShiftState(BuildBoxState.Start);
105     }
106 
107     private void SetCompleteTime()
108     {
109         var nt = GetNowTime();
110         var bt = CalBuildTime(CurResCount);
111         var ct = TimeDataManager.Instance.Add(nt, bt);
112         SetCompleteTime(ct);     
113     }
114 
115     private void SetCompleteTime(TimeData d)
116     {
117         BuildCompleteTime = TimeDataManager.Instance.TimeToString(d);
118     }
119 
120     private void SetCompleteTimeAtNow()
121     {
122         var nt = GetNowTime();
123         SetCompleteTime(nt);
124     }
125 
126     private TimeData GetCompleteTime()
127     {
128         return TimeDataManager.Instance.StringToTime(BuildCompleteTime);
129     }
130 
131     private TimeData GetNowTime()
132     {
133         return TimeDataManager.Instance.GetNowTime();
134     }
135 
136     private TimeData CalBuildTime(int res)
137     {
138         var d = new TimeData();
139         d.Hour = res / 100;
140         d.Minute = res % 100;
141         if (d.Minute > 59)
142         {
143             d.Second = d.Minute - 59;
144             d.Minute = 59;
145         }
146         return d;
147     }
148 
149     private void SetResCount(int change)
150     {
151         CurResCount += change;
152         if (CurResCount > ResMax)
153             CurResCount = ResMax;
154         if (CurResCount < ResMin)
155             CurResCount = ResMin;
156 
157         View.SetRes(CurResCount);
158     }
159 
160     private void ResetResCount()
161     {
162         CurResCount = ResDef;
163         View.SetRes(CurResCount);
164     }
165 
166     private void ShiftState(BuildBoxState state)
167     {
168         switch (state)
169         {
170             case BuildBoxState.Start:
171                 Start.SetActive(true);
172                 Doing.SetActive(false);
173                 Complete.SetActive(false);
174 
175                 ResetResCount();
176                 break;
177             case BuildBoxState.Doing:
178                 Start.SetActive(false);
179                 Doing.SetActive(true);
180                 Complete.SetActive(false);
181 
182                 StartCoroutine(ShowBuildTime());
183                 break;
184             case BuildBoxState.Complete:
185                 Start.SetActive(false);
186                 Doing.SetActive(false);
187                 Complete.SetActive(true);
188 
189                 break;
190         }
191         CurState = state;
192     }
193 
194     private void ClearCompleteTime()
195     {
196         if (PlayerPrefs.HasKey(ID.ToString()))
197             PlayerPrefs.DeleteKey(ID.ToString());
198     }
199 
200     IEnumerator ShowBuildTime()
201     {
202         var ct = GetCompleteTime();
203         if (CheckBuildCompleted(ct))
204         {
205             ShiftState(BuildBoxState.Complete);
206             yield break;
207         }
208         else
209         {
210             for (; ; )
211             {
212                 View.SetTime(CalNeedTime(ct));
213                 yield return new WaitForSeconds(1);
214             }
215         }
216     }
217 
218     private TimeData CalNeedTime(TimeData com)
219     {      
220         var now = GetNowTime();
221         return TimeDataManager.Instance.Sub(com, now);
222     }
223 
224     private bool CheckBuildCompleted(TimeData com)
225     {
226         return TimeDataManager.Instance.CheckTimeBeforeNow(com);
227     }
228 }
View Code
 1 using UnityEngine.UI;
 2 using TMPro;
 3 
 4 public class BuildBoxView : HudView
 5 {
 6     public TextMeshProUGUI ResCount;
 7     public TextMeshProUGUI Time;
 8 
 9     public Button AddRes;
10     public Button CutRes;
11     public Button Build;
12     public Button Get;
13     public Button Speed;
14 
15     public void SetRes(int v)
16     {
17         ResCount.text = v.ToString();
18     }
19 
20     public void SetTime(TimeData data)
21     {
22         Time.text = data.Hour + "" + data.Minute + "" + data.Second;
23     }
24 }
View Code

这里用到的UI基础类可详见之前写过的随笔:

https://www.cnblogs.com/koshio0219/p/12808063.html

单例模式:

https://www.cnblogs.com/koshio0219/p/11203631.html

 

补充:

通用确认弹窗:

 1 using TMPro;
 2 using UnityEngine.Events;
 3 using UnityEngine.UI;
 4 
 5 [System.Serializable]
 6 public class WindowBtClickdEvent : UnityEvent<bool> { }
 7 
 8 public class WindowView : HudBase
 9 {
10     public Button Yes;
11     public Button No;
12     public TextMeshProUGUI Content;
13 
14     public string Text;
15 
16     public WindowBtClickdEvent OnClick;
17 
18     protected override void InitState()
19     {
20         Content.text = Text;
21     }
22 
23     protected override void AddListeners()
24     {
25         Yes.onClick.AddListener(()=> OnClick.Invoke(true));
26         No.onClick.AddListener(() => OnClick.Invoke(false));
27     }
28 
29     protected override void RemoveListeners()
30     {
31         Yes.onClick.RemoveAllListeners();
32         No.onClick.RemoveAllListeners();
33     }
34 }
View Code

 

效果:

 

posted @ 2020-05-29 18:04  汐夜  阅读(1112)  评论(0编辑  收藏  举报