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操作,包括更新界面、处理事件,必须在主线程中执行)。

解决方法

把耗时的信息获取操作做成异步,放在子线程中处理;得到的信息通过Tkinterafter方法回到主线程,并呈现在UI上。
🐈
after()的工作原理——通过事件队列实现跨线程通信
  • 事件队列机制 Tkinter 的主线程运行一个事件循环(mainloop()),不断检查事件队列(如按钮点击、窗口重绘等)。after()的作用是向这个队列插入一个自定义事件。
  • 线程安全的通信 当其他线程调用after(0, callback)时,Tkinter会将callback封装成一个事件,安全地插入主线程的事件队列。主线程在下次处理事件时,会执行这个回调。
  • 主线程启动异步任务__async_fetch_and_log
  • 异步任务__async_fetch_and_log中获取版本信息
  • __log_results_on_main_thread中log信息通过Tkinterafter方法回到主线程

【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)捕获输出;在死循环中读取和处理数据。
notion image
🐈‍⬛
self.after()Tkinter提供的一个核心方法,用于在主线程中延迟执行回调函数,允许从其他线程向主线程安全提交GUI操作请求
notion image
代码:

【网络】本地回环地址和通配地址端口占用问题

问题描述

最近在调试一个本地游戏服务器时,发现一个有趣的现象:
  • 先用 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
示例控件
SScrollBoxSButton
UScrollBoxUButton

应用案例

下面解读UE源码,了解原生UMG控件类UButton是如何封装原生Slate控件类SButton
notion image

Slate Widget

Slate在C++中有两种创建方式:
  • 使用SNew()
  • 使用SAssignNew()
先来看看SButton.h的源码,开头SLATE_BEGIN_ARGS和结尾SLATE_END_ARGS中间包裹的是构造参数列表宏定义参数
  • 构造参数列表:在SNew()SAssignNew()运行时实例化控件时可选择性传入的参数
    • 参数必须以_开头
    • ()中可以指定默认值
  • 宏定义参数:用SLATE_DEFAULT_SLOTSLATE_STYLE_ARGUMENT等宏包裹的参数
    • 是控件类的元数据,用于生成FArguments结构体
    • 构造参数列表中的参数必须来自于宏定义参数列表,否则会报错(如"_WidgetStyle": 不是 "SDoubleScrollBox::FArguments"的成员
再来看看SButton.cpp的源码,注意ConstructOnPaint两个重要方法
  • 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++头文件中的反射标记(如UFUNCTIONUCLASSUPROPERTY),并生成对应的反射代码
  • *.generated.h:包含反射元数据
  • *.gen.cpp:反射的具体实现代码

UTH如何支持反射

UTH的目的就是支持虚幻的反射系统,实现:
  • 蓝图与C++的交互(如在蓝图中调用UFUNCION标记的函数)
  • 序列化和反序列化(UPROPERTY的保存和加载)
  • 编辑器属性面板(UCLASS的细节定制)
但是C++本身没有运行时反射,所以虚幻需要UHT在编译前生成额外的代码,让引擎能动态获取类、函数、变量等信息。其工作流程如下:
  1. 编译前运行
      • 在UnrealBuildTool(UBT)调用编译器之前,UHT先扫描所有头文件
      • 解析UCLASSUFUNCTIONUPROPERTY等标记
  1. 生成反射代码
      • 生成.generated.h.gen.cpp,包含类、属性、函数的反射信息
  1. 编译器处理生成的代码
      • 生成的代码会和引擎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_WINDOWSfalse,导致:
  • UHT生成了MyFunction的反射代码(在MyClass.gen.cpp里)
  • MyFunction的C++实现被平台宏排除了,所以链接器找不到MyFunction从而报错

解决方法

一个方法是将Windows平台专用代码移到单独文件(UHT不会处理未包含的文件),而在上述业务场景中,也可以不包裹UFUNCTION的声明和实现,而是在函数体内部用平台宏包裹代码:
  • 避免UHT生成的代码链接报错
  • 非Windows平台执行空逻辑

【Python】Windows平台单例进程实现方案之互斥量

需求描述

使用Python写一个工具类程序,需要做成单例进程(已有进程运行时不能开启第二个)。最好还能实现当用户试图重复运行时将已有进程的窗口聚焦。

单例实现方案

使用命名互斥量(Named Mutex)实现。
🐈‍⬛
互斥量(Mutex)是Windows系统提供的一种同步对象,用于协调多个线程或进程对共享资源的访问:
  1. 互斥性:同一时间只有一个线程/进程可以拥有Mutex
  1. 命名机制:可以创建具有全局名称的Mutex,使不同进程能够访问同一个同步对象
  1. 所有权:获得Mutex的线程/进程必须释放它,其他线程/进程才能获取
声明一个单例进程类SingletonProcess:
  • 在构造函数中创建互斥量并捕获上次错误
  • 在析构函数中释放互斥量
  • 提供接口查询是否有进程正在运行
当调用CreateMutex创建命名互斥量时:
  • 如果该名称的互斥量不存在,则会创建新的互斥量并返回句柄
  • 如果已存在同名互斥量,仍会返回该互斥量的句柄,这意味着无法通过返回值判断互斥量是否为首次创建
从而需要引入错误码机制,错误码ERROR_ALREADY_EXISTS(183)表示对象已经存在,即互斥量已创建(已有进程在运行)。

聚焦实现方案

当用户试图开启第二个进程时,一旦检测到进程已运行,使用TCP进程通信通知运行中的进程聚焦。
需要一个继承自BaseHTTPRequestHandler的HTTP请求处理类来响应不同请求,重写do_GETdo_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仍然可进行事件循环,保持流畅
以下是一个工作中用多线程实现阻塞操作和响应式用户界面的案例,用子线程执行批处理脚本,捕获输出并回调到主线程界面,显示不同的结果:
 
学习笔记:计算机网络(自顶向下方法)课程笔记自卷笔记:一些乱七八糟的踩坑记录和小知识小技巧
Loading...
Latest posts
工作笔记:一些乱七八糟的踩坑记录
2025-9-2
【TypeScript】浅析TypeScript中的装饰器(旧语法)
2025-8-28
【TypeScript】基于抽象语法树的自定义代码检查实现方案
2025-8-21
自卷笔记:一些乱七八糟的踩坑记录和小知识小技巧
2025-7-23
自制Python任务调度模块-MySchedule
2025-7-23
游戏AI行为决策-行为树(BehaviorTree)框架搭建与应用
2025-7-23