虚幻C++入门个人笔记(3)——接口、智能指针、动画蓝图、行为树、EQS系统

接口

接口的词义广泛,用来陈述功能、选项,与其他程序结构进行沟通的方式。接口抽象出了交互结构,提供了两个未知逻辑交互的便捷性。对于编程中,如何更好地设计低耦合程序起到了至关重要的作用。设计者可以在互不关心的情况下,进行友好的程序设计,并且通过接口来完成设计的整合交互

虚幻引擎中加入了接口设计,从一定程度上“去掉了”多继承。接口可以帮助我们解决在不同类型的类之间却有相同行为的特性。接口设计增加了代码编写的便捷性(处理不同类中的相同行为)

例如在设计射击类游戏时,我们需要子弹与场景中的物体进行交互,场景中的桌椅板凳,角色,怪物(都是独立的对象)都希望受到子弹的攻击伤害,那么子弹在打到目标后要逐一排查,审查目标是否属于上述对象,这很麻烦。但我们可以通过接口,增加上述目标具有受伤的能力。当子弹打到目标时,我们只需要检查目标是否继承受伤的接口,如果有则调用接口函数即可

构建接口类

接口类可以直接在虚幻编辑器中选择继承然后完成构建        ——Unreal接口

编写接口函数

接口如果想在蓝图中使用可以在UCLASS中加入Blueprintable

如果接口在蓝图中被继承,则:

1.如果函数没有返回类型,贼在蓝图中当作事件Event使用

2.如果函数存在返回类型,则在蓝图中当作函数使用(需要在函数的overlap中寻找)

3.函数说明如果使用BlueprintNativeEvent则必须在继承类中定义函数(同名加后缀_Implementation),如果使用BlueprintImplementableEvent修饰则可以选择性重写

接口中要加标记宏UFUNCTION(BlueprintNativeEvent)

实现接口

如果在C++中希望获得接口能力,则需要继承接口。需要注意的是,必须继承I开头的接口名称,并且继承修饰位public。不要尝试重写接口中的函数。如果希望实现接口可以编写同名函数,并用后缀“_Implementation”进行标记。所有的接口函数都必须有定义

注意的地方

1.逻辑写在接口的头文件的I类的public里,不要写错了地方

2.不要加私有域和受保护域

3.不要在U类里写代码

继承操作

1.在你需要继承的接口的Actor的头文件里的class的后面加",public I接口名称",引入头文件

2.virtual 接口中的函数_Implementation() override; //声明并定义

调用操作

调用函数,持有继承接口对象指针,第一步先转换到I类指针,调用Execute_接口函数名,参数第一位需要传递原对象指针,后面直接按照原函数参数填入即可

接口例子:

1.创建接口函数

//接口头文件中

public:

    UFUNCTION(BlueprintNativeEvent,BlueprintCallable)
    void NotifyHurt(int32 Value);

    UFUNCTION(BlueprintNativeEvent,BlueprintCallable)
    int32 NotifyHurt_Two();

2.继承接口类

MyActor头文件中,继承添加上接口

3.创建接口函数定义

//MyActor头文件中

public:
    
    virtual void NotifyHurt_Implementation(int32 Value) override;        //加定义
    
    virtual int32 NotifyHurt_Two_Implementation() override;        //加定义,return0

4.调用接口函数

AMyActor* My = GetWorld()->SpawnActor<AMyActor>(AMyActor::StaticClass());
    if (My)
    {
        IHurtInterface* Interf = Cast<IHurtInterface>(My);
        if (Interf)
        {
            Interf->Execute_NotifyHurt(My, Num);
        }
    }

接口总结

1.接口函数需要定义在I开头的类中,不要修改访问域public关键字,声明需要使用宏标记BlueprintNativeEvent或BlueprintImplementableEvent(专门用来在蓝图中使用)

2.如果需要继承接口,继承I类,继承关系public

3.接口中的函数禁止重载

