前言
做了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)
蓝图就是一个窗口,包含一个点击往服务器线程发送消息的按钮
总结
- Python和C++一样是面向客户端的语言,在多线程、第三方库的使用上要比其它语言方便的多
- 总体上看,虽然上面的代码可以运行,但还是很冗余,但应该有简化的空间
- PuerTS虽然能找到Python库,但不能直接调用蓝图函数库,需要在蓝图上做二次封装,才能使用,也就是说PuerTS只能初始化蓝图类,不能直接初始化Python类
- 总体来说,我对脚本语言的要求是:【可以方便的使用第三方库】大于【可以定义结构体】大于 【有优雅的语法】