在实现放置挂机游戏中的一个重要点就是挂机收益,一般的挂机收益有离线和在线收益两种,我这里都有实现一个demo。
1.定义基本数据类型
cs
public class OfflineProfitData
{
/// <summary>离线开始时间戳</summary>
public long offlineStartTimeStamp;
/// <summary>收益速率(每分钟获得的金币数)</summary>
public float profitPerMinute;
/// <summary>离线前的玩家等级(用于动态计算收益速率)</summary>
public int playerLevel;
/// <summary>备用:服务器时间戳(防篡改,有服务器时使用)</summary>
public long serverTimeStamp;
// 初始化数据
public static OfflineProfitData GetDefault()
{
return new OfflineProfitData
{
offlineStartTimeStamp = GetCurrentUtcTimeStamp(),
profitPerMinute = 100, // 每分钟100金币
playerLevel = 1,
serverTimeStamp = 0
};
}
/// <summary>获取当前UTC时间戳(秒)</summary>
public static long GetCurrentUtcTimeStamp()
{
return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
}
}
其中:
这里的离线时间戳就是用于计算挂机时间的必须的字段,
如玩家等级什么的就更具需求扩展就行
这里还要说明服务器时间戳,是预留的,如果有服务器就可以从服务器获取玩家登录当前的时间戳
我这里默认没有服务器,就使用的玩家本地的时间戳,这有个问题就是玩家可以篡改本地时间,(但纯单机的游戏也一般不关注),玩家想开挂就开吧。
2.实现思路
(1).需要的参数结构
cs
// 配置项
private const float MaxOfflineHours = 12f; // 离线收益上限(最多计算12小时)
private const string SavePath = "OfflineProfitData";// 数据保存路径
private const float OnlineProfitInterval = 10f; // 在线收益触发间隔(值为10秒)
// 数据存储
private OfflineProfitData _offlineData; // 存储的数据
private CancellationTokenSource _onlineProfitCts; // unitask实现的异步
private bool _isOnlineProfitRunning; // 是否运行在线收益
// 防篡改配置
private bool _useServerTime = false;
(2).打开游戏初始,先加载数据,发放挂机奖励,开启在线收益
cs
public void Init()
{
LoadOfflineData();
CalculateAndGrantOfflineProfit();
StartOnlineProfitTimer();
}
(3)退出游戏或者游戏销毁,挂后台,可以更具你的需求,停止在线收益,记录离线时间记录
挂后台回来,就和初始化一样的流程,只是不用再加载数据,
一般是不会用到销毁的情况,如果不需要在线收益,可以直接删了,
cs
/// <summary>
/// 游戏退出/后台时,记录离线开始时间
/// </summary>
public void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
StopOnlineProfitTimer();
RecordOfflineStartTime();
}
else
{
CalculateAndGrantOfflineProfit();
StartOnlineProfitTimer();
}
}
/// <summary>
/// 游戏完全退出时,记录离线开始时间
/// </summary>
public void OnApplicationQuit()
{
StopOnlineProfitTimer();
RecordOfflineStartTime();
}
/// <summary>
/// 场景卸载时处理
/// </summary>
public void OnDestroy()
{
StopOnlineProfitTimer();
_onlineProfitCts?.Dispose();
}
这些步骤实现了就是改进收益的核心完成了,
在补充下具体的方法实现,
在线收益的部分
采用异步while循环执行,循环的条件就是启动时的token,token不取消就一直循环,
cs
#region 在线每分钟收益核心逻辑
/// <summary>
/// 启动在线收益定时器(UniTask)
/// </summary>
public void StartOnlineProfitTimer()
{
if (_isOnlineProfitRunning)
{
Debug.LogWarning("在线收益定时器已运行,无需重复启动");
return;
}
_onlineProfitCts = new CancellationTokenSource();
_isOnlineProfitRunning = true;
OnlineProfitLoopAsync(_onlineProfitCts.Token).Forget();
Debug.Log("在线收益定时器已启动");
}
/// <summary>
/// 停止在线收益定时器
/// </summary>
public void StopOnlineProfitTimer()
{
if (!_isOnlineProfitRunning || _onlineProfitCts == null) return;
_onlineProfitCts.Cancel();
_onlineProfitCts.Dispose();
_onlineProfitCts = null;
_isOnlineProfitRunning = false;
Debug.Log("在线收益定时器已停止");
}
/// <summary>
/// 在线收益异步循环(UniTask替代Coroutine)
/// </summary>
private async UniTask OnlineProfitLoopAsync(CancellationToken cancelToken)
{
try
{
while (!cancelToken.IsCancellationRequested)
{
await UniTask.Delay(TimeSpan.FromSeconds(OnlineProfitInterval), cancellationToken: cancelToken);
if (cancelToken.IsCancellationRequested) break;
float currentProfitPerMinute = GetCurrentPlayerProfitPerHour();
int profit = Mathf.RoundToInt(currentProfitPerMinute);
GrantProfitToPlayer(profit); // 自己实现的道具添加方法,
_offlineData.profitPerMinute = currentProfitPerMinute;
SaveOfflineData();
Debug.Log($"在线收益发放完成:{profit}金币");
}
}
catch (OperationCanceledException)
{
Debug.Log("在线收益异步任务已正常取消");
}
catch (Exception e)
{
Debug.LogError($"在线收益任务异常:{e.Message}\n{e.StackTrace}");
}
finally
{
_isOnlineProfitRunning = false;
}
}
#endregion
挂机收益
核心是记录离线时间
然后就是计算离线的时间,发放对应的奖励,这部分就是自己实现的内容,(一般是添加奖励数据,打开提示界面)
每次领取奖励和离线都需要及时更新记录的时间戳
cs
#region 离线收益
/// <summary>
/// 记录离线开始时间(更新数据并保存)
/// </summary>
public void RecordOfflineStartTime()
{
_offlineData.offlineStartTimeStamp = OfflineProfitData.GetCurrentUtcTimeStamp();
_offlineData.profitPerMinute = GetCurrentPlayerProfitPerHour();
_offlineData.playerLevel = GetCurrentPlayerLevel();
if (_useServerTime)
{
// _offlineData.serverTimeStamp = ServerManager.Instance.GetServerTimeStamp();
}
SaveOfflineData();
Debug.Log("离线开始时间已记录");
}
/// <summary>
/// 计算并发放离线收益
/// </summary>
public void CalculateAndGrantOfflineProfit()
{
long currentTimeStamp = OfflineProfitData.GetCurrentUtcTimeStamp();
long offlineSeconds = currentTimeStamp - _offlineData.offlineStartTimeStamp;
// 防篡改校验:时间为负则归零
if (offlineSeconds < 0)
{
Debug.LogWarning("检测到时间篡改,离线收益归零");
offlineSeconds = 0;
}
// 限制离线时长上限
offlineSeconds = (long)Mathf.Min(offlineSeconds, MaxOfflineHours * 3600);
// 小于1分钟不计算
if (offlineSeconds < 60f)
{
Debug.Log("离线时长过短,无离线收益");
return;
}
// 计算并发放收益
int finalProfit = Mathf.RoundToInt(_offlineData.profitPerMinute * offlineSeconds);
GrantProfitToPlayer(finalProfit);
ShowOfflineProfitUI(finalProfit, offlineSeconds);
// 重置离线时间并保存
_offlineData.offlineStartTimeStamp = currentTimeStamp;
SaveOfflineData();
Debug.Log($"离线收益发放完成:{finalProfit}金币,离线时长:{offlineSeconds:F2}秒");
}
#endregion
数据存储
这就不需多说了,我使用的是unity自带的PlayerPrefs,存储的json数据。
cs
#region 数据持久化
/// <summary>
/// 加载离线收益数据
/// </summary>
private void LoadOfflineData()
{
try
{
_offlineData = PlayerPrefsExtension.GetLocalJson(SavePath, OfflineProfitData.GetDefault());
Debug.Log("离线收益数据加载成功");
}
catch (Exception e)
{
Debug.LogError($"加载离线收益数据失败:{e.Message}");
_offlineData = OfflineProfitData.GetDefault();
}
}
/// <summary>
/// 保存离线收益数据
/// </summary>
private void SaveOfflineData()
{
try
{
PlayerPrefsExtension.SaveLocalJson(SavePath, _offlineData);
Debug.Log("离线收益数据保存成功");
}
catch (Exception e)
{
Debug.LogError($"保存离线收益数据失败:{e.Message}");
}
}
#endregion