4.在继承类中实现接口函数,并添加后缀_Implementation,需要注意,函数前加入虚函数关键字virtual,函数结尾加override,并在cpp文件中实现逻辑

5.所有接口函数均必须要进行定义

6.调用函数,持有继承接口对象指针,第一步先转到I类指针,调用Execute_接口函数名,参数第一位需要传递原对象指针,后面直接按照原函数参数填入即可

7.检查某一个类是否实现了对应接口可以使用如下语法进行检查

obj->GetClass()->ImplementsInterface(U类型::StaticClass());

act->GetClass()->ImplementsInterface(UMyInterface::StaticClass());

act是对象指针

接口优点

1.具备多态特性,接口衍生类支持李氏转换原则

2.接口可以使得整个继承系统更加的干净单一

3.接口可以规范类的具体行为,继承自接口的类必须实现接口中的行为

4.接口可以隔离开发中的开发耦合,我们只需要针对接口去编码,无需关心具体行为

5.接口继承可以使得继承关系中出现真正的操作父类

接口缺点

1.丢失了C++中的广泛继承特性

2.接口拘束了类型的属性拓展,无法进行更详细的内容定义

3.继承关系中容易让人混淆,接口本身不具备真正的继承特性

虚幻中的智能指针

指针

虚幻为了解决C++中指针导致的内存泄漏问题,加入了智能指针(共享指针,共享引用,弱指针),当我们使用动态方式构建对象时,再也不需要担心内存释放的问题,指针的释放规则由引擎制定,包括释放时机

自定义类

智能指针不能应用到U类上,我们在虚幻中创建C++类中继承自虚幻的都是U类,选择无则是自定义类

在构建自定义类时需要以F开头

#pragma once

#include "CoreMinimal.h"
#include <UObject/GCObject.h>

/**
 * 
 */
class ULESSONJK_API FNClass : public FGCObject
{
public:
    FNClass();
    ~FNClass();

protected:
    class USoundBase* Sound;

    virtual void AddReferencedObjects(FReferenceCollector& Collector)override;

}

定义

void FNClass::AddReferencedObjects(FReferenceCollector& Collector)
{
    Collector.AddReferencedObject(Sound);

}

智能指针

虚幻中存在一套非常强大的动态内存管理机制,而这套机制中根本在于智能指针(非侵入式),并且UE的智能指针速度相比STL更快,速度和普通C++指针速度一样

智能指针本质目的是将内存工作进行托管。当两个智能指针指向同一个空间,一个设置为空,另一个不会跟随为空,智能指针设置为空并不是释放内存空间,只是在减少空间引用。

注意:智能指针只能适用于自定义类,U类禁止使用

共享指针和共享引用优点

优点 描述
简洁的语法 可以像操作常规的C++指针那样来复制、解引用及比较共享指针

防止内存泄漏

当没有共享引用时资源自动销毁
弱引用 弱指针允许安全地检查一个对象是否已经被销毁
线程安全 包含了可以通过多个线程安全地进行访问的“线程安全”版本
普遍性 几乎可以创建到任何类型的对象的共享指针
运行时安全 共享引用永远不会为null,且总是可以解引用
不会产生引用循环 使用弱引用来断开引用循环
表明用途 可以轻松地区分对象 拥有者和观察者
性能 共享指针的性能消耗最小,所有操作所占时间都是固定的
强大的功能 支持针‘const’、前置声明的不完全类型、类型转换等
内存 所占内存大小是C++指针在64位系统中所占内存的二倍(外加了一个共享的16字节的引用控制器)

构建智能指针时为什么要添加一个像弱指针一样的东西?

弱指针能够解决循环引用嵌套导致内存泄漏的问题

三个指针

共享指针 描述
共享指针(TSharedPtr) 引用计数的非侵入式的权威智能指针
共享引用(TSharedRef) 不能设置为null值的、引用计数的、非侵入式权威智能指针
弱指针(TWeakPtr) 引用计数的、非侵入式弱指针引用

共享指针

最常用的智能指针,用来托管内存

