Unreal Engine WebUI的核心逻辑剖析

WebUI实现原理解析

· 数字孪生,C++,UE技术分享,UE,WebUI

 

一·、创建SWebBrowserView 

SWebBrowserView是从SCompoundWidget派生的一个类,因此是一个UI界面相关的类,可以在Slate中作为其他界面的一部分使用。 

class WEBBROWSER_API SWebBrowserView 

: public SCompoundWidget 

… 

} 

SWebBrowserView是实现html页面加载的关键,提供了几个核心的功能. 

1,第一个是LoadURL 

通过这个方法加载页面,LoadURL有一些类似的方法,比如LoadSting等,有兴趣的可以深入研究一下。 

void SWebBrowserView::LoadURL(FString NewURL) 

AddressBarUrl = FText::FromString(NewURL); 

if(BrowserWindow.IsValid()) 

BrowserWindow->LoadURL(NewURL); 

2,第二个是ExecuteJavascript 

通过这个方法,可以执行页面中的javascript,比如回调方法。 

void SWebBrowserView::ExecuteJavascript(const FString& ScriptText) 

if(BrowserWindow.IsValid()) 

BrowserWindow->ExecuteJavascript(ScriptText); 

3,第三个是BindUObject 

BindUObject这个方法非常关键,是页面中的javascript调用能够传到UE底层的关键。下面我们再详细展开如何使用BindUObject. 

void SWebBrowserView::BindUObject(const FString& Name, UObject* Object, bool bIsPermanent) 

if(BrowserWindow.IsValid()) 

BrowserWindow->BindUObject(Name, Object, bIsPermanent); 

 

二、绑定监听器,响应JavaScript方法 

1、首先创建监听器对象 

因为BindUObject需要传入的是一个UObject对象,所以监听器对象需要从UObject派生。 

UCLASS() 

class ESWEBUIMAIN_API UESWebInterface : public UObject 

GENERATED_UCLASS_BODY() 

public: 

。。。 

这里的UESWebInterface就可以是一个监听器对象 

2、给监听器对象添加监听方法 

Javascript脚本中要写什么方法,这里就需要添加一个同名的方法,比如triggerUE4EventWithBlank,那么在监听器中就添加一个同名的方法,注意这里必须加UFUNCTION,否则会找不到。 

UCLASS() 

class ESWEBUIMAIN_API UESWebInterface : public UObject 

GENERATED_UCLASS_BODY() 

public: 

UFUNCTION() 

void triggerUE4EventWithBlank(FStringjavascriptFunctionsName, FString arguments); 

。。。 

voidUESWebInterface::triggerUE4EventWithBlank(FString javascriptFunctionsName,FString arguments) {  

。。。  

UE_LOG(LogTemp, Display, TEXT("JAVASCRIPT:%s"),*arguments);  

TArray args;  

arguments.ParseIntoArray(args, TEXT(".;_|"), true);  

。。。 

有了响应方法,最简单的监听器对象就创建好了。 

为了方便蓝图中使用,监听器对象其实也可以从UWidget类派生,这样在蓝图里就是可以看到的一个UI界面对象。并可以创建出来使用了 

3、绑定监听器到SWebBrowserView 

这一步就是调用BindUObject方法了, 以下代码展示了创建WebBrowserView,loadURL以及绑定监听器的过程  

void UESWebInterface::LoadURL(FStringNewURL) 

TSharedPtr ESWebUI; 

FCreateBrowserWindowSettingsbrowserSettings;  

browserSettings.bUseTransparency =transparentBackground;  

browserSettings.BrowserFrameRate =maxMenuFPS;  

browserSettings.bShowErrorMessage = true;  

browserSettings.InitialURL = TEXT("about:blank");  

TSharedPtrBrowserWindow;  

BrowserWindow =IWebBrowserModule::Get().GetSingleton()->CreateBrowserWindow(browserSettings);  

//SNew the web browser view  

ESWebUI = SNew(SWebBrowserCustom,BrowserWindow)  

.InitialURL(TEXT("about:blank"))  

.EnableMouseTransparency(bEnableMouseTransparency)  

.MouseTransparencyDelay(MouseTransparencyDelay)  

.MouseTransparencyThreshold(MouseTransparencyThreshold)  

.ShowControls(false)  

.ShowErrorMessage(true)  

.SupportsTransparency(transparentBackground)  

.SupportsThumbMouseButtonNavigation(false)  

.ShowInitialThrobber(false);  

if (ESWebUI.IsValid())  

{  

//Load URL  

ESWebUI->LoadURL(NewURL);  

//Bind UObject  

ESWebUI->BindUObject("uecom", this,true);  

}  

这里大家可能有点奇怪,绑定对象的时候输入的name参数为什么是”uecom”,让我们继续往下看。  

4、在Javascript中实现方法,能够往监听器发射消息 

在js文件中写下如下两个函数, 其中getParameter函数是参数的合并,把多个参数以” .;_|”隔开合并成一个string,是个辅助函数。 

function getParameter(parameterArray){  

varobj = {  

  functionsName:'',  

  id:'',  

  className:'',  

  value:'',  

  args:''  

};  

vartempArray = new Array();  

if(parameterArray.length > 0){  

    for(var i = 1; i <= parameterArray.length; i++){  

    if(typeof parameterArray[i] == 'object'){  

   obj.id = parameterArray[i].id;  

   obj.className = parameterArray[i].className;  

   obj.value = parameterArray[i].value;  

}  

else{  

  tempArray.push(parameterArray[i]);  

  }  

}  

}  

obj.functionsName = parameterArray[0];  

obj.args = tempArray.join('.;_|');  

returnobj; 

};  

function triggerDoCityEventBlank(){  

varobj = this.getParameter((Array.prototype.slice.call(arguments, 0)));  

{  

    if(window.ue && window.ue.uecom) {  

    console.log("calljs function:", obj);  

    window.ue.uecom.triggerue4eventwithblank(obj.functionsName,obj.args);  

}  

else 

{  

    showAlert(DoCityNotFoundMessage);  

}  

}  

};  

重点可以看一下,triggerDoCityEventBlank这个函数,这个函数看起来没有输入参数,但是js你知道是很灵活的,函数定义的时候可以没参数,但实际调用的时候是可以给参数的。Example:triggerDoCityEventBlank('myJavascriptFunctionsName','optionalParameter 1',1234, 'Hallo World'); 这里的第一个参数需要有一些特殊,后面会提到,但不属于UE WebUIb本身的核心逻辑。 

这里,我们可以看到window.ue.uecom这个域,然后调用了triggerue4eventwithblank这个方法,这就是我们前面绑定的监听器对象名称以及它的方法了,通过这个js的函数调用,就可以实现了向监听器发送消息,相当于调用了UE里面UObject对象的方法。在js调用函数的后,我们可以看到,监听器(UESWebInterface)的triggerue4eventwithblank函数被调用了。这里面的实现过程,感兴趣的可以自己参考源码。 

到这来,我们已经实现了加载页面,并且在页面的js脚本中向UE底层对象发送消息。 

triggerue4eventwithblank看起来就像它的名字一个,触发了ue监听器里面的一个函数,传入的参数是函数的名称和参数,那要正在实现触发的函数,是需要在监听器triggerue4eventwithblank里面,去调用该函数的实现。举个例子,我要”触发”打开关卡的函数,那么函数名可能是OpenLevel,参数是一个关卡的名称,那么在监听器的triggerue4eventwithblank函数里面,就要解析FString javascriptFunctionsName和FString arguments,去实现真正的操作了。这一段有点绕,可以体会一下trigger的含义。 

 

三、UE c++底层向蓝图发送接收到的js消息 

这部分本来不属于WebUI的核心逻辑,不过UE c++底层对象接收到js消息后,经常需要让蓝图层也知道,所以这里也顺便说一下从c++向蓝图发送消息的一种实现方式。 

1、 定义动态委托(Dynamic Delegate) 

我们知道,C++和蓝图的交互比较好的实现方式是动态委托方式。我们先定义一个对象,再从这个对象暴露几个动态委托。我们假设对象的名称为UJavaScriptBean,从UObject对象派生。 

UCLASS(Blueprintable, BlueprintType) 

class ESWEBUIMAIN_API UJavascriptBean : public UObject 

GENERATED_UCLASS_BODY() 

public: 

//声明委托类型,是带一个参数的动态多播委托 

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FJavascriptEventBlank, const TArray<FString>&, args); 

