Unity换装游戏核心实现:C#数据建模、骨骼挂点与存档
2026/9/15 11:49:09 网站建设 项目流程

简介:基于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 Dictionary<OutfitCategory, OutfitData> _currentOutfit = new Dictionary<OutfitCategory, 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 = GetComponentInParent<Animator>(); 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.icon
SelectedMarkImage同一部位被选中时置为激活

代码里生成卡片一般写成这样:

public void BuildCloset(List<OutfitData> 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.GetComponent<Button>(); button.onClick.RemoveAllListeners(); button.onClick.AddListener(() => OnCardClicked(item)); card.GetComponent<Image>().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.FromJson<DressUpSaveData>(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加载对应贴图与模型。这样启动时间能压到可接受范围内,也让源代码里的资源加载逻辑保持单一入口,方便后续排查。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询