UE5 打包后 EXE 程序单实例的两种实现方法
UE5打包后exe程序避免多次打开的两种实现方法
本文整理了UE5打包后防止exe程序多开的两类解决方案,分别为C++代码实现法(基于引擎底层互斥锁,适合开发阶段集成)和快捷方式脚本法(基于系统脚本检测,适合打包后快速配置),可根据开发需求和使用场景选择。
一、C++代码实现法
该方法通过在UE5工程中添加系统级临界区“锁”,实现打包后程序的单实例运行,仅在打包发布版本生效,不影响编辑器开发,核心是利用FWindowsSystemWideCriticalSection创建全局唯一锁,检测到锁已存在时直接关闭新程序实例。
1. 工程前提
创建基于C++的UE5工程,工程会自动生成与项目名同名的.h和.cpp核心模块文件(示例工程名:ACT)。
2. 头文件(.h)编写
在项目同名头文件中重载模块加载/卸载方法,并声明临界区锁对象,代码如下:
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
// 继承自FDefaultGameModuleImpl
class ACT: public FDefaultGameModuleImpl
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override; // 重载模块加载方法
virtual void ShutdownModule() override;// 重载模块卸载方法
private:
FWindowsSystemWideCriticalSection* Check; // 声明全局临界区“锁”对象
};
3. 源文件(.cpp)编写
在源文件中实现模块加载/卸载的具体逻辑,添加编辑器宏判断、锁创建检测和锁释放操作,注意修改模块名称为项目实际名称,代码如下:
// Fill out your copyright notice in the Description page of Project Settings.
#include "ACT.h"
#include "Modules/ModuleManager.h"
#include "WindowsCriticalSection.h"
// 替换为自身项目名,需与工程名一致,否则模块加载函数不执行
IMPLEMENT_PRIMARY_GAME_MODULE( ACT, ACT, "ACT" );
// 模块加载(程序启动时执行)
void ACT::StartupModule()
{
// 宏判断:仅打包发布版本执行加锁逻辑,编辑器版本不生效
#if !WITH_EDITOR
// 创建全局唯一锁,名称自定义(建议UE5-项目名Game格式)
Check = new FWindowsSystemWideCriticalSection(TEXT("#UE5-ACTGame"));
if (Check->IsValid()) // 锁创建成功,正常启动程序
{
}
else // 锁创建失败,说明已有程序实例运行,请求关闭新程序
{
FGenericPlatformMisc::RequestExit(true);
}
#else
#endif
}
// 模块卸载(程序关闭时执行)
void ACT::ShutdownModule()
{
// 锁对象有效时,释放并销毁锁,避免系统资源占用
if(Check)
{
Check->Release(); // 释放临界区锁
delete Check; // 销毁锁对象
Check = nullptr; // 置空指针,防止野指针
}
}
4. 核心逻辑说明
- 程序启动时,模块加载函数
StartupModule会尝试创建指定名称的全局临界区锁; - 若锁已存在(已有程序实例运行),
IsValid()返回false,调用FGenericPlatformMisc::RequestExit(true)强制关闭新实例; - 若锁创建成功,程序正常运行;
- 程序关闭时,模块卸载函数
ShutdownModule自动释放并销毁锁,保证下次启动可正常创建。
二、快捷方式脚本法
该方法无需修改UE5工程代码,通过创建批处理/PowerShell/VBScript脚本检测程序进程或创建系统互斥锁,实现单实例运行,适合打包后快速配置,用户通过点击脚本快捷方式启动程序即可,共提供4种实现方案,各有优劣。
核心使用前提
- 将脚本文件放在与UE5打包后的
exe文件同目录(或在脚本中填写exe绝对路径); - 对脚本创建桌面快捷方式,后续仅通过该快捷方式启动程序;
- 将脚本中的
XXX.exe/XXX替换为实际的exe文件名(不含后缀)。
方案一:简易批处理脚本(最基础,存在轻微竞争条件)
优点:代码简单、易编写;缺点:高频率点击时可能检测失效,无窗口置前功能。
创建.bat后缀文件,代码如下:
@echo off
setlocal
:: 替换为实际的exe文件名(含后缀)
set "EXE_NAME=XXX.exe"
:: exe绝对路径,同目录可直接写%EXE_NAME%
set "EXE_PATH=C:\Users\ASUS\Desktop\Windows\XXX\Binaries\Win64\XXX.exe"
:: 检查进程是否已运行
tasklist /FI "IMAGENAME eq %EXE_NAME%" 2>NUL | find /I "%EXE_NAME%" >NUL
if %ERRORLEVEL% equ 0 (
echo 程序已在运行中...
timeout /t 2 /nobreak >NUL
exit /b
)
:: 进程未运行,启动程序
echo 启动程序...
start "" "%EXE_PATH%"
endlocal
方案二:PowerShell增强批处理(可靠,支持窗口置前)
优点:检测更稳定,可将已运行的程序窗口前置;缺点:依赖PowerShell环境。
创建.bat后缀文件,代码如下:
@echo off
setlocal
:: 替换为实际的exe绝对路径,同目录可直接写XXX.exe
set "EXE_PATH=C:\Users\ASUS\Desktop\Windows\XXX\Binaries\Win64\XXX.exe"
:: PowerShell检测进程,XXX替换为exe文件名(不含后缀)
powershell -Command "if (Get-Process -Name 'XXX' -ErrorAction SilentlyContinue) { exit 1 } else { exit 0 }"
if %ERRORLEVEL% equ 1 (
echo 程序已在运行中,将已运行的窗口置前...
powershell -Command "Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.Interaction]::AppActivate('XXX')"
timeout /t 1 /nobreak >NUL
exit /b
)
:: 进程未运行,启动程序
echo 启动程序...
start "" "%EXE_PATH%"
endlocal
方案三:互斥锁PowerShell脚本(最可靠,绝对单实例)
优点:使用系统级互斥锁,彻底避免多开,支持窗口置前;缺点:需创建两个脚本文件,需隐藏命令行窗口。
该方案是推荐方案,通过System.Threading.Mutex创建全局互斥锁,保证绝对单实例,分为PowerShell脚本和VBScript脚本(用于隐藏命令行窗口)。
步骤1:创建PowerShell脚本(check_and_run.ps1)
创建.ps1后缀文件,代码如下:
# 隐藏PowerShell控制台窗口
Add-Type -Name Window -Namespace Console -MemberDefinition '
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
'
$consolePtr = [Console.Window]::GetConsoleWindow()
[Console.Window]::ShowWindow($consolePtr, 0) | Out-Null
# 自定义互斥锁名称,建议项目名_SingleInstance_Mutex格式
$MutexName = "Global\XXX_SingleInstance_Mutex"
# 替换为实际exe文件名,同目录直接写,否则填绝对路径
$ExePath = "XXX.exe"
try {
# 创建互斥锁
$Mutex = [System.Threading.Mutex]::new($false, $MutexName)
# 尝试获取锁,0表示立即检测,失败则说明已有实例
if (!$Mutex.WaitOne(0)) {
Write-Host "程序已在运行中..."
# 窗口置前,XXX替换为exe文件名(不含后缀)
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.Interaction]::AppActivate("XXX")
Start-Sleep -Seconds 2
exit
}
# 获取锁成功,启动程序
Write-Host "启动程序..."
$process = Start-Process -FilePath $ExePath -PassThru
# 等待程序退出,保证锁正常释放
$process.WaitForExit()
} finally {
# 释放并销毁互斥锁,避免资源占用
if ($Mutex -ne $null) {
$Mutex.ReleaseMutex()
$Mutex.Dispose()
}
}
步骤2:创建VBScript脚本(launch_ps.vbs)
用于隐藏PowerShell的命令行窗口,创建.vbs后缀文件,代码如下:
' 隐藏窗口启动PowerShell脚本,同目录直接写check_and_run.ps1,否则填绝对路径
CreateObject("WScript.Shell").Run "powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File ""check_and_run.ps1""", 0, False
步骤3:使用方式
直接对launch_ps.vbs创建桌面快捷方式,可自定义快捷方式图标,点击该快捷方式启动程序即可。
方案四:VBScript脚本(兼容性最好,适合老系统)
优点:纯VBScript编写,兼容Windows老系统(如Win7),无需依赖其他环境;缺点:功能简单,无窗口置前。
创建.vbs后缀文件,代码如下:
' XXX_Launcher.vbs
Set wmi = GetObject("winmgmts:{impersonationLevel=impersonate}!\.\root\cimv2")
' 替换为实际exe文件名(含后缀)
Set processes = wmi.ExecQuery("SELECT * FROM Win32_Process WHERE Name='XXX.exe'")
If processes.Count > 0 Then
' 程序已运行,弹出提示框,2秒后自动关闭
Set shell = CreateObject("WScript.Shell")
shell.Popup "程序已在运行中!", 2, "提示", 64
Else
' 程序未运行,启动程序,替换为exe绝对路径
Set shell = CreateObject("WScript.Shell")
shell.Run """C:\Users\ASUS\Desktop\Windows\XXX\Binaries\Win64\XXX.exe""", 1, False
End If
4种脚本方案对比
| 方案 | 核心优点 | 核心缺点 | 适用场景 |
|---|---|---|---|
| 方案一 | 代码最简单、易修改 | 存在竞争条件,高频率点击可能失效 | 测试环境、对稳定性要求低的场景 |
| 方案二 | 检测稳定,支持窗口置前 | 依赖PowerShell环境 | 主流Windows系统(Win10/11),需要窗口前置功能 |
| 方案三 | 系统互斥锁,绝对单实例,支持窗口置前 | 需创建两个脚本文件 | 生产环境、对单实例要求严格的场景(推荐) |
| 方案四 | 兼容性最好,支持老系统,无命令行窗口 | 无窗口置前,功能简单 | Windows老系统(Win7及以下) |
三、两种方法整体对比与选型建议
1. 整体对比
| 实现方法 | 开发侵入性 | 生效范围 | 配置复杂度 | 稳定性 | 适用阶段 |
|---|---|---|---|---|---|
| C++代码法 | 需修改UE5工程代码 | 仅打包发布版本,不影响编辑器 | 中等(需编写C++代码) | 极高(引擎底层实现) | 开发阶段、需集成到程序本身 |
| 快捷方式脚本法 | 无侵入,不修改工程代码 | 仅通过脚本快捷方式启动时生效 | 低(直接编写脚本,无需开发) | 高(方案三接近绝对稳定) | 打包后、快速配置,或无C++开发环境的场景 |
2. 选型建议
- 开发阶段/商业项目:选择C++代码法,将单实例逻辑集成到程序本身,避免用户绕开脚本直接启动exe,安全性和稳定性更高;
- 测试阶段/快速配置/无C++环境:选择快捷方式脚本法(方案三) ,无需修改工程,快速实现单实例,满足日常使用需求;
- 老系统兼容:选择快捷方式脚本法(方案四) ,适配Win7等低版本Windows系统。