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::value(args, 0);

#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"