深入理解Unreal中的AI感知系统

news2024/9/21 0:25:07

StimuliSource和PerceptionListener

感知作为一种信号,整个场景中存在这个信号的生产者和消费者。这种信号在Unreal中被叫做刺激Stimuli
生产者就是StimuliSource,通过挂载StimuliSourceComponent并RegisterForSense来注册成为哪些类型刺激的刺激源
消费者就是PerceptionListener,通过挂载AIPerceptionComponent并配置SenseConfig来注册成为哪些类型刺激的监听者

Sense

每个Sense类代表一类感知,不同于StimuliSource和PerceptionListener,Sense并不实际存在于场景中,它是一种定义好的生产刺激的逻辑规则。UE中提供了几种基础常见的感知类型(视觉、听觉和队友等),游戏中涉及到的每种Sense都会唯一的初始化保存在PerceptionSystem中并对应一个SenseID
在这里插入图片描述
每种Sense都有自己的更新时间间隔,在PerceptionSystem的每个Tick中都会通过ProgressTime更新时间间隔,然后到时间后就Tick更新

class AIMODULE_API UAISense : public UObject
{
	bool ProgressTime(float DeltaSeconds)
	{
		TimeUntilNextUpdate -= DeltaSeconds;
		return TimeUntilNextUpdate <= 0.f;
	}

	void Tick()
	{
		if (TimeUntilNextUpdate <= 0.f)
		{
			TimeUntilNextUpdate = Update();
		}
	}
	virtual void RegisterSource(AActor& SourceActors){}
	virtual void UnregisterSource(AActor& SourceActors){}
	FORCEINLINE void OnNewListener(const FPerceptionListener& NewListener) { OnNewListenerDelegate.ExecuteIfBound(NewListener); }
	FORCEINLINE void OnListenerUpdate(const FPerceptionListener& UpdatedListener) { OnListenerUpdateDelegate.ExecuteIfBound(UpdatedListener); }
	FORCEINLINE void OnListenerRemoved(const FPerceptionListener& RemovedListener) { OnListenerRemovedDelegate.ExecuteIfBound(RemovedListener); }
}

每当有新的Actor注册作为这个Sense的触发源Source时会由PerceptionSystem找到对应的Sense调用RegisterSource。
每当有新的Actor注册监听这个Sense时会由PerceptionSystem调用OnNewListener。
每个Sense的工作基本分为三点

  1. 通过监听Listener的增删改事件维护所有该类型Sense的监听者
  2. 通过重写RegisterSource和UnRegisterSource维护所有改类型Sense的刺激源
  3. 在Tick(Update)中计算每个Listener能感知到哪些Source,为每个能感知到的Source创建Stimuli注册给Listener消费。

以常见的视觉感知AISense_Sight为例,每当有新的Source或者Listener都会创建一批新的Query,维护m*n个Query(假设有m个Source和n个Listener)

void UAISense_Sight::RegisterSource(AActor& SourceActor)
{
	RegisterTarget(SourceActor);
}

bool UAISense_Sight::RegisterTarget(AActor& TargetActor, const TFunction<void(FAISightQuery&)>& OnAddedFunc /*= nullptr*/)
{
	SCOPE_CYCLE_COUNTER(STAT_AI_Sense_Sight_RegisterTarget);
	
	FAISightTarget* SightTarget = ObservedTargets.Find(TargetActor.GetUniqueID());
	
	if (SightTarget != nullptr && SightTarget->GetTargetActor() != &TargetActor)
	{
		// this means given unique ID has already been recycled. 
		FAISightTarget NewSightTarget(&TargetActor);

		SightTarget = &(ObservedTargets.Add(NewSightTarget.TargetId, NewSightTarget));
		SightTarget->SightTargetInterface = Cast<IAISightTargetInterface>(&TargetActor);
	}
	else if (SightTarget == nullptr)
	{
		FAISightTarget NewSightTarget(&TargetActor);

		SightTarget = &(ObservedTargets.Add(NewSightTarget.TargetId, NewSightTarget));
		SightTarget->SightTargetInterface = Cast<IAISightTargetInterface>(&TargetActor);
	}

	for (AIPerception::FListenerMap::TConstIterator ItListener(ListenersMap); ItListener; ++ItListener)
	{
		if (RegisterNewQuery(Listener, ListenersTeamAgent, TargetActor, SightTarget->TargetId, TargetLocation, PropDigest, OnAddedFunc))
		{
			bNewQueriesAdded = true;
		}
	}
	// sort Sight Queries
	if (bNewQueriesAdded)
	{
		RequestImmediateUpdate();
	}

	return bNewQueriesAdded;
}
void UAISense_Sight::OnNewListenerImpl(const FPerceptionListener& NewListener)
{
	GenerateQueriesForListener(NewListener, PropertyDigest);
}
void UAISense_Sight::GenerateQueriesForListener(const FPerceptionListener& Listener, const FDigestedSightProperties& PropertyDigest, const TFunction<void(FAISightQuery&)>& OnAddedFunc/*= nullptr */)
{
	bool bNewQueriesAdded = false;
	const IGenericTeamAgentInterface* ListenersTeamAgent = Listener.GetTeamAgent();
	const AActor* Avatar = Listener.GetBodyActor();

	// create sight queries with all legal targets
	for (FTargetsContainer::TConstIterator ItTarget(ObservedTargets); ItTarget; ++ItTarget)
	{
		const AActor* TargetActor = ItTarget->Value.GetTargetActor();
		if (TargetActor == nullptr || TargetActor == Avatar)
		{
			continue;
		}

		const FVector TargetLocation = TargetActor->GetActorLocation();
		if (RegisterNewQuery(Listener, ListenersTeamAgent, *TargetActor, ItTarget->Key, TargetLocation, PropertyDigest, OnAddedFunc))
		{
			bNewQueriesAdded = true;
		}
	}

	// sort Sight Queries
	if (bNewQueriesAdded)
	{
		RequestImmediateUpdate();
	}
}

在Update中先按照距离等参数对其进行重要性排序,然后通过ComputeVisibilityUpdateQueryVisibilityStatus计算和更新可见性,对于可见的Query会调用RegisterStimuli产生刺激给Listener
Update中代码比较复杂就不贴了,可见性的检测基本就是射线检测加一些强制可见和强制不可见的条件逻辑,射线检测可以通过配置选择同步或者异步。先进行重要性排序的原因是在一次Update中有可能不会计算完所有的Query,计算数量受最大耗时MaxTimeSlicePerTick和最大数量MaxTracesPerTick的限制,两者都可以配置中修改

void UAISense_Sight::UpdateQueryVisibilityStatus(FAISightQuery& SightQuery, FPerceptionListener& Listener, const bool bIsVisible, const FVector& SeenLocation, const float StimulusStrength, AActor* TargetActor, const FVector& TargetLocation) const
{
	if (bIsVisible)
	{
		const bool bHasValidSeenLocation = SeenLocation != FAISystem::InvalidLocation;
		Listener.RegisterStimulus(TargetActor, FAIStimulus(*this, StimulusStrength, bHasValidSeenLocation ? SeenLocation : SightQuery.LastSeenLocation, Listener.CachedLocation));
		SightQuery.SetLastResult(true);
		if (bHasValidSeenLocation)
		{
			SightQuery.LastSeenLocation = SeenLocation;
		}
	}
	// communicate failure only if we've seen given actor before
	else if (SightQuery.GetLastResult())
	{
		Listener.RegisterStimulus(TargetActor, FAIStimulus(*this, 0.f, TargetLocation, Listener.CachedLocation, FAIStimulus::SensingFailed));
		SightQuery.SetLastResult(false);
		SightQuery.LastSeenLocation = FAISystem::InvalidLocation;
	}
}

Listener

Listener对应的是FPerceptionListener类型,但它基本只是存储标志位bHasStimulusToProcess和转发消息给真正的Listener也就是AIPerceptionComponent,这里的消息主要是指RegisterStimuli和ProcessStimuli
在AIPerceptionComponent中会维护一个生产消费队列StimuliToProcessRegisterStimuli向其中添加刺激,ProcessStimuli中全部消费掉。对已感知到的对象,会维护在PerceptualData中,在ProcessStimuli中也会对其Stimuli是否过期进行检测

struct AIMODULE_API FActorPerceptionInfo
{
	TWeakObjectPtr<AActor> Target;

	TArray<FAIStimulus> LastSensedStimuli;

