尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity跑酷游戏化妆系统开发:状态映射与数据驱动架构解析

Unity跑酷游戏化妆系统开发:状态映射与数据驱动架构解析 简介这是一份面向Unity开发者的化妆跑酷游戏完整源码包基于C#编写支持Unity 2019.4.3f1及以上版本适合希望学习跑酷玩法、2D UI换肤与移动端广告接入的游戏开发学习者。项目围绕“数字少女化妆”主题包含无尽关卡、流畅操作与精美视觉设计同时适配Android和iOS平台。压缩包共2000个文件体积约115.61MB其中包含321个png贴图资源、138个cs脚本、61个材质文件、50个fbx模型、21个prefab预制体、20个shader着色器以及13个动画文件另有广告配置、场景与UI相关asset结构较完整。资源已吸引77人学习下载适合作为独立游戏Demo或毕业设计的参考。通过阅读源码和预制体可以梳理跑酷游戏的核心循环、角色换装系统、Unity广告接入方式及2D UI换肤设计思路帮助快速搭建可运行的项目原型并理解关键模块的工程组织方式。1. 把化妆做成跑酷玩法而不是一套换装皮肤makeUp runner这个项目最容易误导人的地方是名字里带「runner」很多人默认它就是个普通的 Unity 跑酷游戏化妆只是噱头。实际拆一遍它的 C# 源码你会发现化妆系统不是挂在外面的换装 UI而是和跑酷的得分、节奏、角色状态绑定在一起的第二套玩法循环 —— 玩家选择的妆容决定跑酷中的表现参数跑酷进度反过来又解锁妆容的下一步变化。这在单局 3060 秒的手机跑酷里相当于把「养成」从局外搬到了局内。和纯跑酷比如跑 跳 吃金币相比这套逻辑多出来的是「状态映射」化妆进度、跑酷速度、特效强度三者要联动。结构上适合有一定 Unity 基础、想学习如何把玩法系统拆成数据驱动模块的中级工程师如果你已经做过完整的 UGUI 界面或简单的角色控制可以重点看这套项目是怎么处理“界面表现”和“底层逻辑”之间解耦的。下面从项目结构、跑酷逻辑、换装装配三个角度把能直接复制到你自己项目里的部分拆开讲。2. 先拆项目结构makeUp runner 的 C# 脚本如何分层2.1 按「入口、运行时数据、存档」三层划分而不是按功能划分很多 Unity 初学者拿到 C# 源码喜欢按功能目录整理比如Scripts/UI、Scripts/Player、Scripts/Items。但这个项目建议的划分方式是GameRoot入口、GameBus运行时数据、GameSave存档层。三层之间严格单向引用GameRoot可以引用其余两者GameBus不引用GameRootGameSave不引用任何运行时对象。GameRoot通常挂在场景里的空物体上在Awake()里完成单例初始化然后加载存档、注册全局事件。GameBus是一个用普通 C# 类实现的容器不继承MonoBehaviour存放当前妆容组合、跑酷速度、连击数这类运行期才有的数据。GameSave负责玩家的化妆方案存档、已解锁妆容列表序列化用JsonUtility或System.Text.Json都可以。// GameRoot.cs — 挂在场景根节点上的入口脚本 public class GameRoot : MonoBehaviour { public static GameRoot Instance { get; private set; } [SerializeField] private PlayerController player; [SerializeField] private UIManager uiManager; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; GameSave.Init(); // 读本地存档玩家已解锁列表 GameBus.Init(player, uiManager); } private void Start() { var saved GameSave.Load(); GameBus.ApplySavedLoadout(saved); // 把玩家上次的妆容组合恢复到角色上 } }逻辑说明所有系统不直接在Start()里用FindObjectOfType去找彼此而是在GameBus.Init时统一注入。这样断点调试时只需要盯住GameRoot一个入口依赖关系不会散落各处。参数说明ApplySavedLoadout的对象saved里至少应包含发型、瞳色、服装、肤色的索引如果项目里加入了口红或腮红这类细节妆容需要在外层多加一个数组Slots[]每个槽位存PartType和ItemId两个字段。2.2 化妆数据不适合用「属性字段堆砌」要改成配方表如果皮肤、头发、口红各写一个成员变量后续加新妆容要改动类的定义且配表的人也需要会看代码。更常见的做法是定义一份独立的MakeUpRecipe数据类用序列化列表填充。// MakeUpRecipe.cs — 单个妆容方案会被 GameSave 序列化 [System.Serializable] public class MakeUpRecipe { public string recipeName; // 方案名例如“落日橙” public bool isUnlocked; // 是否已解锁 public Listint itemIds new Listint(); // 对应各部位ID索引含义查枚举 }这个设计对源码阅读者最友好的点在于recipeName只用于存档展示itemIds的顺序和PartType的枚举顺序保持一致扩展新部位时只需要在枚举里追加一项不需要改保存逻辑。3. 跑酷逻辑与化妆系统的联动差值驱动而不是状态机驱动3.1 不要用「当前妆容 Index」直接驱动表现先换算成 01 差值makeUp runner里化妆进度的玩法设计是玩家每通过一个计分点妆容从当前造型向下一造型渐变整体是一个进度概念而不是离散的换装状态。如果直接拿整数索引去驱动换装会在连续跑动中出现“跳变”视觉上就是画面上角色妆面突然闪一下。推荐的方案是先把「跑酷进度」换算成一个浮点makeupProgress范围 01再把这个值传入化妆表现层。换算公式通常与玩家当前到达的ScoreGate计分点挂钩。// RunProgressToMakeupBinder.cs — 跑酷进度转化妆差值 public class RunProgressToMakeupBinder : MonoBehaviour { [SerializeField] private float minScoreGate 100f; // 第一个妆容完全生效分数 [SerializeField] private float maxScoreGate 1000f; // 最终妆容完全生效分数 public float GetMakeupProgress(int currentScore) { if (currentScore minScoreGate) return 0f; float t (currentScore - minScoreGate) / (maxScoreGate - minScoreGate); return Mathf.Clamp01(t); } }逻辑说明GetMakeupProgress只在需要重新计算妆容渐变时调用不要在Update()每帧调用否则会造成大量无意义的浮点运算。在跑酷中通常由计分器脚本在Score变更事件回调里调用此方法再传给 Material 的SetFloat()或 SkinnedMeshRenderer 的SetBlendShapeWeight()。参数说明minScoreGate和maxScoreGate的设定决定了化妆渐变的时间跨度。如果追求“前段变化快、后段变化慢”的节奏可以把上下限改成 AnimationCurve用curve.Evaluate(t)替换掉Clamp01那一行源码逻辑不变。3.2 用 Shader 的 SetFloat 接受化妆进度比切换材质球内存更友好跑到不同进度时就new Material会导致每帧额外 GC。更稳的操作是用一个公共材质球实例在进度变化时只做SetFloat更新。// MakeUpProgressApplier.cs — 挂在角色渲染物体上接收差值并应用 public class MakeUpProgressApplier : MonoBehaviour { [SerializeField] private SkinnedMeshRenderer faceRenderer; [SerializeField] private string progressProperty _MakeupBlend; private Material instancedMat; private void Awake() { // 拷贝一份实例材质避免影响场景里其他同材质角色 instancedMat new Material(faceRenderer.sharedMaterial); faceRenderer.sharedMaterial instancedMat; } public void ApplyProgress(float progress) { instancedMat.SetFloat(progressProperty, progress); } }逻辑说明构造Material时用sharedMaterial而不是material是为了避免在Awake阶段触发隐式实例化。跑酷场景里如果同时有多条跑道、多角色共用材质实例也能减少批次切换次数。参数说明_MakeupBlend这类的 Shader 属性名需要和.shader文件里的Properties对齐否则SetFloat静默失败界面没有报错但运行时妆面不变。排查指路用 Frame Debugger 查看该角色的 Draw Call 是否使用了同一个 Material 实例。3.3 UGUI 滑动条显示妆度用 Image.fillAmount 而不是 Slider.value热词里有一条「Unity 做一个滑动条」在化妆跑酷里正好用上化妆进度如果要展示在 HUD很多人第一反应是放 UGUI 的 Slider 组件但源码里更常见的是用一个Image.fillAmount加一段横条纹理实现。原因很简单Slider 自带 handle、background、fillArea 三层结构做「当前进度」和「下一目标解锁点」的双进度条时需要维护两套子级。用原生 Image 则可以直接用一个数学方法把两个进度点换算成两个fillAmount。// MakeupProgressHUD.cs — 简单进度条只显示当前妆度 public class MakeupProgressHUD : MonoBehaviour { [SerializeField] private Image progressBar; [SerializeField] private float smoothSpeed 5f; private float currentShownValue 0f; private float targetValue 0f; public void SetTarget(float progress) { targetValue progress; } private void Update() { currentShownValue Mathf.Lerp(currentShownValue, targetValue, Time.smoothDeltaTime * smoothSpeed); progressBar.fillAmount currentShownValue; } }逻辑说明Mathf.Lerp配合Time.smoothDeltaTime在帧率波动时动画过渡更均匀。这里的smoothSpeed需要调成 58 之间太小会导致进度条拖沓太大则会失去缓动感。三者的节奏关系形成了这套项目最值得抄的部分跑酷得分决定GetMakeupProgress输出该输出驱动MakeUpProgressApplier的 Shader 参数同时由MakeupProgressHUD显示进度。整个过程不经过字符串事件没有中间层。4. 数字少女换装跑酷装配换妆不换模型4.1 先厘清「数字少女」的模型结构头部、身体、头发分开makeUp runner里的角色不是单张 Mesh而是按部位拆分的head、body、hair、face。每个部位都可能是独立的SkinnedMeshRenderer。换妆的核心操作是替换对应部位的sharedMesh和sharedMaterials而不是把整个角色模型销毁重建。// CharacterOutfitSwapper.cs — 换妆换装通用装配方法 public class CharacterOutfitSwapper : MonoBehaviour { public SkinnedMeshRenderer headRenderer; public SkinnedMeshRenderer bodyRenderer; public SkinnedMeshRenderer hairRenderer; public void SwapOutfit(OutfitData outfit) { headRenderer.sharedMesh outfit.headMesh; headRenderer.sharedMaterials outfit.headMaterials; bodyRenderer.sharedMesh outfit.bodyMesh; bodyRenderer.sharedMaterials outfit.bodyMaterials; hairRenderer.sharedMesh outfit.hairMesh; hairRenderer.sharedMaterials outfit.hairMaterials; } }逻辑说明直接替换sharedMesh而不是mesh是刻意绕开 Unity 对网状资产的自动实例化减少换妆时的内存峰值。注意头发的骨骼通常挂在头的某个子节点下如果换了 hair 之后发丝穿模多半是换了不同骨架版本的 Mesh。4.2 装配映射表用 C# 数组做部位索引代替逐字段赋值源码里有一张很值得复用的映射表用枚举做数组下标。// OutfitMapping.cs — 把配表ID映射到具体资源 public enum BodyPartType { Hair 0, Face 1, Outfit 2 } public class OutfitMapping : MonoBehaviour { public BodyPartType partType; public int itemId; public Mesh targetMesh; public Material[] targetMaterials; }当玩家选择了某个妆容方案MakeUpRecipe.itemIds里存的数字和OutfitMapping.itemId对应通过Dictionaryint, OutfitMapping查表后调用SwapOutfit完成一次性装配。比写switch case更易扩展而且配表人员不进代码即可加新妆。顺带一提UGUI 里换妆面板的滚动列表也用Dictionaryint, OutfitMapping做数据源onClick事件只传itemId界面上不缓存任何资源引用避免 List 里挂满材质导致内存攀升。4.3 换装最易出的 3 个坑源码框架层规避法第一SkinnedMeshRenderer 换 Mesh 后骨骼还没对上妆后手臂扭曲。规避换 mesh 的代码写完后统一调用SkinnedMeshRenderer.BakeMesh做一次离线验证不可行因为骨骼权重绑定在资源运行时无法自动重绑。避免的办法是保证同一角色的所有部位模型都基于同一套骨格绑定导出。第二Material 直接对sharedMaterials赋值后后续如果改妆用了同一个材质球其他角色也会跟着变。规避SwapOutfit里用Instantiate拷贝实际会用到的材质或者读取只读配置。第三换妆瞬间因为重新绑定蒙皮导致一帧卡顿。规避在换妆前设置SkinnedMeshRenderer.enabled false完成赋值再开启把Rebind挪到下一帧的LateUpdate里。Unity 阴影问题热词同样是在换装场景里常见换完妆后新建的材质没有赋ShadowCastingMode会默认接受阴影但不会投射解决方案是在生成材质或装配完成后统一设castShadows On、receiveShadows On。4.4 实时预览耗时较长把妆面合成到 RenderTexture 上跑酷预告阶段角色站在起跑线上UI 实时预览妆面。如果每次调进度都改 SkinnedMeshRenderer 材质参数预览时会有一到两帧闪烁。源码里的成熟做法是给角色面部相机单独设一层FaceLayer相机输出到 RenderTextureUI 直接用 RawImage 显示这张纹理。这样做有额外的好处玩家跑动过程里角色转向不会让妆面被头发遮住预览始终用正交相机正面拍摄视觉反馈准确。这也解释了为什么这类项目里的化妆面板不直接用Camera.main渲染出的画面而是自己维护一套「妆面专用相机」。5. 验证化妆跑酷框架日志、数据与效果三层检查5.1 快速验证 Gating 数值写一个最小跑分脚本改完RunProgressToMakeupBinder的上下限后不用完整跑一局直接在 Inspector 里写一个测试方法模拟不同得分。// MakeupDebugValidator.cs — 放在 Editor 文件夹下使用 [UnityEditor.CustomEditor(typeof(RunProgressToMakeupBinder))] public class MakeupDebugValidator : UnityEditor.Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); var binder (RunProgressToMakeupBinder)target; if (UnityEditor.GUILayout.Button(模拟分数 500)) { Debug.Log($500分的妆容进度为 {binder.GetMakeupProgress(500)}); } } }这段话说明CustomEditor脚本只影响编辑器面板不参与运行时逻辑。用它在不进入 PlayMode 的情况下验证minScoreGate100、maxScoreGate1000时的曲线是否正确。5.2 妆面生效情况的三种日志锚点几乎每个全新的化妆跑酷框架都会遇到「数值变了但表现没变」的问题建议在三个位置埋日志RunProgressToMakeupBinder.GetMakeupProgress返回前输出原始 score 与转换后的makeupProgress确认输入正确。MakeUpProgressApplier.ApplyProgress里输出progress和instancedMat.GetFloat(progressProperty)确认材质实例真的收到了数据。OutfitSwapper.SwapOutfit执行后输出headRenderer.sharedMesh.name确认只是没刷新显示而不是没装配。5.3 帧率对比换妆流程的耗时上限因为化妆进度是 01 的渐变如果用 Shader 参数驱动每帧只做一次SetFloat几乎可以忽略。而换装配件会触发网格切换和蒙皮重计算耗时大尤其角色面数超过 2 万时需要检查Profile里的SkinnedMeshRenderer.Rebind是否成为热点。如果出现明显卡顿常见做法是给换装过程加一个 3~5 帧的携带检查不在同一帧内同时换三个部位而是每个部位间隔一帧处理用空间换时间。最后补一个验证化妆渐变是否平滑的技巧在MakeUpProgressApplier里周期打印progress的增量和时间差增量的方差若超过每帧 0.5%说明 Shader 材质的过渡插值没有按线性走多半是 Shader 内部lerp没做归一化需要在Properties里放开_MakeupBlend的Range让SetFloat输入不被自动钳制。本文还有配套的精品资源点击获取
返回列表