前言

做了UE4一段时间,在自己的Demo中希望底层能更多的使用其它语言的第三方库,语法简单,也为了配置方便,不会将大量的时间用在第三方库与C++的调试中。调研了目前UE4相关的脚本语言,各有优缺点。

下面是我尝试的脚本语言,目标的写一个简单的Demo:使用zmq搭建客户端-服务器,实现相互收发通知:

  • unreal-AngelScript:
  • 优点:支持结构体、枚举、类的定义
  • 不足:不能直接访问蓝图函数,没有第三方库
  • Puerts
  • 优点:支持类的定义,可以和蓝图类互访问
  • 不足:Node使用多线程不方便(使用work_threads和zmq库启动崩溃,没解决)
  • UnrealCSharp
  • 优点:支持结构体、枚举、类的定义,使用.net7+mono
  • 不足:只支持UE5,不支持net.framwork,而且我对.net开发不熟悉,可以创建多线程,但使用zmq搜索不到System.Private.CoreLib.resources.dll,启动崩溃,没解决
  • UnrealSharp
  • 缺点:不支持代码提示,pass
  • UnrealPy(UE4自己提供的python脚本支持)
  • 优点:支持结构体、枚举、类的定义,支持pip
  • 不足:Python定义的结构在蓝图中无法被序列化,即不能在蓝图中使用Python定义的结构体、枚举、类,也不能作为父类继承,只能使用蓝图函数库

经过一番思索,决定使用PuerTS+UnrealPy组合,即PuerTS写上层业务,UnrealPy提供第三方库支持

解决问题

解决问题1:PuerTS不支持结构体、枚举的定义

  • 在虚幻商城搜到了一个插件EasyImportJson,可以导入Json文件转成结构体,是不是可以解析TS文件,读取class的所有属性,转成结构体呢?(已实现)

解决问题2:UnrealPy写的蓝图函数库下次再打开就会报错

  • 同样在虚幻商城搜到了PythonBlueprintFixer,在UE4启动很早的时机就运行python解释器,再打开蓝图就不会报错了。

解决问题3:UnrealPy对象状态保存

  • 既然UnrealPy定义的结构体和类无法在蓝图中使用,是否可以通过UObject类型传递状态信息?
  • 在实际写Demo又发现UObject虽然能将Python对象作为类的成员,但这些python对象在执行完蓝图函数后立刻被释放,所以使用Python定义的UObject只能存储已经存在的结构体或int、string等基本类型,不能保存诸如zmq.Context等类型
  • 既然不能保存到UObject中,那么只能保存对象的索引,然后从全局变量中找到此对象

代码

import unreal
import zmq
import threading

# 全局变量,保存Python数据类型
save = []

# 传给蓝图,记录状态信息
@unreal.uclass()
class ZmqContent(unreal.Object):
    id = unreal.uproperty(int)

    def Init(self, context, socket):
        global save
        save.append((context, socket))
        self.id = len(save) - 1
    
    def Get(self):
        global save
        return save[self.id]

# 服务器监听从客户端发来的message
def Listen(socket: zmq.Socket):
    while True:
        message = socket.recv()
        print(message)
        socket.send_string("resp hello")

# 只能使用蓝图函数库
@unreal.uclass()
class PyBPFunctionLibrary(unreal.BlueprintFunctionLibrary):
    # 初始化服务器
    @unreal.ufunction(static=True, ret=unreal.Object, params=[str])
    def InitServer(port: str):
        context = zmq.Context()
        socket = context.socket(zmq.REP)
        socket.bind("tcp://*:" + port)
        t = threading.Thread(target=Listen, args=(socket,))
        t.start()
        ctx = ZmqContent()
        ctx.Init(context, socket)
        return ctx
    
    # 关闭服务器
    @unreal.ufunction(static=True,params=[unreal.Object])
    def CloseServer(context: ZmqContent):
        _, socket = context.Get()
        socket: zmq.Socket
        socket.close()

    # 初始化客户端
    @unreal.ufunction(static=True,ret=unreal.Object,params=[str])
    def InitClient(port: str):
        context = zmq.Context()
        socket = context.socket(zmq.REQ)
        socket.connect("tcp://localhost:" + port)
        ctx = ZmqContent()
        ctx.Init(context, socket)
        return ctx
    
    # 客户端发送消息给服务器
    @unreal.ufunction(static=True,params=[unreal.Object, str])
    def SendMessage(context: ZmqContent,message: str):
        _, socket = context.Get()
        socket.send_string(message)
        response = socket.recv()
        print(response)

蓝图就是一个窗口,包含一个点击往服务器线程发送消息的按钮

ue5 python导入文件 ue4能用python_Python

总结

  • Python和C++一样是面向客户端的语言,在多线程、第三方库的使用上要比其它语言方便的多
  • 总体上看,虽然上面的代码可以运行,但还是很冗余,但应该有简化的空间
  • PuerTS虽然能找到Python库,但不能直接调用蓝图函数库,需要在蓝图上做二次封装,才能使用,也就是说PuerTS只能初始化蓝图类,不能直接初始化Python类
  • 总体来说,我对脚本语言的要求是:【可以方便的使用第三方库】大于【可以定义结构体】大于 【有优雅的语法】