共享指针是虚幻中最常用的智能指针,在操作上可以帮助我们构建托管内存指针。共享指针本身是非侵入式的,这使得指针的使用与操作和普通指针一致。共享指针支持主动指向空,并且共享指针是线程安全的。节省内存,性能高效

注意:构建自定义类时,需要使用F开头

基本操作语法

声明和初始化

TSharedPtr<FNClass> pN;        //声明一个未被赋值的共享指针

TSharedPtr<FNClass> pN(new FNClass());        //构建一个赋值了的共享指针

TSharedPtr<FNClass> pN=MakeShareable(new FNClass());        //借助MakeShareable构建一个共享指针

//MakeShareable函数是用来构建共享指针的快捷方式

不要将一个指针变量单独作为两次共享指针对象的创建,会出错

例如

FNClass* p1=new FNClass();

TSharedPtr<FNClass> pN(p1);

TSharedPtr<FNClass> pN2(p1);        //会出错

TSharedPtr<FNClass> pN2=pN;        //没问题

解引用和操作

pN->CallFun();        //CallFun是成员函数

pN.Get()->CallFun();

(*pN).CallFun();

使用前要if(pN.IsValid())

释放

pN.Reset();

pN=nullptr;        //标记我不使用这个指针了,并不是真正的释放了,释放由系统自动

获取引用计数器

pN.GetSharedReferenceCount();        //获得当前地址被引用个数

共享引用

共享引用和共享指针一样,都是为了完成内存托管的问题,特点是不能主动释放为空

共享引用禁止为空,表明了共享引用创建后必须给予有效初始化,可以使得代码更加简洁安全,保证了对象访问的安全性。无法主动释放共享内存,可以跟随对象释放减少引用计数器

共享引用的安全性体现在,如果使用共享引用构建的对象,无法将对象空间设置为空。如果想释放内存,可以借助指向其他共享引用来减少引用计数,来释放空间

共享引用本质,无法主动减少引用计数器,只能通过被动方法,例如生命周期终结,共享引用易主

操作语法

声明和初始化

TSharedRef<FNClass> pN;        //错误 执行将导致崩溃,共享引用无法为空

TSharedRef<FNClass> pN(new FNClass());        //正确

解引用操作

TSharedRef<FNClass> pN(new FNClass());        //正确

pN->CallFun();

(*pN).CallFun();

const FNClass& pN1=pN.Get();        //返回const引用,禁止将对象主动释放

和共享指针转换

//共享引用支持隐式转换为共享指针,由于共享引用是安全的,所以转换是隐式转换

TSharedPtr<FNClass> pSN=pN;

//从共享指针转换到共享引用是不安全的,所以需要调用TS函数

TSharedRef<FNClass> pM=pSN.ToSharedRef();

弱指针

智能指针好用但解决不了引用循环的问题,弱指针就是被拿来解决这个问题的

弱指针不会阻止对象的销毁,如果引用对象被销毁,则弱指针也将自动清空。一般弱指针的操作意图是保存了一个到达目标对象的指针,但不会控制该对象的生命周期,弱指针不会增加引用计数,可以用来断开引用循环问题

引用循环问题:当有两个类的时候,A类持有B类智能指针pb,B类持有A类智能指针pa,

A* a;

B* b;

a->pb=b;

b->pa=a;

//当引用计数器为0时智能指针释放

无论是谁销毁了对象,只要其对象被销毁,弱指针都将自动清空,这使得能够安全缓存指向可变对象的指针。这也意味着,弱指针可能会意外清空,并且,可以使用弱指针断开引用循环

当不再存在对对象的共享引用时,弱指针的对象将被销毁

弱指针有助于表明意图。当在某一个类中看到一个弱指针时,就应当明白该类仅缓存指向对象的指针,它并不会控制它的生命周期

操作语法

声明和初始化

TWeakPtr<FNClass> pWN;        //声明一个空的弱指针

TSharedPtr<FNClass> pN;

TWeakPtr<FNClass> pWN1(pN);        //借助共享指针构建

