type
status
date
slug
summary
tags
category
icon
password
fullWidth
fullWidth
【UE】代码设置TriggerVolume后视窗内未更新
问题描述
用代码设置了TriggerVolume的变换之后,视窗内Actor的形状和碰撞都没有更新,需要去面板上“点”一下才行(点哪都行);而面板上的数据已更新和代码设置的一致,由此判断是面板数据不回传导致。
问题分析
改动面板数据后,视窗内物体的属性发生变化,是由于调用了
PostEditChangeProperty
这个回调函数,在这里用Brush->BuildBound
重建形状并用GEditor->RebuildAlteredBSP()
更新碰撞。参考
Brush
类的PostEditChangeProperty
的实现:解决方法
在
BrushBuilder
类重写的PostEditChangeProperty
的方法里,仿照上述方法,调用BuildBound
以重新构建TriggerVolume的形状,调用RebuildAlteredBSP
以重新构建碰撞数据:- BuildBound:
- 用来重建Brush的边界(Bounding Box)的函数。
- 当Brush的形状或位置发生变化时,需要重新计算它的包围盒。
- RebuildAlteredBSP:
- 用来重建BSP(二叉空间分割)树的函数,BSP是UE用于空间分割和碰撞检测的数据结构。
- 当Brush的形状、位置或属性改变时,需要重建BSP以保证碰撞和渲染正确。
接口封装:
【UE】得到鼠标射线向量的接口
需求描述
编辑时用鼠标射线绘制地形、样条是非常实用的功能,考虑封装一个接口。
已有实现
UEditorLevelLibrary
类的GetLevelViewportCameraInfo
方法可以获取当前关卡编辑器的视口摄像机位置和旋转,但这不太有参考价值,这个需求要的是把鼠标的屏幕坐标映射成世界坐标和方位射线。解决方法
从别的模块抄(复制)一个
DeprojectScreenToWorld
方法来,把鼠标的屏幕坐标映射到世界坐标,并得到鼠标射线方向向量。方法实现:
接口封装:
【UE】根据端口号获取运行中进程的exe路径
需求描述
起服之前要查询服务器端口是否被引用进程占用,若被占用则杀死引用进程;因此需要一个接口,根据指定的端口号查找所有占用该端口的进程的可执行文件路径。
解决方法
- 调用
GetTcpTable2
函数获取当前系统的TCP连接表(第一次调用获取所需缓冲区大小,分配足够内存后再次调用获取实际数据)。
- 查找本地端口等于目标端口的连接(注意要将本地端口号从网络字节序转换为主机字节序),通过进程ID打开进程句柄,用
GetModuleFileNameEx
获取可执行文件路径。
MIB_TCPTABLE2
结构体仅包含dwOwningPid
(进程ID),没有直接存储进程的可执行文件路径,必须用OpenProcess
打开进程,才能进一步查询其路径。- 关闭进程句柄,释放TCP表内存。
- 使用
TSet
自动去重后转为TArray
返回。
【UE】输出参数模式
在虚幻引擎的C++代码中,许多API(如
DeprojectScreenToWorld
)采用通过参数传递并内部修改的设计模式。用法
虚幻引擎中这类API通常遵循以下实现惯例:
- 参数命名:输出参数通常以
Out
前缀标识
- 返回值:通常返回
bool
或状态码表示操作是否成功
目的
广泛使用输出参数模式主要出于以下考虑:
- 多返回值需求
- C++原生不支持直接返回多个值,因此通过引用或指针传递多个输出结果
- 性能优化
- 直接修改传入的引用或指针,避免了返回大型对象(如
FHitResult
)时的拷贝开销 - 允许调用者复用已分配的对象(如重复使用同一个
FHitResult
以减少动态内存分配)
- 状态反馈
- 许多函数需要返回
bool
表示操作是否成功,而实际结果通过参数传递 - 调用者可立刻检查
bool
值从而无需处理异常数据
【Python】Tkinter线程通信解决UI卡顿
问题描述
信息的获取要请求服务器,此过程耗时,会阻塞线程导致UI卡顿。而获取信息后要呈现在面板上,因此需要有一个回调到主线程的机制(Tkinter是单线程GUI工具包,所有GUI操作,包括更新界面、处理事件,必须在主线程中执行)。
解决方法
把耗时的信息获取操作做成异步,放在子线程中处理;得到的信息通过
Tkinter
的after
方法回到主线程,并呈现在UI上。after()
的工作原理——通过事件队列实现跨线程通信- 事件队列机制
Tkinter 的主线程运行一个事件循环(
mainloop()
),不断检查事件队列(如按钮点击、窗口重绘等)。after()
的作用是向这个队列插入一个自定义事件。
- 线程安全的通信
当其他线程调用
after(0, callback)
时,Tkinter会将callback
封装成一个事件,安全地插入主线程的事件队列。主线程在下次处理事件时,会执行这个回调。
- 主线程启动异步任务
__async_fetch_and_log
- 异步任务
__async_fetch_and_log
中获取版本信息
__log_results_on_main_thread
中log信息通过Tkinter
的after
方法回到主线程
【Python】使用watchdog库监听文件内容变化
问题描述
有一个业务场景:UE编辑器抛异常时会把异常信息写入到
error.txt
中,使用 Python 开发的工作小助手如果处于后台运行模式,需要监听 error.txt
文件的变化,若发生变化则弹出报错弹窗提示用户联系专人处理。解决方法
使用第三方库
watchdog
。watchdog
是一个 Python 库,用于监听文件系统事件(如创建、修改、删除、移动等),内部实现基于操作系统提供的底层文件监控机制:- Windows:
ReadDirectoryChangesW
- Linux:
inotify
- macOS:
FSEvents
以Window平台为例,WindowsApiEmitter将更底层的系统 API 封装其中:
watchdog
的核心组成:Observer
:启动监听线程,管理事件调度- 在后台运行,监听文件系统变化
- 使用
schedule()
方法注册要监听的目录和事件处理器 - 调用
start()
启动监听线程,stop()
停止监听
FileSystemEventHandler
:处理文件系统事件的基类,可以自定义回调方法on_modified(event)
→ 文件被修改on_created(event)
→ 文件被创建on_deleted(event)
→ 文件被删除on_moved(event)
→ 文件被移动/重命名on_any_event(event)
→ 所有事件都会触发
observer
监听文件变化并处理事件,调试面板新增两个子线程:- 主监控线程(
observer.start()
):由observer.start()
直接创建,管理文件系统事件的监听
- 事件分发线程(
emitter.start()
):watchdog
内部自动创建,用于解耦事件捕获和事件处理,将文件系统事件从内核传递到Python回调
BaseObserver.start()
内部实现:最小化到托盘后启动
observer
:从托盘恢复窗口后关闭
observer
:【Python】子进程执行.bat脚本并捕获输出
需求描述
在开发Git仓库部署工具时,需要执行一个.bat脚本,并实时捕获脚本的输出信息显示在GUI界面上,供使用者查看。这个需求可以拆解成两个问题:
- 子线程控制主线程的GUI变化
- 主进程捕获子进程的控制台输出内容
解决方法
主线程中启动一个子线程,里面用子进程跑.bat脚本:
threading
模块启动子线程,通过self.after()
将GUI更新回调到主线程。
subprocess
模块在子线程中启动子进程,利用管道(PIPE)捕获输出;在死循环中读取和处理数据。

