Unity3D制作桌宠核心代码
uni_shiki
2023年01月25日 20:45

一、背景设置

效果

- 除模型外区域透明显示

- 透明部分鼠标可穿透。即点击透明区域,不聚焦在程序上


编辑器内设置:

Camera组件设置:Clear Flags选择Solid Color,Background改成黑色


代码:

using UnityEngine;

using System.Runtime.InteropServices; // 为了使用DllImport

using System;

/// <summary>

/// 让程序背景透明

/// </summary>

public class BackGroundSet : MonoBehaviour

{

    private IntPtr hwnd;

    private int currentX;

    private int currentY;

    #region Win函数常量

    private struct MARGINS

    {

        public int cxLeftWidth;

        public int cxRightWidth;

        public int cyTopHeight;

        public int cyBottomHeight;

    }

    [DllImport("user32.dll&#​34;)]

    static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

    [DllImport("user32.dll&#​34;)]

    static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

    [DllImport("user32.dll&#​34;)]

    static extern int GetWindowLong(IntPtr hWnd, int nIndex);

    [DllImport("user32.dll&#​34;)]

    static extern int SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags);

    [DllImport("Dwmapi.dll&#​34;)]

    static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS margins);

    [DllImport("user32&#​34;, EntryPoint = "SetLayeredWindowAttributes&#​34;)]

    private static extern uint SetLayeredWindowAttributes(IntPtr hwnd, int crKey, int bAlpha, int dwFlags);

    // 定义窗体样式,-16表示设定一个新的窗口风格

    private const int GWL_STYLE = -16;

    //设定一个新的扩展风格

    private const int GWL_EXSTYLE = -20;

    

    private const int WS_EX_LAYERED = 0x00080000;

    private const int WS_BORDER = 0x00800000;

    private const int WS_CAPTION = 0x00C00000;

    private const int SWP_SHOWWINDOW = 0x0040;

    private const int LWA_COLORKEY = 0x00000001;

    private const int LWA_ALPHA = 0x00000002;

    private const int WS_EX_TRANSPARENT = 0x20;

    #endregion

    void Awake()

    {

        Application.targetFrameRate = 60;

        var productName = Application.productName;

#if !UNITY_EDITOR

        // 获得窗口句柄

        hwnd = FindWindow(null, productName); 

        // 设置窗体属性

        int intExTemp = GetWindowLong(hwnd, GWL_EXSTYLE); // 获得当前样式

        SetWindowLong(hwnd, GWL_EXSTYLE, intExTemp | WS_EX_LAYERED); // 当前样式加上WS_EX_LAYERED     // WS_EX_TRANSPARENT 收不到点击的透明

        SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_BORDER & ~WS_CAPTION); // 无边框、无标题栏

        // 设置窗体位置为右下角

        currentX = Screen.currentResolution.width - 900;

        currentY = Screen.currentResolution.height  - 800;

        SetWindowPos(hwnd, -1, currentX, currentY, 1200, 900, SWP_SHOWWINDOW); // Screen.currentResolution.width 

        // 扩展窗口到客户端区域 -> 为了透明

        var margins = new MARGINS() { cxLeftWidth = -1 }; // 边距内嵌值确定在窗口四侧扩展框架的距离 -1为没有窗口边框

        DwmExtendFrameIntoClientArea(hwnd, ref margins);     

        // 将该窗口颜色为0的部分设置为透明,即背景可穿透点击,人物模型上不穿透

        SetLayeredWindowAttributes(hwnd, 0, 255, 1);

        /// <summary>

        ///设置窗体可穿透点击的透明.

        ///参数1:窗体句柄

        ///参数2:透明颜色  0为黑色,按照从000000到FFFFFF的颜色,转换为10进制的值

        ///参数3:透明度,设置成255就是全透明

        ///参数4:透明方式,1表示将该窗口颜色为0的部分设置为透明,2表示根据透明度设置窗体的透明度

        /// </summary>

#endif

        /// <summary>

        /// 1

        /// 调节窗体透明度可以先使用SetWindowLong为窗体加上WS_EX_LAYERED属性,

        /// 再使用SetLayeredWindowAttributes来指定窗体的透明度。

        /// 这样就可以在程序运行时动态的调节窗体的透明度了。

        /// 2

        /// 给 GWL_EXSTYLE 设置 WS_EX_TRANSPARENT 让窗口透明,此时应用程序只能收到鼠标消息但收不到触摸消息

        /// 3

        /// 前面加上取反操作符"~&#​34;,就可以得到相反效果。比如,WS_CAPTION代表窗口有标题栏,~WS_CAPTION代表窗口没有标题栏

        /// 4

        /// GWL_STYLE指的是那些旧的窗口属性。相对于GWL_EXSTYLEGWL扩展属性而言的

        /// 5

        /// 要给窗口添加某属性,用 | 来连接,要去除某属性,用 & 来连接

        /// </summary>

    }

}


二、鼠标事件

效果

- 监听鼠标左右中键以及拖拽

- 控制触发事件


代码:

参考  https://www.jianshu.com/p/6165a90dcbb2

using System;

using System.Collections;

using System.Collections.Generic;

using System.Runtime.InteropServices;

using UnityEngine;

using UnityEngine.EventSystems;

public class MouseEvents : MonoBehaviour

{

    [DllImport("user32.dll&#​34;)]

    public static extern short GetAsyncKeyState(int vKey);

    private const int VK_LBUTTON = 0x01; //鼠标左键

    private const int VK_RBUTTON = 0x02; //鼠标右键

    private const int VK_MBUTTON = 0x04; //鼠标中键

    private bool _isLeftDown;

    private bool _isRightDown;

    private bool _isMiddleDown;

    public event Action<MouseKey, Vector3> MouseKeyDownEvent;

    public event Action<MouseKey, Vector3> MouseKeyUpEvent;

    public event Action<MouseKey, Vector3> MouseDragEvent;

    public event Action<MouseKey> MouseKeyClickEvent;

    public Vector3 MousePos { get; private set; }

    private bool _hasDragged;

    private Vector3 _leftDownPos;

    private Vector3 _rightDownPos;

    private Vector3 _middleDownPos;

    [SerializeField] private Animator anim;

    [DllImport("user32.dll&#​34;)]

    static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

    /// <summary>

    /// 获得鼠标在屏幕上的位置

    /// </summary>

    /// <param name="lpPoint&#​34;></param>

    /// <returns></returns>

    [DllImport("user32.dll&#​34;)]

    static extern bool GetCursorPos(ref POINT lpPoint);

    /// <summary>

    /// 记录当前鼠标的位置

    /// </summary>

    public POINT point;

    /// <summary>

    /// 设置目标窗体大小,位置

    /// </summary>

    /// <param name="hWnd&#​34;>目标句柄</param>

    /// <param name="x&#​34;>目标窗体新位置X轴坐标</param>

    /// <param name="y&#​34;>目标窗体新位置Y轴坐标</param>

    /// <param name="nWidth&#​34;>目标窗体新宽度</param>

    /// <param name="nHeight&#​34;>目标窗体新高度</param>

    /// <param name="BRePaint&#​34;>是否刷新窗体</param>

    /// <returns></returns>

    [DllImport("user32.dll&#​34;, CharSet = CharSet.Auto)]

    public static extern int MoveWindow(IntPtr hWnd, int x, int y, int nWidth, int nHeight, bool BRePaint);

    /// <summary>

    /// 当前窗体句柄

    /// </summary>

    private IntPtr hwnd;

    private bool dragModle; // 是否在拖拽模型

    [SerializeField] private Transform neck; // 脖子部位,控制模型朝向鼠标位置的开关

    [Header("右键主菜单&#​34;)]

    [SerializeField] private GameObject mainMenu;

    [Header("设置大小页&#​34;)]

    [SerializeField] private GameObject sizePage;

    private void Start()

    {

        Init();

    }

    // 失焦时关闭主菜单

    private void OnApplicationFocus(bool focus)

    {

        if (!focus && mainMenu.activeSelf)

        {

            mainMenu.SetActive(false);

        }

    }

    private void Update()

    {

        // 按下左键

        if (GetAsyncKeyState(VK_LBUTTON) != 0)

        {

            // 左键不在UI上,关闭主菜单

            if (mainMenu.activeSelf && !EventSystem.current.IsPointerOverGameObject())

            {

                mainMenu.SetActive(false);

            }

            if (!_isLeftDown)

            {

                _isLeftDown = true;

                _leftDownPos = MouseKeyDown(MouseKey.Left);

            }

            else if (MousePos != Input.mousePosition)

            {

                MouseKeyDrag(MouseKey.Left);

                if (!_hasDragged)

                {

                    _hasDragged = true;

                    // 开启拖拽模型,根据鼠标位置判断是否可拖拽。显示模型时用碰撞体+射线检测,在模型不显示时可用是否在UI上判断

                    if (Physics.Raycast(Camera.main.ScreenPointToRay(MousePos), out RaycastHit hit, 10f, 1 << LayerMask.NameToLayer("Player&#​34;)) ||( EventSystem.current.IsPointerOverGameObject() && (UIScript.Instance.inGameMode || UIScript.Instance.inWorkMode)))

                    {

                        dragModle = true;

                        mainMenu.SetActive(false);

                    }

                }

            }

        }

        // 按下右键

        if (GetAsyncKeyState(VK_RBUTTON) != 0)

        {

            sizePage.SetActive(false);

            // 右键模型出现主菜单,并根据鼠标位置设置主菜单出现位置,防止主菜单显示到屏幕外,是把屏幕分为四个象限来判断

            if (Physics.Raycast(Camera.main.ScreenPointToRay(MousePos), out RaycastHit hit, 10f, 1 << LayerMask.NameToLayer("Player&#​34;)) || (EventSystem.current.IsPointerOverGameObject() && (UIScript.Instance.inGameMode || UIScript.Instance.inWorkMode)))

            {

                if (point.X <= Screen.currentResolution.width / 2 && point.Y <= Screen.currentResolution.height / 2)

                {

                    mainMenu.transform.localPosition = new Vector3(Input.mousePosition.x * 3 - 1500, Input.mousePosition.y * 3 - 1800, 0);

                }

                else if (point.X > Screen.currentResolution.width / 2 && point.Y <= Screen.currentResolution.height / 2)

                {

                    mainMenu.transform.localPosition = new Vector3(Input.mousePosition.x * 3 - 1500 - 600, Input.mousePosition.y * 3 - 1800, 0);

                }

                else if (point.X <= Screen.currentResolution.width / 2 && point.Y > Screen.currentResolution.height / 2)

                {

                    mainMenu.transform.localPosition = new Vector3(Input.mousePosition.x * 3 - 1500, Input.mousePosition.y * 3 - 1800 + 800, 0);

                }

                else if (point.X > Screen.currentResolution.width / 2 && point.Y > Screen.currentResolution.height / 2)

                {

                    mainMenu.transform.localPosition = new Vector3(Input.mousePosition.x * 3 - 1500 - 600, Input.mousePosition.y * 3 - 1800 + 800, 0);

                }

                mainMenu.SetActive(true);

            }

            if (!_isRightDown)

            {

                _isRightDown = true;

                _rightDownPos = MouseKeyDown(MouseKey.Right);

            }

            else if (MousePos != Input.mousePosition)

            {

                MouseKeyDrag(MouseKey.Right);

                if (!_hasDragged)

                {

                    _hasDragged = true;

                }

            }

        }

        // 按下中键

        if (GetAsyncKeyState(VK_MBUTTON) != 0)

        {

            if (!_isMiddleDown)

            {

                _isMiddleDown = true;

                _middleDownPos = MouseKeyDown(MouseKey.Middle);

            }

            else if (MousePos != Input.mousePosition)

            {

                MouseKeyDrag(MouseKey.Middle);

                if (!_hasDragged)

                {

                    _hasDragged = true;

                }

            }

        }

        // 抬起左键

        if (GetAsyncKeyState(VK_LBUTTON) == 0 && _isLeftDown)

        {

            _isLeftDown = false;

            MouseKeyUp(MouseKey.Left);

            // 无拖拽、downPos==upPos

            if (!_hasDragged && _leftDownPos == MousePos)

            {

                MouseKeyClick(MouseKey.Left);

// 点击模型的触发动作设置

                if(!anim.GetBool("bow&#​34;) && !anim.GetBool("exit&#​34;) && !UIScript.Instance.randomAnimMode && !UIScript.Instance.inGameMode && Physics.Raycast(Camera.main.ScreenPointToRay(MousePos), out RaycastHit hit, 10f, 1 << LayerMask.NameToLayer("Player&#​34;)))

                {

                    int num = UnityEngine.Random.Range(1, 3); // 1/2

                    neck.transform.rotation = Quaternion.Euler(0, 0, 0);

                    switch (num)

                    {

                        case 1:

                            GameObject.FindWithTag("Player&#​34;).GetComponent<Animator>().SetBool("bow&#​34;, true); break;

                        case 2:

                            GameObject.FindWithTag("Player&#​34;).GetComponent<Animator>().SetBool("exit&#​34;, true); break;

                    }

                }

               

                

            }

            _hasDragged = false;

            // 停止拖拽模型

            dragModle = false;

            

        }

        // 抬起右键

        if (GetAsyncKeyState(VK_RBUTTON) == 0 && _isRightDown)

        {

            _isRightDown = false;

            MouseKeyUp(MouseKey.Right);

            if (!_hasDragged && _rightDownPos == MousePos)

            {

                MouseKeyClick(MouseKey.Right);

            }

            _hasDragged = false;

        }

        // 抬起中键

        if (GetAsyncKeyState(VK_MBUTTON) == 0 && _isMiddleDown)

        {

            _isMiddleDown = false;

            MouseKeyUp(MouseKey.Middle);

            if (!_hasDragged && _middleDownPos == MousePos)

            {

                MouseKeyClick(MouseKey.Middle);

            }

            _hasDragged = false;

        }

        

        // 拖拽

        if (dragModle)

        {

            GetCursorPos(ref point); // 获取鼠标在屏幕上的位置(原点在左上).而不是鼠标在unity中的位置(原点在左下)

            MoveWindow(hwnd, point.X - Screen.width / 2, point.Y - Screen.height / 2, Screen.width, Screen.height, true);

        }

    }

    public void Init()

    {

        _isLeftDown = false;

        _isRightDown = false;

        _isMiddleDown = false;

        _hasDragged = false;

        hwnd = FindWindow(null, Application.productName);

        dragModle = false;

        point.X = Screen.currentResolution.width;

        point.Y = Screen.currentResolution.height;

    }

    private Vector3 MouseKeyDown(MouseKey mouseKey)

    {

        RefreshMousePos();

        MouseKeyDownEvent?.Invoke(mouseKey, MousePos);

        return MousePos;

    }

    private Vector3 MouseKeyUp(MouseKey mouseKey)

    {

        RefreshMousePos();

        MouseKeyUpEvent?.Invoke(mouseKey, MousePos);

        return MousePos;

    }

    private Vector3 MouseKeyDrag(MouseKey mouseKey)

    {

        RefreshMousePos();

        MouseDragEvent?.Invoke(mouseKey, MousePos);

        return MousePos;

    }

    private void MouseKeyClick(MouseKey mouseKey)

    {

        MouseKeyClickEvent?.Invoke(mouseKey);

    }

    private Vector3 RefreshMousePos()

    {

        MousePos = Input.mousePosition;

        return MousePos;

    }

    public Vector3 MousePosToWorldPos(Vector3 mousePos)

    {

        var screenInWorldPos = Camera.main.WorldToScreenPoint(mousePos);

        var refPos = new Vector3(mousePos.x, mousePos.y, screenInWorldPos.z);

        var pos = Camera.main.ScreenToWorldPoint(refPos);

        return pos;

    }

}

public enum MouseKey

{

    None,

    Left,

    Right,

    Middle

}

public struct POINT

{

    public int X;

    public int Y;

    public POINT(int x, int y)

    {

        this.X = x;

        this.Y = y;

    }

    public override string ToString()

    {

        return ("X:&#​34; + X + ", Y:&#​34; + Y);

    }    

}


三、人物模型看向鼠标位置

代码:

using UnityEngine;

public class TowardMousePos : MonoBehaviour

{

    [SerializeField] private Transform neck;

    [SerializeField] private Animator anim;

    private void Update()

    {

        // 设置分辨率为1200:900可以计算出鼠标当前位置(设置别的也行,自行调试)

        float x = Mathf.Clamp((Input.mousePosition.x - 1200 * .5f) / Screen.width * .5f * 180, -50, 50);

        float y = Mathf.Clamp((Input.mousePosition.y - 900 * .5f) / Screen.height * .5f * 180, -20, 50);

        // 旋转脖子角度(可加if判断)

            neck.transform.rotation = Quaternion.Euler(-y + 45, -x, 0); // 上下,左右

    }

}


结语:

- 若透明背景未出现,注意PlayerSettings中分辨率模块里的Use DXGI Flip Mode Swapchain for D3D11选项,关闭即可

- 失焦时无法收到Unity内置的InputSystem的事件,监听鼠标位置用GetCursorPos(),监听键盘输入需要钩子(Hook),都需要导入user32.dll

- 写得不好,大佬轻喷