我们是一对夫妻团队,正在制作Petunia's Purgatory,这是一个桌面伴侣游戏,在这个游戏中,你会经营一个可爱的农场,试图不让自己变得疯狂。

当我们决定制作一个游戏,在桌面上运行,但不占据整个屏幕时,我们进行了大量的谷歌搜索,以了解如何做到这一点。

结果,事实并不是非常复杂,但有很多陷阱。我想分享我们所学到的东西,以便其他人也能制作类似的游戏。

无论如何,我们开始了!请注意:这篇文章是比较技术性的,所以你应该对C#有一定的基础。无需真正了解Windows编程(我当然也不了解!)

-----------------------------------

设置

版本信息: 本文是使用 Unity 6.2 for Windows 开发的。我无法保证其他版本和这个版本兼容,也无法保证在Mac或Linux上工作。

概念: 桌面伴侣游戏实际上就是一个正常的Windows应用,但它没有边框。在透明区域,鼠标可以点击通过,因此它可以在桌面上坐着,不会干扰其他应用。

项目设置:

  • 添加一个摄像机,并设置以下内容:
  • 在环境选项卡中,设置背景类型为固体颜色,并设置颜色为纯黑色,Alpha值为0
  • 取消勾选后处理,并确保抗锯齿关闭
    • 某些原因,后处理不支持这种设置
  • 添加一个UI事件系统(GameObject - UI - EventSystem)
  • 在项目设置中,转到Player - Resolution and Presentation并设置以下内容:
  • 在后台运行:True
  • 全屏模式:全屏窗口
  • 可调整大小的窗口:False
  • 在后台可见:True
  • 允许全屏切换:False

----------------------------------------

代码

概念: 我们将使用一些Windows函数来控制游戏窗口的呈现。说实话,我不知道这些函数内部是如何工作的,但它们确实工作!

步骤 #1:基本设置

创建一个新的MonoBehavior脚本(我称之为“透明应用控制器”)并将其附加到一个游戏对象(如您的摄像机)

步骤 #2:Windows函数

添加以下行:

using System.Runtime.InteropServices;

声明以下变量并确保它们以此方式写成:

[DllImport("user32.dll")]
private static extern IntPtr GetActiveWindow();

[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);

[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

const int GWL_EXSTYLE = -20;
const uint WS_EX_LAYERED = 0x00080000;
const uint WS_EX_TRANSPARENT = 0x00000020;
static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
private IntPtr hWnd;

步骤 #3:Unity逻辑

添加以下函数(这将捕获游戏窗口的ID,当它获得焦点时)

private void OnApplicationFocus(bool hasFocus)
{
    if (hasFocus)
    {
        hWnd = GetActiveWindow();
    }
}

在您的 Update()函数中添加以下代码:

PointerEventData pointerEventData  = new PointerEventData(EventSystem.current);
pointerEventData.position = Input.mousePosition;

List<RaycastResult> raycastResultList = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointerEventData, raycastResultList);

bool isOverUI = raycastResultList.Count > 0;


if(isOverUI)
{
    SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED);
}
else
{
    SetWindowLong(hWnd, GWL_EXSTYLE, WS_EX_LAYERED | WS_EX_TRANSPARENT);
}

如果你感兴趣的话,这个代码片段会做以下几件事情:

  • 移除游戏的边框并使任何未渲染的区域透明
  • 检查鼠标指针是否悬停在可点击区域上,如果是,则允许游戏被点击。这个功能可以防止游戏在空白区域阻塞桌面输入

步骤 #4(可选):始终置顶

这个功能是可选的,但如果你想让游戏始终置顶其他窗口,你可以通过添加以下代码来实现:

if(alwaysOnTop)
{
    SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, 0);
}
else
{
    SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, 0);
}

最终说明

  • 在编辑器中,你不会看到这些代码。需要在构建中才能看到它们是否有效
  • 我强烈建议将所有这些代码用 #if !UNITY_EDITOR 包裹起来,以防止在编辑器中出现一些奇怪的行为