资讯动态

Unity透明窗口桌面精灵:Win32 API实现点击穿透与动画跳动

发布时间:2026/10/20 16:22:54 来源:尧图企业网站定制
简介一套基于Unity引擎的桌面精灵完整工程与配套脚本面向希望实现类似QQ宠物透明桌面角色的Unity开发者。工程围绕窗口无边界设置、透明渲染、界面布局、动画控制及鼠标事件处理等关键技术展开并涉及系统接口调用与跨平台兼容处理能帮助掌握桌面透明应用的实现原理。其中包含可直接运行的完整项目涵盖场景、预制体、材质、模型、动画片段、动画控制器与脚本动画控制器中保留了状态机参数和触发逻辑便于学习精灵连续跳动与拖拽交互。整个压缩包共1267个文件涵盖bin运行库、asset场景数据、mat材质、fbx模型、anim动画、prefab预制体、controller动画控制器及cs源代码等整体约41.45MB目录清晰可直接导入Unity编辑器查看或二次开发。目前已有1593人学习下载适合研究透明窗口与动画驱动的初中级Unity开发者借鉴工程组织与代码实现。1. 桌面精灵不难做难的是让窗口“消失”在桌面里动画跳动只是这个项目里最轻松的部分——真正折腾人的是让Unity窗口透明、让鼠标点击穿透、让精灵悬浮在桌面图标之上却又不抢焦点。这次拆的模拟项目X就是用Unity把透明窗口、随机跳动动画、点击穿透和拖拽移动组合成一只真正的桌面宠物精灵。它适合两类人一类想在Windows桌面上放个会动的小东西另一类想搞清楚Unity窗口怎么和Win32打交道。透明是核心动画是表象整篇会把这条链路从窗口句柄一路拆到Animator状态机。2. 技术选型与整体架构Unity只做渲染窗口逻辑全交给Win32 API2.1 为什么是Unity而不是其他方案桌面精灵本质上是“一个异形窗口 动画播放器”。实现路径很多Electron可以但太重内存动辄两三百兆WPF动画能力弱想让精灵有弹性律动还得写一堆故事板选Unity的理由很直接——Animator状态机、Sprite序列帧、C#协程刚好覆盖动画需求的三件套而且打包成Windows原生窗口后能直接拿句柄做文章。但Unity有个反常识的地方它不暴露任何“让窗口透明”的接口。Unity只关心Game视图里渲染什么窗口本身的长相归Windows管。所以项目里所有窗口操作都走了一层P/Invoke封装把user32.dll和dwmapi.dll里的几个函数用C#重新声明一遍。透明窗口、点击穿透、隐藏任务栏、去除圆角全部在这层解决。渲染和窗口控制彻底分离改起来不互相踩脚。2.2 目录结构与脚本分层四个文件夹对应四件事资源结构沿用了Unity常见的按职责分层但脚本目录按“窗口层”和“动画层”切开避免透明逻辑和动画逻辑混在一个脚本里。Assets/ ├── Scenes/ │ └── PetScene.unity ├── Scripts/ │ ├── Core/ │ │ ├── WindowFrame.cs // 获取窗口句柄、设置扩展样式 │ │ ├── ClickThrough.cs // 鼠标穿透开关 │ │ └── DragController.cs // 穿透模式下用全局鼠标状态拖拽窗口 │ ├── Animation/ │ │ ├── BounceLoop.cs // 随机间隔触发跳动 │ │ └── PetAnimator.cs // 动画状态管理、Trigger分发 │ └── UI/ │ └── SettingsPanel.cs // 退出、穿透开关、配置面板 ├── Sprites/ │ ├── Idle/ │ ├── Bounce/ │ └── Hit/ └── Animations/ └── Pet.controllerCore里三个脚本解决的是“窗口怎么活下来”Animation里两个脚本解决的是“精灵怎么动起来”UI层负责给人一个关闭和配置的出口。这个分层想清楚之后后面加多精灵、加配置化也只是在Animation和UI层扩展Core基本不用动。2.3 数据流从鼠标到动画的一整条链路桌面精灵的交互链路和普通Unity游戏完全不同。普通游戏里鼠标事件由EventSystem派发但在穿透模式下鼠标事件根本不进Unity窗口——它直接穿透到桌面去了。所以项目里做了一个“双通道”设计穿透模式下用GetAsyncKeyState轮询全局左键状态配合屏幕坐标判断是否按到了精灵区域按住就拖拽窗口松开后交给动画层播一个落地缓冲动画。非穿透模式下Unity的EventSystem正常接收点击设置面板上的按钮、退出按钮都在这个模式下工作。两条通道在ClickThrough.cs里切换切换的本质就是SetWindowLong那个WS_EX_TRANSPARENT位。整条链路串起来是轮询到拖拽动作 → 定位窗口到新坐标 → 松手触发Bounce → Animator SetTrigger → 播放跳动序列帧 → 回到Idle循环。3. 核心模块拆解透明窗口与点击穿透的一行P/Invoke背后3.1 窗口句柄与扩展样式SetWindowLong的两个关键Flag第一步是拿到Unity游戏窗口的原生句柄。打包后GetActiveWindow返回的就是游戏主窗口编辑模式下同理。拿到句柄后用GetWindowLong读取当前扩展样式按位或上WS_EX_LAYERED和WS_EX_TOOLWINDOW再写回去。using System; using System.Runtime.InteropServices; using UnityEngine; public class WindowFrame : MonoBehaviour { [DllImport(user32.dll)] private static extern IntPtr GetActiveWindow(); [DllImport(user32.dll, EntryPoint GetWindowLong)] private static extern int GetWindowLong32(IntPtr hWnd, int nIndex); [DllImport(user32.dll, EntryPoint SetWindowLong)] private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong); private const int GWL_EXSTYLE -20; private const int WS_EX_LAYERED 0x00080000; private const int WS_EX_TRANSPARENT 0x00000020; private const int WS_EX_TOOLWINDOW 0x00000080; private IntPtr hWnd; private void Start() { // Unity 原生窗口句柄打包后就是游戏主窗口 hWnd GetActiveWindow(); ApplyTransparentStyle(); } private void ApplyTransparentStyle() { int style GetWindowLong32(hWnd, GWL_EXSTYLE); style | WS_EX_LAYERED; // 允许按alpha通道做混合透明 style | WS_EX_TOOLWINDOW; // 不在任务栏和AltTab里出现 SetWindowLong32(hWnd, GWL_EXSTYLE, style); } }GWL_EXSTYLE是扩展样式的索引值为-20。WS_EX_LAYERED让窗口支持逐像素alpha混合这是透明的基础WS_EX_TOOLWINDOW让窗口不在任务栏显示桌面精灵不该占任务栏位置。这里用的是EntryPoint指定GetWindowLong因为64位系统上有GetWindowLongPtr和GetWindowLong两套API指定入口避免平台差异带来的坑。3.2 透明渲染的配套设置Camera和Canvas缺一不可窗口透明只是给了系统“可以透明”的权限真正决定哪些像素透明的是Unity渲染出来的alpha通道。这一步最容易翻车很多人设置了窗口样式后发现整个窗口黑乎乎一片因为Camera默认的Clear Flags是Skybox渲染出来的背景根本不透明。正确配置是Camera的Clear Flags选Solid Color背景色alpha设为0。配置项值说明Clear FlagsSolid ColorSkybox会强制写不透明背景BackgroundRGBA(0, 0, 0, 0)alpha必须为0RGB值无所谓Culling MaskUI Pet只渲染精灵和UI别带环境光HDR关闭HDR缓冲会把alpha信息搞脏Post Processing关闭后处理会重建FBO透明通道直接丢失Canvas这边也有要求Canvas Render Mode必须用Screen Space - Camera且绑定到上面那个透明相机这样UI层才能和精灵一起被渲染到透明画布上。如果用默认的Screen Space - OverlayUI会单独画在一层不透明的画布上精灵透明了但设置面板一块白底。3.3 点击穿透与拖拽共存运行时切换WS_EX_TRANSPARENTWS_EX_TRANSPARENT这个样式一旦设置整个窗口的所有像素都不再接鼠标消息包括Unity的UI。所以项目里做了一个运行时开关平时开启穿透呼出设置面板时临时关闭面板关闭后再加回去。public class ClickThrough : MonoBehaviour { private IntPtr hWnd; private const int GWL_EXSTYLE -20; private const int WS_EX_TRANSPARENT 0x00000020; public void SetPenetrate(bool enable) { int style GetWindowLong32(hWnd, GWL_EXSTYLE); if (enable) style | WS_EX_TRANSPARENT; // 鼠标消息全部穿透到桌面 else style ~WS_EX_TRANSPARENT; // 恢复接收鼠标事件 SetWindowLong32(hWnd, GWL_EXSTYLE, style); } }按钮事件的回调里调SetPenetrate(false)面板关闭时再SetPenetrate(true)整个过程在运行时动态改样式不用重启窗口。这样一来桌面精灵本体可以完全穿透但需要点时又能让出交互权。穿透模式下的拖拽不能靠Unity的OnMouseDown因为鼠标事件根本进不来。实际做法是轮询GetAsyncKeyState检测左键状态再用Input.mousePosition判断光标是否落在精灵周围的可拖拽矩形内。public class DragController : MonoBehaviour { [DllImport(user32.dll)] private static extern IntPtr GetActiveWindow(); [DllImport(user32.dll)] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hAfter, int x, int y, int cx, int cy, uint flags); [DllImport(user32.dll)] private static extern short GetAsyncKeyState(int vKey); private const int VK_LBUTTON 0x01; private Rect dragArea new Rect(0, 0, 256, 256); // 精灵所在屏幕区域 private void Update() { bool leftDown (GetAsyncKeyState(VK_LBUTTON) 0x8000) ! 0; if (leftDown dragArea.Contains(Input.mousePosition)) { int screenX (int)Input.mousePosition.x; int screenY Screen.height - (int)Input.mousePosition.y; SetWindowPos(GetActiveWindow(), IntPtr.Zero, screenX, screenY, 0, 0, 0x0001 | 0x0002); } } }这里注意坐标系的坑Input.mousePosition以屏幕左下角为原点而SetWindowPos用的是左上角原点所以y分量必须翻转。0x0001和0x0002分别是SWP_NOSIZE和SWP_NOZORDER表示只移动位置、不改尺寸和层级。4. 动画跳动系统从Animator状态机到随机节奏控制4.1 动画素材与状态机设计Idle、Bounce、Hit三态这只精灵的动画素材是序列帧切片按行为分成三组放在Sprites目录下Idle是等待时的呼吸浮动Bounce是跳动Hit是被点击时的缩闪。Animator控制器里建了三个状态Idle默认循环Bounce和Hit播放完毕自动回到Idle。动画帧数帧率循环Idle824是Bounce1230否退出时间0.9Hit630否Bounce和Hit都用Trigger切换区别在优先级Hit用Transition的“Has Exit Time Interruption Source”设为Current StateIdle状态下触发Hit可以打断Bounce的尾部保证点击反馈及时。这个细节不设置的话Bounce播完才能响应Hit手感会明显迟钝。4.2 随机间隔触发为什么用协程而不是InvokeRepeating跳动的节奏如果固定间隔看半小时就会觉得机械。项目里用协程在每次跳动后重新随机一个等待时间让精灵的“活跃周期”看起来更像活的。这里刻意避开了InvokeRepeating——它只能固定间隔重复调用想要随机间隔还得CancelInvoke再重新Invoke绕一圈还不如协程清爽。using System.Collections; using UnityEngine; public class BounceLoop : MonoBehaviour { public Animator animator; public float minInterval 2.5f; public float maxInterval 6f; private void Start() { StartCoroutine(RandomBounce()); } private IEnumerator RandomBounce() { while (true) { // 每次跳动后重新随机一次间隔节奏不落俗套 float waitTime Random.Range(minInterval, maxInterval); yield return new WaitForSeconds(waitTime); animator.SetTrigger(Bounce); } } }waitTime的范围是核心调参项min太小精灵会显得亢奋max太大又像卡死。2.5到6秒这个区间比较自然适合当默认值。值得注意的一点WaitForSeconds受Time.timeScale影响如果后面做“桌面专注模式”这种会暂停游戏时间的功能这里要换成WaitForSecondsRealtime否则精灵在暂停状态下陷入静默。4.3 非穿透模式下的点击反馈与动画优先级设置面板打开时窗口恢复接收鼠标事件这期间点击精灵本体应该触发Hit动画。由于Translucent穿透已经关闭Unity的EventSystem能正常工作直接在精灵根物体上挂EventTrigger的PointerDown回调即可。public class PetAnimator : MonoBehaviour { public Animator animator; public void OnPointerDown() { animator.SetTrigger(Hit); } }这里有个容易忽略的关联点击穿透和动画触发是互斥的。穿透模式下鼠标事件进不来Hit触发不了非穿透模式下能触发但精灵挡住桌面。设计上说得通——穿透模式是为了让精灵不干扰桌面操作这时候不需要反馈只有打开配置面板进入“可交互模式”时点击反馈才有意义。两套模式对应两套交互语义比强制让穿透和点击共存干净得多。5. 桌面精灵常见问题排查透明失败、边框残留、点击失灵5.1 窗口黑底精灵外面一圈黑块现象设置完透明样式后窗口确实变方了但背景不是透明而是纯黑。原因Camera的Clear Flags还停留在Skybox或者背景色alpha为1。Unity默认背景完全不透明窗口Layered样式拿到的是一个不透明的alpha通道自然显示为黑底。另一个常见诱因是打开了后处理Bloom或Color Grading会把场景重新合成到一张新贴图上alpha被覆盖成不透明。解决把Camera的Clear Flags改成Solid Color背景色alpha拉成0关闭HDR和后处理。改完在Game视图勾选“Alpha”通道预览直接看到透明的背景再打包测试。记住一个检查顺序先确认Game视图Alpha通道是透明再查窗口样式黑底瓶颈80%出在渲染缓冲这一侧。5.2 Win11下窗口四角有圆角残留和阴影现象精灵本身透明正常但窗口四周出现圆角白边或一块半透明阴影在浅色桌面上特别刺眼。原因Windows 11的DWM默认给所有顶级窗口加圆角裁剪和阴影效果透明窗口也逃不掉。这个效果是系统窗口级渲染Unity管不到。解决用dwmapi.dll的DwmSetWindowAttribute把窗口圆角偏好设置成“不圆角”。[DllImport(dwmapi.dll)] private static extern int DwmSetWindowAttribute(IntPtr hwnd, uint attribute, ref int value, uint size); private const uint DWMWA_WINDOW_CORNER_PREFERENCE 33; private const int DWMWCP_DONOTROUND 1; int pref DWMWCP_DONOTROUND; DwmSetWindowAttribute(hWnd, DWMWA_WINDOW_CORNER_PREFERENCE, ref pref, sizeof(int));属性ID 33是Win11新增的Win10上调用会返回失败忽略返回值即可。设置完最好再SetWindowPos强制刷新一次窗口区域否则有的系统不会立即重绘裁剪区域。5.3 设置面板打开后按钮死活点不动现象配置面板正常显示但鼠标点退出按钮没反应点击事件像直接穿到了桌面。原因WS_EX_TRANSPARENT是窗口级样式对整个窗口生效不只针对精灵本体。打开面板前如果没把这个位去掉面板区域同样被穿透EventSystem收不到任何点击消息。解决面板打开回调里先调SetPenetrate(false)关闭回调里再SetPenetrate(true)。这个开关不能只在Start里设置一次必须做成运行时动态切换。我一般还会加个日志打印当前样式值排查时能立刻看出穿透位状态。5.4 拖拽时窗口位置漂移鼠标跟手但窗口抖动现象穿透模式下拖拽精灵窗口位置不断跳动光标和精灵的偏移越来越大。原因SetWindowPos每次都把窗口左上角移到光标位置而光标实际点在精灵身体的某个偏移位置于是一拖就产生固定偏移动量。同时Input.mousePosition在Unity渲染帧间隔内不断更新窗口移动又触发渲染形成抖动。解决记录鼠标按下瞬间的偏移offset mousePos - windowPos移动时按mousePos - offset计算目标坐标同时给拖拽加一个距离阈值移动超过几个像素才执行SetWindowPos过滤掉误触和微抖动。5.5 开机自启动后透明样式失效现象手动运行exe一切正常放进启动文件夹开机自启后窗口又变成黑底或不透明。原因开机时系统还在初始化显示驱动GetActiveWindow拿到的句柄有效但DWM还没完全接管窗口此时设置WS_EX_LAYERED会被系统忽略。另一个更隐蔽的原因是Unity引擎在Start阶段还没完成首帧渲染窗口缓冲区是空的。解决透明样式设置延迟几帧再执行或者用一个重复检查——每帧读一次样式值确认位已生效最多等2秒。写入注册表自启动时也注意路径检查是否因为WorkingDirectory不对导致Unity资源没加载完。6. 二次开发方向把单精灵改成多精灵管理器的改造思路单只精灵跑通之后自然想加第二只、第三只让他们各跳各的互不干扰。直接复制GameObject是最差的做法——每个实例都挂着同一个AnimatorController引用一个触发Hit所有精灵一起闪。我的改造思路是把精灵配置收归成一个ScriptableObject再通过一个PetManager统一管理实例的创建与初始化。[CreateAssetMenu(fileName PetConfig, menuName DesktopPet/PetConfig)] public class PetConfig : ScriptableObject { public string petName; public Sprite[] idleFrames; public Sprite[] bounceFrames; public float bounceMinInterval 2f; public float bounceMaxInterval 5f; }每只精灵的动画帧、跳动节奏全部装进配置资产。创建新精灵时拖一个PetConfig进去再让PetManager在运行时用同一份Prefab实例化并将脚本里的AnimatorController替换成按配置生成的新控制器public class PetManager : MonoBehaviour { public GameObject petPrefab; public PetConfig[] pets; private void Start() { foreach (var config in pets) { var instance Instantiate(petPrefab, transform); instance.GetComponentPetView().Initialize(config); } } }PetView在Initialize里把AnimationClip按配置组建成独立的AnimatorOverrideController保证每只精灵的Trigger互不串扰。验证时直接创建三份配置一只设定2秒一跳、一只5秒一跳、一只不跳动纯待机跑到桌面上观察各自动作节奏是否独立。从那以后我每次做透明窗口项目都先单窗口跑一遍alpha通道确认再加动画和交互层这个顺序没变过。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