UE_Angelscript 绑定篇——解决方案:BlueprintInternalUseOnly 函数的增强处理方式#

问题的由来,是有很多有用的函数,蓝图里可以调用,但是 AS 里却不行。这些函数有不少是属于 BlueprintAsyncActionBase 及其子类的。

于是,如何暴露 BlueprintAsyncActionBase 下的 BlueprintInternalUseOnly 的函数,成为了出发点。但这不是终点,还有其他一些类也有这类型的有用函数,比如 UGameplayTask 及其子类,它们也需要得到照顾。

为什么不全部暴露所有的 BlueprintInternalUseOnly 函数?是做不到吗?不是的,恰恰相反,AS 脚本是特意不绑定此类型的函数的。BlueprintInternalUseOnly 标记的用处是向蓝图隐藏这个函数(蓝图有另外的机制来调用这个函数),如果都给 AS 暴露出来了,实际上有违类设计者的初衷了,这会不会造成一些混淆,尚未可知。

所以,一般情况下,这类函数就禁止暴露,然后在使用者需要的时候,自行选择暴露出需要的类,才是最好的方式。

方案 1 基础版,直接去掉这个 BlueprintInternalUseOnly,副作用是蓝图出现两个一样的函数#

以下是专门处理 BlueprintAsyncActionBase 及其子类的代码,去掉了 BlueprintInternalUseOnly 的标签,这会导致蓝图出现两个一样的函数,一个是异步的,一个是普通的。

如果不在意蓝图出现两个同样的函数,这么写也是相当方便,项目需要暴露什么类就将什么类的 Meta 修改。

方案 1 来自官方论坛的讨论,做法是在绑定前移除所选类函数上的 BlueprintInternalUseOnly 元数据。

以上代码以上代码来源于 AS 社区的讨论,实际上还有优化空间,比如缓存目标类的 UClass 指针,减少不必要的查找:

static TArray<UClass*> CachedAsyncActionClasses;
if (CachedAsyncActionClasses.IsEmpty())
{
    for (TObjectIterator<UClass> It; It; ++It)
    {
        if (It->IsChildOf<UBlueprintAsyncActionBase>())
        {
            CachedAsyncActionClasses.Add(*It);
        }
    }
}

方案 2 修订版,修改 AS 插件源码:Bind_BlueprintCallable:36#

基于论坛讨论的方案修改。

以下处理了 BlueprintAsyncActionBase 和 UGameplayTask 及其子类,只绑定了返回自身的静态函数。

// BlueprintInternalUseOnly functions are not bound
// Engine Divergence Start - tdxh - BP Async Actions aren't visible in AS
// Original line: if (Function->HasMetaData(NAME_Function_BlueprintInternalUseOnly)) return;
if (Function->HasMetaData(NAME_Function_BlueprintInternalUseOnly))
   {
       TArray<UClass*> ClassesToBind
       {
            UBlueprintAsyncActionBase::StaticClass(),
            UGameplayTask::StaticClass() // For GAS , so many AbilityTask!
       };

       UClass* OwningClass = CastChecked<UClass>(Function->GetOuter());

    bool bIsChildOfAnyClass = ClassesToBind.ContainsByPredicate([OwningClass](const UClass* Class) {
       return OwningClass->IsChildOf(Class);
    });
    if (!bIsChildOfAnyClass) return;

       const FProperty* ReturnProperty = Function->GetReturnProperty();
       if (ReturnProperty)
       {
            const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(ReturnProperty);
            if (ObjectProperty && !ObjectProperty->PropertyClass->IsChildOf(OwningClass))
            {
               return;
            }
       }
}
// Engine Divergence End

因为原始的绑定配置里,碰到 BlueprintInternalUseOnly 就直接不绑定了,现在修改了一下,以下的情况会将这个函数绑定:这个 BlueprintInternalUseOnly 函数的 UClass 是 UBlueprintAsyncActionBase 或其子类,同时这个函数返回的是其自身(也就是常见的返回自身类的 static 函数)。

如果加了 UGameplayTask::StaticClass()这一行,需要在这个脚本模块的 cs 文件 AngelscriptCode.Build.cs 里的 PrivateDependencyModuleNames 添加上依赖模块 "GameplayTasks" 。

