大家好!我们是一对夫妻,正在制作一个名为Petunia's Purgatory的桌面伴侣游戏,游戏中你会经营一个可爱又神秘的农场,试图不被疯狂所困扰。

当我们决定制作一个不占据整个屏幕的桌面伴侣游戏时,我们进行了大量的google搜索来了解如何实现这一点。

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

好吧,开始吧!请注意,这是一个相当技术性的内容,所以你需要了解C#的基本知识。虽然你不需要了解Windows编程(我也不了解!),但你需要有一定的编程基础。

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

设置

版本信息:本文使用的是Unity 6.2 for Windows。我无法保证其他版本的兼容性,且不支持Mac或Linux。

概念:一个桌面伴侣游戏实际上就是一个正常的Windows应用,但它没有边框。它的透明区域允许鼠标点击通过,因此它可以在你的桌面上轻松地与其他应用共存。

项目设置

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

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

代码

概念:我们将使用一些Windows函数来控制游戏窗口的呈现。事实上,我不知道这些函数内部的工作原理,但它们确实有效!

步骤 #1:基本设置

创建一个新MonoBehavior脚本(我称之为“TransparentAppController”),并将其附加到一个游戏对象(例如你的摄像机)

步骤 #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(可选):始终置顶

这是一种可选的设置,如果你想让游戏始终置顶其他窗口,可以通过添加以下代码来实现(你需要声明一个bool变量并设置它的值)。

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

最终说明

  • 在编辑器中,你将无法看到这些设置。你需要制作一个构建才能看到它们是否有效
  • 我强烈推荐在编辑器中用#if !UNITY_EDITOR将所有代码包裹起来,以防止一些奇怪的问题