	/** if != MAX indicates the sense that takes precedense over other senses when it comes
		to determining last stimulus location */
	FAISenseID DominantSense;
}
class AIMODULE_API UAIPerceptionComponent : public UActorComponent
{
	typedef TMap<TObjectKey<AActor>, FActorPerceptionInfo> TActorPerceptionContainer;
	typedef TActorPerceptionContainer FActorPerceptionContainer;
	FActorPerceptionContainer PerceptualData;
		
protected:	
	struct FStimulusToProcess
	{
		TObjectKey<AActor> Source;
		FAIStimulus Stimulus;

		FStimulusToProcess(AActor* InSource, const FAIStimulus& InStimulus)
			: Source(InSource), Stimulus(InStimulus)
		{

		}
	};

	TArray<FStimulusToProcess> StimuliToProcess; 
}
void UAIPerceptionComponent::RegisterStimulus(AActor* Source, const FAIStimulus& Stimulus)
{
	FStimulusToProcess& StimulusToProcess = StimuliToProcess.Add_GetRef(FStimulusToProcess(Source, Stimulus));
	StimulusToProcess.Stimulus.SetExpirationAge(MaxActiveAge[int32(Stimulus.Type)]);
}

Stimuli

刺激是一个消耗品,由PerceptionSystem通过每种Sense的逻辑基于Source和Listener的信息生产出来,提供给Listener消费,最终消费的结果其实就是我们最常用的OnPerceptionUpdate等事件的触发
刺激的属性主要包括Type(由哪种Sense产生)、Tag、Age(处理过期逻辑)和Strengh以及Location

USTRUCT(BlueprintType)
struct AIMODULE_API FAIStimulus
{
	GENERATED_USTRUCT_BODY()

	static const float NeverHappenedAge;

	enum FResult
	{
		SensingSucceeded,
		SensingFailed
	};

protected:
	UPROPERTY(BlueprintReadWrite, Category = "AI|Perception")
	float Age;

	UPROPERTY(BlueprintReadWrite, Category = "AI|Perception")
	float ExpirationAge;
public:
	UPROPERTY(BlueprintReadWrite, Category = "AI|Perception")
	float Strength;
	UPROPERTY(BlueprintReadWrite, Category = "AI|Perception")
	FVector StimulusLocation;
	UPROPERTY(BlueprintReadWrite, Category = "AI|Perception")
	FVector ReceiverLocation;
	UPROPERTY(BlueprintReadWrite, Category = "AI|Perception")
	FName Tag;

	FAISenseID Type;
}

PerceptionSystem

PerceptionSystem是整套感知系统运转的核心,首先在其中维护了所有的Source、Listener和Sense

class AIMODULE_API UAIPerceptionSystem : public UAISubsystem
{
	AIPerception::FListenerMap ListenerContainer;

	UPROPERTY()
	TArray<TObjectPtr<UAISense>> Senses;

	TMap<const AActor*, FPerceptionStimuliSource> RegisteredStimuliSources;
}

在Tick中的流程其实也非常直观可分为三步,全部是围绕Stimuli展开的

  1. 对已有的Stimuli更新Age,并标记过期的Stimuli为需要Listener处理
  2. 为每个Sense进行ProgressTime加Tick,这步可能产生许多新的Stimuli,标记这些Stimuli给Listener
  3. 对所有被前两步标记的Listener调用处理Stimuli