//定义一个委托类型的属性 

UPROPERTY(BlueprintAssignable, Category = "WebUI|Events"

FJavascriptEventBlank onJavascriptEventBlank; 

2、 监听器对象的改造 

为了让监听器对象在接收到js发送的消息后向蓝图发送消息,我们对在监听器中添加一些方法。 

首先添加一个方面BindJS_Event,这是一个蓝图可用的方法,通过这个方法,向蓝图传递出一个UJavascriptBean对象 

UFUNCTION(BlueprintCallable, Category = "WebUI_Library"

void BindJS_Event(const FString JS_Function_Name, UJavascriptBean* &eventTarget); 

方法的实现如下: 

void UESWebInterface::BindJS_Event(const FString JS_Function_Name, UJavascriptBean*&eventTarget){ 

if(!ESWebUI.IsValid()) 

return; 

FString lowerName =JS_Function_Name.ToLower(); 

UPROPERTY() 

UJavascriptBean* javascriptBean =NewObject(UJavascriptBean::StaticClass()); 

javascriptBean->AddToRoot(); 

javascriptBeans.Add(lowerName,javascriptBean); 

eventTarget = javascriptBean; 

我们看到,其实很简单,就是创建了一个UJavascriptBean对象出来。 

其次是修改void UESWebInterface::triggerUE4EventWithBlank函数的实现,添加调用委托的代码: 

void UESWebInterface::triggerUE4EventWithBlank(FStringjavascriptFunctionsName, FString arguments) { 

FString lowerName =javascriptFunctionsName.ToLower(); 

UJavascriptBean** javascriptBeanPointer =javascriptBeans.Find(lowerName); 

if(javascriptBeanPointer == nullptr) { 

return; 

UJavascriptBean* javascriptBean =*javascriptBeanPointer; 

//UE_LOG(LogTemp, Display,TEXT("JAVASCRIPT:%s"), *arguments); 

TArray args; 

arguments.ParseIntoArray(args, TEXT(".;_|"), true); 

AsyncTask(ENamedThreads::GameThread,[javascriptBean, args]() { 

javascriptBean->onJavascriptEventBlank.Broadcast(args); 

}); 

可以看到,我们BindJS_Event时候是可以指定一个FunctionName的,然后triggerUE4EventWithBlank可以更加函数名寻找相应的javascriptBean,并调用它的Broadcast方法让蓝图接收了。 

3、 蓝图里的使用 

蓝图里如何接收动态委托,如下图: 

 

broken image

首先通过BindJS_Event获取到带动态委托的Javascriptbean对象,然后给javascriptBean对象的委托绑定上监听事件,并处理事件。 

至此,我们完成了从网页的js脚本向UE c++对象“触发”消息,并发送消息到蓝图的全过程。Js脚本发送的消息,既可以在C++对象进行处理,也可以在蓝图进行处理。