UnrealEnginPython踩坑记录
最近项目换成了UE4,脚本用到的是python预研,用到的插件是UnrealEnginePython,在使用这个插件的过程中踩到几个坑,在这里mark下。
1. 自动导出接口参数不匹配
这个bug是同事遇到的,报错的情况很诡异,报错log如下:
LogPython: Error xxx/xxx/xxx.py:27 RuntimeWarning: tp_compare didn't return -1 or -2 for exception
ue.log("this is a test log" + str(test_dict.get(10000, None)))
LogPython: Error argument must be string, not int
...
初看这个报错,就找到对应行,结果发现,只是一个对python dict的取值操作,调用的也是dict类型提供的标准函数。最近项目在折腾python版本的问题,以为是同事修改了python底层C++代码,导致的报错。跟同事py了很久,也没找到头绪,在UnrealEnginePython提供的python console命令,直接调用这个dict可以正常取值,而且尝试打印这个get函数的地址,代码逻辑输出的地址跟在python console里输出的是一样的。到这里就陷入了思考了。
中午干饭回来,继续盯log,我偶然发现了个警告
LogTemp: Warning: argument is not a FText
便在工程里搜索了下,全局只有一个地方打印了这个日志。
template<> FText get_value(PyObject* py_object)
{
char *str;
if (!PyArg_Parse(py_object, "s", &str))
UE_LOG(LogTemp, Error, TEXT("argument is not a FText"));
return FText::FromString(FString(UTF8_TO_TCHAR(str)));
}
然后我断点调试跟踪堆栈发现调用关系如下:
EXPORT_UOBJECT_FUNC("set_text",&UTextBlock::SetText),
UTextBlock::SetText 函数声明如下
virtual void SetText(FText InText);
EXPORT_UOBJECT_FUNC 的定义如下:
#define EXPORT_UOBJECT_FUNC(func_name, func)\
{\
func_name, \
[](PyObject *self, PyObject *args)\
{\
return UePyTemplate::invoke_func((ue_PyUObject *)self, args, func);\
},\
METH_VARARGS, ""\
}
R是返回值void
T是类型UTextBlock
Args是传入的参数
self是调用对象
// UEPyTemplate.h
template<typename R, typename T, typename... Args>
PyObject *invoke_func(ue_PyUObject *self, PyObject * args, R(T::*func)(Args...))
INVOKE_UOBJECT_FUNC
#define INVOKE_UOBJECT_FUNC \
{\
UE_PY_CHECK(self);\
T *uobject = ue_py_check_type<T>(self);\
if (!uobject)\
{\
UClass* uclass = T::StaticClass();\
FString class_name = uclass->GetName();\
return PyErr_Format(PyExc_Exception, "uobject is not %s", TCHAR_TO_ANSI(*class_name));\
}\
CHECK_ARGS_COUNT(args, sizeof...(Args));\
PyObject* ret = ret_type<R>::template call_func_with_args<Args...>(uobject, func, args);\
return ret;\
}
我们到处的这个函数只有一个参数,最终会调用CALL_WITH_1_ARG,参数列表是GET_1_ARG获得的,即: get_args
#define GET_1_ARG auto arg1 = get_args<Arg1>::value(args, 0);
#define GET_2_ARG GET_1_ARG auto arg2 = get_args<Arg2>::value(args, 1);
#define GET_3_ARG GET_2_ARG auto arg3 = get_args<Arg3>::value(args, 2);
#define GET_4_ARG GET_3_ARG auto arg4 = get_args<Arg4>::value(args, 3);
#define GET_5_ARG GET_4_ARG auto arg5 = get_args<Arg5>::value(args, 4);
#define GET_6_ARG GET_5_ARG auto arg6 = get_args<Arg6>::value(args, 5);
#define GET_7_ARG GET_6_ARG auto arg7 = get_args<Arg7>::value(args, 6);
#define GET_8_ARG GET_7_ARG auto arg8 = get_args<Arg8>::value(args, 7);
#define GET_9_ARG GET_8_ARG auto arg9 = get_args<Arg9>::value(args, 8);
#define CALL_WITH_1_ARG arg1
#define CALL_WITH_2_ARG CALL_WITH_1_ARG, arg2
#define CALL_WITH_3_ARG CALL_WITH_2_ARG, arg3
#define CALL_WITH_4_ARG CALL_WITH_3_ARG, arg4
#define CALL_WITH_5_ARG CALL_WITH_4_ARG, arg5
#define CALL_WITH_6_ARG CALL_WITH_5_ARG, arg6
#define CALL_WITH_7_ARG CALL_WITH_6_ARG, arg7
#define CALL_WITH_8_ARG CALL_WITH_7_ARG, arg8
#define CALL_WITH_9_ARG CALL_WITH_8_ARG, arg9
#define CALL_FUNC_WITH_ARGS(args_count)\
template<DECLARE_##args_count##_ARG, typename T, typename F>\
static PyObject* call_func_with_args(T* uobject, F func, PyObject* args)\
{\
GET_##args_count##_ARG;\
R ret = (uobject->*func)(CALL_WITH_##args_count##_ARG);\
RETURN_VALUE(ret);\
}
template<typename T>
struct get_args
{
static T value(PyObject *args, int index)
{
return get_args_value<T>(args, index);
}
};
template<typename T>
T get_args_value(PyObject *args, int index)
{
PyObject* py_object = PyTuple_GetItem(args, index);
if (subclass_of<T>::value)
{
return subclass_of<T>::get_subclass_value(py_object);
}
return get_value<T>(py_object);
}
get_value模板函数
template<typename T>
T get_value(PyObject* py_object) {
return uobject_derived_type<T>::get_value(py_object);
}
调用的是特化版本的函数
template<> UNREALENGINEPYTHON_API FText get_value(PyObject* py_object);
产生这个警告的界面里,跟FText相关的只有一个调用
def set_text(self, text_str):
self.uobject.set_text(text_str)
立马打印这text_str,发现传入的参数是int,结合之前FText get_value特化函数,发现了坑点:
函数将int类型的py_object进行字符串类型匹配解析时,没有做类型判定,强行按照c风格字符串进行解析,解析的结果是会将连续的内存块解析成字符串,并且在第一个’\0’空间停止,之前的内存空间数据都被当成了字符串。
template<> FText get_value(PyObject* py_object)
{
char *str;
if (!PyArg_Parse(py_object, "s", &str))
UE_LOG(LogTemp, Error, TEXT("argument is not a FText"));
return FText::FromString(FString(UTF8_TO_TCHAR(str)));
}
下图是设置字符串的结果,字符串内容都是乱码:
这就解释清楚,之前的报错,而且报错的地方经常不固定。
找到原因,修改方法就容易了。顺势排查了一波对字符串参数解析的特化版本,防止后面留坑。
template<> FText get_value(PyObject* py_object)
{
char *str;
if (!PyString_Check(py_object))
{
UE_LOG(LogTemp, Error, TEXT("argument is not a FText"));
return FText::FromString(FString(""));
}
if (!PyArg_Parse(py_object, "s", &str))
{
return FText::FromString(FString(""));
}
return FText::FromString(FString(UTF8_TO_TCHAR(str)));
}
2.UE4引擎代码的坑
做UI的时候需要个屏幕坐标空间转换的函数,在谷歌上找到了个下面这个函数:
UnrealEngine/Engine/Source/Runtime/UMG/Public/Blueprint/SlateBlueprintLibrary.h
/**
* Translates local coordinate of the geometry provided into local viewport coordinates.
*
* @param PixelPosition The position in the game's viewport, usable for line traces and
* other uses where you need a coordinate in the space of viewport resolution units.
* @param ViewportPosition The position in the space of other widgets in the viewport. Like if you wanted
* to add another widget to the viewport at the same position in viewport space as this location, this is
* what you would use.
*/
UFUNCTION(BlueprintPure, Category="User Interface|Geometry", meta=( WorldContext="WorldContextObject" ))
static void LocalToViewport(UObject* WorldContextObject, const FGeometry& Geometry, FVector2D LocalCoordinate,
FVector2D& PixelPosition, FVector2D& ViewportPosition);
然后开始写C++导出接口:
PyObject *py_ue_screen_to_widget_local(ue_PyUObject * self, PyObject * args)
{
ue_py_check(self);
UWidget* widget = ue_py_check_type<UWidget>(self);
if (!widget)
return PyErr_Format(PyExc_Exception, "uobject is not a UWidget");
float x, y;
if (!PyArg_ParseTuple(args, "(ff)", &x, &y))
return nullptr;
FVector2D local_pos;
FVector2D screen_pos(x, y);
FGeometry geometry = widget->GetCachedGeometry();
USlateBlueprintLibrary::ScreenToWidgetLocal(widget, geometry, screen_pos, local_pos);
return py_ue_new_fvector2d(local_pos);
}
screen_pos在进入函数ScreenToWidgetLocal时,数据一切正常,而进入函数后,数据不对了,像没初始化的样子。查了下源码发现FVector2D没有实现拷贝构造函数
FVector2D(const& FVector2D)
{}
其实只是开启了编译优化,代码行号跟变量被优化掉了,被优化的变量没法看到具体的内存值。
3. Unreal C++不允许指针指向不完整的类类型(踩坑)
新增如下代码时,突然VS2019爆出警告 C++不允许指针指向不完整的类类型
//// SHierarchyViewItem.cpp
NewSlot = Parent->AddChild(Widget);
if (Parent->IsA(UCanvasPanel::StaticClass()))
{
UCanvasPanelSlot* NewCanvasSlot = Cast<UCanvasPanelSlot>(NewSlot);
if (nullptr != NewCanvasSlot)
{
NewCanvasSlot->SetAnchors(FAnchors(0.5f, 0.5f));
NewCanvasSlot->SetAlignment(FVector2D(0.5f, 0.5f));
}
}
引入这两个头文件就能解决这个问题
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"