Unity 桌宠 实现
配合Live2D实现桌宠效果

关键代码C#代码,只支持windows:
稍微说明下重点:
1.通过DwmExtendFrameIntoClientArea实现半透明背景效果,当然使用SetLayeredWindowAttributes也可以,但是后者是抠图,直接把固定颜色给扣掉,会出现扣不干净的问题,而且不支持局部半透,效果比较差。
2.使用了DwmExtendFrameIntoClientArea方式实现半透会出现一个问题,没办法实现穿透效果,也就是说焦点如果在Unity上就没办法点击到桌面。所以通过WS_EX_TRANSPARENT来实现完全穿透。
3.但是WS_EX_TRANSPARENT又会造成完全穿透而无法点击到桌宠的问题,这里我启用了windows的全局钩子,该功能是能截获整个系统的消息,如果我们不在正常情况下调用CallNextHookEx可能会导致系统无法正常获取信息的重大问题,我使用它主要为了能在光标处于桌宠的上方时获取Unity的焦点从而实现交互。
4.WindowTray是我的另一个类,因为使用了WS_EX_TOOLWINDOW和WS_EX_TOPMOST拓展样式,隐藏了任务栏和标题栏这些普通窗口效果,WindowTray实现了右下角托盘小图标效果,代码也放在下面了。
5.基本上复制粘贴了就能用,小部分可能需要修改?
6.注意,该效果无法在Unity Editor下预览,需要打包后才能确认。但是基本上不影响开发。目前形式是直接全屏Unity,然后半透显示,性能消耗可能比较高,有技术的朋友可以稍微改一下,把屏幕大小改成和要显示的角色大小一样大就好了,应该也不难。
Main
using UnityEngine;
using System;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using Debug = UnityEngine.Debug;
#if UNITY_STANDALONE_WIN
using System.Runtime.InteropServices;
using System.Diagnostics;
using WindowTray;
#endif
public class S_TransparentWindow : MonoBehaviour
{
public string productName;
public int WindowWidth = 512;
public int WindowHeight = 512;
public float Opacity = 1.0f;
public string TrayIconPath;
private Camera CameraComp = null;
//导入Win32 api
#if UNITY_STANDALONE_WIN
private struct MARGINS
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
}
<span style="" ></span>
public struct RECT
{
public int Left, Top, Right, Bottom;
public RECT(int left, int top, int right, int bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
public RECT(System.Drawing.Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { }
public int X
{
get { return Left; }
set { Right -= (Left - value); Left = value; }
}
public int Y
{
get { return Top; }
set { Bottom -= (Top - value); Top = value; }
}
public int Height
{
get { return Bottom - Top; }
set { Bottom = value + Top; }
}
public int Width
{
get { return Right - Left; }
set { Right = value + Left; }
}
public System.Drawing.Point Location
{
get { return new System.Drawing.Point(Left, Top); }
set { X = value.X; Y = value.Y; }
}
public System.Drawing.Size Size
{
get { return new System.Drawing.Size(Width, Height); }
set { Width = value.Width; Height = value.Height; }
}
public static implicit operator System.Drawing.Rectangle(RECT r)
{
return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height);
}
public static implicit operator RECT(System.Drawing.Rectangle r)
{
return new RECT(r);
}
public static bool operator ==(RECT r1, RECT r2)
{
return r1.Equals(r2);
}
public static bool operator !=(RECT r1, RECT r2)
{
return !r1.Equals(r2);
}
public bool Equals(RECT r)
{
return r.Left == Left && r.Top == Top && r.Right == Right && r.Bottom == Bottom;
}
public override bool Equals(object obj)
{
if (obj is RECT)
return Equals((RECT)obj);
else if (obj is System.Drawing.Rectangle)
return Equals(new RECT((System.Drawing.Rectangle)obj));
return false;
}
public override int GetHashCode()
{
return ((System.Drawing.Rectangle)this).GetHashCode();
}
public override string ToString()
{
return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom);
}
}
<span style="" ></span>
private static extern IntPtr GetActiveWindow();
<span style="" ></span>
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
<span style="" ></span>
private static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS margins);
<span style="" ></span>
private static extern int SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter, int x, int y, int cx, int cy,
int uFlags);
<span style="" ></span>
static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
<span style="" ></span>
static extern bool GetClientRect(IntPtr hWnd,out RECT lpRect);
<span style="" ></span>
static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
<span style="" ></span>
static extern int SetLayeredWindowAttributes(IntPtr hwnd, int crKey, byte bAlpha, int dwFlags);
<span style="" ></span>
private static extern bool SetForegroundWindow(IntPtr hWnd);
<span style="" ></span>
private static extern uint GetWindowLong(IntPtr hwnd, int nIndex);
<span style="" ></span>
private static extern IntPtr FindWindow(string lpClassName , string lpWindowName);
<span style="" ></span>
private static extern int MessageBox(IntPtr hWnd,string lpText, string lpCaption, uint uType);
//钩子
//安装钩子
<span style="" ></span>
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
//卸载钩子
<span style="" ></span>
<span style="font-size: UnmanagedType;" ></span>
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
//向下传递钩子
<span style="" ></span>
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
//获取程序集模块的句柄
<span style="" ></span>
private static extern IntPtr GetModuleHandle(string lpModuleName);
public static IntPtr hwnd;
private IntPtr HWND_TOPMOST = new IntPtr(-1);
private const int GWL_STYLE = -16;
private const int GWL_EXSTYLE = -20;
//Style
private const uint WS_BORDER = 0x00800000; //窗口具有细线边框
private const uint WS_CAPTION = 0x00C00000;//窗口具有标题栏。
private const uint WS_POPUP = 0x80000000;
private const uint WS_VISIBLE = 0x10000000;
//Style EX
private const uint WS_EX_TRANSPARENT= 0x00000020;
private const uint WS_EX_LAYERED = 0x00080000;//完全穿透模式
private const uint WS_EX_TOOLWINDOW = 0x00000080;
private const uint WS_EX_NOREDIRECTIONBITMAP = 0x00200000;
private const uint WS_EX_TOPMOST = 0x00000008;
//
private const int SWP_FRAMECHANGED = 0x0020;
private const int SWP_SHOWWINDOW = 0x0040;
//
private const int WH_MOUSE_LL = 14;
private const int WH_MOUSE = 7;
private const int WH_KEYBOARD_LL = 13;
//鼠标事件映射
private const int WM_MOUSEMOVE = 0x200;
private const int WM_LBUTTONDOWN = 0x201;
private const int WM_RBUTTONDOWN = 0x204;
private const int WM_MBUTTONDOWN = 0x207;
private const int WM_LBUTTONUP = 0x202;
private const int WM_RBUTTONUP = 0x205;
private const int WM_MBUTTONUP = 0x208;
private const int WM_LBUTTONDBLCLK = 0x203;
private const int WM_RBUTTONDBLCLK = 0x206;
private const int WM_MBUTTONDBLCLK = 0x209;
//hook到的消息结构
<span style="" ></span>
public class KeyBoardHookStruct
{
public int vkCode;
public int scanCode;
public int flags;
public int time;
public int dwExtraInfo;
}
<span style="" ></span>
public class POINT
{
public int x;
public int y;
}
<span style="" ></span>
public class MouseHookStruct
{
public POINT pt;
public int hWnd;
public int wHitTestCode;
public int dwExtraInfo;
}
static IntPtr _hMouseHook;
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
private HookProc MouseHookProc;
static private Vector2 MouseTrack = new Vector2();
static private RECT WindowRect = new RECT();
private static bool bIsPenetration = false;
//托盘
S_WindowTray Tray;
private void InitWindowStyle()
{
hwnd = FindWindow(null,productName);
if (hwnd == null)
{
MessageBox(IntPtr.Zero, "查找Unity窗口出错...", "错误", 0);
return;
}
MARGINS margins = new MARGINS() { cxLeftWidth = -1 };
// Set properties of the window
SetWindowLong(hwnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
SetWindowTransparency(true);
DwmExtendFrameIntoClientArea(hwnd, ref margins);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, Screen.currentResolution.width, Screen.currentResolution.height, SWP_SHOWWINDOW);
// SetLayeredWindowAttributes(hwnd, 0, (byte)(Opacity*255.0f), 0x00000001 | 0x00000002);
CreateTray();
//安装全局钩子
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
MouseHookProc = new HookProc(FuncMouseHookProc);
_hMouseHook = SetWindowsHookEx(WH_MOUSE_LL,MouseHookProc,GetModuleHandle(curModule.ModuleName),0);
}
}
private IntPtr FuncMouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
MouseHookStruct MyMouseHookStruct = (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));
int x = MyMouseHookStruct.pt.x;
int y = MyMouseHookStruct.pt.y;
Vector3 pos = Vector3.zero;
pos.x = x;
pos.y = Screen.currentResolution.height - y;
pos.z = Input.mousePosition.z;
MouseTrack.x = pos.x;
MouseTrack.y = pos.y;
{
if (EventSystem.current)
{
var CurrentCursorPos = Camera.main.ScreenToWorldPoint(pos);
PointerEventData eventData = new PointerEventData(EventSystem.current);
//eventData.position = Input.mousePosition;
eventData.position = pos;
// 执行射线检测,查看指定位置是否有UI
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, results);
UnityEngine.Vector3 target = CurrentCursorPos + Camera.main.transform.forward * 500.0f;
RaycastHit[] ObjResults = Physics.RaycastAll(CurrentCursorPos, target);
if (ObjResults.Length > 0 || results.Count > 0)
{
SetWindowPenetration(false);
}
else
{
SetWindowPenetration(true);
}
}
}
return CallNextHookEx(_hMouseHook, nCode, wParam, lParam);
}
public void CreateTray()
{
//任务栏右下角托盘小图标
Tray = new S_WindowTray();
string iconPath = Application.streamingAssetsPath + TrayIconPath;
//Debug.Log(iconPath);
Tray.InitTray(ref hwnd, iconPath);
}
public void SetWindowPenetration(bool isPenetration)
{
uint dwExStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
dwExStyle |= WS_EX_TOOLWINDOW | WS_EX_TOPMOST ;
if (isPenetration && !bIsPenetration)
{
bIsPenetration = true;
dwExStyle |= WS_EX_TRANSPARENT | WS_EX_LAYERED;
//dwExStyle |= WS_EX_LAYERED;
}
else if(!isPenetration && bIsPenetration)
{
dwExStyle &= ~WS_EX_TRANSPARENT;
bIsPenetration = false;
}
SetWindowLong(hwnd, GWL_EXSTYLE, dwExStyle);
}
void Destroy()
{
UnhookWindowsHookEx(_hMouseHook);
_hMouseHook = (IntPtr)0;
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 2, 2, SWP_SHOWWINDOW);
}
#endif
private void OnDestroy()
{
Destroy();
}
void Awake()
{
Application.runInBackground = true;
CameraComp = Camera.main;
#if !UNITY_EDITOR
InitWindowStyle();
#endif
}
}
WindowTray:
using System;
using UnityEngine;
using System.Runtime.InteropServices;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Unity.VisualScripting;
using Application = System.Windows.Forms.Application;
namespace WindowTray
{
public class S_WindowTray
{
//NotifyIcon 设置托盘相关参数
public static NotifyIcon _notifyIcon = new NotifyIcon();
//托盘图标的宽高
private int iconSize = 40;
//做托盘图标的图片,这里用了.png格式
private IntPtr hwnd;
//
private MenuItem exit;
private MenuItem setting;
/*--------Malioc-------*/
//
// private MenuItem maliOfflineCompiler;
// //只显示Spilling
// public static MenuItem MaliocItem_OnlyShowSpilling;
// public static MenuItem MaliocItem_ForUE4;
// public static MenuItem MaliocItem_ForOpenGLES;
// public static MenuItem MaliocItem_ForVulkanGLSL;
/*---------------------*/
private MenuItem AddLine()
{
MenuItem line = new MenuItem("——————");
line.Enabled = false;
line.Break = true;
return line;
}
//调用该方法将运行程序显示到托盘
public void InitTray(ref IntPtr inHwnd , string iconPath)
{
hwnd = inHwnd;
_notifyIcon.Text = "诗酱";
//托盘图标
if (iconPath.Length < 2)
_notifyIcon.Icon = new System.Drawing.Icon(SystemIcons.Warning, iconSize, iconSize);
else
_notifyIcon.Icon = CustomTrayIcon(iconPath, iconSize, iconSize);
_notifyIcon.ContextMenu = new System.Windows.Forms.ContextMenu();
//右键菜单
{
//退出按钮
exit = new MenuItem("退出", ExitApp);
//角色大小
setting = new MenuItem("设置" , Setting);
// //Mali Offline Compiler
// {
// MaliocItem_OnlyShowSpilling = new MenuItem("只显示Spillling", (object sender, EventArgs e) =>
// {
// if (MaliocItem_OnlyShowSpilling.Checked)
// MaliocItem_OnlyShowSpilling.Checked =false;
// else
// MaliocItem_OnlyShowSpilling.Checked = true;
// });
// maliOfflineCompiler = new MenuItem("Malioc");
// MaliocItem_ForUE4 = new MenuItem("For UE4");
// MaliocItem_ForOpenGLES = new MenuItem("For OpenGLES", (object sender, EventArgs e) =>
// {
// MaliocItem_ForOpenGLES.Checked = true;
// MaliocItem_ForVulkanGLSL.Checked = false;
// });
// MaliocItem_ForVulkanGLSL = new MenuItem("For VulkanGLSL", (object sender, EventArgs e) =>
// {
// MaliocItem_ForOpenGLES.Checked = false;
// MaliocItem_ForVulkanGLSL.Checked = true;
// });
//
// MaliocItem_ForUE4.Checked = true;
//
// maliOfflineCompiler.MenuItems.Add(MaliocItem_OnlyShowSpilling);
// maliOfflineCompiler.MenuItems.Add(AddLine());
// maliOfflineCompiler.MenuItems.Add(MaliocItem_ForUE4);
// maliOfflineCompiler.MenuItems.Add(AddLine());
// maliOfflineCompiler.MenuItems.Add(MaliocItem_ForOpenGLES);
// maliOfflineCompiler.MenuItems.Add(MaliocItem_ForVulkanGLSL);
// maliOfflineCompiler.MenuItems.Add(AddLine());
// }
// //
//_notifyIcon.ContextMenu.MenuItems.Add(maliOfflineCompiler);
//_notifyIcon.ContextMenu.MenuItems.Add(AddLine());
_notifyIcon.ContextMenu.MenuItems.Add(setting);
_notifyIcon.ContextMenu.MenuItems.Add(exit);
}
Show();
_notifyIcon.ShowBalloonTip(2500, "", "你好呀~", ToolTipIcon.None);
//这个注释掉,不知道为啥会影响到Hook钩子
//Form.ActiveForm.ShowInTaskbar = false;
//Form.FromHandle(hwnd).FindForm().ShowInTaskbar=false;
}
/// 设置程序托盘图标
private Icon CustomTrayIcon(string iconPath, int width, int height)
{
Bitmap bt = new Bitmap(iconPath);
Bitmap fitSizeBt = new Bitmap(bt, width, height);
return Icon.FromHandle(fitSizeBt.GetHicon());
}
public void Show()
{
_notifyIcon.Visible = true;//托盘按钮是否可见
}
public void Hide()
{
_notifyIcon.Visible = false;//托盘按钮是否可见
}
private void ExitApp(object sender, EventArgs e)
{
Hide();
_notifyIcon.Dispose();
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
System.Windows.Forms.Application.Exit();
//UnityEngine.Application.Quit(0);
#endif
}
private void Setting(object sender, EventArgs e)
{
S_SettingWidget.SettingWidget.SetActive(true);
}
}
}