ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity放置RPG源码拆解:数值底盘、自动战斗与存档优化

2026/9/11 23:21:15 拓冰建站 浏览量
Unity放置RPG源码拆解:数值底盘、自动战斗与存档优化 简介这份Unity 2019.1.14f1版本的放置类RPG项目源码定位为移动端游戏开发参考与二次开发基底适合掌握基础Unity操作、想深入C#游戏逻辑的中级开发者。项目完整度较高包含角色养成、Boss战斗、宠物收养、炼金术等模块也涵盖冒险模式、季节活动、神秘之门等系统便于研究闲置玩法与RPG成长曲线的结合方式。资源打包为zip共2000个文件约359.12MB文件类型覆盖155个prefab预制体、127个mat材质、71个cs脚本、21个asset配置、12个shader着色器以及png美术图、json/txt配置和md文档Unity工程目录结构完整可直接用指定版本打开查看。目前已有530人学习下载从源码中可以学习到放置类游戏的核心循环搭建、离线收益与成就任务系统设计以及UI交互和战斗表现的实现细节同时可复用美术素材和代码框架缩短同类项目原型开发周期。1. 一套Unity放置RPG项目源码先分清能跑和值得抄两件事Almost a Hero 这种命名在放置RPG里很典型主角永远差一步成为英雄但数值从登录那一刻起就在涨。工程视角下一份 Unity 放置类RPG源码的含金量不在特效而在三件事自动战斗的循环设计、滚雪球式的数值产出、能扛住长期游玩的存档体系。对 Unity 开发者来说放置品类的源码是最划算的阅读材料——循环从玩家操作驱动变成系统自动驱动模块边界比常规 RPG 清晰得多5 年以上经验看架构取舍新手看最小可玩闭环。不替任何仓库背书只讲这类源码最常见的工程解法数值底盘怎么搭、自动战斗与存档怎么落地、手游端最容易翻车的性能点在哪。适合做放置玩法的客户端、独立开发者和刚接手这类项目评估存量代码的人。2. 放置RPG数值底盘剖析产出公式、离线收益与升级曲线放置品类的数值就是玩法本身拆源码先看懂它的产出链路。所有放置RPG本质上都是同一台机器时间换资源、资源换成长、成长换更多资源。源码的价值在于让这条链路可配置、可离线计算、可回溯而不是把数值公式散落在各个 MonoBehaviour 里。2.1 核心循环抽象战斗只是产出金币的前置过程放置RPG和传统RPG最本质的区别在输入侧战斗不需要玩家实时确认变成一条定时触发的产出链路。源码里最常见的形式是一个全局心跳类每秒 tick 一次依次完成检查目标、造成伤害、计算掉落、累加资源、判断升级。这段逻辑通常会收敛到纯 C# 类里不挂 MonoBehaviour原因很直接离线结算和在线心跳要复用同一套代码和同一组时间单位一旦战斗逻辑依赖 Update 和 Time.deltaTime离线那段根本没法模拟只能另写一套结果就是两套逻辑迟早分叉。// GameLoopManager.cs —— 放置游戏每秒心跳驱动全部产出 public class GameLoopManager : MonoBehaviour { private float _timer; private const float TickInterval 1f; private void Update() { _timer Time.deltaTime; if (_timer TickInterval) return; _timer 0f; // 顺序固定先战斗后经济离线模拟才能按同一顺序回放 BattleEngine.Simulate(1f); EconomySystem.Tick(1f); QuestSystem.TryAdvance(); } }这里的两个设计点值得抄一是 TickInterval 取 1 秒而不是逐帧结算让产出公式在离线和在线拿到同一组入参二是 BattleEngine 不直接碰 UI只写内存里的 PlayerProgressUI 层通过事件或版本号感知变化避免战斗逻辑和界面耦合。参数上最容易被忽略的是 Time.timeScale如果游戏做了暂停、慢动作演出或者技能时停Update 里的心跳也会被拖慢严格做法是心跳内部用 DateTime.UtcNow 做时间差值不依赖缩放后的 deltaTime这样任何演出效果都不会影响产出节奏。2.2 离线收益结算时间戳、倍率与上限离线收益是放置品类的命脉。源码里最常见的方案是每次玩家资源变动时同步维护一个 lastLoginUnix 时间戳下次启动时用当前 UTC 时间减去上次登录时间得到离线秒数。这个差值必须做两个钳制——离线时长上限和产出衰减否则玩家一个月不登录回来直接点满全天赋数值系统当场破产。// OfflineEarningCalculator.cs —— 离线结算与参数 public struct OfflineReward { public double gold; public double exp; public int seconds; } public static OfflineReward Calculate(PlayerProgress progress, long lastLoginUnix) { long elapsed DateTime.UtcNow.ToUnixTimeSeconds() - lastLoginUnix; elapsed Math.Min(elapsed, progress.MaxOfflineSeconds); // 上限收敛 double goldPerSec progress.GoldPerSecond * 0.6f; // 离线只有在线 60% return new OfflineReward { gold goldPerSec * elapsed, exp progress.ExpPerSecond * elapsed * 0.5f, // 经验再打对折 seconds (int)elapsed }; }参数说明MaxOfflineSeconds 在源码里通常不是常量而是一个玩家可升级的离线时长上限初始 2 小时、点满天赋 8 小时比直接封顶更有养成纵深。0.6 和 0.5 两个衰减系数建议收敛到一张数值配置表后续活动双倍离线收益、VIP 加成都是从这张表上做乘区扩展。结算的调用时机同样关键必须在存档加载完成之后、玩家点击任何按钮之前执行结算完立刻把 lastLoginUnix 刷新为当前时间否则玩家反复杀进程重进就能无限刷离线奖励。2.3 数值曲线选型从线性到指数放置游戏为什么越滚越大放置RPG的爽感来自数值膨胀但膨胀必须有节奏感。20 级时每级加 10 点攻击玩家无感200 级时每级攻击翻倍玩家才愿意等下去。源码里最常见的做法是分段曲线前期用线性保证新手不劝退中期切指数制造质变点后期用对数做软上限防止 double 也扛不住的溢出。曲线类型公式示例典型区间翻车点线性damage base level * step1~50 级后期成长感缺失指数damage base * pow(growth, level)51~300 级底数差 0.01百级后溢出对数软上限damage cap * (1 - exp(-k * level))300 级后k 调不好曲线形同虚设指数曲线在 C# 里有一个隐蔽的精度坑float 超过 167772162^24后整数精度开始掉到了万亿级别会出现金币加了等于没加的假象。所以放置游戏源码里普遍用 double 存战斗数值只在显示层用 K/M/B 缩写字符串。另一个容易被忽略的点是消耗与产出的增速差升级消耗按 1.12 指数涨、产出按 1.08 涨中间 4 个百分点的差值靠玩家主动点技能和离线收益补足这就是放置游戏留存设计里最核心的一对参数改源码数值时先动这两个底数不要单独去改某个技能倍率。提示double 也不是无限精度配置表里所有数值超过 1e12 后统一走科学计数法显示 高位截断的处理避免 UI 上出现一排看不懂的 0。3. 拆解Unity项目源码目录组织、自动战斗与UGUI刷新策略打开一套 Unity 3D 放置项目源码先看目录结构再决定从哪里读起。放置类玩法模块边界清晰好的源码一眼能看出战斗、经济、存档、UI四条主线差的源码把所有逻辑堆在几个巨型 MonoBehaviour 里这种项目修复成本高过重写。如果源码是拿 Unity 2021 写的你用 2023 强行打开大概率碰到一堆 API 过时告警先核对版本号再决定迁移成本。3.1 目录按功能分包公共代码单独收敛新项目比老项目更倾向按功能分包而不是按类型分包放置游戏尤其如此经济系统修改频繁战斗系统相对稳定按功能分后改经济不会碰战斗代码多人协作的冲突面大幅缩小。Assets/Scripts 下常见的参考结构是这样Assets/Scripts/ ├── Battle/ # 战斗引擎、敌人生成、状态机 ├── Economy/ # 金币产出、离线收益、商店定价 ├── Heroes/ # 英雄数据、升级、天赋 ├── Save/ # 存档序列化、校验、迁移 ├── UI/ # 界面控制器、数字滚动、列表回收 └── Config/ # ScriptableObject 数值配置目录说明Config 单独拎出来是因为放置游戏调数值的频率远超其他品类策划不应该改代码。用 [CreateAssetMenu] 让策划在 Project 面板右键直接新建数值配置用 [ContextMenu] 在 Inspector 上挂发一笔钱、刷一波怪这类调试命令这是标准的 Unity 扩展用法。如果源码里没有这层配置抽象说明它还停留在 DEMO 阶段抄的时候把硬编码数值全部抽到 ScriptableObject 或 JSON 里再往下走。编辑器工具类要放进 Editor 文件夹否则会打进发布包。目录组织方式优点缺点适用阶段按类型分包UI/Data/Enemy新成员好找文件改一个玩法要跨多个目录原型期按功能分包Battle/Economy/Save玩法内聚、冲突少公共代码容易重复正式项目按模块分包Hero/Core/UI可整模块替换边界设计成本高中大型项目3.2 自动战斗状态机状态切换和执行分离自动战斗在源码里通常是一台最小状态机最少包含 Idle、Attack、Skill、Dead 四个状态。放置游戏的特点是状态切换完全由时间和数值条件驱动不需要输入事件一个 Update 驱动的状态机足够没必要引入复杂框架。// BattleStateMachine.cs —— 英雄的自动战斗状态机 public enum BattleState { Idle, Attack, Skill, Dead } public class BattleStateMachine : MonoBehaviour { [SerializeField] private HeroStat stat; private BattleState _state; private float _timer; private void Update() { if (_state BattleState.Dead) return; _timer - Time.deltaTime; if (_timer 0f) return; switch (_state) { case BattleState.Idle: _state BattleState.Attack; // 攻击间隔到了切攻击态 _timer 0f; // 置零下一帧立即执行 break; case BattleState.Attack: if (stat.Energy stat.SkillCost) // 能量足够优先放技能 { _state BattleState.Skill; _timer stat.SkillCastTime; } else { DoAttack(); _state BattleState.Idle; _timer stat.AtkInterval; } break; case BattleState.Skill: DoSkill(); _state BattleState.Idle; _timer stat.AtkInterval; break; } } }新手最容易抄错的地方是把 DoAttack 写在 Idle 分支里导致同一次间隔内动作被触发两遍正确做法是状态切换和动作执行分离切换只负责改状态和重置计时器动作只发生在进入目标状态之后。从 Idle 转入 Attack 时把计时器置零让攻击在下一帧执行既不会出现同帧双击也不会因为重置计时器而多吃掉一倍攻击间隔。参数上三处值得注意AtkInterval 不是固定值源码里通常会叠加攻速 buffSkillCastTime 用来占位技能前摇没有这个参数技能动画会被连续 Update 打断Energy 的回复速率决定了战斗节奏放置游戏里普遍做成每 0.5 秒回 1 点、受击杀加成。如果原项目用协程堆攻击延时改成状态机后 Update 频率和内存分配都会明显改善。3.3 UGUI源码级视角下的刷新策略Text和进度条别每秒重建放置游戏的 UI 是几类游戏里最容易写烂的金币每秒变两次、血条每秒跳、离线奖励弹窗频繁触发每个 UI 元素都在 Update 里轮询数据源再赋值的话一场战斗下来 GC 曲线直接起飞。UGUI 源码里 Text.text 的 setter 每次赋值都会请求网格重建内容没变也会走一遍完整管线所以有效的做法是版本号驱动或者值变更通知// GoldTextBinding.cs —— 版本号驱动跳过无变更的每帧赋值 public class GoldTextBinding : MonoBehaviour { [SerializeField] private Text goldText; private long _lastVersion -1; private double _lastValue; private void Update() { if (PlayerProgress.Instance.Version _lastVersion) return; long v PlayerProgress.Instance.Version; double gold PlayerProgress.Instance.Gold; if (v ! _lastVersion || gold ! _lastValue) { goldText.text FormatNumber.ToShort(gold); // 1.2K / 3.4M _lastValue gold; _lastVersion v; } } }参数说明Version 是 PlayerProgress 上的 long 自增计数器每次资源变动都加一UI 脚本只需要记录上一次看到的版本号不需要维护具体数值的订阅关系这是放置项目里性价比最高的 UI 解耦方式。FormatNumber.ToShort 内部用 StringBuilder 拼 K/M/B 缩写这是 UI 层最大的 GC 来源务必避免 string 逐帧拼接。进度条同理Image.fillAmount 每帧赋值即使数值没变也会标记 Canvas 脏赋值前先做一次浮点阈值比较如果源码用补间动画驱动 fillAmount确认动画结束的 OnComplete 回调里有没有释放补间实例否则 Canvas 会一直被无效标记拖着重建。滚动列表超过一屏时RectMask2D 在移动端的裁剪开销不小列表项建议走对象池加内容复用少堆 Mask 和 Canvas。注意Canvas 重建在 Profiler 里显示为 Canvas.SendWillRenderCanvases持续高于 4ms 就先查上面的两类赋值。4. 放置RPG存档三件套Unity序列化、版本迁移与时钟防篡改存档是放置RPG的最后一公里玩法做得再好存读写崩一次就是流失。放置存档有两个硬指标写入频率高每 30 秒或者每次重要操作都要落盘跨版本可迁移加了新英雄新天赋不能把老玩家挡在门外。4.1 三种存档格式的取舍JSON、二进制还是混合方案ScriptableObject 只适合做配置资产不适合做玩家存档编辑器模式下它会自动落盘真机上不好控制写入时机而且玩家数据不应该和项目资产混在同一套资源体系里。JSON 是源码里最常见的落盘格式可读、可 diff、可手工改档缺点是体积大二进制体积小、解析快但版本升级必须维护迁移工具。实际项目我一般用混合方案核心进度用 JSON局外的日志型数据统计、成就、邮件归到另一份小存档避免一次改动全量重写。存档格式体积解析速度可调试性版本迁移成本JSONNewtonsoft大中等高低二进制自研序列化小快差高JSON 二进制混合中中等中中选择依据只有一个你的改版频率。独立项目和快速迭代的产品无脑选 JSON数值系统要长期运营折腾的才值得为二进制投入序列化和迁移成本。4.2 存档结构设计版本号在前字段迁移在后几乎每一份放置游戏源码最后都会被存档兼容性坑一次上线后加了新英雄、新天赋老玩家读旧档直接反序列化异常。标准做法是存档第一层永远带 version 字段读取时拿到旧版本号做链式迁移不要让反序列化器理解所有历史版本一路升到当前版本再进游戏。// SaveManager.cs —— 版本化存档读取与迁移入口 [Serializable] public class GameSaveData { public int version; public double gold; public int heroLevel; public long lastLoginUnix; public Listint unlockedHeroIds; public string checksum; // 附加字段计算哈希时排除自身 } public static class SaveMigrator { private const int CurrentVersion 3; public static GameSaveData Migrate(GameSaveData data) { while (data.version CurrentVersion) { switch (data.version) { case 1: data.unlockedHeroIds ?? new Listint { 0 }; data.version 2; break; case 2: if (data.gold 0) data.gold 0; // 修历史脏数据 data.version 3; break; default: return data; } } return data; } }逻辑说明迁移函数必须是幂等的同一个旧档跑两次得到相同结果反序列化必须用 try-catch 包住失败后优先恢复上一次自动存档而不是直接删档重来这是回流玩家的底线。写入策略上我一般保留三个写点玩家切后台时、每 30 秒定时、每完成一个关卡波次时三个入口共用同一个 Serialize 方法防止漏写和格式不一致。老存档字段缺失时反序列化器会丢默认值所以要给 List 和 double 这类字段做空值兜底而不是指望 Newtonsoft 替你处理好所有兼容。4.3 本地时钟防篡改校验和、时间漂移与WebGL特例离线收益依赖时间戳玩家把系统时间往后拨就能白嫖离线奖励放置游戏必须处理。源码里常见的防线有三层第一层校验和对存档主体排除 checksum 自身算 SHA256 或轻量哈希存进文件任何手工改档都会导致校验失败第二层时间漂移检测存档里带 UTC 基准时间发现系统时间跳变超过阈值就拒绝结算本轮离线收益第三层数值合理性检查离线结算出的资源超过理论上限时自动回落即使前两层被绕过也拿不到超额收益。// SaveValidator.cs —— 存档完整性与时间合法性校验 public static bool Validate(GameSaveData data) { string checksum ComputeChecksum(data); if (checksum ! data.checksum) return false; long elapsed DateTime.UtcNow.ToUnixTimeSeconds() - data.lastLoginUnix; if (elapsed data.maxOfflineSeconds * 2) return false; // 超过理论上限视为作弊 return true; }参数说明maxOfflineSeconds * 2 的系数是给时区跳变和关机时长留的余量发行区域横跨多时区的产品可以放宽到 2.5但不宜再大否则时间拨号作弊就压不住了。风险最高的场景其实是 WebGLUnity 发布 WebGL 时存档落在浏览器 IndexedDBIDBFS 写入失败会导致存档静默丢失所以写档接口必须返回写入结果启动时校验存档文件存在性和完整性失败要能走一次本地回滚。这里可以用 Unity 宏定义给不同平台开不同的存档路径#if UNITY_WEBGL走浏览器存储并加重试#elif UNITY_EDITOR走编辑器临时目录移动端走 persistentDataPath宏同样可以用来区分线上包和调试包的校验日志等级。5. Unity游戏优化用Profiler定位放置RPG的三个性能瓶颈放置游戏画面不复杂但性能问题比动作游戏更隐蔽——问题不在单帧峰值而在持续存在的低效。把 Unity Profiler 的 CPU Usage 时间轴开起来跑十分钟的挂机流程重点查三类开销。第一类Canvas 重建聚合。Text 和 Image 的事件性赋值都会在 Canvas.SendWillRenderCanvases 里体现这一项常驻 4ms 以上就按第三章的版本号方案做绑定改造目标是把同一帧被标记重建的 Canvas 数量压到 2 个以内。第二类Update 调用总量。300 个英雄如果每个挂一个带 Update 的 MonoBehaviour即使空转也有生命周期和函数调用的固定开销把它们收拢进一个 TickManager由管理器统一派发Profiler 里的总调用数能掉一个数量级。第三类GC Alloc 尖峰。观察 Heap.Alloc 曲线放置玩法的常态应控制在每帧 0~2KB出现锯齿状尖峰就挨个排查字符串拼接、LINQ 和匿名委托这三样是 UI 层 GC 的主要来源。shadow 和分辨率属于白送的分Unity 的实时阴影对放置游戏收益很低玩家注意力在数字而不是角色阴影上交付时普遍把 Shadow Quality 降到最低或用范围光替代移动端分辨率设置要做上限保护GPU 耗时超过 16ms 时优先从 1080p 降到 720p体感几乎无差别发热和耗电明显下降。这两项属于高性价比调整比优化几十个空 Update 更值。验证方法上我习惯用 Profiler 的 Deep Profile 单独跑离线收益结算这是最容易出现一帧算一个月收益的路径再把游戏切后台 5 分钟再回前台对比前后内存曲线内存没回落说明有单例或事件没释放。最后把设备切到最低电量模式压测 20 分钟那组数据才是玩家真实的体验线。本文还有配套的精品资源点击获取