注:以上方案 1 和 方案 2 的内容来自于官方论坛的讨论,我只对方案 2 的内容做了一点修改。

注意,方案 2 修改了后,还需要自己绑定 Active 函数或者 ReadyForActivation 函数,在蓝图里不需要主动调用,但是,cpp 和 AS 都需要调用的。 (AngelscriptGAS 插件已经绑定了 ReadyForActivation,同时有大量的 AbilityTask 的函数绑定在了 AngelscriptAbilityTask 命名空间下。)

Active 函数的绑定参考如下。

UCLASS(Meta = (ScriptMixin = "UBlueprintAsyncActionBase"))
class XXXX_API UBlueprintAsyncActionBase_MixinLibrary : public UObject
{
    GENERATED_BODY()

public:
    UFUNCTION(ScriptCallable)
    static void BP_Activate(UBlueprintAsyncActionBase* BlueprintAsyncActionBase)
    {
       return BlueprintAsyncActionBase->Activate();
    };
};

方案 3 增强版:自由配置需要的 BlueprintInternalUseOnly 函数#

原版本的方案 2 ,如果想要增加绑定的类,就需要修改源码,而且需要修改引用的文件,相当麻烦。是不是可以通过配置来实现方便地修改呢?

考虑的方案是通过配置 ini 文件,让 AS 加载的时候读取 ini 文件获取到我们想要跳过的类,然后在绑定的时候进行判断即可。

为了不让每次绑定的时候都读取文件获取相应的类,可以设置一个全局唯一单例来管理。

1,设计配置文件#

首先在引擎的 Config 文件夹下新建一个配置文件,比如 AngelscriptBindSetting.ini,内容写上

; Prefix dont allow
[BindTheClassWithMetaBlueprintInternalUseOnly]
ClassesToBind=BlueprintAsyncActionBase
ClassesToBind=GameplayTask
ClassesToBind=AnimInstance

之后如果想增加,就继续增加行数即可。注意,类名不要前缀。

2,Bind_BlueprintCallable 文件里增加一个单例#

这个单例会在第一次调用的时候构造,之后就不会再重新构造了。 单例的主要作用是读取配置的 ini 文件的内容并且根据内容找到对应的类,将这些类存进 ClassesToBind 这个成员里。

注意,这里的类设计实现了懒加载,只有调用了静态函数 GetInstance 才会生成实例。


class FClassesToBind_BlueprintInternalUseOnly
{
public:
    static FClassesToBind_BlueprintInternalUseOnly& GetInstance()
    {
       static FClassesToBind_BlueprintInternalUseOnly Instance;

       // Only call Initialize() if not already initialized
       if (!Instance.bInitialized)
       {
          Instance.Initialize();
       }

       return Instance;
    }

	bool ShouldBindClass(const UClass* InClass) const
	{
		for (const UClass* ClassToBind : ClassesToBind)
		{
			if (InClass->IsChildOf(ClassToBind))
				return true;
		}
		return false;
	}

    // Initialized and ClassesToBind is not Empty
    bool IsValid() const
    {
       return bInitialized && !ClassesToBind.IsEmpty();
    }

private:
    /** only GetInstance() can access this function */
    FClassesToBind_BlueprintInternalUseOnly()
    {
       // Lazy Initialization to Initialize()
}

    void Initialize()
    {
       if (bInitialized) return;

       TSet<FName> ClassesToBindNames;

       // explicit load AngelscriptBindSetting.ini
       const FString ConfigFilePath = FPaths::Combine(FPaths::EngineConfigDir(), TEXT("AngelscriptBindSetting.ini"));

       if (!FPaths::FileExists(ConfigFilePath))
       {
          UE_LOG(LogTemp, Warning, TEXT("Can not find .ini:  %s"), *ConfigFilePath);
          return;
       };
       GConfig->LoadFile(ConfigFilePath);

       const FString ConfigSection = TEXT("BindTheClassWithMetaBlueprintInternalUseOnly");
       const FString KeyName = TEXT("ClassesToBind");
       TArray<FString> ClassNames;

       if (GConfig->GetArray(*ConfigSection, *KeyName, ClassNames, ConfigFilePath))
       {
          for (const FString& ClassName : ClassNames)
          {
             ClassesToBindNames.Emplace(FName(*ClassName));
             UE_LOG(LogTemp, Log, TEXT("ClassesNameToBind from ini:  %s"), *ClassName);
          }
       }
       else
       {
          UE_LOG(LogTemp, Warning, TEXT("ConfigFile ' %s ' have not ConfigSection: %s or KeyName:  %s"), *ConfigFilePath,*ConfigSection, *KeyName);
          return;
       }

       for (TObjectIterator<UClass> It; It; ++It)
       {
          if (ClassesToBindNames.Contains(It->GetFName()))
          {
             UE_LOG(LogTemp, Log, TEXT("Binding BlueprintInternalUseOnly: %s"), *It->GetName());
             ClassesToBind.Emplace(*It);
          }
       }

       bInitialized = true;
    }

private:
    TSet<UClass*> ClassesToBind;
    bool bInitialized = false;
};

