Unity 离线建造系统
很多游戏,特别是养成类手游,都会有自己独特的建造系统,一个建造装置的状态循环或者说生命周期一般是这样的:
1.准备建造,设置各项资源的投入等
2.等待一段倒计时,正在建造中
3.建造结束,选择是否收取资源
大体上,可以将建造盒子分为以下三种状态,每一个状态的逻辑和显示的页面不同:
- public enum BuildBoxState
- {
- Start,
- Doing,
- Complete
- }
- private void ShiftState(BuildBoxState state)
- {
- switch (state)
- {
- case BuildBoxState.Start:
- Start.SetActive(true);
- Doing.SetActive(false);
- Complete.SetActive(false);
- ResetResCount();
- break;
- case BuildBoxState.Doing:
- Start.SetActive(false);
- Doing.SetActive(true);
- Complete.SetActive(false);
- StartCoroutine(ShowBuildTime());
- break;
- case BuildBoxState.Complete:
- Start.SetActive(false);
- Doing.SetActive(false);
- Complete.SetActive(true);
- break;
- }
- CurState = state;
- }
这里值得思考的并非是状态的切换或者基础的按钮侦听,视图资源更新等。
如何在离线一段时间后重新获取目前对应建造盒子所处的状态才是重点;并且如果处于建造中状态的话,还应该能正确的显示剩余时间的倒计时。
一个非常常见的想法是,在建造开始时记录一份开始建造的时间数据给服务器或存在本地离线数据中,当下一次再登录时读取当前系统的时间,并通过总共需要的建造时长来计算剩余时间。
但假如总共需要的建造时长与当时投入的资源类型和量都有关系,这时就需要至少额外记载一类数据来进行计算。那么,有没有方法仅通过一个数据得到剩余时长呢?
答案是,不记录开始建造的时刻,改为记录拟定建造完成的时刻。
如此一来,每次离线登录后,只需要干两件事既可以判断出所有状态视图:
1.是否存在该建造盒子ID对应的拟定建造完成时刻的数据,如果不存在,一定是处于准备状态,即Start状态。
2.如果存在,对比当前系统时刻与拟定建造完成时刻的数据大小,大于等于则处于完成状态,小于则依然在建造中,并按秒显示差值更新。
记录的时刻如下:
- public string BuildCompleteTime
- {
- get
- {
- if (PlayerPrefs.HasKey(ID.ToString()))
- return PlayerPrefs.GetString(ID.ToString());
- return S_Null;
- }
- set
- {
- PlayerPrefs.SetString(ID.ToString(), value);
- PlayerPrefs.Save();
- }
- }
每次开始时,只需要判断这个数据是否存在:
- protected override void InitState()
- {
- View = HudView as BuildBoxView;
- if (BuildCompleteTime == S_Null)
- {
- ShiftState(BuildBoxState.Start);
- }
- else
- {
- ShiftState(BuildBoxState.Doing);
- }
- }
通过建造中的时刻关系自动判断是否完成:
- IEnumerator ShowBuildTime()
- {
- var ct = GetCompleteTime();
- if (CheckBuildCompleted(ct))
- {
- ShiftState(BuildBoxState.Complete);
- yield break;
- }
- else
- {
- for (; ; )
- {
- View.SetTime(CalNeedTime(ct));
- yield return new WaitForSeconds();
- }
- }
- }
当建造完成点击收取资源时,切换为准备状态的同时,自动清空拟定建造完成时刻的数据记录:
- private void OnClickGet()
- {
- Canvas.SendEvent(new GetItemEvent());
- ClearCompleteTime();
- ShiftState(BuildBoxState.Start);
- }
这里有一个问题是,为什么不在建造完成时就清理数据呢,因为有一种情况是,建造完成后,玩家还没来得及点击收取,就直接进入了离线状态,如果此时再次登录时数据已经清空,那他将做了一场无用功。
说不定直接垃圾游戏毁我青春败我前程了,为了避免这种情况发生,我们只有确保玩家真正收取到资源的那一刻才能清理数据。
到此,整个建造的基础逻辑已经梳理完毕。如果要实现快速建造的话,也只不过是将拟定的完成时间直接设置为此刻即可。如果之前记录的是开始建造的时刻,此时又会进行更多额外计算。
接下来,关于时间的坑这里也略提一二吧,一开始我以为记录时刻只需要记录时分秒即可,因为最多的建造时长也不超过10小时一般,游戏要保证玩家每天登陆,不可能动用海量的时间去建造一个资源。
如若如此,策划很可能会马上被抓出来祭天,并被玩家评论区冰冷的口水淹没。
但后来写着写着就发现了一个问题,那就是好多天没登录的玩家怎么办,只记录时分秒根本没办法判断时间的早晚,后来想一会还是把日期也记录下来吧。
- public struct TimeData
- {
- public int DayOfYear;
- public int Hour;
- public int Minute;
- public int Second;
- }
要是你问,那一年以上没登录怎么办,那只能说,你建造的资源已经被时光的齿轮碾碎了(允悲...)。后来突然想起来如果是某一年的最后一天呢...emm,还是老实写全吧:
- public struct TimeData
- {
- public int Year;
- public int DayOfYear;
- public int Hour;
- public int Minute;
- public int Second;
- public TimeData(int year,int dayOfYear,int hour,int minute,int second)
- {
- Year = year;
- DayOfYear = dayOfYear;
- Hour = hour;
- Minute = minute;
- Second = second;
- }
- }
完整时间数据管理脚本:
- using System;
- public class TimeDataManager : Singleton<TimeDataManager>
- {
- const char S_Time = ':';
- public int GetYearDayCount(int year)
- {
- return year % == ? : ;
- }
- public string TimeToString(TimeData d)
- {
- return d.Year.ToString() + S_Time + d.DayOfYear.ToString() + S_Time + d.Hour.ToString() + S_Time + d.Minute.ToString() + S_Time + d.Second.ToString();
- }
- public TimeData StringToTime(string str)
- {
- var d = new TimeData();
- var s = str.Split(S_Time);
- d.Year = int.Parse(s[]);
- d.DayOfYear = int.Parse(s[]);
- d.Hour = int.Parse(s[]);
- d.Minute = int.Parse(s[]);
- d.Second = int.Parse(s[]);
- return d;
- }
- public TimeData GetNowTime()
- {
- var d = new TimeData();
- var t = DateTime.Now;
- d.Year = t.Year;
- d.DayOfYear = t.DayOfYear;
- d.Hour = t.Hour;
- d.Minute = t.Minute;
- d.Second = t.Second;
- return d;
- }
- public bool CheckTimeBeforeNow(TimeData d)
- {
- var now = GetNowTime();
- if (now.Year < d.Year) { return false; }
- else if (now.Year > d.Year) { return true; }
- else if (now.DayOfYear < d.DayOfYear) { return false; }
- else if (now.DayOfYear > d.DayOfYear) { return true; }
- else if (now.Hour < d.Hour) { return false; }
- else if (now.Hour > d.Hour) { return true; }
- else if (now.Minute < d.Minute) { return false; }
- else if (now.Minute > d.Minute) { return true; }
- else if (now.Second < d.Second) { return false; }
- return true;
- }
- public TimeData Add(TimeData moment,TimeData time)
- {
- var y = moment.Year + time.Year;
- var d = moment.DayOfYear + time.DayOfYear;
- var h = moment.Hour + time.Hour;
- var m = moment.Minute + time.Minute;
- var s = moment.Second + time.Second;
- if (s > )
- {
- s -= ;
- m++;
- }
- if (m > )
- {
- m -= ;
- h++;
- }
- if (h > )
- {
- h -= ;
- d++;
- }
- var ydc = GetYearDayCount(moment.Year);
- if (d > ydc)
- {
- d -= ydc;
- y++;
- }
- return new TimeData(y, d, h, m, s);
- }
- public TimeData Sub(TimeData afterTime,TimeData beforeTime)
- {
- var d = new TimeData();
- d.Second = afterTime.Second - beforeTime.Second;
- d.Minute = afterTime.Minute - beforeTime.Minute;
- d.Hour = afterTime.Hour - beforeTime.Hour;
- d.DayOfYear = afterTime.DayOfYear - beforeTime.DayOfYear;
- d.Year = afterTime.Year - beforeTime.Year;
- if (d.Second < )
- {
- d.Second += ;
- d.Minute--;
- }
- if (d.Minute < )
- {
- d.Minute += ;
- d.Hour--;
- }
- if (d.Hour < )
- {
- d.Hour += ;
- d.DayOfYear--;
- }
- var ydc = GetYearDayCount(beforeTime.Year);
- if (d.DayOfYear < )
- {
- d.DayOfYear += ydc;
- d.Year--;
- }
- return d;
- }
- }
完整建造脚本:
- using System.Collections;
- using UnityEngine;
- public enum BuildBoxState
- {
- Start,
- Doing,
- Complete
- }
- public class BuildBoxCtrl : HudBase
- {
- private BuildBoxView View;
- public BuildBoxState CurState { get; set; }
- public GameObject Start;
- public GameObject Doing;
- public GameObject Complete;
- private const int ResDef = ;
- private const int ResMax = ;
- private const int ResMin = ;
- private const int ResCha = ;
- private int CurResCount;
- private const string S_Null = "";
- public int ID;
- public string BuildCompleteTime
- {
- get
- {
- if (PlayerPrefs.HasKey(ID.ToString()))
- return PlayerPrefs.GetString(ID.ToString());
- return S_Null;
- }
- set
- {
- PlayerPrefs.SetString(ID.ToString(), value);
- PlayerPrefs.Save();
- }
- }
- protected override void InitState()
- {
- View = HudView as BuildBoxView;
- if (BuildCompleteTime == S_Null)
- {
- ShiftState(BuildBoxState.Start);
- }
- else
- {
- ShiftState(BuildBoxState.Doing);
- }
- }
- protected override void AddListeners()
- {
- View.AddRes.onClick.AddListener(() => SetResCount(ResCha));
- View.CutRes.onClick.AddListener(() => SetResCount(-ResCha));
- View.Build.onClick.AddListener(OnClickBuild);
- View.Get.onClick.AddListener(OnClickGet);
- View.Speed.onClick.AddListener(() => Canvas.SendEvent(new ShowConfirmWindowEvent(ID)));
- Canvas.AddListener<ConfirmCompleteEvent>(ConfirmCompleteHandler);
- }
- protected override void RemoveListeners()
- {
- View.AddRes.onClick.RemoveAllListeners();
- View.CutRes.onClick.RemoveAllListeners();
- View.Build.onClick.RemoveAllListeners();
- View.Get.onClick.RemoveAllListeners();
- View.Speed.onClick.RemoveAllListeners();
- Canvas.RemoveListener<ConfirmCompleteEvent>(ConfirmCompleteHandler);
- }
- private void ConfirmCompleteHandler(ConfirmCompleteEvent e)
- {
- if (e.bYes && e.ID == ID)
- {
- SetCompleteTimeAtNow();
- ShiftState(BuildBoxState.Complete);
- }
- }
- private void OnClickBuild()
- {
- var pd = GameData.Instance.PlayerData;
- if (pd.ResourcePoint < CurResCount)
- return;
- pd.ResourcePoint -= CurResCount;
- Canvas.SendEvent(new UpdateUpBoxEvent());
- SetCompleteTime();
- ShiftState(BuildBoxState.Doing);
- }
- private void OnClickGet()
- {
- Canvas.SendEvent(new GetItemEvent());
- ClearCompleteTime();
- ShiftState(BuildBoxState.Start);
- }
- private void SetCompleteTime()
- {
- var nt = GetNowTime();
- var bt = CalBuildTime(CurResCount);
- var ct = TimeDataManager.Instance.Add(nt, bt);
- SetCompleteTime(ct);
- }
- private void SetCompleteTime(TimeData d)
- {
- BuildCompleteTime = TimeDataManager.Instance.TimeToString(d);
- }
- private void SetCompleteTimeAtNow()
- {
- var nt = GetNowTime();
- SetCompleteTime(nt);
- }
- private TimeData GetCompleteTime()
- {
- return TimeDataManager.Instance.StringToTime(BuildCompleteTime);
- }
- private TimeData GetNowTime()
- {
- return TimeDataManager.Instance.GetNowTime();
- }
- private TimeData CalBuildTime(int res)
- {
- var d = new TimeData();
- d.Hour = res / ;
- d.Minute = res % ;
- if (d.Minute > )
- {
- d.Second = d.Minute - ;
- d.Minute = ;
- }
- return d;
- }
- private void SetResCount(int change)
- {
- CurResCount += change;
- if (CurResCount > ResMax)
- CurResCount = ResMax;
- if (CurResCount < ResMin)
- CurResCount = ResMin;
- View.SetRes(CurResCount);
- }
- private void ResetResCount()
- {
- CurResCount = ResDef;
- View.SetRes(CurResCount);
- }
- private void ShiftState(BuildBoxState state)
- {
- switch (state)
- {
- case BuildBoxState.Start:
- Start.SetActive(true);
- Doing.SetActive(false);
- Complete.SetActive(false);
- ResetResCount();
- break;
- case BuildBoxState.Doing:
- Start.SetActive(false);
- Doing.SetActive(true);
- Complete.SetActive(false);
- StartCoroutine(ShowBuildTime());
- break;
- case BuildBoxState.Complete:
- Start.SetActive(false);
- Doing.SetActive(false);
- Complete.SetActive(true);
- break;
- }
- CurState = state;
- }
- private void ClearCompleteTime()
- {
- if (PlayerPrefs.HasKey(ID.ToString()))
- PlayerPrefs.DeleteKey(ID.ToString());
- }
- IEnumerator ShowBuildTime()
- {
- var ct = GetCompleteTime();
- if (CheckBuildCompleted(ct))
- {
- ShiftState(BuildBoxState.Complete);
- yield break;
- }
- else
- {
- for (; ; )
- {
- View.SetTime(CalNeedTime(ct));
- yield return new WaitForSeconds();
- }
- }
- }
- private TimeData CalNeedTime(TimeData com)
- {
- var now = GetNowTime();
- return TimeDataManager.Instance.Sub(com, now);
- }
- private bool CheckBuildCompleted(TimeData com)
- {
- return TimeDataManager.Instance.CheckTimeBeforeNow(com);
- }
- }
- using UnityEngine.UI;
- using TMPro;
- public class BuildBoxView : HudView
- {
- public TextMeshProUGUI ResCount;
- public TextMeshProUGUI Time;
- public Button AddRes;
- public Button CutRes;
- public Button Build;
- public Button Get;
- public Button Speed;
- public void SetRes(int v)
- {
- ResCount.text = v.ToString();
- }
- public void SetTime(TimeData data)
- {
- Time.text = data.Hour + ":" + data.Minute + ":" + data.Second;
- }
- }
这里用到的UI基础类可详见之前写过的随笔:
https://www.cnblogs.com/koshio0219/p/12808063.html
单例模式:
https://www.cnblogs.com/koshio0219/p/11203631.html
补充:
通用确认弹窗:
- using TMPro;
- using UnityEngine.Events;
- using UnityEngine.UI;
- [System.Serializable]
- public class WindowBtClickdEvent : UnityEvent<bool> { }
- public class WindowView : HudBase
- {
- public Button Yes;
- public Button No;
- public TextMeshProUGUI Content;
- public string Text;
- public WindowBtClickdEvent OnClick;
- protected override void InitState()
- {
- Content.text = Text;
- }
- protected override void AddListeners()
- {
- Yes.onClick.AddListener(()=> OnClick.Invoke(true));
- No.onClick.AddListener(() => OnClick.Invoke(false));
- }
- protected override void RemoveListeners()
- {
- Yes.onClick.RemoveAllListeners();
- No.onClick.RemoveAllListeners();
- }
- }
效果:
Unity 离线建造系统的更多相关文章
- Spine学习七 - spine动画资源+ Unity Mecanim动画系统
前面已经讲过 Spine自己动画状态机的动画融合,但是万一有哥们就是想要使用Unity的动画系统,那有没有办法呢?答案是肯定的,接下来,就说说如何实现: 1. 在project面板找打你导入的Spin ...
- 【Python+C#】手把手搭建基于Hugging Face模型的离线翻译系统,并通过C#代码进行访问
前言:目前翻译都是在线的,要在C#开发的程序上做一个可以实时翻译的功能,好像不是那么好做.而且大多数处于局域网内,所以访问在线的api也显得比较尴尬.于是,就有了以下这篇文章,自己搭建一套简单的离线翻 ...
- Unity Ragdoll(布娃娃系统)
逼真的动作如何实现的? 在一些游戏中当NPC或玩家死亡的时候,死亡的肢体动作十分逼真,这一物理现象如何用Unity来实现呢?Unity物理引擎中的Ragdoll系统,可以用来创建这种效果,具体请参阅以 ...
- Unity中对系统类进行扩展的方法
Unity扩展系统类,整合简化代码 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- ...
- Unity* 实体组件系统 (ECS)、C# 作业系统和突发编译器入门
Unity* 中的全新 C# 作业系统和实体组件系统不仅可以让您轻松利用以前未使用的 CPU 资源,还可以帮助您更高效地运行所有游戏代码.然后,您可以使用这些额外的 CPU 资源来添加更多场景动态和沉 ...
- unity C#更改系统默认鼠标指针
最近项目需要替换鼠标的默认图标,实现的效果是初始状态为一种图标,点击鼠标左键要换成另一种图标,按网上通用的方法做了以后,隐藏鼠标指针,在指针的位置画一个图片就可以了,但不知道怎么回事,这种方法画的图标 ...
- Unity Mecanim 动画系统
1. Animator 组件 Controller:使用的Animator Controller文件. Avatar:使用的骨骼文件. Apply Root Motion:绑定该组件的GameObje ...
- Unity子弹生成系统
子弹系统和粒子系统比较类似,为了创建和五花八门的子弹,例如追踪,连续继承,散弹等,需要一个拥有众多参数的子弹生成器,这里叫它Shooter好了. Shooter负责把玩各类子弹造型和参数,创建出子弹, ...
- Unity调用windows系统dialog 选择文件夹
#region 调用windows系统dialog 选择文件夹 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public ...
随机推荐
- python(索引/切片)
一.索引 1.索引值从左到右-->从0开始,索引值从右到左-->从-1开始 取值格式var[index] >>> name = "xinfangshuo&quo ...
- python http server handle json
用Python实现一个http server # python2 # coding = utf-8 from BaseHTTPServer import HTTPServer, BaseHTTPReq ...
- ES[7.6.x]学习笔记(八)数据的增删改
在前面几节的内容中,我们学习索引.字段映射.分析器等,这些都是使用ES的基础,就像在数据库中创建表一样,基础工作做好以后,我们就要真正的使用它了,这一节我们要看看怎么向索引里写入数据.修改数据.删除数 ...
- C# 数据操作系列 - 3. ADO.NET 离线查询
0. 前言 在上一篇中,我故意留下了查询的示范没讲.虽然说可以通过以下代码获取一个DataReader: IDataReader reader = command.ExecuteReader(); 然 ...
- 错误提示 Table '.***_ecms_news_data_' doesn't exist select keyid,dokey,newstempid,closepl,info
错误提示:Table '**.***_ecms_news_data_' doesn't exist select keyid,dokey,newstempid,closepl,infotags,wri ...
- Python 简明教程 --- 3,Python 基础概念
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 控制复杂性是计算机编程的本质. -- Brian Kernighan 了解了如何编写第一个Pytho ...
- 前端面试题-TCP和UDP的区别
TCP和UDP的区别 (1)TCP是面向连接的,udp是无连接的即发送数据前不需要先建立链接. (2)TCP提供可靠的服务.也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UD ...
- python--封装Redis
Redis封装 import redis class MyRedis(): def __init__(self,ip,password,port=6379,db=1):#构造函数 ...
- 深入理解JS中的对象(三):class 的工作原理
目录 序言 class 是一个特殊的函数 class 的工作原理 class 继承的原型链关系 参考 1.序言 ECMAScript 2015(ES6) 中引入的 JavaScript 类实质上是 J ...
- 搞懂:MVVM模型以及VUE中的数据绑定数据劫持发布订阅模式
搞懂:MVVM模式和Vue中的MVVM模式 MVVM MVVM : model - view - viewmodel的缩写,说都能直接说出来 model:模型,view:视图,view-Model:视 ...