
1. 项目概述为什么选择Unity来制作一个原神桌宠如果你是一个喜欢《原神》的玩家同时又对Unity有点兴趣那么把喜欢的角色做成一个能在桌面上跑来跑去、还能跟你互动的“桌宠”绝对是一件既有趣又有成就感的事情。这个项目听起来有点复杂尤其是涉及到“PMX模型”这种专业名词可能会让很多Unity新手望而却步。但我想告诉你只要思路清晰避开几个关键的“坑”从零开始实现一个可拖拽交互的原神桌宠是完全可行的。我自己就是从一个小白开始摸索踩遍了几乎所有能踩的坑才最终让“派蒙”或者“胡桃”成功地在我的电脑桌面上安了家。这个项目的核心价值在于它串联起了从3D模型资源获取、导入、处理到Unity基础交互逻辑实现的一整套流程。你不需要是一个资深的游戏开发者只需要有基础的C#编程知识和一点点耐心。最终你将得到一个完全由你掌控的、独一无二的桌面伙伴。它不仅可以作为你学习Unity的里程碑其背后涉及的模型处理、UI交互、跨窗口通信等知识也是你迈向更复杂项目比如简单的独立游戏或工具应用的坚实一步。接下来我会把我从PMX模型开始到实现一个可拖拽、可点击交互的桌宠的完整过程以及那些让我熬夜掉头发的“避坑点”毫无保留地分享给你。2. 核心思路与工具选型为什么是PMX和Unity在动手之前我们必须搞清楚两件事第一我们用什么资源第二我们用什么工具来实现2.1 为什么选择PMX模型PMX格式是日本3D建模软件“MikuMikuDance”MMD及其相关工具链使用的标准模型格式。对于《原神》这类二次元风格的角色PMX模型资源在社区中非常丰富精度和还原度通常很高而且很多是免费分享的。这是它最大的优势——资源获取门槛低。但是Unity原生并不支持PMX格式。这就引出了我们的第一个关键工具模型转换插件。你不能直接把.pmx文件拖进Unity工程必须通过插件将其转换为Unity能识别的格式如.FBX。这里有一个至关重要的选择CATS Blender Plugin。Blender是一款免费开源的3D创作软件而CATS插件是专门为处理MMD模型PMX格式到其他软件如Unity而生的神器。它的核心功能包括自动修复骨骼、合并材质球、重新计算权重等能解决PMX模型导入Unity时90%的兼容性问题。注意网上也有一些直接转换PMX到FBX的小工具但经过我的实测它们往往处理不了复杂的骨骼和材质关联导入Unity后模型会“散架”或者变成“石膏像”。因此强烈建议走“Blender CATS插件”这条虽然多一步但最稳妥的路径。2.2 为什么选择Unity你可能会问做桌宠不是有更轻量的工具吗比如一些专门的桌宠软件。选择Unity有以下几个无法替代的理由完全的掌控力从模型的动画播放、物理模拟比如头发和裙摆到复杂的交互逻辑点击触发语音、拖拽移动你都可以用C#代码精细控制。强大的跨平台潜力虽然我们做的是Windows桌面应用但Unity工程可以相对轻松地打包到Mac、甚至WebGL网页端为你未来的想法留足了空间。丰富的学习资源Unity拥有全球最大的开发者社区之一任何你遇到的问题几乎都能找到解决方案或讨论。这对于学习者来说是无价之宝。渲染与效果Unity的渲染管线无论是内置管线还是URP可以让你轻松地为桌宠添加阴影、环境光遮蔽、后期特效等让它的视觉效果远超普通桌宠。工具清单总结建模/转换软件Blender (免费)并安装CATS插件。游戏引擎Unity (个人版免费)。建议使用一个稳定的LTS版本如2021.3或2022.3避免最新版可能带来的未知兼容性问题。开发语言C#。额外工具可选音频剪辑软件用于处理角色语音图片编辑软件用于制作UI。3. 从PMX到Unity模型导入与处理的完整流程这是整个项目最基础也最容易出错的一环。很多人在这一步就放弃了因为看到的可能是一堆乱码、粉红错误材质或者扭曲的模型。别慌跟着步骤一步步来。3.1 在Blender中利用CATS插件进行预处理首先你需要下载并安装Blender然后在Blender中安装CATS插件。安装过程很简单在Blender的偏好设置里找到“插件”页面点击“安装”选择你下载的CATS插件压缩包即可。核心操作步骤如下导入PMX模型在Blender中点击“文件” - “导入”选择“MikuMikuDance Model (.pmx)”找到你的原神角色PMX文件。导入后你应该能看到完整的角色模型。运行CATS修复在右侧边栏找到CATS面板。点击“Model”选项卡下的“Fix Model”按钮。这个操作会自动进行一系列修复比如合并材质PMX模型通常有大量零散的材质CATS会智能地将它们合并这对提升Unity中的渲染性能至关重要。修复骨骼确保骨骼结构正确没有断裂。重新计算权重修正模型顶点与骨骼的绑定关系防止动画时模型撕裂。分离表情形态键很多PMX模型带有丰富的表情如眨眼、微笑。在CATS的“Morph”选项卡下你可以预览这些表情。为了在Unity中控制它们我们需要点击“Separate by Morphs”。这会将每个表情拆分为独立的“形态键”Unity的Animator控制器可以调用它们。导出为FBX修复完成后选择整个角色模型包括骨骼和网格点击“文件” - “导出” - “FBX (.fbx)”。在导出设置中务必勾选“选中的物体”、“应用变换”以及“烘焙动画”如果你有动画文件即.vmd文件需要一起导出的话。比例Scale可以设置为1.0向前轴Forward设为“-Z”向上轴Up设为“Y”这与Unity的坐标系匹配。3.2 在Unity中导入与材质修复将导出的FBX文件拖入Unity项目的Assets文件夹。此时你可能会遇到第一个大坑模型显示为洋红色粉红色。这是因为PMX模型使用的着色器和纹理与Unity不兼容。Unity找不到正确的着色器来渲染模型于是使用了默认的错误着色器显示为粉色。解决方案如下创建标准材质在Project窗口右键点击导入的模型文件选择“Extract Materials”提取材质。这会把模型用到的所有材质球提取出来。替换着色器选中所有提取出来的材质球在Inspector面板中将“Shader”从可能出错的“Standard”或“Unlit”等改为适合卡通渲染的着色器。对于原神这种风格推荐使用Unity URP通用渲染管线下的“Universal Render Pipeline/Lit”着色器并将其“Surface Type”改为“Transparent”如果模型有半透明部分并在“Base Map”中赋予对应的纹理贴图。重新关联纹理检查每个材质球的纹理贴图Albedo, Normal Map等是否已正确关联。通常CATS在导出时会将纹理打包Unity会自动关联。如果没有你需要手动将模型文件同一目录下的纹理图片拖到材质球的对应插槽中。完成以上步骤后你的模型应该就能在Unity场景中正常显示了。此时你可以为模型添加一个“Animator”组件并开始制作或导入动画。4. 实现桌宠核心功能窗口化、点击与拖拽模型准备就绪后我们就进入了交互逻辑的编码阶段。这是让桌宠“活”起来的关键。4.1 创建无边框窗口桌面置顶桌宠需要悬浮在桌面最上层并且没有标准窗口的标题栏和边框。在Unity中这需要通过编辑Player Settings来实现。打开“File” - “Build Settings”。选择PC平台Windows/Mac点击“Player Settings”。在“Resolution and Presentation”选项卡下Fullscreen Mode设置为“Windowed”窗口模式。Display Resolution Dialog设置为“Disabled”。Default Screen Width/Height设置一个较小的值比如300x500作为你桌宠窗口的初始大小。最重要的是在“Standalone Player Options”部分取消勾选“Resizable Window”。这样窗口就不能被用户拖拽边缘改变了。为了让窗口始终置顶我们需要调用一点平台相关的代码。这需要用到System.Runtime.InteropServices命名空间。using UnityEngine; using System.Runtime.InteropServices; public class WindowManager : MonoBehaviour { // 导入Windows API函数 [DllImport(user32.dll)] private static extern System.IntPtr GetActiveWindow(); [DllImport(user32.dll)] private static extern bool SetWindowPos(System.IntPtr hWnd, System.IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); // 常量定义使窗口置顶 private static readonly System.IntPtr HWND_TOPMOST new System.IntPtr(-1); private const uint SWP_NOSIZE 0x0001; private const uint SWP_NOMOVE 0x0002; private const uint SWP_SHOWWINDOW 0x0040; void Start() { // 在启动时设置窗口置顶 SetTopMost(); } void SetTopMost() { System.IntPtr windowHandle GetActiveWindow(); SetWindowPos(windowHandle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); } }将这段脚本挂载到场景中的任意GameObject上比如一个空的“GameManager”对象运行后你的Unity应用窗口就会始终显示在其他窗口之上了。4.2 实现鼠标拖拽移动桌宠需要能被鼠标拖拽着在屏幕上任意移动。这需要用到Unity的鼠标事件接口。我们通常为桌宠模型本身或一个控制它的空物体添加一个碰撞体如Box Collider然后通过脚本来处理拖拽逻辑。using UnityEngine; public class DragWindow : MonoBehaviour { private bool isDragging false; private Vector3 offset; private Camera mainCamera; void Start() { mainCamera Camera.main; // 获取主摄像机用于屏幕坐标到世界坐标的转换 } void OnMouseDown() { // 当鼠标在物体上按下时 isDragging true; // 计算鼠标点击位置与物体中心的世界坐标偏移量 offset transform.position - GetMouseWorldPos(); } void OnMouseDrag() { // 当鼠标拖拽时 if (isDragging) { // 物体位置 当前鼠标世界坐标 偏移量 transform.position GetMouseWorldPos() offset; } } void OnMouseUp() { // 当鼠标松开时 isDragging false; } Vector3 GetMouseWorldPos() { // 将鼠标的屏幕坐标转换为世界坐标 // 注意这里假设摄像机是正交投影Orthographic且面向Z轴负方向 Vector3 mousePoint Input.mousePosition; mousePoint.z Mathf.Abs(mainCamera.transform.position.z - transform.position.z); // 设置一个合适的Z值深度 return mainCamera.ScreenToWorldPoint(mousePoint); } }这个脚本实现了基础的拖拽。但这里有一个关键避坑点如果桌宠的模型有多个部分比如身体、头发、衣服是分开的网格直接挂载在模型子部件上会导致拖拽不灵敏。最佳实践是创建一个空的GameObject作为“根节点”或“控制节点”将整个角色模型作为它的子物体然后把DragWindow脚本和碰撞体挂在这个根节点上。4.3 实现点击交互播放动画、音效除了拖拽点击桌宠触发动作比如播放一个打招呼的动画或者说一句话是提升趣味性的核心。我们可以使用OnMouseDown或更现代的Event Trigger组件。这里展示使用Event Trigger的方法它更灵活可以区分点击、进入、退出等事件。为控制桌宠的根节点GameObject添加Event Trigger组件。在Event Trigger组件中添加“Pointer Click”事件类型。将需要响应事件的对象通常是挂载了控制脚本的对象拖入事件列表。选择对应的函数例如PlayRandomAnimation()。控制脚本示例using UnityEngine; public class DesktopPetController : MonoBehaviour { public Animator petAnimator; // 关联角色的Animator组件 public AudioClip[] interactionSounds; // 存放交互音效的数组 private AudioSource audioSource; void Start() { audioSource GetComponentAudioSource(); if (audioSource null) { audioSource gameObject.AddComponentAudioSource(); } } // 这个方法可以被Event Trigger的Pointer Click事件调用 public void OnPetClicked() { PlayRandomInteraction(); } void PlayRandomInteraction() { // 1. 播放随机动画 // 假设我们在Animator Controller中设置了“Wave”, “Jump”, “Idle2”等Trigger参数 string[] triggerNames { “Wave”, “Jump”, “Spin” }; string randomTrigger triggerNames[Random.Range(0, triggerNames.Length)]; petAnimator.SetTrigger(randomTrigger); // 2. 播放随机音效 if (interactionSounds ! null interactionSounds.Length 0) { AudioClip randomSound interactionSounds[Random.Range(0, interactionSounds.Length)]; audioSource.PlayOneShot(randomSound); } Debug.Log(“桌宠被点击了播放了” randomTrigger); } }这样每次点击你的原神桌宠它就会随机做一个动作并说一句话互动感立刻就出来了。5. 动画系统与状态管理让桌宠“活”起来一个呆呆站着的模型是没灵魂的。我们需要让桌宠有自己的“行为模式”比如待机时循环播放呼吸动画偶尔伸个懒腰被点击时做出反应。5.1 配置Animator ControllerUnity的Animator Controller是一个状态机是管理角色动画的核心。创建状态在Animator窗口中创建几个状态例如Idle默认状态连接一个循环的待机动画如轻微的呼吸起伏。Reaction1,Reaction2被点击时触发的反应动画。RandomAction每隔一段时间自动播放的随机小动作。设置过渡从Any State任何状态连接到Reaction1和Reaction2过渡条件设置为对应的Trigger参数如“React1”、“React2”。这样无论当前在播放什么动画点击都能立即触发反应。管理状态优先级反应动画播放完毕后应该自动回到Idle状态。你可以在反应动画的末尾添加一个“Exit Time”事件或者使用脚本在动画播放完后重置Trigger。5.2 编写行为管理脚本我们需要一个总控脚本来协调桌宠的自主行为和被动响应。using UnityEngine; public class PetBehaviourManager : MonoBehaviour { private Animator animator; private float timeSinceLastAction; public float minActionInterval 10f; public float maxActionInterval 30f; private float nextActionTime; void Start() { animator GetComponentAnimator(); CalculateNextActionTime(); timeSinceLastAction 0f; } void Update() { // 计时用于触发随机自主动作 timeSinceLastAction Time.deltaTime; if (timeSinceLastAction nextActionTime) { PlayRandomAutonomousAction(); CalculateNextActionTime(); timeSinceLastAction 0f; } } void CalculateNextActionTime() { nextActionTime Random.Range(minActionInterval, maxActionInterval); } void PlayRandomAutonomousAction() { // 触发一个随机的自主动画比如“Stretch”, “LookAround” string[] autoActions { “Stretch”, “LookAround”, “SitDown” }; string randomAction autoActions[Random.Range(0, autoActions.Length)]; animator.SetTrigger(randomAction); Debug.Log(“桌宠自动执行” randomAction); } // 提供给外部如点击事件调用的反应方法 public void TriggerReaction(string reactionName) { // 可以在这里加入逻辑比如打断当前自主动作 animator.SetTrigger(reactionName); } }这个脚本让桌宠拥有了“生命”它会自己决定什么时候该活动一下而不是永远傻站着。结合之前的点击响应一个既有自主性又能互动的桌宠就初具雏形了。6. 性能优化与打包发布当功能都实现后我们需要考虑如何让它成为一个真正的、可发布的桌面应用并且运行流畅。6.1 性能优化要点桌宠是长期运行的后台应用性能优化至关重要。模型与材质优化多边形数量PMX模型通常面数不低。如果性能吃紧可以考虑在Blender中使用“Decimate”修改器进行适度的减面。材质合并确保在Blender中用CATS合并了材质。一个模型有几十个Draw Call和只有几个Draw Call性能天差地别。纹理尺寸检查纹理贴图尺寸是否过大。对于桌宠这种小窗口应用1024x1024甚至512x512的贴图通常就足够了可以使用工具压缩纹理。动画优化在Unity的动画导入设置中可以适当降低动画的帧率如从60FPS降到30FPS这对视觉影响不大但能减少计算量。对于不常用的动画可以设置其“Legacy”属性或确保它们不会被提前加载到内存中。脚本优化在Update函数中避免进行昂贵的计算如查找游戏对象Find、物理检测Raycast。像我们PetBehaviourManager中的计时器开销就很小。使用Object Pooling对象池来管理可能频繁生成/销毁的粒子特效等。摄像机与渲染使用正交摄像机Orthographic Camera而非透视摄像机Perspective。桌宠是2D/2.5D展示正交摄像机没有透视变形且渲染更高效。关闭不需要的后期处理效果。6.2 打包设置与避坑点目标平台在Build Settings中选择“PC, Mac Linux Standalone”目标平台选择你的操作系统。分辨率与UI缩放在Player Settings的“Resolution and Presentation”中除了之前设置的无边框还要注意“UI Scaling Mode”。如果你的桌宠包含UI比如一个设置面板建议设置为“Scale With Screen Size”并设定一个参考分辨率以确保在不同显示器上UI不会错乱。图标与产品名在“Icon”和“Splash Image”设置中为你的桌宠应用设置一个好看的图标和启动图可选。API Compatibility Level保持默认的“.NET Standard 2.0”或“.NET Framework”通常没有问题。构建后处理构建完成后你会得到一个.exe文件Windows或.app文件Mac。你可以为其创建一个快捷方式放到桌面或启动文件夹。为了让它在开机时自动运行可以将快捷方式放入系统的“启动”文件夹。一个巨大的避坑点杀毒软件误报Unity打包的独立应用尤其是使用了DllImport调用系统API的很容易被Windows Defender或其他杀毒软件误报为病毒或潜在威胁导致无法运行或被直接删除。解决方法在打包前尝试在Player Settings - “Publishing Settings” 中填写“Product Name”和“Company Name”使其看起来更正规。对你最终生成的.exe文件进行数字签名需要购买证书成本较高个人项目不推荐。最实用的办法将你的应用提交给杀毒软件厂商进行白名单认证。例如对于Windows Defender你可以通过微软的“提交文件分析”页面提交你的.exe文件通常几个小时后就会解除误报。这是一个非常关键但常被忽略的步骤。7. 常见问题与排查技巧实录在开发过程中我遇到了无数问题。这里把最常见、最头疼的几个列出来并提供我的解决方案。问题1模型导入Unity后是透明的或者部分透明部分不透明。原因PMX模型中的透明材质如头发发梢、纱裙在转换时其渲染模式Blend Mode和渲染队列Render Queue设置不正确。解决在Unity中检查问题材质的着色器。如果使用URP Lit着色器确保“Surface Type”设置为“Transparent”。“Alpha Clipping”可能需要根据情况勾选并调整“Threshold”值来剔除完全透明的部分。如果模型有多个透明材质叠加顺序错误需要手动调整材质的“Render Queue”值数值越大的越后渲染显示在前面。问题2拖拽时桌宠“粘”在鼠标指针的某个角落而不是跟随点击位置。原因GetMouseWorldPos()函数中鼠标屏幕坐标转换为世界坐标时Z轴深度值计算不准确。解决确保你的摄像机是正交投影Orthographic。在正交投影下世界坐标的Z值不影响屏幕上的XY位置。你可以将转换函数简化Vector3 GetMouseWorldPos() { Vector3 mousePos Input.mousePosition; // 对于正交摄像机ScreenToWorldPoint的Z值表示距离摄像机的深度。 // 通常设置为物体当前的Z坐标与摄像机Z坐标的差值。 mousePos.z transform.position.z - mainCamera.transform.position.z; return mainCamera.ScreenToWorldPoint(mousePos); }更稳健的做法是使用Physics.Raycast从摄像机向鼠标位置发射一条射线检测与桌宠碰撞体所在平面的交点作为拖拽的目标点。问题3点击事件不触发。原因A模型或控制节点上没有碰撞体Collider。Unity的OnMouseDown和Event Trigger都需要碰撞体来检测。解决A为控制桌宠的根节点添加一个大小合适的“Box Collider”或“Sphere Collider”。原因B有其他UI元素如Canvas挡住了点击事件。UI元素的默认层级会拦截射线。解决B检查Event System。确保你的点击区域没有被带有“Graphic Raycaster”的UI Canvas完全覆盖。可以暂时禁用UI Canvas进行测试。问题4打包后运行窗口位置不对或者大小异常。原因Player Settings中的默认分辨率与脚本中设置的窗口位置或拖拽逻辑不匹配。解决在脚本的Start()函数中使用Screen.width和Screen.height来获取当前屏幕分辨率并据此计算初始窗口位置如放在屏幕右下角。同时在拖拽逻辑中加入边界检查防止窗口被拖到屏幕外面。问题5动画播放不流畅有卡顿感。原因可能是动画文件本身帧率过高或者在Update中进行了大量计算。解决在Project窗口选中FBX文件在Inspector的“Animation”选项卡下降低“Imported Animation”的帧率如60-30。使用Profiler窗口Window - Analysis - Profiler分析运行时性能找到瓶颈。很可能是每帧都在进行不必要的Find或GetComponent调用。实现一个属于自己的原神桌宠整个过程就像在拼装一个精致的电子手办。从陌生的PMX格式开始到在Blender里小心翼翼地修复模型再到Unity里一行行代码赋予它生命每一步的解决都伴随着巨大的成就感。最让我印象深刻的不是最终成品而是解决“粉色模型”和“杀毒软件误报”这两个问题时的那种豁然开朗。它们教会我在开发中工具链的衔接和最终的用户环境往往是比核心逻辑更需要耐心去打磨的细节。当你看到自己喜欢的角色真正在桌面上对你眨眼、挥手时你会觉得所有的折腾都是值得的。这个项目最大的收获或许不是学会了某个特定技术而是掌握了“如何让一个想法穿越重重技术关卡最终落地成产品”的完整能力。