LOADING

Unity 桌宠 实现

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);
        }
        

    }
}