TSharedRef<FNClass> pRN;

TWeakPtr<FNClass> pWN2(pRN);        //借助共享引用构建

解引用操作

//弱指针无法直接调用(弱指针调用会出现对象为空,因为弱指针不会阻止对象释放)

//弱指针如果需要操作,需要转换为共享指针

TSharedPtr<FNClass> pN(pWN.Pin());

if(pN.IsValid())        //检查是否转换成功

{

pN->Func();        //使用共享指针的操作方式

}

//Pin函数会组指对象被销毁

检查是否有效

//检查弱指针指向的对象空间是否存在

if(pWN.IsValid())

{}

释放操作

//主动释放,但是并不会影响引用计数

pWN=nullptr;

智能指针总结

一块内存如果存在有效引用(可直接到达内存的操作方式),则我们可以认为当前内存是有效且合理的。但当一块内存不存在引用,则我们可以视为此块内存为被弃用无效的,则可以回收重复利用,这就是内存垃圾回收机制的基本原理

智能指针强调的是当前内存的使用者存在多少,当不存在时进行回收

注意:智能指针构建的均是栈对象数据类型

虚幻中的动画

动画合成:将两段动画片段进行拼接,组合成为新的动画。合成动画禁止使用被调整的动画,例如蒙太奇,混合空间等

动画蓝图:可以用来编写动画逻辑数据蓝图

动画蒙太奇:用来将动画进行逻辑拆分与组合

混合空间:基于基准动作,将多个动画通过使用驱动值(最多两个)进行混合过度

目标偏移:使用动画叠加空间,将多个动画进行组合,最后使用驱动数据(最多两个)进行驱动调整

姿势资源:允许将动画与基准动作之间过度时借助曲线进行调整,可以构建表情动画(表情这东西不是一直播放的)

动画组合方式

附加动画:例如将上半身开枪的动画叠加到任何下半身姿势上,以构建多样性动画

混合动画:混合节点和混合空间

动画状态机

状态机通过图形化方式将骨架网格体的动画拆分为一系列状态,然后按照转换规则管理这些状态,转换规则可控制从一个状态混合到另一个状态

动画通知

在AnimInstance类响应普通通知需要实现“AnimNotify_通知名称”函数,并标记UFUNCTION(),函数编写方式有两种 两种均能响应,蓝图里可以直接加通知名称事件

UFUNCTION()

void AnimNotify_CallBack();

UFUNCTION()

void AnimNotify_CallBack(class UAnimNotify*);

状态通知无法直接响应到AnimInstance类里面 不管是蓝图还是C++都无法接收到状态通知

构建动画通知C++类

1.帧通知

重写通知函数

virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;

2.状态通知

重写状态通知函数

virtual void NotifyBegin(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float TotalDuration) override;

virtual void NotifyTick(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation, float FrameDeltaTime) override;

virtual void NotifyEnd(USkeletalMeshComponent * MeshComp, UAnimSequenceBase * Animation) override;

状态和转换规则里都可以写自定义事件,但变量需要蓝图自己创建,C++的变量好像不会响应

动画曲线

曲线响应

在C++中,通过在AnimaInstance中调用函数GetCurveValue进行获取当前动画中指定名称曲线的值。例如在播放的动画中,存在曲线“TurnSpeed”,我们可以在C++中通过如下的函数获取当前曲线值

float Value=GetCurveValue(TEXT("TurnSpeed"));//有返回值的函数

在蓝图中可以直接通过GetCurveValue节点即可响应

通过曲线驱动材质

在动画播放过程中,我们可以通过添加曲线来驱动材质中的某个参数变化,从而达到曲线引导材质制作效果。例如玩家的脸红,眼球变色,身体上的灯闪烁等

1.需要在动画中添加曲线,在曲线管理器中找到曲线后,勾选类型为材质

2.在网格材质中添加材质表达式标量参数,并命名为与曲线名称相同的名字

3.编译材质,预览动画即可看到效果

IK

FK正向运动学,由数据文件驱动骨骼,骨骼带动子父级,动画是正向

