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 ...
随机推荐
- MODIS系列之NDVI(MOD13Q1)五:NDVI处理流程
前言:(个人建议) 1.进行数据处理工作,由于通常数据量比较大.所以在个人电脑中,要将基础数据.不断增长的过程数据.结果数据等分门别类,使得简单易找. 2.将重要数据备份,因为在数据处理过程中,由于出 ...
- 线段树 离散化 E. Infinite Inversions E. Physical Education Lessons
题目一:E. Infinite Inversions 这个题目没什么思维量,还比较简单,就是离散化要加上每一个值的后面一个值,然后每一个值放进去的不是1 ,而是这个值与下一个点的差值. 因为这个数代表 ...
- zabbix 告警信息与恢复信息
名称: Action-Email 默认接收人: 故障{TRIGGER.STATUS},服务器:{HOSTNAME1}发生: {TRIGGER.NAME}故障! 默认信息: 告警主机:{HOSTNAME ...
- Proteus传感器+气体浓度检测的报警方式控制仿真
Proteus传感器+气体浓度检测的报警方式控制仿真 目录 Proteus传感器+气体浓度检测的报警方式控制仿真 1 实验意义理解 2 主要实验器件 3 实验参考电路 4 实验中的问题思考 4.1 实 ...
- Maxim实时时钟芯片设计指南5413-二进制编码十进制(BCD)格式实时时钟中的状态机逻辑
网上DS12C887的文章涉及到时间的存储格式使用的都是二进制代码,究竟使用BCD码该如何操作?正好美信官网上有一篇文章.美信官网不稳定,先贴到这里,有时间再翻译. 原文链接 State Machin ...
- 一文教你快速搞懂速度曲线规划之S形曲线(超详细+图文+推导+附件代码)
本文介绍了运动控制终的S曲线,通过matlab和C语言实现并进行仿真:本文篇幅较长,请自备茶水: 请帮忙点个赞
- [BC冠军赛(online)]小结
A Movie 题意:给你n个区间,判断能否选出3个不相交的区间. 思路:令f(i)表示能否选出两个不相交区间并且以区间i为右区间的值,g(i)表示能否选出两个不相交区间并且以区间i为左区间的值,如果 ...
- 【Docker】在本地打包maven程序为docker镜像报错: Connect to localhost:2375 [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1]
错误信息: [ERROR] Failed to execute goal com.spotify:docker-maven-plugin:1.0.0:build (default-cli) on pr ...
- 数据库连接池Druid的介绍,配置分析对比总结
Druid的简介 Druid首先是一个数据库连接池.Druid是目前最好的数据库连接池,在功能.性能.扩展性方面,都超过其他数据库连接池,包括DBCP.C3P0.BoneCP.Proxool.JBos ...
- 通过PAML中的CODEML模块计算dnds的过程以及踩坑
最近帮女朋友做毕业设计的时候用到了 PAML这个软件的codeml功能,发现网上相关的资料很少,于是把自己踩的一些坑分享一下,希望能帮到其他有相同困难的人 一.下载与安装 PAML软件下载地址 htt ...