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

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. Java大数据秋招面试题

    以下为整理的自己秋招遇到的面试题:主要是Java和大数据相关题型:根据印象整理了下,有些记不起来了. 死锁.乐观锁.悲观锁synchronized底层原理及膨胀机制ReetrantLock底层原理,源 ...

  2. muduo网络库源码学习————线程本地单例类封装

    muduo库中线程本地单例类封装代码是ThreadLocalSingleton.h 如下所示: //线程本地单例类封装 // Use of this source code is governed b ...

  3. 用纯css、JavaScript、jQuery简单的轮播图

    完成一个可以自动切换或点击数字的轮播图 HTML代码只需要一个div 包含着一个图片和一个列表,我们主要的思路就是通过点击相应的数字,改变图片的 路径. 有4张图片都在img文件夹里,名称为  img ...

  4. vue 之 slot插槽

    插槽,也就是slot,是组件的一块HTML模板,这块模板显示不显示.以及怎样显示由父组件来决定. 实际上,一个slot最核心的两个问题这里就点出来了,父组件决定插槽显示或不显示以及怎样显示,子组件决定 ...

  5. CSS中的间距设置与盒子模型

    CSS间距 内补白 外补白 盒子模型 CSS间距 很多时候我们为了美观,需要对内容进行留白设置,这时候就需要设置间距了. 内补白 设置元素的内间距 padding: 检索或设置对象四边的内部边距 pa ...

  6. Zabbix 添加vmware esxi监控

    1) Import the provided template. - TEMPLATE.VMWARE_ESXi_6.0_CIM.xml 2) Install Dependencies: # yum - ...

  7. SORM框架01

    架构图 Query接口:负责查询(对外提供的核心服务类) QueryFactory类:负责根据配置信息创建Query对象 TypeConvertor接口:类型转换 TableContext类:负责获取 ...

  8. [hdu5312]数的拆分,数学推导

    题意:给定一个序列,a[n]=3n(n-1)+1,n>=1,求给定的m(m<=1e9)最少可以用几个a里面的数表示(可以重复) 思路:对答案分类 (1)假定答案为1,则m必定是a中的某一个 ...

  9. IDEA快捷键(windows)

    Ctrl+Shift + Enter,语句完成“!”,否定完成,输入表达式时按 “!”键Ctrl+E,最近的文件Ctrl+Shift+E,最近更改的文件Shift+Click,可以关闭文件Ctrl+[ ...

  10. RobotFramework自动化测试之元素定位

    前言:最近在做基于RF框架的Web自动化测试,其中涉及到元素的定位,主要用到id.name.xpath.css四中定位方法,尤其后面的两种方法特别有效,可以解决大部分的定位问题. id和name定位 ...