Unity如何让游戏在外部启动器指定的显示器启动?
解决方案:通过全局坐标定位目标显示器
核心思路
放弃依赖Unity的-monitor参数(Electron与Unity的显示器枚举顺序无统一标准,极易不匹配),改为传递启动器窗口的全局中点坐标给Unity,再通过原生系统API获取显示器的全局布局,匹配坐标后指定游戏启动的显示器。
步骤1:Electron端传递全局中点坐标
在Electron启动器中,计算窗口的全局中点坐标,作为命令行参数传给Unity游戏:
// Electron主进程代码 const { app, BrowserWindow } = require('electron'); let mainWindow; app.whenReady().then(() => { mainWindow = new BrowserWindow({ /* 你的窗口配置 */ }); // 监听窗口关闭事件,启动Unity游戏 mainWindow.on('close', (e) => { e.preventDefault(); const bounds = mainWindow.getBounds(); // 计算全局中点坐标(Electron的getBounds返回的是桌面全局坐标) const midX = bounds.x + bounds.width / 2; const midY = bounds.y + bounds.height / 2; // 启动Unity游戏,传入坐标参数 const { spawn } = require('child_process'); spawn('YourUnityGame.exe', [`--launcher-mid=${midX},${midY}`]); mainWindow.destroy(); }); });
步骤2:Unity端解析坐标并匹配显示器
Unity本身没有直接获取显示器全局坐标的API,需要通过原生系统API实现,以下是Windows平台的示例:
1. 编写Win32 API调用的C#封装
在Unity中创建C#脚本,引入Win32函数实现显示器全局布局查询:
using System; using System.Runtime.InteropServices; using UnityEngine; public class DisplayLocator : MonoBehaviour { // Win32 API 结构体与函数定义 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] private struct MONITORINFOEX { public int cbSize; public RECT rcMonitor; public RECT rcWork; public uint dwFlags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string szDevice; } [StructLayout(LayoutKind.Sequential)] private struct RECT { public int left; public int top; public int right; public int bottom; } private delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumDelegate lpfnEnum, IntPtr dwData); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi); private static int targetDisplayIndex = 0; void Awake() { // 解析命令行参数 string[] args = Environment.GetCommandLineArgs(); foreach (string arg in args) { if (arg.StartsWith("--launcher-mid=")) { string[] coords = arg.Split('=')[1].Split(','); if (coords.Length == 2 && int.TryParse(coords[0], out int midX) && int.TryParse(coords[1], out int midY)) { FindDisplayByGlobalPoint(midX, midY); // 激活目标显示器 if (targetDisplayIndex < Display.displays.Length) { Display.displays[targetDisplayIndex].Activate(); // 可选:强制设置对应显示器的分辨率 Screen.SetResolution(Screen.currentResolution.width, Screen.currentResolution.height, true, targetDisplayIndex); } } break; } } } private void FindDisplayByGlobalPoint(int x, int y) { int displayIndex = 0; EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (hMonitor, hdcMonitor, ref RECT lprcMonitor, dwData) => { MONITORINFOEX monitorInfo = new MONITORINFOEX(); monitorInfo.cbSize = Marshal.SizeOf(monitorInfo); GetMonitorInfo(hMonitor, ref monitorInfo); // 判断中点是否在当前显示器的全局矩形范围内 if (x >= monitorInfo.rcMonitor.left && x <= monitorInfo.rcMonitor.right && y >= monitorInfo.rcMonitor.top && y <= monitorInfo.rcMonitor.bottom) { targetDisplayIndex = displayIndex; return false; // 找到目标后停止枚举 } displayIndex++; return true; }, IntPtr.Zero); } }
2. 部署脚本
将该脚本挂载到Unity场景的持久化GameObject(如主相机)上,确保游戏启动时自动执行Awake逻辑。
macOS平台适配补充
若需支持macOS,替换上述Win32部分为Cocoa API调用,核心逻辑一致:
using System; using System.Runtime.InteropServices; using UnityEngine; public class DisplayLocatorMac : MonoBehaviour { [DllImport("libobjc.dylib")] private static extern IntPtr objc_getClass(string className); [DllImport("libobjc.dylib")] private static extern IntPtr sel_registerName(string selectorName); [DllImport("libobjc.dylib")] private static extern IntPtr msgSend(IntPtr receiver, IntPtr selector); [DllImport("libobjc.dylib")] private static extern IntPtr msgSend(IntPtr receiver, IntPtr selector, IntPtr arg1); // 自定义NSRect结构体对应macOS的CGRect [StructLayout(LayoutKind.Sequential)] private struct NSRect { public float x; public float y; public float width; public float height; } void Awake() { // 解析命令行参数逻辑同Windows // 调用macOS显示器查找逻辑 // FindDisplayByGlobalPoint(midX, midY); } private void FindDisplayByGlobalPoint(int x, int y) { IntPtr nsScreenClass = objc_getClass("NSScreen"); IntPtr screensSel = sel_registerName("screens"); IntPtr screensArray = msgSend(nsScreenClass, screensSel); IntPtr countSel = sel_registerName("count"); int screenCount = (int)msgSend(screensArray, countSel); for (int i = 0; i < screenCount; i++) { IntPtr objectAtIndexSel = sel_registerName("objectAtIndex:"); IntPtr screen = msgSend(screensArray, objectAtIndexSel, (IntPtr)i); IntPtr frameSel = sel_registerName("frame"); NSRect frame = Marshal.PtrToStructure<NSRect>(msgSend(screen, frameSel)); // 判断中点是否在当前显示器的全局frame内 if (x >= frame.x && x <= frame.x + frame.width && y >= frame.y && y <= frame.y + frame.height) { targetDisplayIndex = i; break; } } } }
关键注意事项
- Windows平台无需额外打包DLL,直接调用系统
user32.dll即可;macOS平台需确保脚本在打包时被正确编译。 - 测试时覆盖多显示器的各种排列方式(左右并排、上下堆叠、不同分辨率),验证中点坐标的匹配准确性。
- 若启动器跨多屏,Electron的
getBounds()返回的是全局桌面坐标,无需额外转换。
内容的提问来源于stack exchange,提问作者MiffTheFox
相关产品推荐
相关产品推荐