另外,值得一提的是,如果想把 ini 文件放在项目文件夹的 Config 下,只需要修改 EngineConfigDir 为 ProjectConfigDir 就可以,这样的好处是每个项目都不同,额,不过我不需要,就在引擎里放了。当然,两个地方都放,也可以,这就需要两个地方都读取和加载了,需要的时候可以修改代码实现。

3, 修改 BindBlueprintCallable 函数

前置判断 ClassesToBind,如果为空,就直接跳过了。其余基本和之前的方案一致。

// BlueprintInternalUseOnly functions are not bound
// Engine Divergence Start - tdxh - BP Async Actions aren't visible in AS
// Original line: if (Function->HasMetaData(NAME_Function_BlueprintInternalUseOnly)) return;
if (Function->HasMetaData(NAME_Function_BlueprintInternalUseOnly))
   {
    if (!FClassesToBind_BlueprintInternalUseOnly::GetInstance().IsValid()) return;

       UClass* OwningClass = CastChecked<UClass>(Function->GetOuter());

    if (!FClassesToBind_BlueprintInternalUseOnly::GetInstance().ShouldBindClass(OwningClass)) return;
}
// Engine Divergence End

方案 2 里有一段代码是用来检查函数返回值是不是当前 Class 或其子类的,它限制只有这类返回值的函数才被绑定,但在这里删除掉了,因为有很多函数的函数值就不是返回自身类型的,那些函数也可以暴露给 AS 使用。

注:此方案主要提供了一个思路,通过配置来决定什么内容应该绑定到 AS 。代码来源是我写的,未经过大量的测试,所以请以参考思路为主。

总结#

如果之前专门针对这些 BlueprintInternalUseOnly 进行过绑定,现在又采用这种方式将 BlueprintInternalUseOnly 函数暴露出来,可能会产生一些 AS 代码的冲突,比如同名函数的混淆(这来源于类似于 BP\_开头的函数会被截掉前缀,然后同名且同参数了),会要求修改 AS 的代码,实际上不需要,而是取消之前的一些绑定。

另外,有时候出现的不是同名函数,而是重复绑定的错误(这来源于绑定的时候用的同名,而不加任何前缀后缀): Failed in call to function 'RegisterObjectMethod' with 'UBlueprintAsyncActionBase' and 'void Activate()' (Code: asALREADY_REGISTERED, -13)

这时候就需要注释掉一些之前的绑定代码了。

最后再提一下,代码都需要测试的哦。这些代码在我的机器和运行环境下,目前没有问题。


×

联系

问题反馈

问题与反馈请提交至 GitHub Issues。

合作

合作事宜请发送邮件至 contact@cuviss.com。

×

关于

服务条款

各插件的许可证见社区贡献页。

网站内容可在注明来源后引用。

软件和文档按“现状”提供,不作任何保证。

Unreal Engine 等第三方项目及名称相关商标归各自所有者所有。soloAS 与 Epic Games、Hazelight 均无关联。

致谢和案例展示请求均不具约束力,详见首页说明。

更新于 2026-10-24

隐私政策

本网站不使用分析工具、Cookie 或跟踪,也没有数据表单或第三方脚本。

网站托管于 GitHub Pages。GitHub 可能按其隐私声明处理标准访问日志。

通过邮件发送的内容会提供给我们;通过 GitHub Issues 发送的内容会提供给 GitHub。

更新于 2026-10-24

目录

页面列表

本页目录