UE5 打包后 EXE 程序单实例的两种实现方法

13 阅读8分钟

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. 核心逻辑说明

  1. 程序启动时,模块加载函数StartupModule会尝试创建指定名称的全局临界区锁;
  2. 若锁已存在(已有程序实例运行),IsValid()返回false,调用FGenericPlatformMisc::RequestExit(true)强制关闭新实例;
  3. 若锁创建成功,程序正常运行;
  4. 程序关闭时,模块卸载函数ShutdownModule自动释放并销毁锁,保证下次启动可正常创建。

二、快捷方式脚本法

该方法无需修改UE5工程代码,通过创建批处理/PowerShell/VBScript脚本检测程序进程或创建系统互斥锁,实现单实例运行,适合打包后快速配置,用户通过点击脚本快捷方式启动程序即可,共提供4种实现方案,各有优劣。

核心使用前提

  1. 将脚本文件放在与UE5打包后的exe文件同目录(或在脚本中填写exe绝对路径);
  2. 对脚本创建桌面快捷方式,后续仅通过该快捷方式启动程序;
  3. 将脚本中的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. 选型建议

  1. 开发阶段/商业项目:选择C++代码法,将单实例逻辑集成到程序本身,避免用户绕开脚本直接启动exe,安全性和稳定性更高;
  2. 测试阶段/快速配置/无C++环境:选择快捷方式脚本法(方案三) ,无需修改工程,快速实现单实例,满足日常使用需求;
  3. 老系统兼容:选择快捷方式脚本法(方案四) ,适配Win7等低版本Windows系统。