Unreal Engine - AngelScript

AngelScript是第三方的虚幻引擎脚本系统,背后是《双人成行》的制作公司。在开发过程中,脚本肯定还是比直接写C++来得更加高效。官方文档地址是https://angelscript.hazelight.se/,有热心网友翻译了其中一部分,地址在这里。这篇文章主要是总结一下官方文档的要点。

Angel Script与原生C++有一定的区别,大部分是简化。下面用*表示C++中没有的特性

AS语言相关

  1. 支持全局函数、全局变量(但必须是const
  2. 默认都是public,不论成员函数还是全局函数。好像不支持全局私有函数
  3. 支持关键字参数,类似于C#,主要是默认参数比较多的时候可以单独指定其中几个,还可以乱序

函数

  1. UFUNCTION标记后默认BlueprintCallable,可以用NotBlueprintCallable*
  2. 常用函数已省略前缀BP_K2_ReceiveReceived,类似于蓝图,比如简化后的Tick
  3. BlueprintEvent*取代BlueprintImplementableEventBlueprintNativeEvent,可以让蓝图覆盖实现,脚本中一定要有默认实现,留空就好了
  4. 省略Meta,比如DisplayName = "FriendlyName"就是Meta = (DisplayName = "FriendlyName")
  5. 全局函数标记之后蓝图也能调用
  6. 所有成员函数不标记就是虚函数,可以在脚本子类函数加override(是语法层面的关键字,不是UFUNCTION)覆写,用Super调父类实现。 如果父类函数标记了BlueprintEvent,子类函数需要BlueprintOverride才能覆写。 如果父类函数是C++的BlueprintNativeEvent,无法直接用Super调用父类实现,只能在C++里单独抽出来给子类调

属性

  1. UPROPERTY标记后默认EditAnywhereBlueprintReadWrite,随意编辑和访问。支持额外的属性:NotEditable*(细节面板不显示)、EditConst*(细节面板只读)、BlueprintHidden*、NotVisible*

    还有个CallInEditor*,可以在细节面板添加按钮,这个函数不会被打包发布

    如果用了EditCondition,可以在meta里额外添加EditConditionHides*,不可编辑就隐藏

    其它:

    1
    2
    3
    4
    5
    /* ShowOnActor makes the component's propertiesvisible when you select the actor, not just when you select the component. 
    * 'ShowOnlyInnerProperties' will expand the properties to outer layer
    */
    UPROPERTY(DefaultComponent, ShowOnActor, meta =(ShowOnlyInnerProperties)) // 后两个标记都不是引的
    USplineComponent SplineComponent;

  2. 支持类似C#的属性,以GetSet开头的函数使用property关键字即可,比如ActorLocation就是GetActorLocation() 底层UObjectAActor可能是重新实现了,有些原本有的函数都替换成了属性,函数反而去掉了

  1. 默认UCLASS,默认继承自UObject。(脚本里就算不标记UPROPERTY也会GC,所以标记的作用就是为了暴露给蓝图,而不是像C++一样为了参与GC,这也是为什么默认可编辑的原因)
  2. 判断类型:IsA(),类型转换:Cast<>()
  3. 不需要构造函数,构造函数中创建的组件属性需要标记DefaultComponent* 默认第一个组件是根组件,可以用RootComponent*指定根组件 默认挂到根组件,可以用Attach =*指定父组件,可以用AttachSocket =*指定挂载点
  4. 原本构造函数中执行的语句前面要加default关键字,比如default MyCom.SomeVal = 123default MyCom.SetComeVal(123) (不使用构造函数的好处是显示指定默认值,方便热重载)
  5. 可以覆写蓝图的构造函数ConstructScript,在这里可以动态创建组件
    1
    2
    Billboard = UBillboardComponent::Create(this, n"Billboard");
    Billboard.SetHiddenInGame(false);

组件

使用组件类的静态函数获取和创建组件

1
2
3
4
ComClass::Get(Actor); // 找第一个,没有则返回nullptr
ComClass::Get(Actor, n"ComName"); // 找指定名字的
ComClass::GetOrCreate(Actor); // 找不到就创建
ComClass::GetOrCreate(Actor, n"ComName"); // 找不到就创建,并指定名字,同理还有Create
如果是是有动态类型TSubclassOf<>UClass,可以用类似Actor.GetComponent的函数传递

使用硬编码资源路径:

1
default Mesh.StaticMesh = Asset("/Engine/BasicShapes/Cone.Cone");

Actor

  1. 实例化

    用Actor类的静态函数创建:

    1
    2
    3
    FVector SpawnLocation;
    FRotator SpawnRotation;
    ActClass:Spawn(SpawnLocation, SpawnRotation);
    实例化蓝图类,需要用全局函数
    1
    2
    3
    4
    TSubclassOf<ABaseActor> ActorClass; // 可以暴露给蓝图指定为派生的蓝图类
    auto SpawnedActor = Cast<ABaseActor>(
    SpawnActor(ActorClass, SpawnLocation, SpawnRotation)
    );

  2. 构造函数是ConstructionScript

  3. GetComponentsByClass获取所有组件,GetAllActorsOfClass获取所有实例,都是由TArray指定类型

  4. 覆盖基类的组件类型OverrideComponent(*)

    1
    2
    3
    4
    5
    6
    7
    // 基类
    UPROPERTY(DefaultComponent, RootComponent)
    USceneComponent SceneRoot;

    // 子类
    UPROPERTY(OverrideComponent = SceneRoot)
    UStaticMeshComponent RootStaticMesh;
    C++是在构造函数里调用ObjectInitializer.SetDefaultSubobjectClass()

函数库

对C++中的函数库进行了简化,一般的蓝图函数库都被自动导出了(除了UKismetMathLibrary,使用的是FMath),而且命名空间做了简化,各种Statics和Library前后缀都去掉了(这是函数导出的时候自动进行的),包括:

1
2
3
4
5
6
7
U...Statics
U...Library
U...FunctionLibrary
UKismet...Library
UKismet...FunctionLibrary
UBlueprint...Library
UBlueprint...FunctionLibrary
简化为:
1
2
3
4
5
Math:: - All standard math functionality,对应FMath
Gameplay:: - Game functionality such as streaming, damage, player handling,对应UGameplayStatics
System:: - Engine functionality such as timers, traces, debug rendering,对应UKismetSystemLibrary
Niagara:: - Spawning and controlling particle systems,对应UNiagaraFunctionLibrary
Widget:: - UMG widget functionality,对应UWidgetBlueprintLibrary

字符串

  1. n"xxx",xxx就是一个FName,还可以作为函数名(必须标记了UFUNCTION)来绑定委托

    自定义Log类别,直接调用全局函数:Log(n"MyLog", "hello!")

  2. f"{xx}",类似于C#中的字符串格式化$"{xx}",用{{指定{

枚举

  1. 支持整数转换:EExampleEnum(0)

结构体

  1. 是值类型,MyStruct st;就构造好了一个实例
  2. 可以用UPROPERTY,但是不能用UFUNCTION
  3. 作为函数参数时,默认(不需要手动标const)是const &(类似于C#的in),MyStruct& Para才可以改变(类似于C#的ref) 同理还有MyStruct&out(类似于C#的out),使用out的参数会在蓝图里出现对应的输出引脚

网络

TODO

委托和事件

委托,最多绑定一个函数,底层实现是DECLARE_DYNAMIC_DELEGATE()。可以作为函数参数,可以视为函数指针。

1
2
3
4
5
delegate void FExampleDelegate(UObject Object, float Value); // 声明一个委托类型,跟C#一模一样
FExampleDelegate StoredDelegate;
StoredDelegate.BindUFunction(this, n"OnDelegateExecuted"); // 绑定成员函数(需要标记UFUNCTION),类似C#中的=
StoredDelegate = FExampleDelegateSignature(this, n"OnDelegateExecuted"); // 同上
StoredDelegate.ExecuteIfBound(this, DeltaSeconds); // 执行
事件,可以绑定多个函数,底层实现是DECLARE_DYNAMIC_MULTICAST_DELEGATE()。不可以作为函数参数,但是可以定义为属性,主要用来绑定回调和广播事件。
1
2
3
4
event void FExampleEvent(int Counter); // 声明一个事件类型
FExampleEvent OnExampleEvent;
OnExampleEvent.AddUFunction(this, n"FirstHandler"); // 绑定成员函数(需要标记标记了UFUNCTION),类似C#中的+=
OnExampleEvent.Broadcast(CallCounter); // 执行

扩展函数

类似于C#,可以把静态函数当做成员函数来调用

1
2
3
4
mixin void SetVectorToZero(FVector& Vector) // 前面用mixin标记,,第一个参数就是扩展的类型,注意结构体要用&
{
Vector = FVector(0, 0, 0);
}

仅编辑器代码

  1. 用条件编译宏:EDITORRELEASE(Shipping or Test)、TEST(Debug, Development, or Test)
  2. 用特殊文件夹:EditorExamplesDev

模拟测试:UnrealEditor-Cmd.exe "MyProject.uproject" -as-simulate-cooked -run=AngelscriptTest

子系统

可以认为是一些有用的单例,通过类的静态函数Get即可获取,脚本提供了一些实例可以继承扩展

1
2
3
4
5
UScriptWorldSubsystem for world subsystems
UScriptGameInstanceSubsystem for game instance subsystems
UScriptLocalPlayerSubsystem for local player subsystems
UScriptEditorSubsystem for editor subsystems
UScriptEngineSubsystem for engine subsystems

脚本测试

TODO

注意

  1. float默认是float64,需要可以定义为float32
  2. 不支持Interface,可以手动给接口添加静态工具函数来绕过这个限制

C++相关

自动导出

  1. 类:类标记了BlueprintType或者有函数是BlueprintCallable。类可以指定NotInAngelscript*

  2. 结构体:结构体标记了BlueprintType或者有属性暴露给蓝图。结构体可以标记NoAutoAngelscriptBind*

  3. 属性:BlueprintReadWriteBlueprintReadOnly(后者在脚本里是const),除此之外,各种Edit开头的也可以访问,但是仅限于default语句。属性可以标记ScriptReadWrite*、ScriptReadOnly*、NotInAngelscript*

  4. 函数:标记了BlueprintCallableBlueprintPure,除此之外,标记了BlueprintImplementableEventBlueprintNativeEvent的可以被脚本覆写。函数可以标记ScriptCallable*,NotInAngelscript*

    标记了弃用的函数不会导出

    静态函数会把所在类作为命名空间来导出,并且类名会被自动简化

  5. 枚举:全部

经过测试发现,编写C++代码后,需要重启编辑器才能在脚本里访问新添加的接口。按照官方文档里说的:

when the engine starts, the angelscript plugin automatically goes through all of unreal's reflection data.

所以也就是只有在启动项目的时候才会扫描导出的接口。 并且经过进一步测试,只要改了头文件就需要重启。需要测试C++接口的时候还是用蓝图比较方便

而且静态函数会自动简化,原版的类名基本就不能用了。

扩展函数

派生UOBject并使用元数据标记Meta = (ScriptMixin = "MyClassName"),为C++类添加扩展函数,从而可以在脚本里使用

  1. 扩展结构体,第一个参数为引用
  2. 扩展类,第一个参数为指针

脚本预编译

都是针对打包后的游戏

  1. JIT,使用命令行参数-as-generate-precompiled-data运行一次游戏,会生成Script/PrecompiledScript.Cache,下次运行就会使用缓存
  2. 翻译为C++,上面运行后还会生成一个AS_JITTED_CODE/文件夹,里面是C++代码,放到项目里重新打包(Cache不就失效了吗?TODO)

使用中的问题

角色输入绑定

Pawn的SetupPlayerInputComponent函数没有导出给蓝图,脚本自然也用不了。查看Pawn里函数调用的地方可知,其实等价于在开始游戏的时候添加一个InputComponent然后在作为参数传递给子类来处理的。而这个InputComponent虽然是Actor的,但是也没有导出,所以在脚本里面只好自行再定义一个InputComponent,为了防止重名,就叫它ScriptInputComponent吧。然后在BeginPlay里用这个组件绑定输入。

绑定输入的时候使用的BindAction函数后面的参数有点不一样,需要构造一个动态委托,感觉有点麻烦。但是试了下发现这样的做法也是有原因的:

  1. InputComponent的BindAction函数其实是没有导出的,现在调用的函数其实是mixin来的,所有有些不一样

  2. 为了实现可以在写脚本的时候检查函数名是否存在,如果写个辅助函数来避免写那个老长的委托签名的话就没有参数检查了。而且函数重命名后FName还会自动跟着改,很方便,不容易出错