逆向运动学(IK)提供一种从末端执行器的位置处理关节旋转的方法,而不是通过直接关节旋转。在实践中,提供一个执行器的位置,然后IK解决方案解决旋转,使最终的关节尽可能与该位置重合

行为树

以数据为驱动元,进行逻辑节点检查,寻找合理逻辑叶子节点,然后进行动作执行

UE行为树特点

1.事件驱动型

事件发生了才做出反应,没发生不反应

2.叶子节点不是条件语句,是任务语句

UE中引入了Decorator系统作为条件语句。行为树标准模型中,条件语句为叶子节点,除去成功和失败不执行任何操作。在传统模型中,条件语句混于叶子节点中,因此需要花费更多时间辨认

3.并发行为处理

标准行为树通常使用Parallel Composite节点来处理并发行为。Parallel节点同时执行其所有子项。在一个或多个子项数完成时,特殊规则将决定如何执行(取决于所需行为)

UE4行为树抛弃了复杂Parallel节点,使用Simple Parallel节点合称为Services的特殊节点,以实现同类行为

(抛弃复杂Parallel节点原因)

1容易让人迷惑,在多平行行为中容易迷失,无法追寻逻辑方向

2Parallel不利于性能优化,无法方便构建事件驱动型集合

(Simple Parallel节点)

1.只允许有两个选项,一个必为单独任务节点(含可选decorators)、另一个为一个完整的分支树

2.支持Parallel节点的多数用法

3.利用Simple Parallel节点简便地进行一些事件驱动型优化

UE4并发行为树的优点

1.清晰明了:使用Services和Simple Parallel节点可创建出易于理解的简单行为树

2.易于纠错:使图表更清晰,便于纠错。除此之外,更少的同时执行路径十分便于观察图表中实际发生的状况

3.优化简单:如果没有较多同时执行的分支树,事件驱动型图表将更易于优化

UE4行为树

行为树5节点

1.Composite,这种节点定义一个分支的根以及该分支如何被执行的基本规则(Sequence顺序执行,Select选择,谁能干谁干,Simple不需要你管我,只要到我我就会执行一个独立行为树)(Composite下面可以接Task也可以接Composite)(Composite内可以附着Decorators)

2.Task,这种节点是行为树的叶子,实际“执行”操作,不含输出连接(只有两种结果,干完了或者没干完)

3.Decorator,即为条件语句。这种节点附着于其他节点,决定着树中的一个分支,甚至单个节点是否能被执行(开关,分支是否执行)

4.Services,这种节点附着在Composite或者Task节点上,只要其分支节点被执行,它们便将按所定义的频率执行。它们常用于检查和更新黑板(专注做一件事的时候,Services会做额外的检查服务)

5.Root,根节点,无法被附着,可以用来设置黑板数据(没卵用 就是汇总)(Root根节点下面只能连Composite不能连任务节点)

黑板用来专门记录逻辑检查数据的

Blackboard黑板

1.行为树的核心模块

2.用于支撑行为树中的数据存储和读取

3.相当于我们的草稿纸,我们可以把需要参与逻辑的数据罗列在上面

4.使用方便,数据扩展性强

5.有较好的维护性

黑板的相关API

OwnerComp.GetBlackboardComponent()->GetValueAsObject(TEXT("Target"))

OwnerComp.GetBlackboardComponent()->SetValueAsObject(TEXT("Target"), Hero);

Composite执行规则

1.Sequence顺序执行,从左至右执行,只要有一个子项条件不成立,则停止执行剩余项。所有子项成立,则Sequence成立

2.Select选择,谁能干谁干,从左至右执行,只要有一个子项条件成立,则停止向下执行。任意子项成立,则Selector成立

3.Simple不需要你管我,只要到我我就会执行一个独立行为树,并行树,允许单个任务在任务树旁执行。Finish Mode中的设置将确定节点是否立即完成、是否终止次要树,或是是否延迟次要树的完成

Task任务节点