self.after()
是Tkinter
提供的一个核心方法,用于在主线程中延迟执行回调函数,允许从其他线程向主线程安全提交GUI操作请求。
代码:
【网络】本地回环地址和通配地址端口占用问题
问题描述
最近在调试一个本地游戏服务器时,发现一个有趣的现象:
- 先用 Python 启动一个 Socket 监听
127.0.0.1:9500
。
- 然后启动游戏服务器,默认监听
0.0.0.0:9500
。
- 结果发现两个服务都能正常运行,没有端口冲突。
控制台显示:
通配地址
0.0.0.0
和回环地址127.0.0.1
地址 | 0.0.0.0 | 127.0.0.1 (localhost) |
作用 | 监听所有网络接口(本机+外部) | 仅监听本地回环(本机内部通信) |
可访问性 | 外部设备可以访问(如手机、其他电脑) | 仅本机可以访问 |
典型用途 | Web 服务器、游戏服务器、数据库 | 本地开发调试、内部进程通信 |
原因分析
TCP套接字由
(IP, Port)
唯一标识,而不是单纯靠端口号。因此 Python 程序监听的127.0.0.1:9500
和游戏服务器监听的 0.0.0.0:9500
是两个不同的监听端点,不会冲突。但如果让Python程序的套接字绑定到 0.0.0.0:9500
再启动服务器,发现发生端口冲突,服务器无法启动。【UE】通过源码了解Slate Widget和UMG Widget
Slate Widget
Slate是完全代码化的,所有的布局和组件创建只能用C++实现:
- 命名规则:类名以S开头
- 依赖框架:底层的UI控件,直接处理渲染、输入、布局
- 生命周期:没有垃圾回收(GC)机制,通过
TSharedPtr
智能指针手动管理内存
- 反射:无法序列化,不支持保存和加载
UMG Widget
UMG是基于Slate封装开发的GUI,可以认为UMG是Slate的壳:
- 命名规则:类名以U开头
- 依赖框架:基于Slate的高级封装
- 生命周期:依赖
UObject
系统进行垃圾回收,自动管理内存
- 反射:可以在蓝图中序列化,支持保存和加载
对比总结
- 基于
UWidget
的类会直接显示在项目文件夹中,并且可以被蓝图生成
- 基于
SWidget
的类无法在项目中看到,但是可以在C++中进行使用
特性 | S 开头(Slate) | U 开头(UMG) |
框架 | 底层的Slate UI框架 | 基于Slate的高级封装 |
内存管理 | TSharedPtr 手动管理 | UObject 自动 GC |
编程方式 | 仅C++ | C++和蓝图 |
反射(Reflection) | ❌不支持 | ✅支持 |
使用场景 | 引擎编辑器UI、复杂自定义UI | 游戏运行时UI、蓝图可视化UI |
性能 | ⚡更高(直接控制) | ⏳稍低(有额外封装) |
继承关系 | 继承自 SWidget | 继承自 UWidget |
示例控件 | SScrollBox , SButton | UScrollBox , UButton |
应用案例
下面解读UE源码,了解原生UMG控件类
UButton
是如何封装原生Slate控件类SButton
的
Slate Widget
Slate在C++中有两种创建方式:
- 使用
SNew()
- 使用
SAssignNew()
先来看看
SButton.h
的源码,开头SLATE_BEGIN_ARGS
和结尾SLATE_END_ARGS
中间包裹的是构造参数列表和宏定义参数- 构造参数列表:在
SNew()
或SAssignNew()
运行时实例化控件时可选择性传入的参数 - 参数必须以
_
开头 ()
中可以指定默认值
- 宏定义参数:用
SLATE_DEFAULT_SLOT
、SLATE_STYLE_ARGUMENT
等宏包裹的参数 - 是控件类的元数据,用于生成
FArguments
结构体 - 构造参数列表中的参数必须来自于宏定义参数列表,否则会报错(如
"_WidgetStyle": 不是 "SDoubleScrollBox::FArguments"的成员
)
再来看看
SButton.cpp
的源码,注意Construct
和OnPaint
两个重要方法Construct
方法是Slate控件的构造函数- 构建控件层级结构
- 设置参数默认值
- 初始化控件的内部状态
- 绑定事件处理函数
OnPaint
方法在控件状态变化时调用- 绘制控件的视觉表现
- 处理不同状态下的样式变化(正常、悬停、按下、禁用等)
UMG Widget
看看
Button.h
的源码,Slate控件SButton
的智能指针MyButton
储存在UButton
类中看看
Button.cpp
的源码,发现它通过重写RebuildWidget
,用SNew()
或SAssignNew()
创建底层的Slate控件SButton
的实例UButton
重写属性同步方法SynchronizeProperties
,控件首次创建和蓝图中修改属性时调用UButton
重写资源释放方法ReleaseSlateResources
,在控件销毁时调用【UE】平台宏包裹UFUNCTION导致某些平台UHT生成的代码报编译失败
UHT简介
UHT(Unreal Header Tool)是虚幻引擎的代码生成工具,负责解析C++头文件中的反射标记(如
UFUNCTION
、UCLASS
、UPROPERTY
),并生成对应的反射代码:*.generated.h
:包含反射元数据
*.gen.cpp
:反射的具体实现代码
UTH如何支持反射
UTH的目的就是支持虚幻的反射系统,实现:
- 蓝图与C++的交互(如在蓝图中调用
UFUNCION
标记的函数)
- 序列化和反序列化(
UPROPERTY
的保存和加载)
- 编辑器属性面板(
UCLASS
的细节定制)
但是C++本身没有运行时反射,所以虚幻需要UHT在编译前生成额外的代码,让引擎能动态获取类、函数、变量等信息。其工作流程如下:
- 编译前运行
- 在UnrealBuildTool(UBT)调用编译器之前,UHT先扫描所有头文件
- 解析
UCLASS
、UFUNCTION
、UPROPERTY
等标记
- 生成反射代码
- 生成
.generated.h
和.gen.cpp
,包含类、属性、函数的反射信息
- 编译器处理生成的代码
- 生成的代码会和引擎C++代码一起编译,实现运行时反射
问题描述
在开发一个引擎插件时,使用了平台宏
#if PLATFORM_WINDOWS
-#endif
分别包裹一个UFUNCTION
的声明和实现,通过了Windows平台的自动化测试,MacOS平台却编译失败了,报错:…/MyClass.gen.cpp:144:5:2: error: no member names ‘MyFunction’ in ‘MyClass’
原因分析
UHT在编译预处理之前运行,所以它不管当前平台,都会记录
MyFunction
的存在,但在MacOS编译时,PLATFORM_WINDOWS
是false
,导致:- UHT生成了
MyFunction
的反射代码(在MyClass.gen.cpp
里)
- 但
MyFunction
的C++实现被平台宏排除了,所以链接器找不到MyFunction
从而报错
解决方法
一个方法是将Windows平台专用代码移到单独文件(UHT不会处理未包含的文件),而在上述业务场景中,也可以不包裹
UFUNCTION
的声明和实现,而是在函数体内部用平台宏包裹代码:- 避免UHT生成的代码链接报错
- 非Windows平台执行空逻辑
【Python】Windows平台单例进程实现方案之互斥量
需求描述
使用
Python
写一个工具类程序,需要做成单例进程(已有进程运行时不能开启第二个)。最好还能实现当用户试图重复运行时将已有进程的窗口聚焦。单例实现方案
使用命名互斥量(Named Mutex)实现。
互斥量(Mutex)是Windows系统提供的一种同步对象,用于协调多个线程或进程对共享资源的访问:
- 互斥性:同一时间只有一个线程/进程可以拥有Mutex
- 命名机制:可以创建具有全局名称的Mutex,使不同进程能够访问同一个同步对象
- 所有权:获得Mutex的线程/进程必须释放它,其他线程/进程才能获取
声明一个单例进程类
SingletonProcess
:- 在构造函数中创建互斥量并捕获上次错误
- 在析构函数中释放互斥量
- 提供接口查询是否有进程正在运行
当调用
CreateMutex
创建命名互斥量时:- 如果该名称的互斥量不存在,则会创建新的互斥量并返回句柄
- 如果已存在同名互斥量,仍会返回该互斥量的句柄,这意味着无法通过返回值判断互斥量是否为首次创建
从而需要引入错误码机制,错误码
ERROR_ALREADY_EXISTS(183)
表示对象已经存在,即互斥量已创建(已有进程在运行)。聚焦实现方案
当用户试图开启第二个进程时,一旦检测到进程已运行,使用TCP进程通信通知运行中的进程聚焦。
需要一个继承自
BaseHTTPRequestHandler
的HTTP请求处理类来响应不同请求,重写do_GET
和do_POST
,在收到POST请求时解析Json并执行do_command
:开启TCP服务线程:
【Python】全局解释器锁限制了多线程的性能
问题描述
工作中写了一个工具,需要递归扫描目录中的文件并判断修改时间和服务器同步时间是否一致,属于CPU密集型操作,本想着用多线程解决,将扫描方法改成多线程后,耗时反而上升了。
原因分析
Python是解释性语言,边解释边执行:
- 解析源代码文本,将其编译为字节码
- 采用基于栈的解释器来运行字节码
- 不断循环这个过程,直到程序结束或被终止
为了避免资源争夺、保证程序执行的稳定性,引入了特性全局解释器锁(Global Interpreter Lock, GIL)来确保每个线程在执行代码时都拥有独占的解释器资源。
在多线程情况下,每个线程不能真正并行执行,而是通过在不同线程之间切换实现“看起来”同时执行的效果。
不仅如此,GIL的存在还会让程序运行地比单线程更慢,因为多线程环境下线程之间的切换会引入额外开销。
测试验证
写了个单元测试测了一下多线程执行多次加法运算:
分别测试了一下2线程、4线程和16线程的运行情况:
- 单线程 VS 2线程
- 单线程 VS 4线程
- 单线程 VS 16线程
何时用多线程
由于GIL的存在,Python的多线程无法利用多核进行CPU密集型任务的并行计算(如科学计算、图像处理、复杂算法)。但它极其适合以下I/O密集型场景:
- 网络I/O:如HTTP 服务、爬虫、API调用,线程在等待服务器响应时会被挂起,释放 GIL
- 文件I/O:如日志处理、大文件读写,线程在等待磁盘读写操作时也会释放GIL
- 阻塞操作:如等待用户输入、等待子进程结束、等待数据库查询返回
- 维护响应式的用户界面:使得子线程挂起时主线程的UI仍然可进行事件循环,保持流畅
以下是一个工作中用多线程实现阻塞操作和响应式用户界面的案例,用子线程执行批处理脚本,捕获输出并回调到主线程界面,显示不同的结果:
- Author:Yuki
- URL:http://shirakoko.xyz/article/work-note
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts