34. UE5 RPG实现鼠标点击移动

在前面,我们实现过使用键盘按键wasd去实现控制角色的移动,现在,我们实现了InputAction按键触发,后面,实现一下通过鼠标点击地面实现角色移动。
我们将实现两种效果的切换,如果你点击地面快速松开,角色将自动寻路到目标为止。如果你按住鼠标不放,角色将自动朝向鼠标所指向的位置移动。接下来,我们实现它吧!

首先在PlayerController(玩家控制器类)里面增加一些参数,用来记录一些信息,主要对应的是查询鼠标悬停的目标是否有敌人,按下的时间,以及自动寻路的参数。

	FVector CachedDestination = FVector::ZeroVector; //存储鼠标点击的位置
	float FollowTime = 0.f; // 用于查看按住了多久
	bool bAutoRunning = false; //当前是否自动移动
	bool bTargeting = false; //当前鼠标是否选中敌人

	UPROPERTY(EditDefaultsOnly)
	float ShortPressThreshold = 0.3f; //定义鼠标悬停多长时间内算点击事件

	UPROPERTY(EditDefaultsOnly)
	float AutoRunAcceptanceRadius = 50.f; //当角色和目标距离在此半径内时,将关闭自动寻路

	UPROPERTY(VisibleAnywhere)
	TObjectPtr<USplineComponent> Spline; //自动寻路时生成的样条线

在PlayerController构造函数,将Spline初始化

Spline = CreateDefaultSubobject<USplineComponent>("Spline");

我们之前在鼠标事件里面追踪鼠标悬停并给敌人描边的效果,能够获取到敌人
在这里插入图片描述
接下来就是在鼠标按下,悬停,抬起三个事件中去实现逻辑,我们之前在里面实现了对技能的触发
在这里插入图片描述
后面,我们将点击移动的功能融入进去。

实现长按角色跟随鼠标移动

首先,在鼠标按下事件中,判断传入的Tag是否为鼠标左键事件,在内部判断点击的是否为敌人,如果不是敌人,那就是地面,如果ThisActor有值,那就是点击到了敌人。
bTargeting为记录当前按下时,是否选中了敌人。
bAutoRunning 为是否自动寻路中,在鼠标按下,将自动关闭此项,在鼠标抬起时再重新计算是否需要自动移动。
FollowTime用于统计按下时间,来去顶当前操作为点击还是长按。

void APlayerControllerBase::AbilityInputTagPressed(const FGameplayTag InputTag)
{
	if(InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		bTargeting = ThisActor != nullptr; //ThisActor为鼠标悬停在敌人身上才会有值
		bAutoRunning = false;
		FollowTime = 0.f; //重置统计的时间
	}
}

接下来在鼠标悬停事件中,我们将实现角色跟随鼠标移动逻辑。
我们先实现之前的触发技能的逻辑,这里只需要判断一下是否鼠标左键的事件,如果是,将执行内部逻辑,不再运行后面的逻辑。

void APlayerControllerBase::AbilityInputTagHold(const FGameplayTag InputTag)
{
	if(!InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		if(GetASC())
		{
			GetASC()->AbilityInputTagHold(InputTag);
		}
		return;
	}

后面逻辑,我们先对bTargeting 进行布尔判断,如果它的值为true,证明点中了敌人,要执行攻击敌人逻辑,这个我们后面再实现
在另外的分支就是没有选中敌人,我们可以执行鼠标悬停角色跟随逻辑。
首先对悬停时间进行统计,如果悬停时间过短,我们后面将在鼠标抬起事件中去实现角色自动寻路功能。

FollowTime += GetWorld()->GetDeltaSeconds(); //统计悬停时间来判断是否为点击

接着,我们通过GetHitResultUnderCursor函数去获取鼠标拾取的位置
参数说明

  1. ECollisionChannel TraceChannel:这个参数决定了射线将与哪些类型的物体碰撞。例如,你可以设置射线只与静态网格体(ECC_StaticMesh)或角色(ECC_Pawn)等碰撞。

  2. bool bTraceComplex:这个布尔值决定了射线是否应该与复杂碰撞体(比如带有多边形碰撞体的网格体)进行碰撞检测。如果设置为 true,射线将与复杂碰撞体碰撞;如果设置为 false,则只会与简单的碰撞体(比如球体或盒体)碰撞。

  3. FHitResult& HitResult:这是一个引用参数,用于返回射线与场景中的物体碰撞的结果。如果射线没有碰撞到任何物体,这个参数将不会被修改。

将拾取到的位置缓存到变量内

		FHitResult Hit;
		if(GetHitResultUnderCursor(ECC_Visibility, false, Hit))
		{
			CachedDestination = Hit.ImpactPoint;
		}

有了目标位置后,我们通过目标位置减去角色所在位置,就可以得到一个朝向,GetSafeNormal()为归一化向量,然后使用AddMovementInput去移动角色

		if(APawn* ControlledPawn = GetPawn())
		{
			const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
			ControlledPawn->AddMovementInput(WorldDirection);
		}

实现自动寻路

如果鼠标按下抬起的时间间隔比较小,我们将其定义为鼠标点击事件,并将执行自动寻路逻辑。这些内容我们将在鼠标抬起时的函数中进行实现。
在实现此逻辑之前,我们先将之前实现的技能函数实现,之前实现主要是一个触发技能鼠标抬起的回调,现在还未实现内容。
我们先判断传入的tag如果不是鼠标左键的tag,将执行此逻辑。

void APlayerControllerBase::AbilityInputTagReleased(const FGameplayTag InputTag)
{
	if(!InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		if(GetASC())
		{
			GetASC()->AbilityInputTagReleased(InputTag);
		}
		return;
	}
}

接着还要判断是否选中了敌人,选中敌人的话执行技能逻辑。

if(bTargeting)
	{
		if(GetASC())
		{
			GetASC()->AbilityInputTagReleased(InputTag);
		}
	}
	else...

在else里面,我们就可以正式去实现自动寻路的功能了
首先判断一下,当前抬起时是否能够触发点击事件,ShortPressThreshold的值是我们可以自定义的,比如按下到抬起如果小于0.3s,我们将其定义为点击,那它的值我们就定义为0.3,FollowTime是在按下后,在Hold状态的间隔时间持续添加,来实现对按

if(FollowTime <= ShortPressThreshold)

接着我们去通过内置的插件去拾取自动寻路的路线,这个使用的UE引擎的方法,它需要三个值,当前世界上下文中的对象(传入自身即可),起始点,结束点,查询的到将返回一个UNavigationPath

if(UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), CachedDestination))

要使用寻路函数需要对应的模块,我们在官网看到它需要NavigationSystem模块
在这里插入图片描述
要使用此模块,我们需要在Build.cs里面将NavigationSystem加入,然后编译的时候将会将此模块自动编入

PrivateDependencyModuleNames.AddRange(new string[] { "GameplayTags", "GameplayTasks", "NavigationSystem" });

如果能够查询的到,我们首先将样条属性内的内容先清除

Spline->ClearSplinePoints(); //清除样条内现有的点

然后for循环找到路径里面的点,添加到样条中,为了防止错误,我们使用DrawDebugSphere函数在场景中debug测试。

for(const FVector& PointLoc : NavPath->PathPoints)
{
	Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World); //将新的位置添加到样条曲线中
	DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Orange, false, 5.f); //点击后debug调试
}

添加完成后,我们还需要将记录在自动寻路状态的变量设置为true,在帧更新中,我们将通过这个变量进行角色移动

bAutoRunning = true; //设置当前正常自动寻路状态,将在tick中更新位置

接着可以编译测试是否能够实现在场景获取到路径点,我们要测试导航功能,需要在场景里面设置导航体积,只有在导航体积内,才能够触发此功能,先在场景添加一个此体积。
在这里插入图片描述
按快捷键P,可以显示可导航区域,我们可以调整体积范围来实现可导航范围。
在这里插入图片描述
顺便加几个遮挡物,导航体积会自动计算,将其可移动范围剔除掉。
在这里插入图片描述
然后测试点击地面,会发现生成了对应的点的位置
在这里插入图片描述

在帧循环实现自动移动

现在自动寻路的路径有了,我们要实现角色的自动移动,那么,需要在帧循环里面实现。

//帧更新
void APlayerControllerBase::PlayerTick(float DeltaTime)
{
	Super::PlayerTick(DeltaTime);
	//鼠标位置追踪是否悬停在敌人身上
	CursorTrace();
	
}

首先我们获取并判断一下Pawn是否存在

if(APawn* ControlledPawn = GetPawn())

由于无法确保角色位置和样条曲线百分之百重合在一块,所以我们先获取一下角色距离样条最近的位置

const FVector LocationOnSpline = Spline->FindLocationClosestToWorldLocation(ControlledPawn->GetActorLocation(), ESplineCoordinateSpace::World);

然后通过此位置去获取在样条上的方向

const FVector Direction = Spline->FindDirectionClosestToWorldLocation(LocationOnSpline, ESplineCoordinateSpace::World);

让角色通过此方向去移动

ControlledPawn->AddMovementInput(Direction);

我们还需要在角色移动到终点时,设置它停止寻路,所以,我们还需要查询当前角色所在位置和终点位置的距离,如果小于我们设置的AutoRunAcceptanceRadius距离内,我们将停止自动寻路。

const float DistanceToDestination = (LocationOnSpline - CachedDestination).Length();
if(DistanceToDestination <= AutoRunAcceptanceRadius)
{
	bAutoRunning = false;
}

ps:这里还有一个问题,就是如果你点击位置为自动寻路无法到达的位置,导航还是会生成一条路径,但是我们无法达到最终点,这样无法停止自动寻路。所以,我们需要在鼠标抬起时,将路径的终点设置给CachedDestination,这也是自动寻路的最终点。
我们获取数组中的最后一个点,按照索引来算,就是数组的长度-1

CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];

如果需要服务器,我们需要在导航系统设置运行客户端导航,这样,会在所有的客户端生成导航体积。
在这里插入图片描述
如果我们需要点击到柱子后面的地面,而不是忘柱子那里移动,我们需要修改柱子的碰撞预设为自定义
在这里插入图片描述
我们鼠标拾取地面位置是通过Visibility去拾取的,那么,将此相应通道关闭。
在这里插入图片描述
接下来,我们就可以测试了,查看是否有bug。
下面列出来PlayerController的完整代码

// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "GameplayTagContainer.h"
#include "PlayerControllerBase.generated.h"

class USplineComponent;
class UInputConfig;
class UInputMappingContext;
class UInputAction;
struct FInputActionValue;
class IEnemyInterface;
class UAbilitySystemComponentBase;

/**
 * 玩家控制器
 */
UCLASS()
class AURA_API APlayerControllerBase : public APlayerController
{
	GENERATED_BODY()

public:
	APlayerControllerBase();
	virtual void PlayerTick(float DeltaTime) override;

protected:
	virtual void BeginPlay() override; //游戏开始时触发
	virtual void SetupInputComponent() override; //在生成输入组件时触发

private:
	UPROPERTY(EditAnywhere, Category="Input")
	TObjectPtr<UInputMappingContext> CharacterContext;

	UPROPERTY(EditAnywhere, Category="Input")
	TObjectPtr<UInputAction> MoveAction;

	void Move(const struct FInputActionValue& InputActionValue);

	void CursorTrace(); //鼠标位置追踪拾取
	IEnemyInterface* LastActor; //上一帧拾取到的接口指针
	IEnemyInterface* ThisActor; //这一帧拾取到的接口指针

	void AbilityInputTagPressed(FGameplayTag InputTag);
	void AbilityInputTagReleased(FGameplayTag InputTag);
	void AbilityInputTagHold(FGameplayTag InputTag);

	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputConfig> InputConfig;

	UPROPERTY()
	TObjectPtr<UAbilitySystemComponentBase> AbilitySystemComponentBase;

	UAbilitySystemComponentBase* GetASC();

	FVector CachedDestination = FVector::ZeroVector; //存储鼠标点击的位置
	float FollowTime = 0.f; // 用于查看按住了多久
	bool bAutoRunning = false; //当前是否自动移动
	bool bTargeting = false; //当前鼠标是否选中敌人

	UPROPERTY(EditDefaultsOnly)
	float ShortPressThreshold = 0.3f; //定义鼠标悬停多长时间内算点击事件

	UPROPERTY(EditDefaultsOnly)
	float AutoRunAcceptanceRadius = 50.f; //当角色和目标距离在此半径内时,将关闭自动寻路

	UPROPERTY(VisibleAnywhere)
	TObjectPtr<USplineComponent> Spline; //自动寻路时生成的样条线

	void AutoRun();
};

// 版权归暮志未晚所有。


#include "Player/PlayerControllerBase.h"

#include "AbilitySystemBlueprintLibrary.h"
#include "EnhancedInputSubsystems.h"
#include "MyGameplayTags.h"
#include "NavigationPath.h"
#include "NavigationSystem.h"
#include "AbilitySystem/AbilitySystemComponentBase.h"
#include "Components/SplineComponent.h"
#include "Input/InputComponentBase.h"
#include "Interaction/EnemyInterface.h"

APlayerControllerBase::APlayerControllerBase()
{
	bReplicates = true; //是否将数据传送服务器更新
	LastActor = nullptr;
	ThisActor = nullptr;

	Spline = CreateDefaultSubobject<USplineComponent>("Spline");
}

//帧更新
void APlayerControllerBase::PlayerTick(float DeltaTime)
{
	Super::PlayerTick(DeltaTime);
	//鼠标位置追踪是否悬停在敌人身上
	CursorTrace();
	//自动寻路
	AutoRun();
}

void APlayerControllerBase::AutoRun()
{
	if(!bAutoRunning) return;
	if(APawn* ControlledPawn = GetPawn())
	{
		//找到距离样条最近的位置
		const FVector LocationOnSpline = Spline->FindLocationClosestToWorldLocation(ControlledPawn->GetActorLocation(), ESplineCoordinateSpace::World);
		//获取这个位置在样条上的方向
		const FVector Direction = Spline->FindDirectionClosestToWorldLocation(LocationOnSpline, ESplineCoordinateSpace::World);
		ControlledPawn->AddMovementInput(Direction);

		const float DistanceToDestination = (LocationOnSpline - CachedDestination).Length();
		if(DistanceToDestination <= AutoRunAcceptanceRadius)
		{
			bAutoRunning = false;
		}
	}
}

//鼠标位置追踪
void APlayerControllerBase::CursorTrace()
{
	FHitResult CursorHit;
	GetHitResultUnderCursor(ECC_Visibility, false, CursorHit); //获取可视的鼠标命中结果
	if(!CursorHit.bBlockingHit) return; //如果未命中直接返回

	LastActor = ThisActor;
	ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());

	/**
	 * 射线拾取后,会出现的几种情况
	 * 1. LastActor is null   ThisActor is null 不需要任何操作
	 * 2. LastActor is null   ThisActor is valid 高亮ThisActor
	 * 3. LastActor is valid   ThisActor is null 取消高亮LastActor
	 * 4. LastActor is valid   ThisActor is valid LastActor != ThisActor 取消高亮LastActor 高亮ThisActor
	 * 5. LastActor is valid   ThisActor is valid LastActor == ThisActor 不需要任何操作
	 */

	if(LastActor == nullptr)
	{
		if(ThisActor != nullptr)
		{
			//case 2
			ThisActor->HighlightActor();
		} // else case 1
	}
	else
	{
		if(ThisActor == nullptr)
		{
			//case 3
			LastActor->UnHighlightActor();
		}
		else
		{
			if(LastActor != ThisActor)
			{
				//case 4
				LastActor->UnHighlightActor();
				ThisActor->HighlightActor();
			} //else case 5
		}
	}
	
}

void APlayerControllerBase::AbilityInputTagPressed(const FGameplayTag InputTag)
{
	if(InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		bTargeting = ThisActor != nullptr; //ThisActor为鼠标悬停在敌人身上才会有值
		bAutoRunning = false;
		FollowTime = 0.f; //重置统计的时间
	}
}

void APlayerControllerBase::AbilityInputTagReleased(const FGameplayTag InputTag)
{
	if(!InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		if(GetASC())
		{
			GetASC()->AbilityInputTagReleased(InputTag);
		}
		return;
	}

	if(bTargeting)
	{
		if(GetASC())
		{
			GetASC()->AbilityInputTagReleased(InputTag);
		}
	}
	else
	{
		const APawn* ControlledPawn = GetPawn();
		if(FollowTime <= ShortPressThreshold && ControlledPawn)
		{
			if(UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), CachedDestination))
			{
				Spline->ClearSplinePoints(); //清除样条内现有的点
				for(const FVector& PointLoc : NavPath->PathPoints)
				{
					Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World); //将新的位置添加到样条曲线中
					// DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Orange, false, 5.f); //点击后debug调试
				}
				//自动寻路将最终目的地设置为导航的终点,方便停止导航
				CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
				bAutoRunning = true; //设置当前正常自动寻路状态,将在tick中更新位置
			}
		}
	}
}

void APlayerControllerBase::AbilityInputTagHold(const FGameplayTag InputTag)
{
	if(!InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		if(GetASC())
		{
			GetASC()->AbilityInputTagHold(InputTag);
		}
		return;
	}

	if(bTargeting)
	{
		if(GetASC())
		{
			//点击敌人目标,将攻击敌人
			GetASC()->AbilityInputTagHold(InputTag);
		}
	}
	else
	{
		FollowTime += GetWorld()->GetDeltaSeconds(); //统计悬停时间来判断是否为点击

		FHitResult Hit;
		if(GetHitResultUnderCursor(ECC_Visibility, false, Hit))
		{
			CachedDestination = Hit.ImpactPoint;
		}

		if(APawn* ControlledPawn = GetPawn())
		{
			const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
			ControlledPawn->AddMovementInput(WorldDirection);
		}
	}
}

UAbilitySystemComponentBase* APlayerControllerBase::GetASC()
{
	if(AbilitySystemComponentBase == nullptr)
	{
		AbilitySystemComponentBase = Cast<UAbilitySystemComponentBase>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetPawn()));
	}

	return AbilitySystemComponentBase;
}

void APlayerControllerBase::BeginPlay()
{
	Super::BeginPlay();
	check(CharacterContext); //判断是否存在

	//从本地角色身上获取到它的子系统
	UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
	// check(Subsystem); //检查子系统是否存在 不存在会打断
	if(Subsystem)
	{
		//将自定义的操作映射上下文添加到子系统中
		Subsystem->AddMappingContext(CharacterContext, 0); //可以存在多个操作映射,根据优先级触发
	}

	bShowMouseCursor = true; //游戏中是否显示鼠标光标
	DefaultMouseCursor = EMouseCursor::Default; //鼠标光标的样式

	FInputModeGameAndUI InputModeData;
	InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); //将鼠标锁定在视口内
	InputModeData.SetHideCursorDuringCapture(false); //鼠标被捕获时是否隐藏
	SetInputMode(InputModeData); //设置给控制器
}

void APlayerControllerBase::SetupInputComponent()
{
	Super::SetupInputComponent();

	UInputComponentBase* EnhancedInputComponent = CastChecked<UInputComponentBase>(InputComponent); //获取到增强输入组件

	EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlayerControllerBase::Move); //绑定移动事件

	EnhancedInputComponent->BindAbilityAction(InputConfig, this, &ThisClass::AbilityInputTagPressed,&ThisClass::AbilityInputTagReleased, &ThisClass::AbilityInputTagHold);
}

void APlayerControllerBase::Move(const FInputActionValue& InputActionValue)
{
	const FVector2D InputAxisVector = InputActionValue.Get<FVector2D>(); //获取输入操作的2维向量值
	const FRotator Rotation = GetControlRotation(); //获取控制器旋转
	const FRotator YawRotation(0.f, Rotation.Yaw, 0.f); //通过控制器的垂直朝向创建一个旋转值,忽略上下朝向和左右朝向

	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); //获取世界坐标系下向前的值,-1到1
	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); //获取世界坐标系下向右的值,-1到1

	if(APawn* ControlledPawn = GetPawn<APawn>())
	{
		ControlledPawn->AddMovementInput(ForwardDirection, InputAxisVector.Y);
		ControlledPawn->AddMovementInput(RightDirection, InputAxisVector.X);
	}
}

代码整理

上面的代码有很多可以优化的点,我们一步一步优化,让代码看起来更清晰,能节省性能的也将性能节省下来。

首先能优化的点就是鼠标位置拾取,这个是比较耗性能的点,需要在鼠标位置发射一条射线,然后和场景内可交互的模型进行碰撞检测,我们在代码里面使用两次,一次是实现悬停让敌人高亮的效果。另一次是鼠标悬停,角色跟随鼠标拾取位置移动时。这两个拾取,我们其实只需要拾取一次,增加一个类的成员变量,悬停里面去判断结果。
添加一个成员变量

FHitResult CursorHit; //鼠标拾取结果,可以复用

直接在CursorTrace函数里面获取,把局部的变量删除。

GetHitResultUnderCursor(ECC_Visibility, false, CursorHit); //获取可视的鼠标命中结果

在鼠标悬停事件中,直接使用它的结果即可

if(CursorHit.bBlockingHit)
{
	CachedDestination = CursorHit.ImpactPoint;
}

优化第二项,鼠标位置追踪函数里面,我们写了很长的逻辑,而且重复的判断很多,接下来,我们将优化这里。

//鼠标位置追踪
void APlayerControllerBase::CursorTrace()
{
	GetHitResultUnderCursor(ECC_Visibility, false, CursorHit); //获取可视的鼠标命中结果
	if(!CursorHit.bBlockingHit) return; //如果未命中直接返回

	LastActor = ThisActor;
	ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());

	/**
	 * 射线拾取后,会出现的几种情况
	 * 1. LastActor is null   ThisActor is null 不需要任何操作
	 * 2. LastActor is null   ThisActor is valid 高亮ThisActor
	 * 3. LastActor is valid   ThisActor is null 取消高亮LastActor
	 * 4. LastActor is valid   ThisActor is valid LastActor != ThisActor 取消高亮LastActor 高亮ThisActor
	 * 5. LastActor is valid   ThisActor is valid LastActor == ThisActor 不需要任何操作
	 */

	if(LastActor == nullptr)
	{
		if(ThisActor != nullptr)
		{
			//case 2
			ThisActor->HighlightActor();
		} // else case 1
	}
	else
	{
		if(ThisActor == nullptr)
		{
			//case 3
			LastActor->UnHighlightActor();
		}
		else
		{
			if(LastActor != ThisActor)
			{
				//case 4
				LastActor->UnHighlightActor();
				ThisActor->HighlightActor();
			} //else case 5
		}
	}
	
}

修改完成后,如下,我们只需要在两个指针不同的时候,进行处理即可,然后判断当前变量指针是否存在,存在再调用对应的函数即可。

//鼠标位置追踪
void APlayerControllerBase::CursorTrace()
{
	GetHitResultUnderCursor(ECC_Visibility, false, CursorHit); //获取可视的鼠标命中结果
	if(!CursorHit.bBlockingHit) return; //如果未命中直接返回

	LastActor = ThisActor;
	ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());

	if(ThisActor != LastActor)
	{
		if(ThisActor) ThisActor->HighlightActor();
		if(LastActor) LastActor->UnHighlightActor();
	}
	
}

修改完成后的代码:

// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "GameplayTagContainer.h"
#include "PlayerControllerBase.generated.h"

class USplineComponent;
class UInputConfig;
class UInputMappingContext;
class UInputAction;
struct FInputActionValue;
class IEnemyInterface;
class UAbilitySystemComponentBase;

/**
 * 玩家控制器
 */
UCLASS()
class AURA_API APlayerControllerBase : public APlayerController
{
	GENERATED_BODY()

public:
	APlayerControllerBase();
	virtual void PlayerTick(float DeltaTime) override;

protected:
	virtual void BeginPlay() override; //游戏开始时触发
	virtual void SetupInputComponent() override; //在生成输入组件时触发

private:
	UPROPERTY(EditAnywhere, Category="Input")
	TObjectPtr<UInputMappingContext> CharacterContext;

	UPROPERTY(EditAnywhere, Category="Input")
	TObjectPtr<UInputAction> MoveAction;

	void Move(const struct FInputActionValue& InputActionValue);

	void CursorTrace(); //鼠标位置追踪拾取
	IEnemyInterface* LastActor; //上一帧拾取到的接口指针
	IEnemyInterface* ThisActor; //这一帧拾取到的接口指针
	FHitResult CursorHit; //鼠标拾取结果,可以复用

	void AbilityInputTagPressed(FGameplayTag InputTag);
	void AbilityInputTagReleased(FGameplayTag InputTag);
	void AbilityInputTagHold(FGameplayTag InputTag);

	UPROPERTY(EditDefaultsOnly, Category="Input")
	TObjectPtr<UInputConfig> InputConfig;

	UPROPERTY()
	TObjectPtr<UAbilitySystemComponentBase> AbilitySystemComponentBase;

	UAbilitySystemComponentBase* GetASC();

	FVector CachedDestination = FVector::ZeroVector; //存储鼠标点击的位置
	float FollowTime = 0.f; // 用于查看按住了多久
	bool bAutoRunning = false; //当前是否自动移动
	bool bTargeting = false; //当前鼠标是否选中敌人

	UPROPERTY(EditDefaultsOnly)
	float ShortPressThreshold = 0.3f; //定义鼠标悬停多长时间内算点击事件

	UPROPERTY(EditDefaultsOnly)
	float AutoRunAcceptanceRadius = 50.f; //当角色和目标距离在此半径内时,将关闭自动寻路

	UPROPERTY(VisibleAnywhere)
	TObjectPtr<USplineComponent> Spline; //自动寻路时生成的样条线

	void AutoRun();
};

// 版权归暮志未晚所有。


#include "Player/PlayerControllerBase.h"

#include "AbilitySystemBlueprintLibrary.h"
#include "EnhancedInputSubsystems.h"
#include "MyGameplayTags.h"
#include "NavigationPath.h"
#include "NavigationSystem.h"
#include "AbilitySystem/AbilitySystemComponentBase.h"
#include "Components/SplineComponent.h"
#include "Input/InputComponentBase.h"
#include "Interaction/EnemyInterface.h"

APlayerControllerBase::APlayerControllerBase()
{
	bReplicates = true; //是否将数据传送服务器更新
	LastActor = nullptr;
	ThisActor = nullptr;

	Spline = CreateDefaultSubobject<USplineComponent>("Spline");
}

//帧更新
void APlayerControllerBase::PlayerTick(float DeltaTime)
{
	Super::PlayerTick(DeltaTime);
	//鼠标位置追踪是否悬停在敌人身上
	CursorTrace();
	//自动寻路
	AutoRun();
}

void APlayerControllerBase::AutoRun()
{
	if(!bAutoRunning) return;
	if(APawn* ControlledPawn = GetPawn())
	{
		//找到距离样条最近的位置
		const FVector LocationOnSpline = Spline->FindLocationClosestToWorldLocation(ControlledPawn->GetActorLocation(), ESplineCoordinateSpace::World);
		//获取这个位置在样条上的方向
		const FVector Direction = Spline->FindDirectionClosestToWorldLocation(LocationOnSpline, ESplineCoordinateSpace::World);
		ControlledPawn->AddMovementInput(Direction);

		const float DistanceToDestination = (LocationOnSpline - CachedDestination).Length();
		if(DistanceToDestination <= AutoRunAcceptanceRadius)
		{
			bAutoRunning = false;
		}
	}
}

//鼠标位置追踪
void APlayerControllerBase::CursorTrace()
{
	GetHitResultUnderCursor(ECC_Visibility, false, CursorHit); //获取可视的鼠标命中结果
	if(!CursorHit.bBlockingHit) return; //如果未命中直接返回

	LastActor = ThisActor;
	ThisActor = Cast<IEnemyInterface>(CursorHit.GetActor());

	if(ThisActor != LastActor)
	{
		if(ThisActor) ThisActor->HighlightActor();
		if(LastActor) LastActor->UnHighlightActor();
	}
	
}

void APlayerControllerBase::AbilityInputTagPressed(const FGameplayTag InputTag)
{
	if(InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		bTargeting = ThisActor != nullptr; //ThisActor为鼠标悬停在敌人身上才会有值
		bAutoRunning = false;
		FollowTime = 0.f; //重置统计的时间
	}
}

void APlayerControllerBase::AbilityInputTagReleased(const FGameplayTag InputTag)
{
	if(!InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		if(GetASC()) GetASC()->AbilityInputTagReleased(InputTag);
		return;
	}

	if(bTargeting)
	{
		if(GetASC()) GetASC()->AbilityInputTagReleased(InputTag);
	}
	else
	{
		const APawn* ControlledPawn = GetPawn();
		if(FollowTime <= ShortPressThreshold && ControlledPawn)
		{
			if(UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControlledPawn->GetActorLocation(), CachedDestination))
			{
				Spline->ClearSplinePoints(); //清除样条内现有的点
				for(const FVector& PointLoc : NavPath->PathPoints)
				{
					Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World); //将新的位置添加到样条曲线中
					// DrawDebugSphere(GetWorld(), PointLoc, 8.f, 8, FColor::Orange, false, 5.f); //点击后debug调试
				}
				//自动寻路将最终目的地设置为导航的终点,方便停止导航
				CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
				bAutoRunning = true; //设置当前正常自动寻路状态,将在tick中更新位置
			}
		}
	}
}

void APlayerControllerBase::AbilityInputTagHold(const FGameplayTag InputTag)
{
	if(!InputTag.MatchesTagExact(FMyGameplayTags::Get().InputTag_LMB))
	{
		if(GetASC()) GetASC()->AbilityInputTagHold(InputTag);
		return;
	}

	if(bTargeting)
	{
		if(GetASC()) GetASC()->AbilityInputTagHold(InputTag);
	}
	else
	{
		FollowTime += GetWorld()->GetDeltaSeconds(); //统计悬停时间来判断是否为点击

		if(CursorHit.bBlockingHit) CachedDestination = CursorHit.ImpactPoint; //获取鼠标拾取位置

		if(APawn* ControlledPawn = GetPawn())
		{
			const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
			ControlledPawn->AddMovementInput(WorldDirection);
		}
	}
}

UAbilitySystemComponentBase* APlayerControllerBase::GetASC()
{
	if(AbilitySystemComponentBase == nullptr)
	{
		AbilitySystemComponentBase = Cast<UAbilitySystemComponentBase>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetPawn()));
	}

	return AbilitySystemComponentBase;
}

void APlayerControllerBase::BeginPlay()
{
	Super::BeginPlay();
	check(CharacterContext); //判断是否存在

	//从本地角色身上获取到它的子系统
	UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
	// check(Subsystem); //检查子系统是否存在 不存在会打断
	if(Subsystem)
	{
		//将自定义的操作映射上下文添加到子系统中
		Subsystem->AddMappingContext(CharacterContext, 0); //可以存在多个操作映射,根据优先级触发
	}

	bShowMouseCursor = true; //游戏中是否显示鼠标光标
	DefaultMouseCursor = EMouseCursor::Default; //鼠标光标的样式

	FInputModeGameAndUI InputModeData;
	InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); //将鼠标锁定在视口内
	InputModeData.SetHideCursorDuringCapture(false); //鼠标被捕获时是否隐藏
	SetInputMode(InputModeData); //设置给控制器
}

void APlayerControllerBase::SetupInputComponent()
{
	Super::SetupInputComponent();

	UInputComponentBase* EnhancedInputComponent = CastChecked<UInputComponentBase>(InputComponent); //获取到增强输入组件

	EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlayerControllerBase::Move); //绑定移动事件

	EnhancedInputComponent->BindAbilityAction(InputConfig, this, &ThisClass::AbilityInputTagPressed,&ThisClass::AbilityInputTagReleased, &ThisClass::AbilityInputTagHold);
}

void APlayerControllerBase::Move(const FInputActionValue& InputActionValue)
{
	const FVector2D InputAxisVector = InputActionValue.Get<FVector2D>(); //获取输入操作的2维向量值
	const FRotator Rotation = GetControlRotation(); //获取控制器旋转
	const FRotator YawRotation(0.f, Rotation.Yaw, 0.f); //通过控制器的垂直朝向创建一个旋转值,忽略上下朝向和左右朝向

	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); //获取世界坐标系下向前的值,-1到1
	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); //获取世界坐标系下向右的值,-1到1

	if(APawn* ControlledPawn = GetPawn<APawn>())
	{
		ControlledPawn->AddMovementInput(ForwardDirection, InputAxisVector.Y);
		ControlledPawn->AddMovementInput(RightDirection, InputAxisVector.X);
	}
}

处理bug

在ASC里面我们实现了一个EffectApplied的函数,在GE被应用的时候,会触发此函数回调,用于显示被使用的是什么物品。然后在EffectApplied函数中使用自定义委托广播出去,在WidgetController中监听自定义委托。但是,现在OnGameplayEffectAppliedDelegateToSelf无法在客户端运行。

void UAbilitySystemComponentBase::AbilityActorInfoSet()
{
	OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAbilitySystemComponentBase::EffectApplied);
}

查看源码,这个注释明确告诉我们在服务器运行的
在这里插入图片描述
我们现在的解决逻辑是,OnGameplayEffectAppliedDelegateToSelf只在服务器上面运行,在服务器上面触发了EffectApplied,那EffectApplied回调能不能实现在客户端也能运行。方法是可行的

首先,我们需要给EffectApplied函数增加标记,告诉UE它是一个在客户端运行的成员函数,这样,在服务器调用它后,它会被复制到客户端去运行。作为规范,我们需要在客户端运行函数前面加上Client,这样可以明白是一个客户端运行的函数。

	//标记Client告诉UE这个函数应该只在客户端运行,设置Reliable表示这个函数调用是可靠的,即它确保数据能够到达客户端
	UFUNCTION(Client, Reliable)
	void ClientEffectApplied(UAbilitySystemComponent* AbilitySystemComponent, const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveGameplayEffectHandle) const;

接着将回调绑定的名称改掉。

void UAbilitySystemComponentBase::AbilityActorInfoSet()
{
	OnGameplayEffectAppliedDelegateToSelf.AddUObject(this, &UAbilitySystemComponentBase::ClientEffectApplied);
}

最重要的是,客户端函数的实现需要加上_Implementation后缀,这样就实现了在客户端调用。

void UAbilitySystemComponentBase::ClientEffectApplied_Implementation(UAbilitySystemComponent* AbilitySystemComponent,
                                                const FGameplayEffectSpec& EffectSpec, FActiveGameplayEffectHandle ActiveGameplayEffectHandle) const
{
	// GEngine->AddOnScreenDebugMessage(1, 8.f, FColor::Blue, FString("Effect Applied!"));
	FGameplayTagContainer TagContainer;
	EffectSpec.GetAllAssetTags(TagContainer);

	EffectAssetTags.Broadcast(TagContainer);
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

突破编程_前端_SVG(circle 圆形)

1 circle 元素的基本属性和用法 SVG 的 <circle> 元素用于在SVG文档中绘制圆形。它具有几个基本属性&#xff0c;允许定义圆形的大小、位置、填充颜色和边框样式。以下是 <circle> 元素的基本属性及其详细解释&#xff1a; 1.1 cx 和 cy 描述&#xff1a;这两个…

Docker容器嵌入式开发:Docker Ubuntu18.04配置mysql数据库

在 Ubuntu 18.04 操作系统中安装 MySQL 数据库的过程。下面是安装过程的详细描述&#xff1a; 首先&#xff0c;使用以下命令安装 MySQL 服务器&#xff1a; sudo apt install mysql-server系统会提示是否继续安装&#xff0c;按下 Y 键确认。 安装过程中&#xff0c;系统会…

模板进阶 | 非类型模板参数 | 类模板的特化 | 模板的分离编译 | 模板的优缺点

非类型模板参数 我们可以认为非类型模板参数就是一个常量&#xff0c;在我们的类里面我们是不能对它进行改造 为什么会有这样的场景&#xff0c;其次就是C语言那里我们一般使用什么。 场景1 #include<iostream> using namespace std;#define N 10 template<class T…

uniapp开发小程序手写板、签名、签字

可以使用这个插件进行操作 手写板-签名签字-lime-signature - DCloud 插件市场 但是目前这个插件没有vue3 setup Composition API的写法。所以对于此文档提供的可以直接使用,需要使用Composition API方式实现的,可以继续看。 因为Composition API方式,更加的简单、灵活,…

2024 CleanMyMac X 优化储存苹果电脑空间 的好帮手

在数字时代&#xff0c;我们的Mac设备承载着越来越多的重要信息和日常任务。然而&#xff0c;随着时间的推移&#xff0c;这些设备可能会变得缓慢、混乱&#xff0c;甚至充满不必要的文件。这就是CleanMyMac X发挥作用的地方。 CleanMyMac X是一款功能强大的Mac优化工具&#…

基于java+springboot+vue实现的药品管理系统(文末源码+Lw)23-297

摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;药品信息因为其管理内容繁杂&#xff0c;管理数量繁多导致手工进行处理不能满足广…

动态规划原理及其在优化问题中的应用解析

动态规划原理及其在优化问题中的应用解析 一、最优子结构二、重叠子问题三、何时使用动态规划法四、伪代码示例五、C代码示例七、详细说明动态规划原理7.1、最优子结构7.2 重叠子问题7.3 动态规划的实现 八、结论 动态规划是一种解决优化问题的方法&#xff0c;它通过将原问题分…

Triton Server Python 后端优化

接上文 不使用 Docker 构建 Triton 服务器并在 Google Colab 平台上部署 HuggingFace 模型 MultiGPU && Multi Instance Config 追加 instance_group [{count: 4kind: KIND_GPUgpus: [ 0, 1 ]} ]Python Backend Triton 会根据配置信息启动四个实例&#xff0c;…

win10系统中exe文件打不开

问题描述 昨天下载了某个驱动安装程序之后&#xff0c;点击.exe文件没有反应。 解决方法 1. 开启兼容模式运行 右键点击属性 点击【兼容性】&#xff0c;并且【以兼容模式运行程序】 2. 给exe文件换个文件夹再次尝试 我使用第一个方法没有用&#xff0c;之后尝试了把文…

Eureka-搭建Eureka步骤

简介&#xff1a; Eureka是Netflix开发的服务发现框架&#xff0c;本身是一个基于REST的服务&#xff0c;主要用于定位运行在AWS域中的中间层服务&#xff0c;以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中&#xff0c;以实…

转让北京100万旅行社带国内旅行社许可证条件和要求

旅行社的主要分类为国际旅行社和国内旅行社两类。国际旅行社拥有更为广泛的经营范围&#xff0c;不仅涵盖国内旅游业务&#xff0c;还包括出境旅游业务以及入境旅游业务&#xff1b;相比之下&#xff0c;国内旅行社则专注于国内旅游市场以及入境旅游业务。当前情况下&#xff0…

Pandas部分应掌握的重要知识点

目录 Pandas部分应掌握的重要知识点一、DataFrame数据框的创建1、直接基于二维数据创建&#xff08;同时使用index和columns参数&#xff09;2、基于excel文件中的数据来创建 二、查看数据框中的数据和联机帮助信息1、查看特殊行的数据2、查看联机帮助的两种常见方法&#xff0…

MDK平台 - Code, RO-data , RW-data, ZI-data详解

文章目录 1 . 前言2 . Code, RO-data , RW-data, ZI-data解析3 . RAM上电复位4 . 细节扩展5 . 总结 【全文大纲】 : https://blog.csdn.net/Engineer_LU/article/details/135149485 1 . 前言 MDK编译后&#xff0c;会列出Code, RO-data , RW-data, ZI-data&#xff0c;以下解析…

2024年会计、审计、财务与经济管理国际会议(ICAAFEM2024)

2024年会计、审计、财务与经济管理国际会议&#xff08;ICAAFEM2024&#xff09; 会议简介 2024年国际会计、审计、财务和经济管理会议&#xff08;ICAAFEM2024&#xff09;将在云南省昆明市举行。会议旨在为从事“会计、审计、财务、经济管理”研究的专家学者提供一个平台&am…

java快速幂算法

快速幂算法 参考视频(参考五角七边up大佬&#xff09; 幂运算的介绍 幂运算是指将一个数自身乘以自身多次的运算&#xff0c;其表达式为 a n a^n an&#xff0c;其中 a a a 是底数&#xff0c; n n n 是指数。 快速幂解释 快速幂算法是一种用于快速计算幂运算的算法&…

easyui combobox下拉框组件输入检索全模糊查询

前引&#xff1a; easyui下拉组件&#xff08;combobox&#xff09;&#xff0c;输入检索下拉内容&#xff0c;是默认的右模糊匹配&#xff0c;而且不支持选择。因业务要求需要做成全模糊查询&#xff0c;目前网上搜索有两种方案&#xff1a; 1.修改easyui源码&#xff0c;这个…

Windows联网状态工具TCPView

文章目录 TCPView命令行工具更多Sysinternals Suite工具 TCPView TCPView用于显示系统上所有 TCP 和 UDP 终结点的详细列表&#xff0c;包括本地和远程地址以及 TCP 连接的状态&#xff0c;界面如下。 列表的表头含义如下 表头含义表头含义Process name应用名称Process id进程…

STM32H7各块RAM的位置和作用

STM32H7各块RAM的位置和作用 RAM各块RAM的特性各块RAM的时钟问题RAM分配方案 摘抄于armfly-V7开发板bsp手册&#xff0c;仅供个人学习。 RAM 这个图可以方便识别总线所外挂的外设&#xff0c;共分为三个域&#xff1a;D1 Domain&#xff0c;D2 Domain 和 D3 Domain。 ◆ ITCM 和…

【Hello算法】 > 第 2 关 >数据结构 之 数组与链表

数据结构 之 数组与链表 1&#xff1a;Understanding data structures &#xff01;——了解数据结构——1.1&#xff1a;Classification-分类-1.2&#xff1a;Type-类型- 2&#xff1a;Arrays are the bricks that make up the wall of data structures *——数组是组成数据结…

【Java核心能力】美团优选后端一面:Java 八股文相关内容

欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术的推送&#xff01; 在我后台回复 「资料」 可领取编程高频电子书&#xff01; 在我后台回复「面试」可领取硬核面试笔记&#xff01; 文章导读地址…