Task里重写写逻辑的函数

virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;

1.真正在干活的角色

2.可以重写,支持扩展

3.UE提供了很多基本的任务节点

3.1 Make Noise        制造噪音,需要Pawn带PawnNoiseEmitter组件,其他Pawn如果带有PawnSensing组件,则可以听到噪音

3.2 Move To        移动到目标点,通过黑板提供

3.3 Play Sound        播放声音

3.4 Run Behavior        执行其他行为树 两个行为树会共用一个黑板

3.5 Wait, Wait BB Time        等待时间,无法被中止

3.6 Play Anim        促使控制Pawn播放动画

Task里绑定键值到API巴拉巴拉的,键值结构体里选黑板中的元素,这样就不用在C++里每次都写TEXT(“某某黑板值”),容易出错

1.重写以下函数

virtual void InitializeFromAsset(UBehaviorTree& Asset) override;

还有声明以下变量(这是例子)

    UPROPERTY(EditAnywhere)
    struct FBlackboardKeySelector OriginPosKey;

    UPROPERTY(EditAnywhere)
    struct FBlackboardKeySelector PatrolPosKey;

2.定义中

    Super::InitializeFromAsset(Asset);

    UBlackboardData* BBData = GetBlackboardAsset();//获取关联黑板数据
    if (BBData)
    {
        OriginPosKey.ResolveSelectedKey(*BBData);    //关联键值到黑板数据
        PatrolPosKey.ResolveSelectedKey(*BBData);    

    }

3.添加类型约束器

在构造函数中

//添加黑板数据拾取键值的约束器

OriginPosKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UBTTaskNode_FindPatrol, OriginPosKey));
    PatrolPosKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UBTTaskNode_FindPatrol, PatrolPosKey));

Decorator

1.Blackboard检查一个当先给定的黑板值是否设定 经常用

2.Compare Blackboard Entries比较黑板值

3.Composite检查黑板值

4.Conditional Loop条件循环

5.Cone Check锥形检测

6.Cooldown冷却时间(锁定节点或分支,直至时间结束)

7.Does Path Exist检查AB两点间路径是否可行

8.Force Success强制成功

9.Gameplay Tag Condition检查标签是否存在

10.Is Blackboard Value of Given Class通过检查黑板中的值是否使用的某一个类进行构建

11.Keep in Cone持续检查某一个目标是否在锥形范围内

12.Loop以一定次数或是无限次数循环当前的执行条件

13.Reached Move Goal检查移动是否可行

14.Set Tag Cooldown设置冷却时间

15.Tag Cooldown冷却时间

16.Time Limit节点逻辑时间定时器

检查当前装饰器是否往下执行

virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)override;

构造函数bNotifyTick = true后void UBTDecorator_CheckInArea::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)才会执行

观察者中止逻辑

1.NONE,不终止执行

2.Self,中止Self,以及在此节点下运行的所有子树

3.Lower Priority,中止此节点右方的所有节点

4.Both,中止Self、此节点下运行的所有子树、以及此节点右方的所有节点

以上逻辑操作,适用于装饰器(当父节点是Selector时则具备所有终结逻辑)

Service服务

Service里重写写逻辑的函数

virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

1.附着在Composite节点上或Task节点

2.只要节点内执行,则Service会运行

3.可以按照指定的频率执行

4.主要意图在于检查和更新黑板内容

5.它们以行为树系统的形态取代了传统平行节点

6.我们一般会重写此节点

C++重写Service

要在模块里加上"GamplayTasks"或者写以下两个函数并加定义,否则编译不过

virtual void OnGameplayTaskActivated(class UGameplayTask&) override;

virtual void OnGameplayTaskDeactivated(class UGameplayTask&) override;

Simple Parallel

左边是主树,右边是从树

结束模式中直接是主树结束次树直接结束,已延迟是主树结束后等待次树结束

EQS系统

