【Unity实现Oneshot胶卷显形】游戏窗口化与Win32API的使用

前言

  • 今天和朋友在聊天的时候聊到了Oneshot中的游戏中地表区章节拖动游戏窗口到屏幕之外使胶片显形的游戏现象。胶片显形状的事情,于是就尝试复刻判断条件。

  • 游戏中的现象是这样的:玩家在窗口化游戏的前提下,把整个窗口拖动到任务栏以下后,游戏中的胶卷发生了替换。

  • 实现的具体逻辑很简单,只需要判断窗口化的游戏窗口是否已经移动玩家的屏幕下方即可,注意这里应该判断到屏幕底端的位置而不是到状态栏的位置,因为玩家可能隐藏了状态栏。因此完整实现逻辑如下:

    • 窗口化实现
    • 窗口触底检测

1 窗口化实现

1-1 代码实现
  • Unity中,实现游戏窗口的方式很简单:
  • 我们随机在空场景新建一个全新的脚本并挂载
cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WindowSet : MonoBehaviour
{
    void Awake()
    {
        Screen.fullScreenMode = FullScreenMode.Windowed;
        Screen.SetResolution(1280, 720, false);
    }
}
  • Screen.fullScreenMode = FullScreenMode.Windowed;将设置游戏的显示模式
模式 含义
FullScreenWindow 无边框全屏
ExclusiveFullScreen 独占全屏
Windowed 窗口模式
  • Screen.SetResolution(1280, 720, false);将设置窗口的尺寸,最后一个参数fullscreen将判断是否强制全屏
含义
true 强制全屏
false 窗口模式

1-2 测试
  • 同时我们需要注意,窗口化的实现最好在build后测试,因为在Unity中的Editor 里的 GameView并非独立窗口

  • 我们点击左上角的Build And Run进行编译项目,生成 Windows 程序然后直接直接启动游戏(其实就是打包生成exe),初次运行需要指定打包后生成的位置,随便创建个文件夹放置就行了。

  • 等待进度条结束,我们就可以看到窗口化运行的游戏窗口了


窗口触底检测

2-1 小工具DebugLog
  • 通常我们在游戏测试阶段的时候,会使用Debug.Log语句来打印输出内容,但是这依赖于编辑器页面的Console模块

  • 这时候我们需要在游戏窗口内置一个运行时日志控制台,作用是在游戏运行时直接输出在屏幕上,就比如这样:

  • 具体实现逻辑如下:

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RuntimeConsole : MonoBehaviour
{
    Queue<string> logs = new Queue<string>();

    void OnEnable()
    {
        Application.logMessageReceived += HandleLog;
    }

    void OnDisable()
    {
        Application.logMessageReceived -= HandleLog;
    }

    void HandleLog(string logString, string stackTrace, LogType type)
    {
        logs.Enqueue(logString);

        if (logs.Count > 10)
            logs.Dequeue();
    }

    void OnGUI()
    {
        int y = 10;
        foreach (var log in logs)
        {
            GUI.Label(new Rect(10, y, 800, 20), log);
            y += 20;
        }
    }
}
  • 上述代码构建了一个日志队列Queue<string> logs,并存储最近十条日志

  • OnEnable将Unity 全局日志事件绑定,使触发来源包括:

    • Debug.Log()
    • Debug.LogWarning()
    • Debug.LogError()
  • void OnGUI()用的是Unity 老式 IMGUI 绘制系统

  • 有了上述脚本,随便挂在空物体上,以后程序调试的时候使用日志输出游戏内也可以看到了。


2-2 触底判断
  • 首先我们需要知道的是,Unity本身没有提供"获取窗口在屏幕中真实位置"的跨平台 API,因此在 Windows 平台下,通常需要借助:
    • Windows API(user32.dll
    • 原生插件(Native Plugin
  • 这里我们直接用最方便的user32.dll(啥也不用装)
  • 完整代码先放在这,然后我们仔细看做了啥。
cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;

public class WindowTracker : MonoBehaviour
{
    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        public int left;
        public int top;
        public int right;
        public int bottom;
    }

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

    [DllImport("user32.dll")]
    static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);

    IntPtr hwnd;

    void Start()
    {
      
        hwnd = GetActiveWindow();
    }

    //标志位,判断是否触发了逻辑(这里只用于单次判断)
    bool isTrigger = false;
    void Update()
    {
        

        RECT rect;
        if (GetWindowRect(hwnd, out rect))
        {
          
            Debug.Log($"Pos: ({rect.left}, {rect.top}),isTrigger:{isTrigger}");
            //判断是否触发
            CheckTrigger(rect);
        }
    }

    void CheckTrigger(RECT rect)
    {   
        int screenHeight = Screen.currentResolution.height;
        float windowHeight = rect.bottom - rect.top;
        // 计算进入屏幕外的深度
        float depth = rect.bottom - screenHeight;
        //换算成百分比
        float percent = Mathf.Clamp01(depth / windowHeight);
        //打印百分比
        Debug.Log($"percent:{percent}");
        // 判断是否贴近底部
        if (percent >0.9)
        {
            //触发逻辑,这里可以绑定事件去触发图片替换
            //这里测试就设置一次,因为窗口化拖到屏幕外我们是看不到输出的(乐)
            isTrigger = true;
        }
    }
}

