【UE4C++-ActionRougelike-18】积分系统+持续伤害+物品互动UI


【UE4C++-ActionRougelike-18】积分系统+持续伤害+物品互动UI

一、积分系统(作业五)

1、创建玩家状态类

以PlayerState为父类创建玩家状态类MyPlayerState,在MyPlayerState中维护玩家积分属性Credits,并提供了用于修改玩家积分的函数。使用委托来处理积分变化时的更新,与绑定导致每一帧都会调用相比,减少性能开销。

//使用基于委托来更新UI要优于直接将Credit绑定到UI上,后者会每一帧调用
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCreditsChanged, AMyPlayerState*, PlayerState, int32, NewCredits, int32, Delta);
UCLASS()
class ACTIONROGUELIKE_API AMyPlayerState : public APlayerState
{
protected:
    //玩家积分
	int32 Credits;
public:
    //增加玩家的积分 Credits
	UFUNCTION(BlueprintCallable, Category = "PlayerState|Credits") // < Category|SubCategory
	void AddCredits(int32 Delta);
	//减少玩家的积分 Credits
	UFUNCTION(BlueprintCallable, Category = "PlayerState|Credits")
	bool RemoveCredits(int32 Delta);
	//OnCreditsChanged 委托,当玩家积分变化时触发
	UPROPERTY(BlueprintAssignable, Category = "Events")
	FOnCreditsChanged OnCreditsChanged;
};

在MyPlayerState.cpp中实现增加积分和减少积分的方法

void AMyPlayerState::AddCredits(int32 Delta)
{
    // 防止用户添加负数或零
    if (!ensure(Delta > 0.0f))
    {
        return;
    }
    // 增加积分Delta
    Credits += Delta;
    // 触发 OnCreditsChanged 委托中的事件
    OnCreditsChanged.Broadcast(this, Credits, Delta);
}
bool AMyPlayerState::RemoveCredits(int32 Delta)
{
    // 防止用户添加或减去负数或零
    if (!ensure(Delta > 0.0f))
    {
        return false;
    }
    // 如果玩家当前的积分 Credits 小于 Delta,则返回 false
    if (Credits < Delta)
    {
        return false;
    }
    // 减少积分 Delta
    Credits -= Delta;
    // 触发 OnCreditsChanged 委托中的事件
    OnCreditsChanged.Broadcast(this, Credits, -Delta);
    return true;
}

2、实时显示得分

在Credits_Widget中将表示积分值的Text控件设置为变量CreditCountText,在EventGraph中绑定OnCreditsChanged委托触发后执行的逻辑:更新UI,播放积分修改脉冲动画。

UI更新积分

3、获取和花费积分

3.1 杀死机器人增加积分

在MyGameMode.h中增加杀死AI的得分奖励值,在MyGameMode.cpp中的OnActorKilled增加杀死目标获得积分的逻辑

//MyGameMode.h
//杀死AI机器人的得分奖励值
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "AI")
int32 CreditsPerKill;

//MyGameMode.cpp
void AMyGameModeBase::OnActorKilled(AActor* VictimActor, AActor* Killer)
{
    // 杀死目标获得 Credits
	APawn* KillerPawn = Cast<APawn>(Killer);
	if (KillerPawn)
	{
        // 获取击杀者的 AMyPlayerState 组件,并给予击杀者 Credits 奖励
		if (AMyPlayerState* PS = KillerPawn->GetPlayerState<AMyPlayerState>())
		{
			PS->AddCredits(CreditsPerKill);
		}
	}
}
3.2 拾取健康药水花费积分

在MyPickup_Health.h中增加属性CreditCost表示拾取所需的积分花费,在MyPickup_Health.cpp中的健康药水交互方法Interact_Implementation中在拾取药水钱,需要判断玩家的Credits是否足够支付积分花费。

//MyPickup_Health.h
//拾取所需的积分花费
UPROPERTY(EditAnywhere, Category = "HealthPotion")
int32 CreditCost;
//MyPickup_Health.cpp
void AMyPickup_Health::Interact_Implementation(APawn* InstigatorPawn)
{
	if (!ensure(InstigatorPawn))
	{
		return;
	}
	UMyAttributeComponent* AttributeComp = Cast<UMyAttributeComponent>(InstigatorPawn->GetComponentByClass(UMyAttributeComponent::StaticClass()));
	// 玩家是否满血
	if (ensure(AttributeComp) && !AttributeComp->IsFullHealth())
	{
		if (AMyPlayerState* PS = InstigatorPawn->GetPlayerState<AMyPlayerState>())
		{
			// 如果玩家的 Credits 足够支付 Pickup_Health 的花费,则启用 Pickup,并扣除 Credits
			if (PS->RemoveCredits(CreditCost) && AttributeComp->ApplyHealthChange(this, AttributeComp->GetHealthMax()))
			{
				HideAndCooldownPickup();
			}
		}	
	}
}
3.3 拾取硬币增加积分

以MyPickup为父类创建硬币拾取物类MyPickup_Credits,添加属性CreditsAmount表示硬币拾取物能够增加的Credits值。

UCLASS()
class ACTIONROGUELIKE_API AMyPickup_Credits : public AMyPickup
{
protected:
	//Pickup 为玩家增加的 Credits 值
	UPROPERTY(EditAnywhere, Category = "Credits")
	int32 CreditsAmount;
public:
	virtual void Interact_Implementation(APawn* InstigatorPawn) override;
	AMyPickup_Credits();
};

在MyPickup_Credits.cpp中初始化CreditsAmount,在交互接口实现中为交互对象(玩家)增加Credits

AMyPickup_Credits::AMyPickup_Credits()
{
	CreditsAmount = 80;
}

// 实现 Pickup 父类的虚函数,给玩家增加 CreditsAmount 数量的 Credits
void AMyPickup_Credits::Interact_Implementation(APawn* InstigatorPawn)
{
	if (!ensure(InstigatorPawn))
	{
		return;
	}
	if (AMyPlayerState* PS = InstigatorPawn->GetPlayerState<AMyPlayerState>())
	{
		PS->AddCredits(CreditsAmount);
		HideAndCooldownPickup();
	}
}
3.4 效果演示
积分系统效果演示

4、随机生成拾取物

4.1 创建EQS查询

创建EQS查询Query_FindPickupSpawns用于获取拾取物的随机生成地点。这里选用了Grid(方形)来作生成器节点,具体参数可自行设置;添加了PathFinding测试,TestMode设置为PathExist,过滤掉不能抵达的地点;Context设置为自定义Context:玩家开始出生点。

随机生成拾取物EQS
4.2 生成拾取物

在MyGameModeBase.h中添加随机生成拾取物所需的属性和方法。

class ACTIONROGUELIKE_API AMyGameModeBase : public AGameModeBase
{
protected:
	//生成拾取物的EQS查询
	UPROPERTY(EditDefaultsOnly, Category = "Pickups")
	UEnvQuery* PickupSpawnQuery;
	//拾取物类数组
	UPROPERTY(EditDefaultsOnly, Category = "Pickups")
	TArray<TSubclassOf<AActor>> PickupClasses;
	//拾取物之间的间距
	UPROPERTY(EditDefaultsOnly, Category = "Pickups")
	float RequiredPickupDistance;
	//拾取物数量上限
	UPROPERTY(EditDefaultsOnly, Category = "Pickups")
	int32 DesiredPickupCount;
	//拾取物生成回调函数
	UFUNCTION()
	void OnPickupSpawnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus);
}

在MyGameModeBase.cpp中添加随时生成道具的逻辑

//游戏开始时随机生成拾取物
void AMyGameModeBase::StartPlay()
{
	Super::StartPlay();
	//设置了一个定时器TimerHandle_SpawnBots,定时调用SpawnBotTimerElapsed()函数,时间间隔为SpawnTimerInterval秒,循环执行
	GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &AMyGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
		
	// 确保至少有一个掉落物类
	if (ensure(PickupClasses.Num() > 0))
	{
		// 运行环境查询系统(EQS)来查找潜在的掉落物生成位置
		UEnvQueryInstanceBlueprintWrapper* QueryInstance = UEnvQueryManager::RunEQSQuery(this, PickupSpawnQuery, this, EEnvQueryRunMode::AllMatching, nullptr);
		if (ensure(QueryInstance))
		{
            //将当前对象绑定到查询完成事件,查询完成后会调用 OnPickupSpawnQueryCompleted() 函数。
			QueryInstance->GetOnQueryFinishedEvent().AddDynamic(this, &AMyGameModeBase::OnPickupSpawnQueryCompleted);
		}
	}
}
//随机生成拾取物
void AMyGameModeBase::OnPickupSpawnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
	 // 如果 EQS 查询失败,打印警告日志并退出函数
	if (QueryStatus != EEnvQueryStatus::Success)
	{
		UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
		return;
	}
	// 获取 EQS 查询结果中的所有位置
	TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
	// 用于记录已使用的位置,便于后面检查位置间距
	TArray<FVector> UsedLocations;
	//道具数量
	int32 SpawnCounter = 0;
	// 如果还没有生成足够数量的道具且仍有可用位置,则继续生成道具
	while (SpawnCounter < DesiredPickupCount && Locations.Num() > 0)
	{
		// 从剩余的位置中随机选取一个位置
		int32 RandomLocationIndex = FMath::RandRange(0, Locations.Num() - 1);
		FVector PickedLocation = Locations[RandomLocationIndex];
		// 将已选位置从数组中删除,避免重复选取
		Locations.RemoveAt(RandomLocationIndex);

		// 检查位置间距是否符合要求
		bool bValidLocation = true;
		for (FVector OtherLocation : UsedLocations)
		{
			float DistanceTo = (PickedLocation - OtherLocation).Size();

			if (DistanceTo < RequiredPickupDistance)
			{
				bValidLocation = false;
				break;
			}
		}
		// 如果位置间距不符合要求,跳过该位置
		if (!bValidLocation)
		{
			continue;
		}
		// 随机选取一种道具类别
		int32 RandomClassIndex = FMath::RandRange(0,PickupClasses.Num() - 1);
		TSubclassOf<AActor> RandomPickupClass = PickupClasses[RandomClassIndex];
		// 在随机位置生成道具
		GetWorld()->SpawnActor<AActor>(RandomPickupClass, PickedLocation, FRotator::ZeroRotator);
		// 记录该位置,便于后面检查位置间距
		UsedLocations.Add(PickedLocation);
		SpawnCounter++;
	}
}
4.3 效果演示
随机生成拾取物效果

二、持续伤害效果

1、创建持续效果能力类

以MyAction为父类创建持续效果能力类MyActionEffect。添加Duration和Period以及对应定时器句柄用来控制持续效果的持续时间和间隔时间。

UCLASS()
class ACTIONROGUELIKE_API UMyActionEffect : public UMyAction
{
	GENERATED_BODY()
public:
	void StartAction_Implementation(AActor* Instigator) override;
	void StopAction_Implementation(AActor* Instigator) override;
protected:
	// 持续时间
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Effect")
	float Duration;
	// 间隔时间
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Effect")
	float Period;
	// 定时器句柄,用于管理两次效果应用之间的时间间隔
	FTimerHandle PeriodHandle;
	// 定时器句柄,用于管理效果的持续时间
	FTimerHandle DurationHandle;
    // 执行持续期间效果
	UFUNCTION(BlueprintNativeEvent, Category = "Effect")
	void ExecutePeriodicEffect(AActor* Instigator);
public:
	UMyActionEffect();
};

在MyActionEffect.cpp中实现持续效果开始和结束函数。

UMyActionEffect::UMyActionEffect()
{
	bAutoStart = true;
}
void UMyActionEffect::StartAction_Implementation(AActor* Instigator)
{
	Super::StartAction_Implementation(Instigator);
	// 如果持续时间大于0,设置持续时间并在到达时间后停止动作
	if (Duration > 0.0f)
	{
		// 创建一个计时器,计时器结束时调用 StopAction 函数
		FTimerDelegate Delegate;
		Delegate.BindUFunction(this, "StopAction", Instigator);
		GetWorld()->GetTimerManager().SetTimer(DurationHandle, Delegate, Duration, false);
	}
	// 如果周期时间大于0,设置周期并在每个周期上执行 ExecutePeriodicEffect() 函数
	if (Period > 0.0f)
	{
		// 创建一个周期性计时器,每周期结束时调用 ExecutePeriodicEffect 函数
		FTimerDelegate Delegate;
		Delegate.BindUFunction(this, "ExecutePeriodicEffect", Instigator);
		GetWorld()->GetTimerManager().SetTimer(PeriodHandle, Delegate, Period, true);
	}
}
void UMyActionEffect::StopAction_Implementation(AActor* Instigator)
{
	// 如果上一个周期已经完成,执行最后一个周期
	if (GetWorld()->GetTimerManager().GetTimerRemaining(PeriodHandle) < KINDA_SMALL_NUMBER)
	{
		ExecutePeriodicEffect(Instigator);
	}
	// 父类的停止动作函数
	Super::StopAction_Implementation(Instigator);
	// 清除计时器
	GetWorld()->GetTimerManager().ClearTimer(PeriodHandle);
	GetWorld()->GetTimerManager().ClearTimer(DurationHandle);
	// 从拥有此动作的组件中删除此动作
	UMyActionComponent* Comp = GetOwningComponent();
	if (Comp)
	{
		Comp->RemoveAction(this);
	}
}
void UMyActionEffect::ExecutePeriodicEffect_Implementation(AActor* Instigator)
{
	// 实现周期性效果的函数,需要在子类中具体实现
}

注:这里在MyActionComponent中需要添加RemoveAction方法

//MyActionComponent.h
UFUNCTION(BlueprintCallable, Category = "Actions")
void RemoveAction(UMyAction* ActionToRemove);
//MyActionComponent.cpp
void UMyActionComponent::RemoveAction(UMyAction* ActionToRemove)
{
	// 检查 ActionToRemove 是否存在且未运行
	if (!ensure(ActionToRemove && !ActionToRemove->IsRunning()))
	{
		return;
	}
	// 从 Actions 数组中移除 ActionToRemove
	Actions.Remove(ActionToRemove);
}

2、创建持续伤害效果类

以MyActionEffect为父类创建持续伤害类蓝图类Effect_Burning。在Effect_Burning蓝图中实现父类中的ExecutePeriodicEffect方法,具体逻辑为造成伤害。设置持续时间和周期时间,并且设置GameplayTag为Buring。

持续伤害效果类

3、应用持续伤害效果

  • 在MyMagicProjectile.h中添加BurningActionClass来表示持续伤害效果类

  • 在MyMagicProjectile.cpp中OnActorOverlap方法中,应用伤害后增加持续伤害效果类Action。

//MyMagicProjectile.h
UPROPERTY(EditDefaultsOnly, Category = "Damage")
TSubclassOf<UMyActionEffect> BurningActionClass;
//MyMagicProjectile.cpp中的OnActorOverlap方法内
if (UMyGameplayFunctionLibrary::ApplyDirectionalDamage(GetInstigator(), OtherActor, DamageAmount, SweepResult))	
{
    Explode();
    if (ActionComp && BurningActionClass && HasAuthority())
    {
        ActionComp->AddAction(GetInstigator(), BurningActionClass);	
    }	
}

最后在MagicProjectile_BP设置BurningActionClass为Effect_Burning。

4、效果演示

持续伤害效果演示

三、改进物品互动UI

1、寻找可交互对象

在SInteractionComponent中添加AActor变量FocusedActor用来表示当前被玩家注视的物品,FindBestInteractable用来查找当前可以交互的对象;DefaultWidgetClass表示当前显示物品互动UI的Widget类

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class ACTIONROGUELIKE_API USInteractionComponent : public UActorComponent
{
public:
    USInteractionComponent();
    // 主动交互函数
    void PrimaryInteract();
protected:
    virtual void BeginPlay() override;
    // 查找最佳可交互对象
    void FindBestInteractable();
    // 当前焦点对象
    UPROPERTY()
    AActor* FocusedActor;
    // 跟踪射线的距离
    UPROPERTY(EditDefaultsOnly, Category = "Trace")
    float TraceDistance;
    // 跟踪射线的半径
    UPROPERTY(EditDefaultsOnly, Category = "Trace")
    float TraceRadius;
    // 跟踪射线的碰撞通道
    UPROPERTY(EditDefaultsOnly, Category = "Trace")
    TEnumAsByte<ECollisionChannel> CollisionChannel;
    // 默认用户界面类
    UPROPERTY(EditDefaultsOnly, Category = "UI")
    TSubclassOf<UMyWorldUserWidget> DefaultWidgetClass;
    // 默认用户界面实例
    UPROPERTY()
    UMyWorldUserWidget* DefaultWidgetInstance;
public:
    virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
};

