前言:在休闲类游戏中,经常能见到这样的功能:每游玩一次游戏就会消耗一点体力,如果体力不足时则会隔一段时间恢复(比如30分钟恢复一点体力)。最近在项目中遇到了这样的需求,记录一下实现过程。
内容很多,直接上代码,但是这次我不直接复制粘贴了,一步一步来(为了防止大家觉得我水文章)。
第一步:首先,创建一个体力管理脚本,然后实现一个简单的单例模式,单例的目的是为了能让在其他脚本中可以调用体力变更的行为,比如游戏失败了要扣一点体力,或者玩家用金币购买体力,再或者看广告增加一点体力。
using UnityEngine;
using System;
/// <summary>
/// 体力管理脚本
/// </summary>
public class StaminaMgr : MonoBehaviour
{
#region 单例模式
public static StaminaMgr instance;
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
#endregion
}
第二步:声明所需的变量,我这里的变量可供参考,大家也可以自己设计。注释已经写的很清楚了,一个最大体力值,一个当前体力值,这里我采用了封装的形式,大家也可以不使用封装,直接将当前体力值的访问修饰符改为public的也可以。
//最大体力值
public int maxHp = 5;
//当前体力值
[SerializeField] private int nowHp;
//封装当前体力值属性 外界无法直接更改
public int NowHp
{
get { return nowHp; }
}
接着是其他的变量:
regenTime:就是 恢复一点体力值所需的时间(单位是秒),我这里的实际项目需求是30分钟,值应该为1800,这里暂时定为20秒是为了方便测试。
regenEndTime:这个是所有的体力完全恢复的时间点(举个栗子:比如当前时间点是12点整,如果我消耗了一点体力,那么恢复时间就是12点半,如果我消耗了两点体力恢复时间就是1点),为DateTime类型,在System命名空间下,没用过的可以去查一下相关的内容,我就不过多赘述了。
regenTimeStr:这个是需要显示的UI上的Text组件的剩余时间字符串
isInfinite: 是否开启无限体力标识,这个是我自己项目中的需求,可以不写。
//体力恢复一点的所需时间
private float regenTime = 20;
//体力完全恢复的时间点
public DateTime regenEndTime;
//当前体力恢复剩余时间
public string regenTimeStr;
//是否无限体力
public bool isInfinite = false;
第三步:写一个初始化的方法,在游戏一开始时调用,大家可以在Start方法中调用,也可以在Awake中和单例模式的实现写在一起,我就不演示调用的代码了(我自己是在Awake中调用的)
private void Init()
{
//获取当前体力
nowHp = PlayerPrefs.GetInt("NowHp", maxHp);
//获取体力恢复完全的时间
string time = PlayerPrefs.GetString("RegenEndTime", string.Empty);
if (!string.IsNullOrEmpty(time))
{
regenEndTime = DateTime.Parse(time);
}
else
{
regenEndTime = DateTime.MinValue;
}
//检查体力是否需要恢复
CheckStaminaRegen();
}
代码详解:
nowHp = PlayerPrefs.GetInt("NowHp", maxHp);
首先我们获取当前的体力值,这里我采用了PlayerPrefs的持久化方式保存这个值,即使游戏退出了也不影响数据的变更。如果是第一次读取,那么默认获取的就是最大体力值,也就是5。
string time = PlayerPrefs.GetString("RegenEndTime", string.Empty);
再获取体力恢复完全的时间,也用PlayerPrefs存储,这个默认值是一个空字符串。
if (!string.IsNullOrEmpty(time))
{
regenEndTime = DateTime.Parse(time);
}
else
{
regenEndTime = DateTime.MinValue;
}
然后我们判断这个字符串是否不为空,不为空就说明我们当前的体力值是没有满的,我们保存过了一个恢复的时间,再将这个string 类型的值转换成DateTime格式 并且赋值给之前声明的变量regenEndTime 。如果为空的话就说明是默认值,或者体力已经恢复满了,将regenEndTime赋值为DateTime中的最小值。
CheckStaminaRegen();
调用的这个方法是检查体力是否需要恢复,也就是体力是不是没满,在下一步中介绍。
第四步:写一个public的消耗体力的方法,可供外部调用
//消耗体力的方法
public void LoseStamina()
{
if (nowHp > 0)
{
//体力减一
nowHp--;
PlayerPrefs.SetInt("NowHp", nowHp);
//如果是第一次消耗体力 也就是 5-1=4
if (nowHp == 4)
{
//体力完全恢复时间为 当前时间加20秒
regenEndTime = DateTime.Now.AddSeconds(20);
//保存恢复完成时间
PlayerPrefs.SetString("RegenEndTime", regenEndTime.ToString());
}
else if (nowHp >= 0) //不是第一次消耗体力 在之前完全恢复时间的基础上加20秒
{
//获取体力恢复完全的时间
string time = PlayerPrefs.GetString("RegenEndTime", string.Empty);
DateTime lastTime = DateTime.Parse(time);
regenEndTime = lastTime.AddSeconds(regenTime);
//保存恢复完成时间
PlayerPrefs.SetString("RegenEndTime", regenEndTime.ToString());
}
}
else
{
nowHp = 0;
PlayerPrefs.SetInt("NowHp", 0);
}
}
代码详解:
if (nowHp > 0)
首先我们判断当前体力是否大于0,说明不是空体力可以进行消耗操作,在这个条件结构的else中,我们写的是
else
{
nowHp = 0;
PlayerPrefs.SetInt("NowHp", 0);
}
如果体力不大于0说明体力已经是空的了,也没有办法再消耗了,我们将nowHp赋值为0,然后保存在PlayerPrefs中。
if (nowHp > 0)
{
nowHp--;
PlayerPrefs.SetInt("NowHp", nowHp);
接着再说回如果当前体力大于0的逻辑,nowHp当前体力值减一,再保存,然后紧接着我们判断
if (nowHp == 4) 当前体力值是否为4,因为我的最大体力值设置为5,如果为4的话,也就是说明之前体力是满的,恢复时间也是默认值。
if (nowHp == 4)
{
regenEndTime = DateTime.Now.AddSeconds(20);
PlayerPrefs.SetString("RegenEndTime", regenEndTime.ToString());
}
在这个逻辑中,既然知道了恢复时间regenEndTime这个变量是默认值,那么我们直接在当前时间DateTime.Now 的基础上加20秒,这个20其实应该写成之前设置的体力恢复一点的间隔时间变量,我这里是投机取巧,大家可以根据需要自己改,再将这个值赋值给regenEndTime这个变量。 这里举个栗子:如果当前时间是12点整,那么此时的regenEndTime的值应该为12点零20秒。
紧接着再判断另一种可能:else if (nowHp >= 0) 如果当前体力值大于等于0,其实不加这个判断直接写else也是可以的,因为我们上面也判断过了,体力肯定是大于0的。这种情况说明之前扣除过体力,没有完全恢复,也就是PlayerPrefs中保存的体力恢复时间不是默认值,那么我们就需要在这个保存的时间点的基础上再加20秒。
else if (nowHp >= 0)
{
string time = PlayerPrefs.GetString("RegenEndTime", string.Empty);
DateTime lastTime = DateTime.Parse(time);
regenEndTime = lastTime.AddSeconds(regenTime);
PlayerPrefs.SetString("RegenEndTime", regenEndTime.ToString());
}
首先获取在PlayerPrefs中保存的字符串,然后将其转换成DateTime格式,就是方法中的第一行和第二行,然后第三行的代码是将这个转换好的时间变量加20秒,再赋值给我们的regenEndTime变量。第四行也很好理解,就是将这个变量转换成string格式再次保存到PlayerPrefs中。
第五步:我们现在有了体力消耗的方法,接下来也是最重要的一步了(敲黑板),这里我写了两个方法,分别为获取恢复所有体力的所需时间方法,和检查体力是否需要恢复方法:
//获取恢复所有体力的所需时间
float GetRegenTime()
{
//获取当前时间与 恢复完成时间的时间间隔
TimeSpan regenInterval = regenEndTime - DateTime.Now;
// 计算剩余恢复时间 (秒)
float remainingTime = (float)regenInterval.TotalSeconds;
//转换成float类型后返回
return remainingTime;
}
//检查体力是否需要恢复
void CheckStaminaRegen()
{
//如果是默认值 说明没消耗体力 不继续检测
if (regenEndTime == DateTime.MinValue)
{
return;
}
//获取恢复所有体力的所需时间
float timer = GetRegenTime();
//如果这个结果是负的 说明体力恢复完成
if (timer <= 0)
{
nowHp = maxHp;
PlayerPrefs.SetInt("NowHp", maxHp);
regenEndTime = DateTime.MinValue;
PlayerPrefs.SetString("RegenEndTime", string.Empty);
}
else
{
//如果这个结果大于0 说明还有时间 需要进行倒计时
//计算回满需要多少体力 回满所需时间 / 一点体力恢复时间 =》 向上取整
int num = (int)Math.Ceiling(timer / 20.0);
//当回满所需值 num大于或者等于5时 此时的体力一定是0点
if (num >= 5)
nowHp = maxHp;
//否则更新当前的体力值 最大值 - 回满所需值 = 当前值
else
nowHp = maxHp - num;
//保存体力值
PlayerPrefs.SetInt("NowHp", nowHp);
}
}
代码详解:内容有些多,但我保证肯定讲的大家都能听懂(其实注释已经够清晰了哈哈)
首先是GetRegenTime方法,在这个方法中我们计算体力恢复完全的时间与现在的时间还差多少秒,并且返回这个秒数:
第一行 TimeSpan regenInterval = regenEndTime - DateTime.Now; 这里是获取当前时间与恢复完成时间的时间间隔,regenEndTime - DateTime.Now就是我们保存过的体力完全恢复的时间点 减去 当前的时间,在DateTime类中,两个时间相减会得到一个TimeSpan类型的值,比如今天0点减去昨天0点,时间过去了一整天。这里大家就理解为过去了多久就可以了。
float remainingTime = (float)regenInterval.TotalSeconds;
第二行代码,这里是在计算剩余恢复时间:
首先(float)regenInterval.TotalSeconds; TimeSpan类型中的.TotalSeconds 就是获取所有的秒数。regenInterval 是我们刚刚计算的时间间隔变量,这里得到的也就是当前时间距离体力完全恢复时间还有多少秒,再将这个秒数转换为float类型赋值给一个float 类型的变量remainingTime
return remainingTime; 最后我们返回这个float类型的变量就可以了。
然后说CheckStaminaRegen() 这个方法:这个方法主要是给体力赋值的,我们在之前Init初始化方法中调用了一次,大家也可以根据需要进行自主调用(比如打开体力的界面调用一次)
首先一上来就判断是恢复完全时间否为默认值
if (regenEndTime == DateTime.MinValue)
{
return;
} 如果是默认的那也没必要再检测了,直接return。
float timer = GetRegenTime();
如果不是默认值,我们就用刚刚的方法获取恢复完全体力的时间,记住这个float类型的timer是总共的秒数。
if (timer <= 0) 接着我们马上判断这个timer秒数是否小于0,如果它是负的说明体力早就恢复完成了,时间都已经过了,那我们就直接把体力回满就行了:
nowHp = maxHp;
PlayerPrefs.SetInt("NowHp", maxHp);
regenEndTime = DateTime.MinValue;
PlayerPrefs.SetString("RegenEndTime", string.Empty);
将nowHp当前体力值设置为最大值, 然后持久化保存, 在将完全恢复时间regenEndTime赋值为默认值并且也持久化保存,很好理解吧。
这在个判断的else中,说明获取到的秒数大于0,也就是说体力现在肯定是没满的:
int num = (int)Math.Ceiling(timer / 20.0); 这一行是得到回满体力需要多少点,有点复杂,我一点一点说,首先timer / 20.0 这里是总共需要的秒数 除以 我们恢复一点体力的时间,在使用Math.Ceiling这个数学方法将这个结果向上取整,再转换成int整数类型 赋值给声明的 num变量。 举个栗子:(不说小时了)假设所有体力都恢复完全的时间点为1分50秒,现在的时间为1分0秒,那么此时我们恢复所有的体力需要50秒,也就是timer的值, 恢复一点体力需要20秒,50 / 20 等于2.5, 再向上取整就是3,那么num的值为3。为什么要向上取整呢?因为哪怕还有一秒钟体力就回满了,你现在的体力值也是4,没错吧?
然后的代码: if (num >= 5)
nowHp = maxHp;
如果这个回满所需值num大于等于5的话,其实应该不会超过5,这说明现在的体力值一定是0点,我们直接将当前体力值赋值为最大值就可以了。
else
nowHp = maxHp - num;
否则,这个值可以是1,2,3,4
计算出了回满所需值 我们就直接用最大值减去这个值 就得到了我们当前应该剩多少体力了。
最后将当前体力值持久化保存就可以了 PlayerPrefs.SetInt("NowHp", nowHp);
为了防止有人不理解,我再举个栗子(几个🌰了?):在我要做的项目中,需求是如果消耗了一点体力,就需要30分钟才可以恢复,并且这个恢复时间是在你消耗体力的那一刻就开始计算了,比如我体力之前是满的,5点体力,在12点整的时候我玩这个游戏输了一局,那么就会调用体力管理脚本中的LoseStamina 方法消耗一点体力。 这个时候,体力的完全恢复时间就是12点30分,当前体力值为4。紧接着没过几分钟,比如12点03分,和12点05分,这两个时间点我又分别输了一局,又调用了两次LoseStamina 方法,那么此时此刻,完全恢复时间应该是1点30分,当前体力值为2点。然后我不想玩了,我把游戏退了,注意这个数据是保存在PlayerPrefs中的持久化数据。 然后时间来到了12点40,我又进入了游戏,当前的体力值应该是多少? 肯定是3点体力对吧,这个时候会调用初始化方法Init,我们之前保存的持久化数据中,当前体力值为2,Init方法运行到第一行的时候nowHp的值读取为2。然后在Init方法的最后我们调用了CheckStaminaRegen 方法,检测体力是否需要恢复,在这个方法中,代码会运行到float timer = GetRegenTime(); 这一行,此时timer的结果应该是1点30分减去 现在的时间12点40分,一共50分钟,也就是3000秒。
紧接着进行判断逻辑,代码会运行到
else
{
int num = (int)Math.Ceiling(timer / 20.0);
这一行,这里因为我说的栗子是我项目中的情况,这个20应为30分钟,也就是1800,3000 / 1800 = 1.66666(无限循环省略后面的小数),再向上取整就是2,然后的判断逻辑代码回运行到
else
nowHp = maxHp - num;
这一行,当前体力等于 最大体力值 5 - 2 = 3。
这样讲的就够详细了吧,我相信草履虫都是能听懂的(手动狗头)
第六步: 现在我们就需要将剩余的时间显示的UI上了,做一个体力恢复倒计时的显示,这里我写了一个获取当前这一点体力恢复的时间方法,因为在我的项目中,需求是显示还剩多少时间恢复一点体力:
//获取当前这一点体力的恢复时间
TimeSpan GetNowRegenTime()
{
// 计算当前体力对应的恢复时间
int hpDifference = 4 - nowHp;
DateTime nowRegenTime = regenEndTime.AddSeconds(-regenTime * hpDifference);
// 获取当前时间与恢复完成时间的时间间隔
TimeSpan regenInterval = nowRegenTime - DateTime.Now;
return regenInterval;
}
代码详解:
int hpDifference = 4 - nowHp;
DateTime nowRegenTime = regenEndTime.AddSeconds(-regenTime * hpDifference);
这里在计算当前体力对应的恢复时间,假设当前体力值为3那么我计算的就是从3点体力恢复到4点体力需要多少时间。hpDifference的值是4 - 3 = 1,regenTime这个变量是恢复一点体力的所需时间,我们之前暂时设置成了20秒,-regenTime * hpDifference 这里就是负的20 * 1 等于 -20。我们再用AddSeconds 这个添加秒数的方法,将完全恢复的时间添加负20秒,那其实就是减去20秒了。假设完全恢复时间是1分40秒,减去20秒就是1分20秒,赋值给nowRegenTime这个变量。
得到了这一点体力恢复时间点,我们就再计算当前时间距离恢复时间需要多久,直接返回这个时间间隔:
TimeSpan regenInterval = nowRegenTime - DateTime.Now;
return regenInterval;
接着我们可以在Update方法中持续调用这个方法获取时间间隔,然后转换成字符串显示在UI上就可以啦:
private void Update()
{
if (nowHp < maxHp)
{
TimeSpan timer = GetNowRegenTime();
int minutes = (int)timer.TotalMinutes;
int seconds = timer.Seconds;
regenTimeStr = $"{minutes}:{seconds:D2}";
if (timer.Seconds == 0)
{
//体力加一
nowHp++;
PlayerPrefs.SetInt("NowHp", maxHp);
}
}
}
这里也简单解释一下其中的代码
int minutes = (int)timer.TotalMinutes; 这一行是获取分钟数
int seconds = timer.Seconds; 这一行是获取秒数
regenTimeStr = $"{minutes}:{seconds:D2}"; regenTimeStr这个变量是在第二步中定义的string变量,后面的值最后是这个样子的: 30:00 29:59 29:58
得到这个字符串之后,我们用一个Text文本组件赋值这个字符串就可以显示在UI上啦~
以上就是我的全部代码了,如果需要组合在一起的完整代码可以私信我找我要,无偿!
最后推荐大家关注一下我的公众号,除了Unity知识点文章外还有免费的游戏开发资产分享哦~