2-3 分析
cs 复制代码
 [DllImport("user32.dll")]
static extern IntPtr GetActiveWindow();

[DllImport("user32.dll")]
static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
  • 上面这两函数来自user32.dll,他们分别的作用是:
    • GetActiveWindow:获取当前窗口句柄
    • GetWindowRect:获取窗口位置,需要传入窗口句柄
  • 通过上述代码的组合我们就可以获取到当前窗口的位置
cs 复制代码
hwnd = GetActiveWindow();
GetWindowRect(hwnd, out rect)

  • 同时为了接收GetWindowRect传回的参数,我们需要借助cs的特殊机制out = 输出参数(output parameter),他允许函数"直接修改调用者的变量"
  • 因此我们需要构建一个结构体来对接参数
cs 复制代码
public struct RECT
{
    public int left;
    public int top;
    public int right;
    public int bottom;
}
字段 含义
left 左边 X
top 上边 Y
right 右边 X
bottom 下边 Y

cs 复制代码
void CheckTrigger(RECT rect)
{   
	int screenHeight = Screen.currentResolution.height;
	float windowHeight = rect.bottom - rect.top;
	// 计算进入屏幕外的深度
	float depth = rect.bottom - screenHeight;
	//换算成百分比
	float percent = Mathf.Clamp01(depth / windowHeight);
	//打印百分比
	Debug.Log($"percent:{percent}");
	// 判断是否贴近底部
	if (percent >0.9)
	{
		//触发逻辑,这里可以绑定事件去触发图片替换
		//这里测试就设置一次,因为窗口化拖到屏幕外我们是看不到输出的(乐)
		isTrigger = true;
	}
}
  • 紧接着我们只需要计算游戏窗口距离玩家游戏屏幕底端的距离,计算出超出屏幕底端的部分并换算出半分比即可,如果后续游戏逻辑需要实时根据深度进行特殊判断,直接使用换算后的percent即可。

3 测试

  • 确保上述三个脚本在游戏中均被挂载运行,然后点击Filebuild and run

  • 此时会出现我们的游戏窗口,可以看到此时的isTrigger仍是False并没有触发

  • 我们拖动窗口,可以看到percent百分比在增加,直到我们拖动窗口完全消失后再次拖动回来,可以发现isTrigger被设置为True


小结

  • 本文使用Unity复刻Oneshot中的游戏中窗口化拖动到屏幕之外使胶片显形的游戏现象。
  • 如有错误,欢迎指出!
  • 谢谢观看
相关推荐
AIwenIPgeolocation2 小时前
IP地址数据服务:赋能游戏行业体验优化与精细化运营
网络协议·tcp/ip·游戏
迪捷软件2 小时前
显控系统虚拟仿真的工程化路径
游戏引擎·cocos2d
前端不太难4 小时前
AISystem:鸿蒙游戏中的 AI 行为驱动
人工智能·游戏·harmonyos
开开心心就好6 小时前
无品牌限制的手机电视投屏工具推荐
科技·游戏·智能手机·edge·电脑·逻辑回归·powerpoint
凡情6 小时前
android隐私合规检测
android·unity
小贺儿开发6 小时前
Unity3D 本地 Stable Diffusion 文生图效果演示
人工智能·unity·stable diffusion·文生图·ai绘画·本地化
Swift社区7 小时前
传统游戏引擎 vs 鸿蒙 System 架构
架构·游戏引擎·harmonyos
mxwin1 天前
Unity Shader 半透明物体为什么不能写入深度缓冲?
unity·游戏引擎·shader
晚枫歌F1 天前
三层时间轮的实现
网络·unity·游戏引擎