【UE5 C++课程系列笔记】27——多线程基础——ControlFlow插件的基本使用
目录
步骤
一、搭建基本同步框架
二、添加委托
三、添加蓝图互动框架
四、修改为异步框架
完整代码
通过一个游戏初始化流程的示例来介绍“ControlFlows”的基本使用。
步骤
一、搭建基本同步框架
1. 勾选“ControlFlows”插件
2. 新建一个空白C++类,这里命名为“ControlFlowSubSystem”
让“ControlFlowSubSystem” 继承“GameInstanceSubsystem”,然后添加反射所需代码
重写父类“ShouldCreateSubsystem”、“Initialize”和“Deinitialize”方法
3. 在Build.cs中添加“ControlFlows”模块
引入所需库
定义一个蓝图可调用的方法“InitLevel”,表示要执行的初始化流程;定义一个布尔类型变量“bIniting”用于表示当前是否处于初始化流程;再定义5个函数用于表示初始化流程的各个步骤。
“InitLevel”实现如下:
首先,通过检查 bIniting
来判断当前是否已经处于初始化过程中。如果 bIniting
为 true
,意味着初始化正在进行或者已经执行过了,此时输出一条日志信息然后直接返回,避免重复执行初始化流程。只有当 bIniting
为 false
时,才会将其设置为 true
,表示即将开始初始化流程。
第31行代码通过调用 FControlFlowStatics::Create
静态函数创建一个 FControlFlow
类型的控制流实例 Flow
。在创建过程中,传入了 this
指针和一个字符串,这个字符串作为控制流的唯一标识符。
第33~37行代码通过多次调用 Flow.QueueStep
函数,向刚创建的控制流实例中依次添加了多个需要按顺序执行的步骤。
第40行代码调用 Flow.ExecuteFlow()
函数启动控制流的执行。此时,FControlFlow
实例会按照之前添加步骤的顺序,依次调用对应的成员函数,确保整个初始化流程按照预定的顺序有条不紊地进行,直到所有步骤都执行完毕,完成整个初始化过程。
用于表示初始化流程步骤的5个函数实现如下,当执行到最后一个步骤时。将 bIniting
改为 false
,表示初始化流程已经结束。
4. 在关卡蓝图中调用“InitLevel”函数
执行结果如下,可以通过日志信息看到完整执行了整个初始化流程。
5. 为了观察每个步骤在哪一帧执行,可以通过添加如下代码实现:
运行结果如下,可以看到所有表示流程步骤的函数都是在同一帧执行的,这可能会造成游戏帧率下降,因此这并不符合我们的需求。
二、添加委托
6. 下面先创建两个委托,通过委托来向外界传递任务进度等信息。
申明两个动态多播委托类型
在第11行代码中,FControlFlows_InitProgress
是要声明的委托类型的名称,FGuid
是UE中用于表示全局唯一标识符(GUID)的类型,在这里它作为委托参数的类型,而 InitAsyncID
是给这个参数起的变量名。当委托被调用时,会传递一个 FGuid
类型的全局唯一标识符。该委托在被调用时,还会传递一个表示进度值的浮点型数据,这个值可以用来直观地展示当前异步初始化任务已经完成的比例或者进度情况。
在第12行代码中,FControlFlows_InitResult
代表所声明的委托类型名称,FGuid
与InitAsyncID
和前面的委托类似,bool
与 bResult
用于指示异步初始化任务最终是成功还是失败,直观地告知结果状态。Message
表示委托调用时还会附带更详细的关于初始化结果的说明。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FControlFlows_InitProgress, FGuid, InitAsyncID, float, ProgressValue); //进度更新
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FControlFlows_InitResult, FGuid, InitAsyncID, bool, bResult, FString, Message); //初始化结果
声明两个委托类型的成员变量,并且通过 UPROPERTY(BlueprintAssignable)
元数据标签对这两个属性进行修饰,使其具备了在蓝图系统中可被绑定具体的回调函数的特性。
7. 为表示初始化步骤的5个成员函数添加进度值输入参数
8. 设置进行“InitLocalAsset”步骤时,初始化进度为10%;进行“InitNetInfo”步骤时,初始化进度为20%;进行“InitUserInfo”步骤时,初始化进度为50%;进行“NotifyMainUI”步骤时,初始化进度为80%;进行“FinishThisInit”步骤时,初始化进度为100%;
9. 声明InitAsyncID
用于唯一标识某次初始化的异步任务
执行“InitLevel”函数进行初始化时为InitAsyncID
赋值
当进行各初始化的步骤时,通过InitProgress
委托广播当前的初始化进度,传入对应的InitAsyncID
和本步骤的当前进度值InProgressValue
,这样外部绑定了该委托的对象就能接收到进度更新情况。
在初始化流程的最后一个步骤对应的函数中,添加如下一行代码,通过InitResult
委托广播当前的初始化结果。
三、添加蓝图互动框架
10. 添加一个控件蓝图,用于显示当前初始化进度,这里命名为“WBP_ControlFlowsMainUI”
打开“WBP_ControlFlowsMainUI”,添加如下控件,主要是添加一个文本控件用于显示进度值,按钮用于调用“InitLevel”从而开始初始化
在事件构造和结构时分别绑定和解绑“InitProgress”、“InitResult”委托
重命名委托绑定的匹配函数为“UpdateProgress”和“InitResult”
匹配函数“UpdateProgress”和“InitResult”函数逻辑如下,将委托传递的GUID、进度值和初始化结果信息打印出来,并显示进度值。如果初始化成功后就将界面隐藏,如果失败就显示进度值为0.00
当按钮点击后调用“InitLevel”开始初始化
11. 在关卡蓝图中让界面显示出来
此时运行后界面如下
点击初始化按钮后打印信息如下:
此时就完成了初始化流程的同步框架实现,接下来我们希望将同步改为异步实现。
四、修改为异步框架
12. 引入所需头文件
13. 更改“InitLocalAsset”、“InitNetInfo”、“InitUserInfo”函数逻辑如下
主要通过使用AsyncTask
函数创建了一个外层的异步任务,并指定其可以在任意线程上执行。在这个异步任务的 lambda 表达式内部首先调用FPlatformProcess::Sleep(0.2)
模拟一个耗时操作,接着又创建了一个内层的异步任务,指定在游戏线程上执行后续的耗时操作,以及向外广播初始化的进度信息。
完善“InitUserInfo”逻辑如下
第81行首先获取位于项目保存目录下的文件名为 ControlFlowsUserInfo.txt
的目标文件相对路径,然后将相对路径转换为绝对路径。
第82行将初始化任务的唯一标识 InitAsyncID
转换为字符串形式赋值给 UserInitAsyncID。
第83行获取当前的日期时间并转换为 HTTP 日期格式赋值给 UserInitDataTime
。
第84~86行创建一个 TArray<FString>
类型的数组 MyStringInfo
,并将前面获取的UserInitAsyncID
和 UserInitDataTime
字符串添加进去,准备将这些信息保存到文件中。
第87行使用 FFileHelper::SaveStringArrayToFile
函数将包含用户初始化相关信息的字符串数组 MyStringInfo
保存到指定路径 UserInfoPath
的 ControlFlowsUserInfo.txt
中,并且指定了编码选项为ForceUTF8WithoutBOM
,确保文件内容以指定的编码格式存储。
编译后运行,此时当我们点击初始化按钮后,看到输出日志信息如下,可以发现初始化流程的步骤不再是一帧内执行的了。
并且在“Saved”文件夹中多了一个名为 ControlFlowsUserInfo.txt
的文件
ControlFlowsUserInfo.txt
的内容如下,包括了初始化任务的唯一标识和文件存储时间。
如果初始化流程中的某一步失败了,通过 InitResult
委托向外广播初始化失败的结果信息,第113行调用 SubFlow->CancelFlow()标识
取消当前正在执行的控制流,从而及时终止整个初始化流程。将 bIniting
变量设置为 false
,表示当前不再处于初始化过程中,同时将 InitAsyncID
重置为默认值,为下一次可能的初始化操作做好准备。
完整代码
“ControlFlowSubSystem.h”
// Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h"
#include "ControlFlow.h"
#include "ControlFlowManager.h"
#include "ControlFlowNode.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Async/Async.h"
#include "ControlFlowSubSystem.generated.h"DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FControlFlows_InitProgress, FGuid, InitAsyncID, float, ProgressValue); //进度更新
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FControlFlows_InitResult, FGuid, InitAsyncID, bool, bResult, FString, Message); //初始化结果#define INITRESULT falseUCLASS()
class STUDY_API UControlFlowSubSystem : public UGameInstanceSubsystem
{GENERATED_BODY()public:virtual bool ShouldCreateSubsystem(UObject* Outer) const override;virtual void Initialize(FSubsystemCollectionBase& Collection) override;virtual void Deinitialize() override;UFUNCTION(BlueprintCallable)void InitLevel();public:UPROPERTY(BlueprintAssignable)FControlFlows_InitProgress InitProgress;UPROPERTY(BlueprintAssignable)FControlFlows_InitResult InitResult;protected:bool bIniting = false;FGuid InitAsyncID = FGuid();void InitLocalAsset(FControlFlowNodeRef SubFlow, double InProgressValue);void InitNetInfo(FControlFlowNodeRef SubFlow, double InProgressValue);void InitUserInfo(FControlFlowNodeRef SubFlow, double InProgressValue);void NotifyMainUI(FControlFlowNodeRef SubFlow, double InProgressValue);void FinishThisInit(FControlFlowNodeRef SubFlow, double InProgressValue);
};
“ControlFlowSubSystem.cpp”
// Fill out your copyright notice in the Description page of Project Settings.#include "ControlFlowSubSystem.h"bool UControlFlowSubSystem::ShouldCreateSubsystem(UObject* Outer) const
{return true;
}void UControlFlowSubSystem::Initialize(FSubsystemCollectionBase& Collection)
{Super::Initialize(Collection);
}void UControlFlowSubSystem::Deinitialize()
{Super::Deinitialize();
}void UControlFlowSubSystem::InitLevel()
{if (bIniting){UE_LOG(LogTemp, Warning, TEXT("Initing..."));return;}InitAsyncID = FGuid::NewGuid();bIniting = true;uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("InitFlow -- FrameIndex: %d"), FrameIndex);FControlFlow& Flow = FControlFlowStatics::Create(this, TEXT("ControlFlow_InitLevel"));Flow.QueueStep(TEXT("InitLocalAsset"), this, &UControlFlowSubSystem::InitLocalAsset, 0.1);Flow.QueueStep(TEXT("InitNetInfo"), this, &UControlFlowSubSystem::InitNetInfo, 0.2);Flow.QueueStep(TEXT("InitUserInfo"), this, &UControlFlowSubSystem::InitUserInfo, 0.5);Flow.QueueStep(TEXT("NotifyMainUI"), this, &UControlFlowSubSystem::NotifyMainUI, 0.8);Flow.QueueStep(TEXT("FinishThisInit"), this, &UControlFlowSubSystem::FinishThisInit, 1.0);UE_LOG(LogTemp, Warning, TEXT("ExecuteFlow -- FrameIndex: %d"), FrameIndex);Flow.ExecuteFlow();
}void UControlFlowSubSystem::InitLocalAsset(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("InitLocalAsset -- FrameIndex: %d"), FrameIndex);AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);InitProgress.Broadcast(InitAsyncID, InProgressValue);SubFlow->ContinueFlow();});});
}void UControlFlowSubSystem::InitNetInfo(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("InitNetInfo -- FrameIndex: %d"), FrameIndex);AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);InitProgress.Broadcast(InitAsyncID, InProgressValue);SubFlow->ContinueFlow();});});
}void UControlFlowSubSystem::InitUserInfo(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("InitUserInfo -- FrameIndex: %d"), FrameIndex);AsyncTask(ENamedThreads::AnyThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);FString UserInfoPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectSavedDir()/TEXT("ControlFlowsUserInfo.txt"));FString UserInitAsyncID = InitAsyncID.ToString();FString UserInitDataTime = FDateTime::Now().ToHttpDate();TArray<FString> MyStringInfo;MyStringInfo.Add(UserInitAsyncID);MyStringInfo.Add(UserInitDataTime);FFileHelper::SaveStringArrayToFile(MyStringInfo, *UserInfoPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);AsyncTask(ENamedThreads::GameThread, [this, SubFlow, InProgressValue]() {FPlatformProcess::Sleep(0.2);InitProgress.Broadcast(InitAsyncID, InProgressValue);SubFlow->ContinueFlow();});});
}void UControlFlowSubSystem::NotifyMainUI(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("NotifyMainUI -- FrameIndex: %d"), FrameIndex);if (INITRESULT){InitProgress.Broadcast(InitAsyncID, InProgressValue);SubFlow->ContinueFlow();}else{AsyncTask(ENamedThreads::GameThread, [this]() {InitResult.Broadcast(InitAsyncID, false, TEXT("Init Failed"));});SubFlow->CancelFlow();bIniting = false;InitAsyncID = {};}
}void UControlFlowSubSystem::FinishThisInit(FControlFlowNodeRef SubFlow, double InProgressValue)
{uint64 FrameIndex = GFrameCounter;UE_LOG(LogTemp, Warning, TEXT("FinishThisInit -- FrameIndex: %d"), FrameIndex);InitProgress.Broadcast(InitAsyncID, InProgressValue);InitResult.Broadcast(InitAsyncID, true, TEXT("Init Success"));SubFlow->ContinueFlow();bIniting = false;InitAsyncID = {};
}
相关文章:

【UE5 C++课程系列笔记】27——多线程基础——ControlFlow插件的基本使用
目录 步骤 一、搭建基本同步框架 二、添加委托 三、添加蓝图互动框架 四、修改为异步框架 完整代码 通过一个游戏初始化流程的示例来介绍“ControlFlows”的基本使用。 步骤 一、搭建基本同步框架 1. 勾选“ControlFlows”插件 2. 新建一个空白C类,这里…...

有收到腾讯委托律师事务所向AppStore投诉带有【水印相机】主标题名称App的开发者吗
近期,有多名开发者反馈,收到来自腾讯科技 (深圳) 有限公司委托北京的一家**诚律师事务所卞,写给AppStore的投诉邮件。 邮件内容主要说的是,腾讯注册了【水印相机】这四个字的商标,所以你们这些在AppStore上的app&…...
标定 3
标定场景与对应的方式 标定板标定主要应用场景: (1)无法获取到执行机构物理坐标值,比如相机固定,执行机构为传送带等 (2)相机存在畸变等非线性标定情况,需要进行畸变校正 (3)标定单像素精度 (4)获取两个相机之间的坐标系关系 标定板操作步骤: (1)确定好拍…...

用 C# 绘制谢尔宾斯基垫片
谢尔宾斯基垫片是一个三角形,分解成多个小三角形,如右图所示。有几种方法可以生成这种垫片。这里展示的方法是其中一种比较令人惊讶的方法。 程序从三个点开始(图中圆圈所示)。“当前位置”从其中一个点开始。为了生成后续点&…...
java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
今天在朋友机子上运行代码,在生成token的时候,遇到了这样一个问题: Caused by: java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter at io.jsonwebtoken.impl.Base64Codec.decode(Base64Codec.java:26) ~[jjwt-0.9.1.jar:0.…...

双因素身份验证技术在NPI区域邮件安全管控上的解决思路
在制造业中,NPI(New Product Introduction,新产品导入)区域是指专门负责新产品从概念到市场推出全过程的部门或团队。NPI 的目标是确保新产品能够高效、高质量地投入生产,并顺利满足市场需求。在支撑企业持续创新和竞争…...

java后端对接飞书登陆
java后端对接飞书登陆 项目要求对接第三方登陆,飞书登陆,次笔记仅针对java后端,在看本笔记前,默认已在飞书开发方已建立了应用,并获取到了appid和appsecret。后端要做的其实很简单,基本都是前端做的&…...

记录一次Android Studio的下载、安装、配置
目录 一、下载和安装 Android Studio 1、搜索下载Android studio 2、下载成功后点击安装包进行安装: 3、这里不用打勾,直接点击安装 : 4、完成安装: 5、这里点击Cancel就可以了 6、接下来 7、点击自定义安装:…...

直流无刷电机控制(FOC):电流模式
目录 概述 1 系统框架结构 1.1 硬件模块介绍 1.2 硬件实物图 1.3 引脚接口定义 2 代码实现 2.1 软件架构 2.2 电流检测函数 3 电流环功能实现 3.1 代码实现 3.2 测试代码实现 4 测试 概述 本文主要介绍基于DengFOC的库函数,实现直流无刷电机控制&#x…...

73.矩阵置零 python
矩阵置零 题目题目描述示例 1:示例 2:提示: 题解思路分析Python 实现代码代码解释提交结果 题目 题目描述 给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 示例…...
垃圾收集算法
分代收集理论 分代收集理论,建立在两个分代假说之上。 弱分代假说:绝大多数对象都是朝圣夕灭的。 强分代假说:熬过越多次垃圾收集的过程的对象就越难以消亡。 这两个分代假说奠定了垃圾收集器的一致设计原则:收集器应该将Java…...
SQL-leetcode-262. 行程和用户
262. 行程和用户 表:Trips --------------------- | Column Name | Type | --------------------- | id | int | | client_id | int | | driver_id | int | | city_id | int | | status | enum | | request_at | varchar | --------------------- id 是这张表的主键…...

太原理工大学软件设计与体系结构 --javaEE
这个是简答题的内容 选择题的一些老师会给你们题库,一些注意的点我会做出文档在这个网址 项目目录预览 - TYUT复习资料:复习资料 - GitCode 希望大家可以给我一些打赏 什么是Spring的IOC和DI IOC 是一种设计思想,它将对象的创建和对象之间的依赖关系…...

Leetcode 139. 单词拆分 动态规划
原题链接:Leetcode 139. 单词拆分 递归,超时 class Solution { public:bool isfind(string s,map<string,int>& mp){for(auto x:mp){string wordx.first;if(sword) return true;int nword.size();if(n>s.size()) continue;string s1s.subs…...

python异常机制
异常是什么? 软件程序在运行过程中,非常可能遇到刚刚提到的这些问题,我们称之为异常,英文是Exception,意思是例外。遇到这些例外情况,或者交异常,我们怎么让写的程序做出合理的处理,…...
运行爬虫时可能遇到哪些常见问题?
在运行Python爬虫时,可能会遇到以下一些常见问题及相应的解决方法: 1. 请求频繁被封 IP 问题描述:爬虫请求频繁时,网站可能会识别到异常行为并封禁 IP,从而导致后续请求失败。解决方法: 使用代理…...

BGP与CN2的区别 详解两者在网络传输中的应用与优势
在现代互联网环境中,选择合适的网络传输协议和解决方案对于企业的业务运行至关重要。BGP(Border Gateway Protocol)和CN2(China Telecom Next Carrier Network)是两种广泛应用的网络技术,但它们的设计理念、…...

Spring 项目 基于 Tomcat容器进行部署
文章目录 一、前置知识二、项目部署1. 将写好的 Spring 项目先打包成 war 包2. 查看项目工件(Artifact)是否存在3. 配置 Tomcat3.1 添加一个本地 Tomcat 容器3.2 将项目部署到 Tomcat 4. 运行项目 尽管市场上许多新项目都已经转向 Spring Boot࿰…...
“负载均衡”出站的功能、原理与场景案例
在企业日常网络中,外网访问速度不稳定是一个常见问题。特别是多条外网线路并行时,不合理的流量分配会导致资源浪费甚至网络拥堵。而出站负载均衡,正是解决这一问题的关键技术。 作为一种先进的网络流量管理技术,其核心是优化企业内…...

02-51单片机数码管与矩阵键盘
一、数码管模块 1.数码管介绍 如图所示为一个数码管的结构图: 说明: 数码管上下各有五个引脚,其中上下中间的两个引脚是联通的,一般为数码管的公共端,分为共阴极或共阳极;其它八个引脚分别对应八个二极管…...

C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...

Windows安装Miniconda
一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客...

C++ 设计模式 《小明的奶茶加料风波》
👨🎓 模式名称:装饰器模式(Decorator Pattern) 👦 小明最近上线了校园奶茶配送功能,业务火爆,大家都在加料: 有的同学要加波霸 🟤,有的要加椰果…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现企业微信功能
1. 开发环境准备 安装DevEco Studio 3.1: 从华为开发者官网下载最新版DevEco Studio安装HarmonyOS 5.0 SDK 项目配置: // module.json5 {"module": {"requestPermissions": [{"name": "ohos.permis…...
django blank 与 null的区别
1.blank blank控制表单验证时是否允许字段为空 2.null null控制数据库层面是否为空 但是,要注意以下几点: Django的表单验证与null无关:null参数控制的是数据库层面字段是否可以为NULL,而blank参数控制的是Django表单验证时字…...

关于easyexcel动态下拉选问题处理
前些日子突然碰到一个问题,说是客户的导入文件模版想支持部分导入内容的下拉选,于是我就找了easyexcel官网寻找解决方案,并没有找到合适的方案,没办法只能自己动手并分享出来,针对Java生成Excel下拉菜单时因选项过多导…...

JDK 17 序列化是怎么回事
如何序列化?其实很简单,就是根据每个类型,用工厂类调用。逐个完成。 没什么漂亮的代码,只有有效、稳定的代码。 代码中调用toJson toJson 代码 mapper.writeValueAsString ObjectMapper DefaultSerializerProvider 一堆实…...
raid存储技术
1. 存储技术概念 数据存储架构是对数据存储方式、存储设备及相关组件的组织和规划,涵盖存储系统的布局、数据存储策略等,它明确数据如何存储、管理与访问,为数据的安全、高效使用提供支撑。 由计算机中一组存储设备、控制部件和管理信息调度的…...
Yii2项目自动向GitLab上报Bug
Yii2 项目自动上报Bug 原理 yii2在程序报错时, 会执行指定action, 通过重写ErrorAction, 实现Bug自动提交至GitLab的issue 步骤 配置SiteController中的actions方法 public function actions(){return [error > [class > app\helpers\web\ErrorAction,],];}重写Error…...