350 lines
18 KiB
Markdown
350 lines
18 KiB
Markdown
|
---
|
|||
|
title: Ue4 AI相关功能笔记
|
|||
|
date: 2021-09-07 19:57:53
|
|||
|
tags: AI
|
|||
|
rating: ⭐️
|
|||
|
---
|
|||
|
## 其他文档
|
|||
|
UE4场景询问系统浅析(EQS与行为树
|
|||
|
http://www.uejoy.com/?p=500
|
|||
|
|
|||
|
UE4 AIModule源码阅读之AI感知系统
|
|||
|
https://zhuanlan.zhihu.com/p/356716030
|
|||
|
|
|||
|
## AI感知系统(AI Perception System)
|
|||
|
文档:https://docs.unrealengine.com/4.26/zh-CN/InteractiveExperiences/ArtificialIntelligence/AIPerception/
|
|||
|
|
|||
|
给角色类添加AIPerception组件并且添加所需要类型的感知,即可实现感知效果。
|
|||
|
|
|||
|
- AI伤害:对Event Any Damage、Event Point Damage或Event Radial Damage作出反应
|
|||
|
- AI听觉:可用于检测由 报告噪点事件(Report Noise Event) 产生的声音
|
|||
|
- AI感知报告:在PredictionTime秒内向请求者提供PredictedActor的预计位置。
|
|||
|
- AI视觉:当一个Actor进入视觉半径后,AI感知系统将发出更新信号,并穿过被看到的Actor(举例而言,一个玩家进入该半径,并被具备视觉感知的AI所察觉)。
|
|||
|
- AI团队:这会通知感知组件的拥有者同团队中有人处在附近(发送该事件的游戏代码也会发送半径距离)。
|
|||
|
- AI触觉:配置能够检测到AI与物体发生主动碰撞、,或是与物体发生被动碰撞。。举例而言,在潜入类型的游戏中,您可能希望玩家在不接触敌方AI的情况下偷偷绕过他们。使用此感官可以确定玩家与AI发生接触,并能用不同逻辑做出响应。
|
|||
|
|
|||
|
除了可以使用绑定FActorPerceptionUpdatedDelegate委托来实现获取目标Actor外,还可以使用GetHostileActorsBySense()(蓝图版本GetCurrentlyPerceivedActors())
|
|||
|
|
|||
|
处理函数ProcessStimuli()是从UAIPerceptionSystem::Tick()调用的,所以只需要修改Tick频率即可。PrimaryActorTick.TickInterval即可。
|
|||
|
|
|||
|
UAISense_Prediction一般与UAISense_Sight连用,在UAISense_Sight丢失目标后若干秒(使用计时器),调用RequestControllerPredictionEvent,其中PredictionTime为预测时间长短。之后在处理函数中将事件Location传递给黑板变量。
|
|||
|
|
|||
|
### UPawnSensingComponent
|
|||
|
除了UAIPerceptionComponent还可以使用UPawnSensingComponent,有OnSeePawn与OnHearNoise委托。不过UPawnSensingComponent有个问题即它只能“看到”或者“听到”Pawn。
|
|||
|
|
|||
|
案例代码:
|
|||
|
```
|
|||
|
ARPGAICharacter::ARPGAICharacter(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer.SetDefaultSubobjectClass<ULCMCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
|
|||
|
{
|
|||
|
AIControllerClass = ARPGAIController::StaticClass();
|
|||
|
bUseControllerRotationYaw = true;
|
|||
|
|
|||
|
SensingComponent = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("SensingComponent"));
|
|||
|
SensingComponent->SetPeripheralVisionAngle(90.f);
|
|||
|
}
|
|||
|
|
|||
|
void ARPGAICharacter::PossessedBy(AController* NewController)
|
|||
|
{
|
|||
|
Super::PossessedBy(NewController);
|
|||
|
|
|||
|
if (SensingComponent)
|
|||
|
{
|
|||
|
SensingComponent->OnSeePawn.AddDynamic(this, &ARPGAICharacter::OnSeeTarget);
|
|||
|
SensingComponent->OnHearNoise.AddDynamic(this, &ARPGAICharacter::OnHearNoise);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void ARPGAICharacter::OnSeeTarget(APawn* Pawn)
|
|||
|
{
|
|||
|
ARPGAIController* AIController = Cast<ARPGAIController>(GetController());
|
|||
|
//Set the seen target on the blackboard
|
|||
|
if (AIController)
|
|||
|
{
|
|||
|
GLog->Log("Oh hello there");
|
|||
|
AIController->SetSeenTarget(Pawn);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void ARPGAICharacter::OnHearNoise(APawn* Pawn,const FVector& Location, float Volume)
|
|||
|
{
|
|||
|
ARPGAIController* AIController = Cast<ARPGAIController>(GetController());
|
|||
|
//Set the seen target on the blackboard
|
|||
|
if (AIController)
|
|||
|
{
|
|||
|
GLog->Log(FString::Printf(TEXT("Hear Noise:Vec3(%f,%f,%f) Volume:%f"),Location.X,Location.Y,Location.Z,Volume));
|
|||
|
AIController->SetHearTarget(Pawn,Location,Volume);
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
控制类中一般是将这些数据添加到黑板中。对应HearNoise则需要在角色中添加UPawnNoiseEmitterComponent
|
|||
|
,之后调用MakeNoise实现。
|
|||
|
```
|
|||
|
PawnNoiseEmitterComponent->MakeNoise(this, 1.f, GetActorLocation());
|
|||
|
```
|
|||
|
|
|||
|
## 刺激源(Stimuli Source)
|
|||
|
该组件封装了场景中可以感知的内容以及可以感知的内容。它通常添加在AI需要感知的Actor中。
|
|||
|
|
|||
|
## 调试方法
|
|||
|
您可以使用AI调试工具调试AI感知。操作方法是在游戏运行时按下撇号(')键,然后按数字键4调出感知信息。
|
|||
|
|
|||
|
## 设计思路
|
|||
|
对于ARPG游戏来说我们只需AI听觉、AI感知报告、AI视觉与AI团队。
|
|||
|
AI听觉:让AI确定TargetLocation,让AI去最后听到声音的位置查看。
|
|||
|
AI视觉:让AI发现TargetActor。
|
|||
|
AI团队:让AI将自己的TargetActor发送给其他AI。
|
|||
|
|
|||
|
## UE4 AI阻挡AI解决策略
|
|||
|
CharacterMovement->bUseRVOAvoidance = true
|
|||
|
|
|||
|
## RAMA的插件
|
|||
|
### nav修改Volume
|
|||
|
https://nerivec.github.io/old-ue4-wiki/pages/ai-custom-pathing-how-to-use-nav-modifiers-query-filters.html
|
|||
|
|
|||
|
## 随机Task实现(蓝图实现)
|
|||
|
https://answers.unrealengine.com/questions/658501/random-task-in-ai.html
|
|||
|
|
|||
|
使用Service节点随机给黑板随机生成一个数字,在下面连接Tasks上加上一个BlackboardBasedCondition限制节点,来实现。
|
|||
|
|
|||
|
## BT节点
|
|||
|
### 黑板相关
|
|||
|
可以在构造函数中给FBlackboardKeySelector设置过滤
|
|||
|
```
|
|||
|
//筛选Actor类型
|
|||
|
BlackboardKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UBTService_DefaultFocus, BlackboardKey), AActor::StaticClass());
|
|||
|
//筛选Vector类型
|
|||
|
BlackboardKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UBTService_DefaultFocus, BlackboardKey));
|
|||
|
MyVectorKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyVectorKey));
|
|||
|
|
|||
|
MyObjectKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyObjectKey), AActor::StaticClass());
|
|||
|
|
|||
|
MySpecialActorClassKey.AddClassFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MySpecialActorClassKey), AMySpecialActor::StaticClass());
|
|||
|
|
|||
|
MyEnumKey.AddEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), StaticEnum<EMyEnum>());
|
|||
|
|
|||
|
MyEnumKey.AddNativeEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), "MyEnum");
|
|||
|
|
|||
|
MyIntKey.AddIntFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyIntKey));
|
|||
|
|
|||
|
MyFloatKey.AddFloatFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyFloatKey));
|
|||
|
|
|||
|
MyBoolKey.AddBoolFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyBoolKey));
|
|||
|
|
|||
|
MyRotatorKey.AddRotatorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyRotatorKey));
|
|||
|
|
|||
|
MyStringKey.AddStringFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyStringKey));
|
|||
|
|
|||
|
MyNameKey.AddNameFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyNameKey));
|
|||
|
```
|
|||
|
```
|
|||
|
//通过行为树组件获取到黑板组件
|
|||
|
UBlackboardComponent* MyBlackboard = OwnerComp.GetBlackboardComponent();
|
|||
|
//获取黑板键值
|
|||
|
UObject* KeyValue = MyBlackboard->GetValue<UBlackboardKeyType_Object>(BlackboardKey.GetSelectedKeyID());
|
|||
|
//设置黑板键值
|
|||
|
UBlackboardComponent->SetValueAsObject(TargetActor.SelectedKeyName, nullptr);
|
|||
|
```
|
|||
|
如果设置了FBlackboardKeySelector变量,还需要在重写InitializeFromAsset(UBehaviorTree& Asset):
|
|||
|
```
|
|||
|
void UMyBTTask::InitializeFromAsset(UBehaviorTree& Asset)
|
|||
|
{
|
|||
|
Super::InitializeFromAsset(Asset);
|
|||
|
|
|||
|
UBlackboardData* BBAsset = GetBlackboardAsset();
|
|||
|
if (ensure(BBAsset))
|
|||
|
{
|
|||
|
MySpecialKey.ResolveSelectedKey(*BBAsset);
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### 实例化
|
|||
|
Service节点是默认不被实例化的,这代表这个节点会共用内存。实例化Service的两种方法:
|
|||
|
|
|||
|
#### 第一种: 使用内存块.
|
|||
|
优点: 更加节省,不需要实例化节点,小巧方便.
|
|||
|
缺点: 内存块的大小是固定的,如果涉及到动态分配内存的类型,例如数组,则无法使用此方法.
|
|||
|
|
|||
|
在源码BTService.h文件中,可以看到一段二十行的英文注释,关于实例化的描述翻译如下。
|
|||
|
|
|||
|
>由于其中一些可以针对特定的AI进行实例化,因此以下虚拟函数未标记为const:
|
|||
|
OnBecomeRelevant(来自UBTAuxiliaryNode)
|
|||
|
OnCeaseRelevant(来自UBTAuxiliaryNode)
|
|||
|
TickNode(来自UBTAuxiliaryNode)
|
|||
|
OnSearchStart
|
|||
|
如果未实例化您的节点(默认行为),请不要在这些函数内更改对象的任何属性!
|
|||
|
模板节点使用相同的树资产在所有行为树组件之间共享,并且必须将其运行时属性存储在提供的NodeMemory块中(分配大小由GetInstanceMemorySize()确定)
|
|||
|
|
|||
|
GetInstanceMemorySize()函数的返回值决定了Service节点可以被分配到用来存储自身属性的内存大小。
|
|||
|
而此函数是在UBTAuxiliaryNode的父类UBTNode中声明的,并且默认返回值为0!
|
|||
|
|
|||
|
>UBTNode不光是UBTAuxiliaryNode的父类,也是UBTTaskNode(任务节点)的父类。
|
|||
|
也就是说,行为树中 Service(服务节点),Decorator(装饰器节点),Task(任务节点)都是默认不被实例化的!
|
|||
|
至于如何实例化,我们完全可以直接参考源码提供的BTService_RunEQS的写法。
|
|||
|
|
|||
|
首先在自定义Service头文件中声明一个结构体,将需要的变量都放结构体里。
|
|||
|
在类中声明一个此结构体的变量,并重写GetInstanceMemorySize函数返回结构体所需内存大小。在InitializeMemory函数中可以对结构体变量进行初始化。
|
|||
|
|
|||
|
#### 第二种: 直接实例化节点.
|
|||
|
优点: 可以使用任何复杂类型的属性.
|
|||
|
缺点: 会为每个节点创建一个新对象.
|
|||
|
|
|||
|
这种只要在构造函数中设置标志位 bCreateNodeInstance = true; ,你就可以在头文件中声明任意变量使用了.
|
|||
|
|
|||
|
### 更改描述及命名
|
|||
|
重写GetStaticDescription函数。
|
|||
|
|
|||
|
NodeName可以从Details面板修改,也可以在类构造函数中直接 NodeName = “ ”
|
|||
|
但这里的NodeName并不会修改在AddService时列表中显示的名称。
|
|||
|
列表中的名称是由类名决定的,除了修改类名,也可以用类说明符DisplayName修改。
|
|||
|
```
|
|||
|
UClass(DisplayName="xxxx")
|
|||
|
```
|
|||
|
### 蓝图节点为啥会是双本版设计
|
|||
|
|
|||
|
源码逻辑:
|
|||
|
当AIOwner为AIController时,优先调用带AI后缀的函数。(若只实现了不带AI后缀的,也会调用成功)
|
|||
|
当AIOwner不为AIController时,只调用不带AI后缀的函数。
|
|||
|
```
|
|||
|
void UBTService_BlueprintBase::SetOwner(AActor* InActorOwner)
|
|||
|
{
|
|||
|
ActorOwner = InActorOwner;
|
|||
|
AIOwner = Cast<AAIController>(InActorOwner);
|
|||
|
}
|
|||
|
|
|||
|
void UBTService_BlueprintBase::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
|||
|
{
|
|||
|
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
|
|||
|
//判断是否Owner为AIController并且有任一版本函数被实现
|
|||
|
if (AIOwner != nullptr && (ReceiveTickImplementations & FBTNodeBPImplementationHelper::AISpecific))
|
|||
|
{
|
|||
|
ReceiveTickAI(AIOwner, AIOwner->GetPawn(), DeltaSeconds);
|
|||
|
}
|
|||
|
//是否实现了不带AI后缀的函数
|
|||
|
else if (ReceiveTickImplementations & FBTNodeBPImplementationHelper::Generic)
|
|||
|
{
|
|||
|
ReceiveTick(ActorOwner, DeltaSeconds);
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
## 只有附加在Composites(合成节点)上的Service,才会调用OnSearchStart。
|
|||
|
Composites(合成节点)也就是指Sequence,Selector,SimpleParallel节点。
|
|||
|
|
|||
|
还有种特殊情况:当Service附加在与Root相连的第一个合成节点上时,SearchStart会被触发两次。
|
|||
|
|
|||
|
## Compisiton
|
|||
|
复合节点:此类节点定义分支的根以及执行该分支的基本规则。
|
|||
|
|
|||
|
## Decorator
|
|||
|

|
|||
|
|
|||
|
可以理解为条件节点,连接Composite或者Task节点,决定树中的分支、甚至单个节点能否被执行。
|
|||
|
|
|||
|
c++实现可以参考:
|
|||
|
- UBTDecorator_BlueprintBase
|
|||
|
- UBTDecorator_BlackboardBase
|
|||
|
- UBTDecorator_CheckGameplayTagsOnActor
|
|||
|
- UBTDecorator_Loop
|
|||
|
|
|||
|
蓝图节点中可重写的函数有:
|
|||
|
- ReceiveTick:通用版本的tick函数。如果条件符合会在TickNode()中调用。
|
|||
|
- ReceiveTickAI:AI版本的tick函数。如果条件符合会在TickNode()中调用。
|
|||
|
- ReceiveExecutionStart:通用版本的Start函数。如果条件符合会在OnNodeActivation()中调用。
|
|||
|
- ReceiveExecutionStartAI:AI版本的Start函数。如果条件符合会在OnNodeActivation()中调用。
|
|||
|
- ReceiveExecutionFinish:通用版本的Finish函数。如果条件符合会在OnNodeDeactivation()中调用。
|
|||
|
- ReceiveExecutionFinishAI:AI版本的Finish函数。如果条件符合会在OnNodeDeactivation()中调用。
|
|||
|
- ReceiveObserverActivated:通用版本的Activated函数。如果条件符合会在OnBecomeRelevant()中调用。
|
|||
|
- ReceiveObserverActivatedAI:AI版本的Activated函数。如果条件符合会在OnBecomeRelevant()中调用。
|
|||
|
- ReceiveObserverDeactivated:通用版本的Deactivated函数。如果条件符合会在OnCeaseRelevant()中调用。
|
|||
|
- ReceiveObserverDeactivatedAI:AI版本的Deactivated函数。如果条件符合会在OnCeaseRelevant()中调用。
|
|||
|
- PerformConditionCheck:通用版本的ConditionCheck函数。如果条件符合会在CalculateRawConditionValueImpl()中调用。
|
|||
|
- PerformConditionCheckAI:AI版本的ConditionCheck函数。如果条件符合会在CalculateRawConditionValueImpl()中调用。
|
|||
|
|
|||
|
## Service
|
|||
|

|
|||
|
|
|||
|
可以理解为并行逻辑节点,连接连接Composite或者Task节点,运行到当前分支时,Service节点将按照之前设定的频率执行。通常是使用这些Service节点检查、更新黑板数据。
|
|||
|
|
|||
|
c++可以参考:
|
|||
|
- UBTService_BlueprintBase
|
|||
|
- UBTService_BlackboardBase
|
|||
|
- UBTService_RunEQS
|
|||
|
|
|||
|
蓝图节点中可重写的函数有:
|
|||
|
- ReceiveTick
|
|||
|
- ReceiveTickAI
|
|||
|
- ReceiveSearchStart
|
|||
|
- ReceiveSearchStartAI
|
|||
|
- ReceiveActivation
|
|||
|
- ReceiveActivationAI
|
|||
|
- ReceiveDeactivation
|
|||
|
- ReceiveDeactivationAI
|
|||
|
|
|||
|
### 相关事件
|
|||
|
- OnSearchStart:所挂靠的合成节点被激活时调用的函数。
|
|||
|
- TickNode:Tick执行的函数(间隔自定义)。可在构造函数设置 bNotifyTick 来决定是否调用。
|
|||
|
- OnBecomeRelevant:节点被激活时调用(等同于蓝图Service里的Receive Activation) 需要在构造函数中设置 bNotifyBecomeRelevant = true来开启调用。
|
|||
|
- OnCeaseRelevant:节点被激活时调用(等同于蓝图Service里的Receive Deactivation) 需要在构造函数中设置 bNotifyCeaseRelevant = true来开启调用。
|
|||
|
- TickNode,OnBecomeRelevant和OnCeaseRelevant并不是UBTService类的虚函数,而是UBTService的父类UBTAuxiliaryNode的。装饰器UBTDecorator也继承此类。
|
|||
|
- GetInstanceMemorySize:当节点被实例化时分配的内存大小,关于实例化后面会详谈。
|
|||
|
|
|||
|
- Tick函数执行间隔 = Random(Interval-RandomDeviation,Interval+RandomDeviation)
|
|||
|
在这也就是 = Random(0.5-0.1 , 0.5+0.1), 每次调用都会计算一次。
|
|||
|
- CallTickonSearchStart:若为True,则会在SearchStart函数触发时,调用一次Tick函数。 若为False,则即使节点已被激活,也要等待一次间隔才能执行第一次Tick函数。
|
|||
|
- RestartTimeronEachActivation:若为True,则每次节点被激活都会从0开始计算间隔。 若为False,则会记录下上一次已经等待的间隔,在下一次激活时沿用。
|
|||
|
|
|||
|
## Task
|
|||
|
可以理解为具体执行的任务(逻辑)。此类节点是行为树的叶。它们是可执行的操作,没有输出连接。
|
|||
|
|
|||
|
UBTTask_BlueprintBase与UBTTask的区别在于
|
|||
|
以UBTTask_PlaySound这种简单UBTTask为例,主要实现了ExecuteTask(),其他是显示图标与描述的GetStaticDescription()与GetNodeIconName()
|
|||
|
|
|||
|
蓝图节点中可重写的函数有:
|
|||
|
- ReceiveTick:通用版本的tick函数。如果条件符合会在TickTask()中调用。
|
|||
|
- ReceiveTickAI:AI版本的tick函数。如果条件符合会在TickTask()中调用。
|
|||
|
- ReceiveExecute:通用版本的Execute函数。如果条件符合会在ExecuteTask()中调用。
|
|||
|
- ReceiveAbort:通用版本的Abort函数。如果条件符合会在AbortTask()中调用。
|
|||
|
- ReceiveExecuteAI:AI版本的Execute函数。如果条件符合会在ExecuteTask()中调用。
|
|||
|
- ReceiveAbortAI:AI版本的Abort函数。如果条件符合会在AbortTask()中调用。
|
|||
|
|
|||
|
### RunBehavior 与 RunBehaviorDynamic
|
|||
|
可以加载其他行为树并且运行。
|
|||
|
|
|||
|
- RunBehavior的一个限制是在运行时不能改变该子树资源。存在该限制的原因是给子树的根等级装饰器节点提供支持,这些装饰器节点将注入到父树中。此外,在运行时不能修改运行树的结构。
|
|||
|
- RunBehaviorDynamic使用行为树组件上的SetDynamicSubtree 函数即可以在运行时分配子树资源。本函数不会为子树的根等级装饰器节点提供支持。
|
|||
|
|
|||
|
## EQS
|
|||
|
大致流程:
|
|||
|
通过生成器在场景生成检测点获取当前位置的各种数据,之后再通过条件判断获取想要的查询结果。EQS的一些使用范例包括:找到最近的回复剂或弹药、判断出威胁最大的敌人,或者找到能看到玩家的视线(下面就会显示这样的一个示例)。
|
|||
|
|
|||
|
### 在Actor类中使用EQS
|
|||
|
结合原生代码使用EQS
|
|||
|
虽然EQS查询通常是在行为树中运行,但也可以直接从原生代码使用它。以下示例展示了一个虚构的查询,要在指定区域内为角色或物品寻找安全生成地点:
|
|||
|
```
|
|||
|
// 以下名称必须与查询中使用的变量名一致
|
|||
|
static const FName SafeZoneIndexName = FName(TEXT("SafeZoneIndex"));
|
|||
|
static const FName SafeZoneRadiusName = FName(TEXT("SafeZoneRadius"));
|
|||
|
|
|||
|
// 运行查询,根据区域索引和安全半径寻找安全的生成点
|
|||
|
bool AMyActor::RunPlacementQuery(const UEnvQuery* PlacementQuery)
|
|||
|
{
|
|||
|
if (PlacementQuery)
|
|||
|
{
|
|||
|
// 设置查询请求
|
|||
|
FEnvQueryRequest QueryRequest(PlacementQuery, this);
|
|||
|
|
|||
|
// 设置查询参数
|
|||
|
QueryRequest.SetIntParam(SafeZoneIndexName, SafeZoneIndexValue);
|
|||
|
QueryRequest.SetFloatParam(SafeZoneRadiusName, SafeZoneRadius);
|
|||
|
|
|||
|
// 执行查询
|
|||
|
QueryRequest.Execute(EEnvQueryRunMode::RandomBest25Pct, this, &AFortAthenaMutator_SpawningPolicyBase::OnEQSSpawnLocationFinished);
|
|||
|
|
|||
|
// 返回true说明查询已开始
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
// 返回false说明查询未能开始
|
|||
|
return false;
|
|||
|
}
|
|||
|
```
|