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 ...
随机推荐
- muduo网络库源码学习————线程本地单例类封装
muduo库中线程本地单例类封装代码是ThreadLocalSingleton.h 如下所示: //线程本地单例类封装 // Use of this source code is governed b ...
- C# 基础知识系列- 14 IO篇 流的使用
0. 前言 继续之前的C# IO流,在前几篇小短片中我们大概看了下C# 的基础IO也对文件.目录和路径的操作有了一定的了解.这一篇开始,给大家演示一下流的各种操作.以文件流为例,一起来看看如何操作吧. ...
- Sunday算法:字符串匹配算法进阶
背景 我们第一次接触字符串匹配,想到的肯定是直接用2个循环来遍历,这样代码虽然简单,但时间复杂度却是\(Ω(m*n)\),也就是达到了字符串匹配效率的下限.于是后来人经过研究,构造出了著名的KMP算法 ...
- centos下的redis一键安装shell脚本
#!/bin/bash yum install cpp binutils glibc-kernheaders glibc-common glibc-devel gcc make wget #安装依赖库 ...
- JavaScript 浅析数组对象与类数组对象
数组(Array对象) 数组的操作 创建数组方法 添加与修改数组元素 删除数组元素 使用数组元素 遍历数组元素 多维数组 数组相关的函数 concat() join() pop() push() sh ...
- 我的第一个UWP程序
1.为什么喜欢UWP 本人无悔入网易云音乐,各种设备上都少不了这个红色图标的软件 从win10发布,网易做了UWP版本的云音乐 应用轻巧.简洁.功能全,接着又下了许多UWP的应用 都给人不一样的感觉, ...
- 【Spark】RDD的依赖关系和缓存相关知识点
文章目录 RDD的依赖关系 宽依赖 窄依赖 血统 RDD缓存 概述 缓存方式 RDD的依赖关系 RDD和它依赖的父RDD的关系有两种不同的类型,即窄依赖(narrow dependency) 和宽依赖 ...
- Mysql常用sql语句(16)- inner join 内连接
测试必备的Mysql常用sql语句系列 https://www.cnblogs.com/poloyy/category/1683347.html 前言 利用条件表达式来消除交叉连接(cross joi ...
- CF#637 C. Nastya and Strange Generator
C. Nastya and Strange Generator 题意 有一个随机全排列生成器,给出你一个全排列,让判断是否可以通过这个生成器产生. 生成器工作方式: 第i步为数字i寻找位置pos. 首 ...
- uCOS2014.1.11
typedef unsigned char BOOLEAN;typedef unsigned char INT8U; /* Unsigned 8 bit quantity */ty ...