在SInteractionComponent.cpp中实现FindBestInteractable。步骤为:遍历所有可交互对象,获取第一个实现了 USGameplayInterface 接口的对象,将该对象设置为FocusedActor,创建Widget实例附着到FocusedActor上。

// 查找最佳可交互对象
void USInteractionComponent::FindBestInteractable()
{
	// 获取调试绘制交互状态的开关值
	bool bDebugDraw = CVarDebugDrawInteraction.GetValueOnGameThread();
	// 添加要查询的对象类型
	FCollisionObjectQueryParams ObjectQueryParams;
	ObjectQueryParams.AddObjectTypesToQuery(CollisionChannel);
	// 获取视点位置和旋转
	AActor* MyOwner = GetOwner();
	FVector EyeLocation;
	FRotator EyeRotation;
	MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotation);
	FVector End = EyeLocation + (EyeRotation.Vector() * TraceDistance);

	TArray<FHitResult> Hits;
	FCollisionShape Shape;
	Shape.SetSphere(TraceRadius);
	// 执行射线检测,获取所有可交互对象
	bool bBlockingHit = GetWorld()->SweepMultiByObjectType(Hits, EyeLocation, End, FQuat::Identity, ObjectQueryParams, Shape);
	// 根据检测结果设置调试绘制的颜色
	FColor LineColor = bBlockingHit ? FColor::Green : FColor::Red;
	// 清空焦点对象引用
	FocusedActor = nullptr;
	// 遍历所有可交互对象,获取第一个实现了 USGameplayInterface 接口的对象
	for (FHitResult Hit : Hits)
	{
		if (bDebugDraw)
		{
			DrawDebugSphere(GetWorld(), Hit.ImpactPoint, TraceRadius, 32, LineColor, false, 2.0f);
		}

		AActor* HitActor = Hit.GetActor();
		if (HitActor)
		{
			// 如果HitActor可以被交互,令FocusedActor为当前HitActor
			if (HitActor->Implements<USInteractionInterface>())
			{
				FocusedActor = HitActor;
				break;
			}
		}
	}
	// 如果存在焦点对象,则创建或更新用户界面
	if (FocusedActor)
	{
         // 如果不存在控件实例且DefaultWidgetClass存在,创建控件实例DefaultWidgetInstance
		if (DefaultWidgetInstance == nullptr && ensure(DefaultWidgetClass))
		{
			DefaultWidgetInstance = CreateWidget<UMyWorldUserWidget>(GetWorld(), DefaultWidgetClass);
		}
		if (DefaultWidgetInstance)
		{
             //控件实例附着在FocusedActor上
			DefaultWidgetInstance->AttachedActor = FocusedActor;
			if (!DefaultWidgetInstance->IsInViewport())
			{
				DefaultWidgetInstance->AddToViewport();
			}
		}
	}
	// 否则移除用户界面
	else
	{
		if (DefaultWidgetInstance)
		{
			DefaultWidgetInstance->RemoveFromParent();
		}
	}
	if (bDebugDraw)
	{
		DrawDebugLine(GetWorld(), EyeLocation, End, LineColor, false, 2.0f, 0, 2.0f);
	}
}

void USInteractionComponent::PrimaryInteract()
{
	if (FocusedActor == nullptr)
	{
		GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, "No Focus Actor to interact.");
		return;
	}
	APawn* MyPawn = Cast<APawn>(GetOwner());
	ISInteractionInterface::Execute_Interact(FocusedActor, MyPawn);
}

2、创建物品互动UI

以MyWorldUserWidget为父类创建蓝图类DefaultInteraction_Widget,并设置好交互UI布局

默认交互Widget

并在PlayerCharacter_BP中的InteractionComp中设置DefaultWidgetClass为DefaultInteraction_Widget。

3、效果演示

物品互动UI效果演示

评论
  目录