void UAIPerceptionSystem::Tick(float DeltaSeconds)
{
	bool bSomeListenersNeedUpdateDueToStimuliAging = false;
	if (NextStimuliAgingTick <= CurrentTime)
	{
		constexpr double Precision = 1./64.;
		const float AgingDt = FloatCastChecked<float>(CurrentTime - NextStimuliAgingTick, Precision);
		bSomeListenersNeedUpdateDueToStimuliAging = AgeStimuli(PerceptionAgingRate + AgingDt);
		NextStimuliAgingTick = CurrentTime + PerceptionAgingRate;
	}

	bool bNeedsUpdate = false;
	for (UAISense* const SenseInstance : Senses)
	{
		bNeedsUpdate |= SenseInstance != nullptr && SenseInstance->ProgressTime(DeltaSeconds);
	}

	if (bNeedsUpdate)
	{
		for (UAISense* const SenseInstance : Senses)
		{
			if (SenseInstance != nullptr)
			{
				SenseInstance->Tick();
			}
		}
	}
	{
		const bool bStimuliDelivered = DeliverDelayedStimuli(bNeedsUpdate ? RequiresSorting : NoNeedToSort);

		if (bNeedsUpdate || bStimuliDelivered || bSomeListenersNeedUpdateDueToStimuliAging)
		{
			for (AIPerception::FListenerMap::TIterator ListenerIt(ListenerContainer); ListenerIt; ++ListenerIt)
			{
				check(ListenerIt->Value.Listener.IsValid());

				if (ListenerIt->Value.HasAnyNewStimuli())
				{
					ListenerIt->Value.ProcessStimuli();
				}
			}
		}
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2147025.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

聚焦API安全未来,F5打造无缝集成的解决方案

研究发现&#xff0c;目前超过90%的基于Web的网络攻击都以API端点为目标。随着对API使用需求的增加&#xff0c;这些攻击还会持续增长。现代企业需要一种动态防御策略&#xff0c;在风险升级成代价高昂、令人警惕且往往无法预防的API安全漏洞之前&#xff0c;发现并降低风险。 …

Android 开发高频面试题之——Flutter

Android开发高频面试题之——Java基础篇 flutter高频面试题记录 Flutter1. dart中的作用域与了解吗2. dart中. .. ...分别是什么意思?3. Dart 是不是单线程模型?如何运行的?4. Dart既然是单线程模型支持多线程吗?5. Future是什么6. Stream是什么7. Flutter 如何和原生交互…

微信getUserProfile不弹出授权框

当我们在微信小程序开发工具中想要使用getUserProfile来获取个人信息的时候&#xff0c;会发现不弹出授权框&#xff0c;这是什么原因呢&#xff1f; 早在2022年的小程序官方公告中就已经明确给出了小程序用户头像昵称获取规则调整公告 因此如果还想继续使用getUserProfile的弹…

uni-app-通过vue-cli命令行快速上手

环境安装 全局安装 vue-cli npm install -g vue/cli创建uni-app 使用正式版&#xff08;对应HBuilderX最新正式版&#xff09; vue create -p dcloudio/uni-preset-vue my-project使用alpha版&#xff08;对应HBuilderX最新alpha版&#xff09; vue create -p dcloudio/uni-p…

ARMxy车辆数据采集Linux智能控制器

在当今科技日新月异的时代&#xff0c;高效智能的边缘计算设备在众多领域发挥着关键作用。我们的 ARM 边缘计算机&#xff0c;凭借其卓越的性能和广泛的适用性&#xff0c;成为车队管理智能化的核心力量。 一、强大硬件配置&#xff0c;完美适配车队管理需求 ARM 边缘计算机支…

2024 全新利器:API 微查接口登场

在信息时代的浪潮中&#xff0c;数据查询和核验成为了人们生活中必不可少的一部分。然而&#xff0c;为了满足各种不同的查询需求&#xff0c;开发和维护一个高效、全面的查询系统对于个人或者小团队来说往往是一项不小的挑战。好在现在有了全新的利器&#xff1a;API 微查接口…

CTF 技能树 LOG -GIT泄露 笔记

log 使用虚拟机kali操作 python2 安装 apt-get install python2 进入root用户&#xff0c;下载克隆git hack库 git clone https://github.com/BugScanTeam/GitHack sudo passwd root 修改root 命名密码为root 切换登录 su root 终端进入home/kali/GitHack/ python GitH…

【模板进阶】类型参数的推断

一、使用 B o o s t Boost Boost库来借助推断 通常&#xff0c;我们可以使用 t y p e i d ( ) typeid() typeid()来推断一个类型&#xff0c;但是有时候 t y p e i d typeid typeid不够准确&#xff0c;因此&#xff0c;我们借助 B o o s t Boost Boost库里面的 t y p e _ i d…

yolo车位数据集

停车场车位检测数据集是一个非常有价值的数据资源&#xff0c;它对于开发和训练能够自动识别停车位是否被占用的计算机视觉系统至关重要。以下是对这样一个数据集的详细介绍&#xff0c;以及如何使用这个数据集来训练YOLO&#xff08;You Only Look Once&#xff09;这样的目标…

springcloud整合nacos、sentinal、springcloud-gateway,springboot security、oauth2总结

源码地址:下载地址 使用该架构的项目地址:下载地址 下面教大家整合nacos、sentinal、springcloud-gateway,springboot security、oauth2做一个分布式架构 1、第一步整合nacos 1、下载alibaba的nacos 下载地址&#xff0c;然后使用单机模式启动nacos sh startup.sh -m standalon…

游戏如何检测加速外挂

在游戏面临的众多外挂风险中&#xff0c;除了常见的内存修改挂、注入挂等作弊手段&#xff0c;黑灰产还常用「加速」手段实现作弊。 游戏安全风险分布占比图 「加速」顾名思义是指改变游戏内的速度。游戏在运行中需要以帧为单位播放画面&#xff0c;而计算每帧动画播放所需时间…

游戏如何应对云手机刷量问题

云手机的实现原理是依托公有云和 ARM 虚拟化技术&#xff0c;为用户在云端提供一个安卓实例&#xff0c;用户可以将手机上的应用上传至云端&#xff0c;再通过视频流的方式&#xff0c;远程实时控制云手机。 市面上常见的几款云手机 原本需要手机提供的计算、存储等能力都改由…

深度图变换器的新突破:DeepGraph

人工智能咨询培训老师叶梓 转载标明出处 在图变换器领域&#xff0c;尽管其全局注意力机制在图结构数据处理上显示出了巨大潜力&#xff0c;但现有的图变换器模型却普遍较浅&#xff0c;通常不超过12层。这一现象引发了学者们对于“增加层数是否能进一步提升图变换器性能”的深…

Vue3 多组复选框重置(v-if 强制刷新组件)

通过v-if指令强制刷新&#xff0c;当v-if的值发生变化时&#xff0c;组件都会被重新渲染一遍。因此&#xff0c;利用v-if指令的特性&#xff0c;可以达到强制刷新组件的目的。 先用个简单例子 --> 项目中使用 <template><comp v-if"refresh"></c…

普罗米修斯监控

目录 概念 部署方法 1. 二进制&#xff08;源码包&#xff09; 2. 部署在k8s集群当中&#xff0c;用pod形式部署 概念 prometheus是开源的系统监控和告警。在k8s分布式的容器化管理系统当中&#xff0c;一般都是搭配prometheus来进行监控。它是服务监控系统&#xff0c;也…

运动规划第二节【深蓝学院,高飞】笔记

文章目录 Graph Search BasisConfiguration SpaceConfiguration Space ObstacleWorkspace and Configuration Space Obstacle Graph and Search MethodGraph Search OverviewGraph TraversalBreadth First Search (BFS)Depth First Search (DFS)versus Heuristic SearchGreedy …

武汉大学:如何做好高校电子邮件账号安全防护

上个世纪七十年代&#xff0c;电子邮件占据了互联网的前身ARPANET上流量的75%&#xff0c;是最主要的应用。随着互联网的发展&#xff0c;电子邮件在全面普及后&#xff0c;被各种各样的即时通讯软件抢走了不少风头。然而&#xff0c;其始终还是被社会所认可的主流网络通讯渠道…

网络高级day01(Modbus 通信协议)

目录 1》modbus分类 1> Modbus RTU 2> Modbus ASCLL 3> Modbus TCP 2》Modbus TCP的特点 3》Modbus TCP协议 1> 报文头&#xff08;一共7个字节&#xff09; 2> 寄存器 3> 功能码 4> 数据 01H 功能码分析 05H 功能码分析 0FH 功能码分析 1》modbus…

git reflog 和 git log 的详解和区别

文章目录 1. git log 介绍基本用法&#xff1a;输出内容&#xff1a;常见选项&#xff1a;git log 的局限性&#xff1a; 2. git reflog 介绍基本用法&#xff1a;输出内容&#xff1a;git reflog 输出字段&#xff1a;常见选项&#xff1a;主要用途&#xff1a;示例&#xff1…

Rasa对话模型——做一个语言助手

1、Rasa模型 1.1 模型介绍 Rasa是一个用于构建对话 AI 的开源框架&#xff0c;主要用于开发聊天机器人和语音助手。Rasa 提供了自然语言理解&#xff08;NLU&#xff09;和对话管理&#xff08;DM&#xff09;功能&#xff0c;使开发者能够创建智能、交互式的对话系统。 1.2…