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

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 离线建造系统的更多相关文章

  1. Spine学习七 - spine动画资源+ Unity Mecanim动画系统

    前面已经讲过 Spine自己动画状态机的动画融合,但是万一有哥们就是想要使用Unity的动画系统,那有没有办法呢?答案是肯定的,接下来,就说说如何实现: 1. 在project面板找打你导入的Spin ...

  2. 【Python+C#】手把手搭建基于Hugging Face模型的离线翻译系统,并通过C#代码进行访问

    前言:目前翻译都是在线的,要在C#开发的程序上做一个可以实时翻译的功能,好像不是那么好做.而且大多数处于局域网内,所以访问在线的api也显得比较尴尬.于是,就有了以下这篇文章,自己搭建一套简单的离线翻 ...

  3. Unity Ragdoll(布娃娃系统)

    逼真的动作如何实现的? 在一些游戏中当NPC或玩家死亡的时候,死亡的肢体动作十分逼真,这一物理现象如何用Unity来实现呢?Unity物理引擎中的Ragdoll系统,可以用来创建这种效果,具体请参阅以 ...

  4. Unity中对系统类进行扩展的方法

    Unity扩展系统类,整合简化代码 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- ...

  5. Unity* 实体组件系统 (ECS)、C# 作业系统和突发编译器入门

    Unity* 中的全新 C# 作业系统和实体组件系统不仅可以让您轻松利用以前未使用的 CPU 资源,还可以帮助您更高效地运行所有游戏代码.然后,您可以使用这些额外的 CPU 资源来添加更多场景动态和沉 ...

  6. unity C#更改系统默认鼠标指针

    最近项目需要替换鼠标的默认图标,实现的效果是初始状态为一种图标,点击鼠标左键要换成另一种图标,按网上通用的方法做了以后,隐藏鼠标指针,在指针的位置画一个图片就可以了,但不知道怎么回事,这种方法画的图标 ...

  7. Unity Mecanim 动画系统

    1. Animator 组件 Controller:使用的Animator Controller文件. Avatar:使用的骨骼文件. Apply Root Motion:绑定该组件的GameObje ...

  8. Unity子弹生成系统

    子弹系统和粒子系统比较类似,为了创建和五花八门的子弹,例如追踪,连续继承,散弹等,需要一个拥有众多参数的子弹生成器,这里叫它Shooter好了. Shooter负责把玩各类子弹造型和参数,创建出子弹, ...

  9. Unity调用windows系统dialog 选择文件夹

    #region 调用windows系统dialog 选择文件夹 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public ...

随机推荐

  1. 一只简单的网络爬虫(基于linux C/C++)————支持动态模块加载

    插件在软件设计中有很大的好处,可以方便地扩展各种功能,使用插件技术能够在分析.设计.开发.项目计划.协作生产和产品扩展等很多方面带来好处: (1)结构清晰.易于理解.由于借鉴了硬件总线的结构,而且各个 ...

  2. thinkphp 5.x~3.x 文件包含漏洞分析

    漏洞描述: ThinkPHP在加载模版解析变量时存在变量覆盖的问题,且没有对 $cacheFile 进行相应的消毒处理,导致模板文件的路径可以被覆盖,从而导致任意文件包含漏洞的发生. 主要还是变量覆盖 ...

  3. 【Java8新特性】还没搞懂函数式接口?赶快过来看看吧!

    写在前面 Java8中内置了一些在开发中常用的函数式接口,极大的提高了我们的开发效率.那么,问题来了,你知道都有哪些函数式接口吗? 函数式接口总览 这里,我使用表格的形式来简单说明下Java8中提供的 ...

  4. 树的最小支配集 E - Cell Phone Network POJ - 3659 E. Tree with Small Distances

    E - Cell Phone Network POJ - 3659 题目大意: 给你一棵树,放置灯塔,每一个节点可以覆盖的范围是这个节点的所有子节点和他的父亲节点,问要使得所有的节点被覆盖的最少灯塔数 ...

  5. Linux创建软硬链接和打包压缩、解压缩

    软硬链接 ln = link make links between files 语法: 软链接 ln -s 源文件 链接名称 实例: ln -s HelloWord.java hw.lnk 给Hell ...

  6. Java集合简单介绍

    再最前面分享一下我再学习集合时的方法: 1.首先了解各集合的定义和特点 2.集合的构造方法和常用方法(增删改查等) 3.了解集合使用的场景,再什么情况下使用什么类型的集合(关键是集合的特性) 4.了解 ...

  7. 微信小程序-swiper(轮播图)抖动问题

    ps:问题 组件swiper(轮播图)真机上不自动滚动 一直卡在那里抖动 以前遇到这个问题,官方一直没有正面回复.就搁置了,不过有大半年没写小程序了也没去关注,今天就去看了下官方文档,发觉更新了点好东 ...

  8. 记录一下关于在工具类中更新UI使用RunOnUiThread犯的极其愚蠢的错误

    由于Android中不能在子线程中更新ui,所以平时在子线程中需要更新ui时可以使用Android提供的RunOnUiThread接口,但是最近在写联网工具类的时候,有时候会出现联网异常,这个时候为了 ...

  9. Java的Object.wait(long)在等待时间过去后会继续往后执行吗

    Java的Object.wait(long)在等待时间过去后会继续往后执行吗 Object.wait(long)方法相比于wait,多了个等待时长,那么当等待时长过去后,线程会继续往下执行吗? 单个线 ...

  10. (2)通信中为什么要进行AMC?

    AMC,Adaptive Modulation and Coding,自适应调制与编码. 通信信号的传输环境是变化不定的,信道环境时好时差.在这种情景下,我们不可能按照固定的MCS进行信号发送.假如信 ...