资讯动态

Unity换装游戏核心实现:C#数据建模、骨骼挂点与存档

发布时间:2026/9/15 11:49:12 来源:尧图企业网站定制
简介基于Unity 2021.3.15f1及以上版本开发的舞会之夜装扮少女游戏完整源码项目主要面向Unity游戏开发者、休闲游戏玩家以及C#编程语言学习者。zip压缩包内共包含2000个文件其中以PNG美术贴图和meta配置文件占据大多数同时还有prefab预制体、anim动画片段、controller动画控制器、cs游戏脚本、shader着色器以及aar/jar格式的Android依赖库等资源包整体大小约345.65MB。该项目场景设定在高中毕业舞会前夜内置6位女主角玩家可通过完全免费且无内购的超过200件装扮物品为角色进行全方位造型类别涵盖连衣裙、半身裙、上衣、帽子、鞋子、斗篷、手套、项链、鲜花、手提包以及发型等多个系列搭配完成后还能通过拍照功能生成一张专属的舞会公主纪念照。源码工程在目录和模块划分上层次清晰涉及Unity界面布局、换装数据管理、场景切换、动画控制和移动端打包配置等典型开发环节既适合作为Unity换装游戏实战学习资料也可作为二次开发或玩法扩展的基础框架。目前该资源已有219人学习下载。1. 舞会之夜装扮Unity少女换装游戏到底要解决什么问题如果只看标题Prom Night Dress Up舞会之夜装扮像是衣柜UI加上一个3D角色但把它当成正式换装项目看问题会立刻多出一串礼服要跟着骨盆和裙摆骨骼动、长发不能穿进肩膀、上次搭配的造型能不能在下次启动时原样还原。标题里的Unity、C#和源码真正指向的其实是三个词数据建模、骨骼挂点、状态存档。这里不打算逐行复述某个具体仓库的源码而是讲一套能落地的常见做法用C#在Unity里把装扮系统的数据层、换装逻辑、UI交互和存档串起来。所谓豪华一般体现在部件数量、换装流畅度和试穿动效上这套做法同样适用于古风换装、Avatar装扮、舞会Party主题的衍生玩法。如果你已经准备了一个带人形骨骼的角色模型跟着做就能跑通最小流程。读这篇文章的人我默认你至少能新建Unity工程并给物体挂脚本。Rig、Avatar会涉及但不会展开用到时会写全参数。下面从最容易让项目失控的数据结构开始。2. 装扮数据先建模把服装、发型、配饰从散乱Prefab变成C#配置2.1 为什么换装项目最忌直接把Prefab拖进场景很多刚起步的换装Demo会把所有衣服都放在角色模型下运行时用SetActive切显示。这在只有两三件裙子时确实最快但项目一旦进入“要保存玩家搭配”阶段这套做法会同时踩中三个坑第一衣服状态散落在场景层级里代码没法知道玩家当前穿了什么第二存档时只能硬编码物体名美术只要把Prefab重命名存档就全部失效第三多人协作时场景里塞满服装引用每一次合入都在制造冲突。我一般会在项目第一天就定下一条规则凡是能穿到角色身上的东西都必须是独立的Prefab并且由一张数据表来引用它。场景里只保留空角色和衣柜逻辑衣服不直接出现在层级里。这样做表面看多了几步配置实际上后续的试穿、存档、换装联动都会轻松很多。2.2 用ScriptableObject把整套装扮固化成一份装扮条目当你打开一个装扮项目源码最先找的应该是OutfitData或者类似名字的类。它通常继承ScriptableObject因为Unity里SO天生适合做配置可以直接在Project窗口右键创建、可以在Inspector里配字段、序列化成本低多人协作时每人改自己的资产文件冲突机会远小于场景。using UnityEngine; public enum OutfitCategory { Hairstyle, Dress, Shoes, Accessory } [CreateAssetMenu(fileName NewOutfit, menuName DressUp/OutfitData)] public class OutfitData : ScriptableObject { [Header(标识)] public string itemId; public OutfitCategory category; [Header(资源)] public GameObject prefab; public Sprite icon; [Header(附注)] public string displayName; [TextArea] public string description; }这段代码里最关键的是itemId和category。itemId是存档用的唯一标识必须保证全局不重复不能直接拿prefab.name做替代否则美术一个重命名就会让玩家的档位变成空穿。category用枚举而不用字符串是因为后面要按部位分组做穿上/脱下逻辑switch一个枚举比else if一串字符串可靠得多。2.3 部件分组与图标预览数据结构里的三个必填字段一个能正常工作的装扮条目至少需要下面三个字段成对出现字段类型为什么必须有itemIdstring存档与恢复画面时用来找回具体配置categoryOutfitCategory决定替换哪一组骨骼挂点并覆盖同类旧部件prefabGameObject唯一的运行时实例化源不引用它就没东西可穿icon虽然不是运行时必需但衣柜面板一定会用到建议和prefab放在同一个SO里避免UI再去资源目录里用路径加载既慢又容易拼错路径。有了OutfitData以后当前角色的装扮状态应该用一个字典来保存key是部位类型value是目前穿着的OutfitData这样语义最直接。代码里通常长这样public class DressUpModel : MonoBehaviour { public Transform rootBone; private DictionaryOutfitCategory, OutfitData _currentOutfit new DictionaryOutfitCategory, OutfitData(); public OutfitData GetCurrent(OutfitCategory category) { return _currentOutfit.ContainsKey(category) ? _currentOutfit[category] : null; } }注意这里用Dictionary而不是List因为“每个部位同一时间只能穿一件”是装扮品类的硬规则用key查值天然防重复。后文所有试穿和存档逻辑都会围绕这个字段展开。3. 换装核心逻辑Unity骨骼挂点与C#切换流程3.1 静态换装与动态挂骨两种方案各自的成本实现换装最常见的有两条路。一条是静态换装每个款式做一个完整的角色模型切换时销毁整个角色再实例化另一个。做法简单但任何一个配饰变化都要复制一份全身模型内存翻着倍往上涨而且没法支持上装下装分开混搭在舞会之夜这种强调混搭的游戏里很快会被淘汰。另一条是动态挂骨衣服、头发、首饰各自是独立Prefab运行时在角色骨骼上找到对应锚点把它们实例化成骨骼的子物体。它的核心优势是组合自由且只在需要时占用内存代价是要求美术严格按照一套骨骼命名规范建模例如裙子必须绑定在Hips或Pelvis下头发在Head下项链锁在Chest或Neck上。两者怎么选可以参考下面这个表方案组合自由度内存动画跟随适合阶段静态换整体角色低高天然跟随原型或极小规模动态挂骨高低需要命名规范正式项目3.2 在C#里实现穿上与脱下动态挂骨的第二步是把“拖进Hierarchy”变成代码。在2.3的DressUpModel字段基础上以穿舞会长裙为例核心C#换装代码是这样public void Wear(OutfitData data) { if (data null) return; // 同一部位旧衣服先脱掉避免两件裙子叠加 Unwear(data.category); Transform anchor GetAnchor(data.category); if (anchor null) { Debug.LogError($缺少挂点: {data.category}请检查骨骼命名); return; } GameObject go Instantiate(data.prefab, anchor); go.transform.localPosition Vector3.zero; go.transform.localRotation Quaternion.identity; go.transform.localScale Vector3.one; _currentOutfit[data.category] data; _wornObjects[data.category] go; } public void Unwear(OutfitCategory category) { if (_wornObjects.ContainsKey(category) _wornObjects[category] ! null) Destroy(_wornObjects[category]); _wornObjects.Remove(category); _currentOutfit.Remove(category); }这段代码有三个细节值得注意挂点获取用的是GetAnchor而不是直接transform.Find理由是Find需要完整路径路径一长就容易拼错实例化后要把localPosition、localRotation、localScale全部复位因为美术建模基准可能和挂点坐标系有偏差不复位就会出现衣服浮在半空或歪着穿的情况旧物件直接Destroy代替SetActive(false)否则场景里会堆积大量看不见的隐藏节点拖累后续加载。挂点获取函数我一般配合HumanBodyBones枚举来写Animator能直接给出标准骨骼兼容性好于硬编码名字private Transform GetAnchor(OutfitCategory category) { Animator animator GetComponentInParentAnimator(); if (animator null) return null; switch (category) { case OutfitCategory.Hairstyle: return animator.GetBoneTransform(HumanBodyBones.Head); case OutfitCategory.Dress: return animator.GetBoneTransform(HumanBodyBones.Hips); case OutfitCategory.Shoes: return animator.GetBoneTransform(HumanBodyBones.LeftFoot); case OutfitCategory.Accessory: return animator.GetBoneTransform(HumanBodyBones.Chest); default: return null; } }HumanBodyBones的取值在Unity里是标准化的只要是合法人形Avatar这些骨骼就一定存在。Shoes只取了LeftFoot实际项目里需要额外处理右脚的对称挂点否则右鞋会穿在左脚骨骼上。3.3 套装联动同一主题的上下装怎么一起换舞会之夜装扮里经常有成套的礼服加配饰玩家点一次穿上套装就要同时替换发型、裙子、鞋子和胸针。如果UI侧自己调四次Wear代码会散成一片而且容易忘记先卸旧套装。常见做法是再做一层套装数据[CreateAssetMenu(fileName NewSuit, menuName DressUp/SuitData)] public class SuitData : ScriptableObject { public string suitId; public OutfitCategory[] overrideCategories; public OutfitData[] parts; public void ApplyTo(DressUpModel model) { foreach (OutfitCategory category in overrideCategories) model.Unwear(category); foreach (OutfitData part in parts) model.Wear(part); } }overrideCategories是套装覆盖到的所有部位里面可能有重复所以在Wear内部再做一次旧物销毁也能兜底。先Unwear再逐个Wear的顺序不能反过来否则在同一帧里会出现旧衣服还没销毁、新衣服又挂上同一根骨骼的瞬间虽然后一帧正常但阴影和裙摆物理会突然闪一下。3.4 裙摆与头发的物理表现舞会氛围讲究观感静态挂点只保证位置物理效果需要额外处理。常见做法是在裙摆Prefab上挂Cloth组件或者在头发链上挂SpringBone。这里只提醒一句物理组件应挂在被实例化的衣服Prefab上而不是角色身上否则套装切换后旧物理组件仍然在检测新衣服穿模和抖动会变得很难查。4. 装扮界面的Unity交互从点击到试穿的完整UGUI链路4.1 UGUI衣柜面板滑动列表、选中状态与点击热区衣柜面板通常是一块ScrollRect加上一个GridLayoutGroup卡片动态生成。需要明确的是换装UI里最常见的性能瓶颈不是单张卡片而是重复实例化。每打开一次衣柜就清空重建一次会带来明显的卡顿。我会做一个ObjectPool只创建所需数量上限的卡牌数据不足的卡片SetActive(false)翻页时复用。每张卡牌的结构建议是Button底图、Icon Image、选中框三件套节点组件说明CardRootButton Image管理点击和选中状态切换IconImage用于展示OutfitData.iconSelectedMarkImage同一部位被选中时置为激活代码里生成卡片一般写成这样public void BuildCloset(ListOutfitData allItems) { for (int i 0; i allItems.Count; i) { OutfitData item allItems[i]; GameObject card _pool.Get(); card.transform.SetParent(_gridRoot, false); Button button card.GetComponentButton(); button.onClick.RemoveAllListeners(); button.onClick.AddListener(() OnCardClicked(item)); card.GetComponentImage().sprite item.icon; card.transform.Find(SelectedMark).gameObject.SetActive(false); } }注意闭包陷阱OnCardClicked参数直接捕获循环变量item在C#里for循环的变量是共享的所以必须像这里一样先把allItems[i]存到局部变量item里再拿去构造闭包否则所有按钮都会指向最后一件衣服。Unity里扩大按钮点击范围是个常见需求。我一般不改源码而是给Button所在节点加一个比自己实际显示区域大一圈的LayoutElement配合一个半透明Image作为点击接收区。这个半透明Image不会出现在UI渲染层却能让射线检测面积变大对触屏上手指精度不高的场景很有效。4.2 服装卡牌点击后的试穿流程C#事件链卡牌点击后应触发试穿、刷新选中态、播放音效三步。为了让衣柜UI和换装逻辑解耦我不会让卡牌脚本直接引用DressUpModel而是用点击事件转发public class ClosetPanel : MonoBehaviour { public DressUpModel dressUpModel; private void OnCardClicked(OutfitData data) { dressUpModel.Wear(data); RefreshSelectedState(data.itemId); AudioManager.Instance.PlaySFX(try_on); } }这段逻辑的重点是分类讨论点击的是已选中的同类部件时应只高亮当前这件的卡片其他卡片取消高亮如果点击的是已穿着的同款一般设计为不再次实例化可以做一个重复点击保护记录上一次穿戴的itemId相同就直接return掉避免玩家狂点时反复生成销毁衣服。Unity的Button在快速连点下每次都会触发onClick这个保护在触屏上尤其重要。4.3 用RenderTexture给3D模型做2D预览窗少女装扮游戏的衣柜通常不是纯2D贴图而是让角色在侧边转一圈展示动态效果。实现上最稳的办法不是把3D模型塞进Canvas而是用一个专门负责预览的相机把角色渲染到RenderTexture上UI再用RawImage显示这张纹理。配置要点是三处。第一预览相机裁掉不需要的UI层和场景层Culling Mask只留角色层和美术背景层。第二RenderTexture的分辨率建议设成256x256及以上WebGL上要控制在1024以内太高会明显吃掉带宽和内存。第三RawImage的材质不要随便动默认Unlit/Transparent最省事调成内置UI材质后RenderTexture内容反而容易发白或透明。实际操作里我还会把预览角色放在一个专门的展示节点上例如把角色本身和衣架放在同一层相机固定视角朝它这样装备任何衣服时都能保证构图稳定。4.4 角色身上浮动的WorldUI与遮挡问题很多装扮游戏会在角色头顶显示名字或心情气泡直接用World Space Canvas挂上去稍微转身就会被角色模型自己挡住。这个问题最直接的解法是把浮动UI放到单独一层UI Overlay并让主相机不渲染这层然后挂一个子相机以较近的裁剪平面去渲染它。裁剪平面和相机深度要提前测好否则会出现UI在角色前面却透视扭曲的情况。如果只是想给玩家在试穿时显示当前部件名字挂在屏幕角落的Screen Space UI通常会比WorldUI更省心也能避免Unity WorldUI无遮挡这类社区里反复讨论的遮挡问题。5. 装扮存档与Unity性能收尾5.1 一套装扮一组ID用JSON保存当前搭配试穿做完最后一步是存档。装扮状态本质上是每个部位当前的一个itemId所以存档用JSON最直观。C#侧定义一个可序列化的类[Serializable] public class DressUpSaveData { public string[] itemIds; // 当前穿着的部件ID列表 }保存时把_currentOutfit的value收集成数组用JsonUtility序列化后写进PlayerPrefs。不要直接存Prefab路径或场景对象名字只要回到OutfitData的itemId之后的配置调整、资源重命名都不会影响老玩家的存档。JsonUtility不支持直接序列化Dictionary所以这里必须转成数组。public void Save() { DressUpSaveData saveData new DressUpSaveData(); saveData.itemIds GetCurrentIDs(); string json JsonUtility.ToJson(saveData); PlayerPrefs.SetString(prom_dress_up_save, json); PlayerPrefs.Save(); }5.2 加载时的骨骼回写与异常兜底游戏启动后读取存档遍历itemIds逐个调用Wear。需要注意两点找不到itemId对应的OutfitData时不能崩溃应跳过并打印警告说明可能是版本迭代删除了该部件角色Animator尚未初始化时GetBoneTransform可能返回空所以加载要放在动画初始化完成之后或主动调用Rebind。public void Load() { string json PlayerPrefs.GetString(prom_dress_up_save, ); if (string.IsNullOrEmpty(json)) return; DressUpSaveData saveData JsonUtility.FromJsonDressUpSaveData(json); foreach (string id in saveData.itemIds) { OutfitData data outfitDatabase.GetByID(id); if (data ! null) Wear(data); else Debug.LogWarning($存档中的部件不存在: {id}); } }如果用IL2CPP构建移动端异常栈只指向GameAssembly.dll排错时优先把关键ID打到日志里否则很难定位具体服装条目。WebGL平台下PlayerPrefs背后是IndexedDB隐私模式或存储配额不足容易触发IDBFS写入失败表现是启动时存档还在刷新后全部丢失。处理方法是写入后立刻回读验证失败时降级为仅内存保存并在界面提示玩家无法持久化存档不静默吞错。5.3 阴影、合批与DrawCall装扮项目的三个优化点最后落到Unity阴影问题与性能。打扮类角色部件多DrawCall常见超标三个值得优先做的调整一是共享材质模板所有裙摆和上衣尽量用同一套贴图和标准化材质球让它们进同一个合批批次二是关闭远距离部件或次要配饰的阴影投射避免裙摆骨骼剧烈动画带来阴影闪烁三是把静态背景和衣架合并成一张图减少场景DrawCall占比。如果做的是WebGL版本还可以把服装资源按衣柜分页异步加载首屏只加载默认装扮点击到具体页时再用AssetBundle或Addressables加载对应贴图与模型。这样启动时间能压到可接受范围内也让源代码里的资源加载逻辑保持单一入口方便后续排查。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价