全名Environment Query System(环境询问系统),旨在帮助用户构建环境信息查询器,收集环境信息,进行分析测试。根据给定的条件,寻找合适的位置进行返回。EQS系统在虚幻中是测试系统,使用需要先进行开启,EQS必须跟寻路网格一起使用

原理:通过给定的生成器(用于环境选点),应用测试(条件排查),选取最符合测试结果的位置,并进行返回

操作步骤

开启EQS系统(编辑器 实验项 AI中)//版本老则需要开启

创建EQS(内容浏览器 右键 AI中)

选取生成器(用于在环境中收集信息点)

编写测试项(可以多条件共用)

行为书中运用EQS

将EQS内容返回到黑板

使用黑板数据

生成器

将使用EQS的询问者为中心,以给定的规则进行采样点选取,将选取结果进行备选预留

目的:选一堆点然后筛选

测试节点

用于将环境生成器中采集到的点进行筛选,通过给定的筛选条件选择出最合适的点进行返回

测试节点

1.Distance

距离测试节点,将选择器选取的点用给予参照内容进行距离测试

2.Dot

使用点乘的方式,为选点打分,至于范围0~1,点乘有负数,打分无负数,正面为1,背面为0,呈现扇形左右递增向1.也可以对结果进行绝对值设定(勾选绝对值,结果为点乘后绝对值值域0~1,正面1,背面1,左右两边0),点乘中的向量选取可以使用两点方式或Rotation方式

3.Gameplay Tags

使用标签进行询问测试(Gameplay Tags本身是为Actor查询操作提供分层,一般用于查询,只有当生成器是Actor时有效)

4.Overlap

在采样点,使用通道检测方式,返回是否有符合通道标记的内容存在,如存在返回1,反之为0。用于检测查询某一个点附近是否存在某一样东西

5.Pathfinding

将采样点与内容进行导航寻路器测试,可达分为1,不可达为0,它将用于最终测试点生成,返回一个可达测试点

6.Pathfinding Batch

将采样点与内容进行导航寻路器测试,可达分为1,不可达为0,它会返回所有可达点,供下个测试项目使用

7.Project

投射测试,可以用来修正生成器采样点位置。两种模式:导航投射,在采样点为基准,垂直向上向下进行导航检测,碰到导航覆盖点则修正采样点位置;形状检测,不考虑导航,只要在通道碰撞成立,则修正采样点到新的位置

8.Trace

射线测试,用来测试采样点到Context参照点之间是否存在遮挡,这是重要节点,一般可以用来寻找可以用来躲避的采样点(从采样点到敌人之间存在遮挡则认定可以躲避),存在遮挡分数为1,不存在分数为0。注意:需要考虑高度问题,采样点一般均在地面,从地面直接发射射线到Context可能会出现被非常矮的物体阻挡,可以通过调整Height Offset解决此问题

EQS测试角色

用于在未进行场景运行时,在编辑器模式下提供测试信息结果反馈

构建:在创建蓝图类,父类选择EQSTestingPawn

然后将构建好的EQS文件设置在Pawn的细节面板中

以角色为目标来实现测试点

蓝图中如果目标想要变的话(就是需要到达一个场景中的角色的某位置),就需要新建一个EnvQueryContext_BlueprintBase对象,然后修改重载函数,在里面直接获取玩家角色返回

C++中新建一个EnvQueryContext类对象,然后重载以下函数

virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData)const override;

定义中

Super::ProvideContext(QueryInstance, ContextData);
    
    AActor* Act= Cast<AActor>(QueryInstance.Owner.Get());

    if (!Act)
    {
        return;
    }
    ACharacter* Player= UGameplayStatics::GetPlayerCharacter(Act->GetWorld(), 0);
    UEnvQueryItemType_Actor::SetContextHelper(ContextData, Player);

以上两种操作都需要把环境查询里的EnvQueryContext_Querier改为对应的EnvQueryContext(该操作把需要模拟点位置角色的信息改为了场景中的操作角色,即把EQS测试角色设为了在场景中操作角色的位置)

猜你喜欢

转载自blog.csdn.net/hoppingg/article/details/126952322