放置挂机游戏的离线和在线收益unity实现

在实现放置挂机游戏中的一个重要点就是挂机收益,一般的挂机收益有离线和在线收益两种,我这里都有实现一个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
相关推荐
康de哥2 小时前
在OpenCode中配置unity3d-mcp
unity·glm-4.7·minimax m2.1·opencode·unity3d-mcp
爱说实话3 小时前
C# 20260112
开发语言·c#
在路上看风景3 小时前
1.5 AssetDataBase
unity
无风听海4 小时前
C#中实现类的值相等时需要保留null==null为true的语义
开发语言·c#
上海云盾安全满满4 小时前
棋牌APP被攻击了要怎么办
网络·安全·游戏
云草桑4 小时前
海外运单核心泡货计费术语:不计泡、计全泡、比例分泡
c#·asp.net·net·计泡·海运
精神小伙就是猛4 小时前
C# Task/ThreadPool async/await对比Golang GMP
开发语言·golang·c#
工程师0074 小时前
C#状态机
开发语言·c#·状态模式·状态机