UE 互动学习之路
火药桶爆炸
创建一个SExplosiveBarrel类,基类为AActor。该类包含两个关键组件:MeshComp和ForceComp,分别控制模型和爆炸的力场。
爆炸其实本质是受到碰撞检测的影响,通过不同的碰撞设置我们可以实现,爆炸是否影响到自身
完整代码如下:
// SExplosiveBarrel.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SExplosiveBarrel.generated.h"
class URadialForceComponent;
class UStaticMeshComponent;
UCLASS()
class ACTIONROGUELIKE_API ASExplosiveBarrel : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASExplosiveBarrel();
protected:
UPROPERTY(VisibleAnywhere)
TObjectPtr<UStaticMeshComponent> MeshComp;
UPROPERTY(VisibleAnywhere)
TObjectPtr<URadialForceComponent> ForceComp;
virtual void PostInitializeComponents() override;
UFUNCTION()
void OnActorHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,
FVector NormalImpulse, const FHitResult& Hit);
};
// SExplosiveBarrel.cpp
#include "SExplosiveBarrel.h"
#include "PhysicsEngine/RadialForceComponent.h"
#include "DrawDebugHelpers.h"
// Sets default values
ASExplosiveBarrel::ASExplosiveBarrel()
{
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>("MeshComp");
MeshComp->SetSimulatePhysics(true);
// Enabling Simulate physics automatically changes the Profile to PhysicsActor in Blueprint, in C++ we need to change this manually.
MeshComp->SetCollisionProfileName(UCollisionProfile::PhysicsActor_ProfileName);
RootComponent = MeshComp;
ForceComp = CreateDefaultSubobject<URadialForceComponent>("ForceComp");
ForceComp->SetupAttachment(MeshComp);
// Leaving this on applies small constant force via component 'tick' (Optional)
ForceComp->SetAutoActivate(false);
ForceComp->Radius = 750.0f;
ForceComp->ImpulseStrength = 2500.0f; // Alternative: 200000.0 if bImpulseVelChange = false
// Optional, ignores 'Mass' of other objects (if false, the impulse strength will be much higher to push most objects depending on Mass)
ForceComp->bImpulseVelChange = true;
// Optional, default constructor of component already adds 4 object types to affect, excluding WorldDynamic
ForceComp->AddCollisionChannelToAffect(ECC_WorldDynamic);
}
void ASExplosiveBarrel::PostInitializeComponents()
{
// Don't forget to call parent function
Super::PostInitializeComponents();
MeshComp->OnComponentHit.AddDynamic(this, &ASExplosiveBarrel::OnActorHit);
}
void ASExplosiveBarrel::OnActorHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
ForceComp->FireImpulse();
UE_LOG(LogTemp,Log,TEXT("OnActorHit in Explosive Barrel"));
UE_LOG(LogTemp,Warning,TEXT("OtherActor: %s , at the game time: %f"), *GetNameSafe(OtherActor),GetWorld()->TimeSeconds);
FString CombinedString = FString::Printf(TEXT("Hit at the location: %s"), *Hit.ImpactPoint.ToString());
DrawDebugString(GetWorld(),Hit.ImpactPoint,CombinedString,nullptr,FColor::Green,2.0f,true);
}
在UE中创建ExplosiveBarrel蓝图类继承自SExplosiveBarrel,并给他分配一个网格体和材质。
打开箱子
1. 创建箱子和UI类
在这节内容,需要实现角色按下键盘E来打开物品箱。首先在UE中创建一个SGameplayInterface类,继承自Unreal接口类,会发现.h文件中生成了两个类:USGameplayInterface和ISGameplayInterface。根据代码注释,第一个类不应该被修改,相关功能需要添加到第二个类中。这个类的作用是作为共享的公共接口,具体实现需要其他类来重写
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class USGameplayInterface : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class ACTIONROGUELIKE_API ISGameplayInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
};
具体的实现方式, 是使用UFUNCTION宏来修饰我们自己编写的Interact函数,使其可以在UE蓝图中使用和编辑。同时设置这个函数的输入,可以传入不同的APawn对象(调用这个函数的主体)来方便我们控制相关动画的显示。相关的UFUNCTION用法有:
BlueprintCallable | 可在蓝图中调用 |
---|---|
BlueprintImplementableEvent | 可在蓝图中实现 |
BlueprintNativeEvent | 蓝图可调用可实现;需要被重写,但也有默认实现 |
//SGameplayInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SGameplayInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class USGameplayInterface : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class ACTIONROGUELIKE_API ISGameplayInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
UFUNCTION(BlueprintCallable,BlueprintNativeEvent)
void Interact(APawn* InstigatorPawn);
};
然后,从AActor和ISGameplayInterface派生一个SItemChest箱子类,并添加两个Mesh控件,分别表示箱子的底座和盖子。因为给Interact()设置了UFUNCTION(BlueprintNativeEvent),在UE中规定了需要使用如下语法来实现(重写?)这个函数。根据官方文档的说明,这种用法很类似C++中多态的实现。
// SItemChest.h
class ACTIONROGUELIKE_API ASItemChest : public AActor,public ISGameplayInterface
{
public:
// UFUNCTION(BlueprintNativeEvent)修饰后必须添加_Implementation
void Interact_Implementation(APawn* InstigatorPawn);
protected:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* BaseMesh;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* LidMesh;
};
// SItemChest.cpp
ASItemChest::ASItemChest()
{
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>("BaseMesh");
RootComponent = BaseMesh;
LidMesh = CreateDefaultSubobject<UStaticMeshComponent>("LidMesh");
LidMesh->SetupAttachment(BaseMesh);
}
2. 创建蓝图类
在UE中创建一个SItemChest的蓝图类箱子,命名为TreasureChest。在课程项目提供的ExampleContent文件夹中有箱子的网格体,将其分别设置给TreasureChest的Base和Lid即可。
然后通过“变换”属性调整一下盖子的位置,使其刚好贴合在底座的上方。
3. 控制箱子打开
我们可以在视口中试验一下,拖拽调整盖子的角度就可以实现箱子的开合效果,调整时可以注意细节面板中“变换” -> “旋转”属性的变化,发现是Pitch在改变。
因此,只要通过改变Pitch变量就可以实现箱子的开合动画。此外,为了更方便的控制打开的角度,在.h中声明了浮点型TargetPitch并使用UPROPEERTY(EditAnywhere)宏修饰,然后在.cpp构造函数中赋初值。
// SItemChest.cpp
void ASItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
LidMesh->SetRelativeRotation(FRotator(TargetPitch,0,0));
}
这个方法实现的动画比较生硬。但目前的重心不在制作动画,后续会使用Tick函数实现更加精确丝滑的动画控制。
4. 控制动画
要实现开箱动画的控制,首先需要绑定按键事件,按下后执行某个函数,这个函数可以判断视线内一定距离内是否有箱子,有的话就将箱子打开。
根据设计模式的相关理论,开发时要尽量降低各个功能模块的耦合性,从而避免后期代码的臃肿冗余。因此在实现这个功能时,就不继续在SCharacter类中编写具体代码,而是创建一个类来专门负责实现这部分的逻辑,然后将其与SCharacter类组合即可。同时课程中也提到,因为所有角色都可以进行攻击,之前实现的攻击的相关代码最好也单独封装提供调用,这在后续会进行优化。
要实现这个功能,可以使用UE中的ActorComponent类。顾名思义,这个类可以像普通的Component一样附加到Actor上。因此派生出SInteractionComponent类,我们需要在其中实现检查周围有哪些物体可以互动,即碰撞查询(collision query),所以在.h中声明PrimaryInteract()来实现这个功能需求。
//SInteractionComponent.h
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ACTIONROGUELIKE_API USInteractionComponent : public UActorComponent
{
GENERATED_BODY()
public:
void PrimaryInteract();
}
然后在SCharacter的两个文件中声明和创建SInteractionComponent的实例,顺便再声明一下将要绑定的按键操作PrimaryInteract。
//SCharacter.h
UCLASS()
class ACTIONROGUELIKE_API ASCharacter : public ACharacter
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere)
USInteractionComponent* InteractionComp;
void PrimaryInteract();
}
//SCharacter.cpp
ASCharacter::ASCharacter()
{
InteractionComp = CreateDefaultSubobject<USInteractionComponent>("InteractionComp");
}
void ASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAction("PrimaryInteract",IE_Pressed,this,&ASCharacter::PrimaryInteract);
}
void ASCharacter::PrimaryInteract()
{
if(InteractionComp)
{
InteractionComp->PrimaryInteract();
}
}
要实现碰撞检测,游戏开发中常用发射射线的方法,即从我们角色的眼镜发出一定长度的射线,当射线碰撞到第一个物体后在函数中返回这个对象。在UE中LineTraceSingleByObjectType()函数可以实现这个功能,其四个参数分别为:检测结果、射线起点、射线终点、检测参数。
//SInteractionComponent.cpp
void USInteractionComponent::PrimaryInteract()
{
FHitResult Hit; // 检测结果
FVector EyeLocation; // 角色眼睛位置
FRotator EyeRotation; // 角色视线方向
AActor* MyOwner = GetOwner(); // 获取控制角色
// 将玩家视线的位置和方向输出到EyeLocation和EyeRotation
MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotation);
// 沿着视线方向,模型的眼睛位置开始1000cm距离的点为终点
FVector End = EyeLocation + (EyeRotation.Vector() * 1000);
FCollisionObjectQueryParams ObjectQueryParams; // 查询参数
ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldDynamic); // 选择查询场景动态对象
GetWorld()->LineTraceSingleByObjectType(Hit, EyeLocation, End, ObjectQueryParams);
}
最后是根据碰撞结果来调用打开箱子的函数,细节已经在注释中说明:
//SInteractionComponent.cpp
// 从判断结果中获取检测到的Actor,没检测到则为空
AActor* HitActor = Hit.GetActor();
if (HitActor) {
// 如果检测到actor不为空,再判断actor有没有实现SurGameplayInterface类
if (HitActor->Implements<USurGameplayInterface>()) {
// 我们定义的Interact()传入为Pawn类型,因此做类型转换
APawn* MyPawn = Cast<APawn>(MyOwner);
// 多态,根据传入的HitActor调用相应函数
// 第一个参数不能为空,所以外层已经判空;第二个参数是我们自定义的,暂时没有影响,可以不判空
ISurGameplayInterface::Execute_Interact(HitActor, MyPawn);
// 用于debug,绘制这条碰撞检测的线,绿色
DrawDebugLine(GetWorld(), EyeLocation, End, FColor::Green, false, 3);
}
}
else{ DrawDebugLine(GetWorld(), EyeLocation, End, FColor::Red, false, 3); }
最后在UE中绑定键盘操作,然后测试代码效果,发现角色已经可以成功打开箱子了
简易开关
1.创建蓝图类
创建一个基于Actor的蓝图类,命名为“LeverBP”(BP表示Blue Prints),作为示例所需的操作杆。然后打开其蓝图编辑器,利用界面左上角绿色的“添加组件”按钮为它添加两个静态网格体。通过鼠标拖拽,将一个网格体变为根组件,这个操作对应C++中的RootComponent = XXX;语句。
然后将父子网格体依次设置为底座和把手,为了后续开发方便,我们可以分别将根控件和子控件重命名为“BaseMesh”和“HandleMesh”。
随后,我们开始实现开关的控制功能。实现的思路很简单,在操作杆前按下E键后将把手朝下,然后打开宝箱即可。在上节实现打开箱子这个功能时,我们通过实现SGamePlayInterface中的Interact函数来实现宝箱打开,因此对于操纵杆这个交互我们同样需要实现这个接口。所以,接下来的操作就是把上一节课中C++代码换成了蓝图操作的版本,大家可以自行将相应步骤进行对照比较。
在LeverBP的蓝图编辑器 -> 最上方的“类设置” -> “已实现的接口” -> 添加SurGamePlayInterface类,此时在界面左侧“我的蓝图”中就会出现Interact接口。右键Interact接口 -> 实现事件,就会在事件图表中创建相应的节点。
2. 改变把手角度
将HandMesh拖拽进事件图表中创建节点,从该节点中拖拽以调用Set Relative Rotation函数,从而改变把手的角度。而要改变的角度就用Make Rotator节点来设置,其值取HandleMesh的Pitch值的相反数既可以
UE的蓝图系统中,白线是执行线,表示从头到尾依次执行节点;而其他的黄色、蓝色、紫色等都是数据线,表示数据的流向、函数输入输出。于是对上述节点进行连接
究其原理,是我们在绑定E键的PrimaryInteract中写了判断射线检测对象是否实现Interact的语句:if (HitActor->Implements())。我们通过在蓝图中实现Interact,使得代码往下运行,并且传入了操纵杆的HitActor以调用蓝图中实现的内容(更改把手角度)。
3. 打开宝箱
在左侧“我的蓝图” -> 变量中新建“SelectedActor”,用于存放需要打开的宝箱对象。在其右侧的细节面板中,将“变量类型”从布尔值更改为Actor(蓝色图标),然后勾选“可编辑实例”,编译保存。这样就可以点击世界中的操纵杆模型,并在其右侧的细节面板中更加方便地设置该变量
设置LeverBP调用的Actor
回到SGameplayInterface.h中,将void Interact(APawn* InstigatorPawn);的宏变为UFUNCTION(BlueprintCallable, BlueprintNativeEvent),保存编译,使该接口可以在蓝图中被调用。
回血点
1.创建基类
//SPowerupActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SGameplayInterface.h"
#include "SPowerupActor.generated.h"
class USphereComponent;
UCLASS()
class ACTIONROGUELIKE_API ASPowerupActor : public AActor, public ISGameplayInterface
{
GENERATED_BODY()
protected:
UPROPERTY(EditAnywhere, Category = "Powerup")
float RespawnTime;
UFUNCTION()
void ShowPowerup();
void HideAndCooldownPowerup();
void SetPowerupState(bool bNewIsActive);
UPROPERTY(VisibleAnywhere, Category = "Components")
USphereComponent* SphereComp;
public:
void Interact_Implementation(APawn* InstigatorPawn) override;
public:
ASPowerupActor();
};
//SPowerupActor.cpp
#include "SPowerupActor.h"
#include "Components/SphereComponent.h"
ASPowerupActor::ASPowerupActor()
{
SphereComp = CreateDefaultSubobject<USphereComponent>("SphereComp");
SphereComp->SetCollisionProfileName("Powerup");
RootComponent = SphereComp;
RespawnTime = 10.0f;
}
void ASPowerupActor::Interact_Implementation(APawn* InstigatorPawn)
{
// logic in derived classes...
}
void ASPowerupActor::ShowPowerup()
{
SetPowerupState(true);
}
void ASPowerupActor::HideAndCooldownPowerup()
{
SetPowerupState(false);
FTimerHandle TimerHandle_RespawnTimer;
GetWorldTimerManager().SetTimer(TimerHandle_RespawnTimer, this, &ASPowerupActor::ShowPowerup, RespawnTime);
}
void ASPowerupActor::SetPowerupState(bool bNewIsActive)
{
SetActorEnableCollision(bNewIsActive);
// Set visibility on root and all children
RootComponent->SetVisibility(bNewIsActive, true);
}
2.新建回血点
//SPowerup_HealthPotion.h
#pragma once
#include "CoreMinimal.h"
#include "SPowerupActor.h"
#include "SPowerup_HealthPotion.generated.h"
class UStaticMeshComponent;
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API ASPowerup_HealthPotion : public ASPowerupActor
{
GENERATED_BODY()
protected:
UPROPERTY(VisibleAnywhere, Category = "Components")
UStaticMeshComponent* MeshComp;
// float healt amount?
public:
void Interact_Implementation(APawn* InstigatorPawn) override;
ASPowerup_HealthPotion();
};
//SPowerup_HealthPotion.cpp
#include "SPowerup_HealthPotion.h"
#include "SAttributeComponent.h"
ASPowerup_HealthPotion::ASPowerup_HealthPotion()
{
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>("MeshComp");
// Disable collision, instead we use SphereComp to handle interaction queries
MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
MeshComp->SetupAttachment(RootComponent);
}
void ASPowerup_HealthPotion::Interact_Implementation(APawn* InstigatorPawn)
{
if (!ensure(InstigatorPawn))
{
return;
}
USAttributeComponent* AttributeComp = Cast<USAttributeComponent>(InstigatorPawn->GetComponentByClass(USAttributeComponent::StaticClass()));
// Check if not already at max health
if (ensure(AttributeComp) && !AttributeComp->IsFullHealth())
{
// Only activate if healed successfully
if (AttributeComp->ApplyHealthChange(AttributeComp->GetHealthMax()))
{
HideAndCooldownPowerup();
}
}
}
重生点
删除之前的PlayerCharacter,创建几个PlayerStart
在GameModeBP中将Default Pawn Class设置为PlayerCharacter
接下来将DefaultGameMode 设置为 GameModeBase
现在我们的出生点都是随机的
改善重生点
//SGameModeBase.h
protected:
UFUNCTION()
void RespawnPlayerElasped(AController* Controller);
public:
virtual void OnActorKilled(AActor* VictimActor,AActor* Killer);
//SGameModeBase.cpp
void ASGameModeBase::RespawnPlayerElasped(AController* Controller)
{
if(ensure(Controller))
{
Controller->UnPossess();
RestartPlayer((Controller));
}
}
void ASGameModeBase::OnActorKilled(AActor* VictimActor, AActor* Killer)
{
ASCharacter* Player = Cast<ASCharacter>(VictimActor);
if (Player)
{
FTimerHandle TimerHandle_RespawnDelay;
FTimerDelegate Delegate;
Delegate.BindUFunction(this, "RespawnPlayerElapsed", Player->GetController());
float RespawnDelay = 2.0f;
GetWorldTimerManager().SetTimer(TimerHandle_RespawnDelay, Delegate, RespawnDelay, false);
}
UE_LOG(LogTemp, Log, TEXT("OnActorKilled: Victim: %s, Killer: %s"), *GetNameSafe(VictimActor), *GetNameSafe(Killer));
}
//SAttributeComponent.cpp
bool USAttributeComponent::ApplyHealthChange(AActor* InstigatorActor,float Delta)
{
//Died
if(Delta <0.0f && Health == 0.0f)
{
ASGameModeBase* GM = GetWorld()->GetAuthGameMode<ASGameModeBase>();
if(GM)
{
GM->OnActorKilled(GetOwner(),InstigatorActor);
}
}
}
点数系统和随机产生药水瓶(两个都失败,以后找找原因)
//SGamemodeBase.h
protected:
// Read/write access as we could change this as our difficulty increases via Blueprint
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "AI")
int32 CreditsPerKill;
UPROPERTY(EditDefaultsOnly, Category = "Powerups")
UEnvQuery* PowerupSpawnQuery;
/* All power-up classes used to spawn with EQS at match start */
UPROPERTY(EditDefaultsOnly, Category = "Powerups")
TArray<TSubclassOf<AActor>> PowerupClasses;
/* Distance required between power-up spawn locations */
UPROPERTY(EditDefaultsOnly, Category = "Powerups")
float RequiredPowerupDistance;
/* Amount of powerups to spawn during match start */
UPROPERTY(EditDefaultsOnly, Category = "Powerups")
int32 DesiredPowerupCount;
UFUNCTION()
void OnBotSpawnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus);
UFUNCTION()
void OnPowerupSpawnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus);
//SGamemodeBase.cpp
ASGameModeBase::ASGameModeBase()
{
SpawnTimerInterval = 2.0f;
CreditsPerKill = 20;
DesiredPowerupCount = 10;
RequiredPowerupDistance = 2000;
PlayerStateClass = ASPlayerState::StaticClass();
}
void ASGameModeBase::StartPlay()
{
Super::StartPlay();
// Continuous timer to spawn in more bots.
// Actual amount of bots and whether its allowed to spawn determined by spawn logic later in the chain...
GetWorldTimerManager().SetTimer(TimerHandle_SpawnBots, this, &ASGameModeBase::SpawnBotTimerElapsed, SpawnTimerInterval, true);
// Make sure we have assigned at least one power-up class
if (ensure(PowerupClasses.Num() > 0))
{
// Run EQS to find potential power-up spawn locations
UEnvQueryInstanceBlueprintWrapper* QueryInstance = UEnvQueryManager::RunEQSQuery(this, PowerupSpawnQuery, this, EEnvQueryRunMode::AllMatching, nullptr);
if (ensure(QueryInstance))
{
QueryInstance->GetOnQueryFinishedEvent().AddDynamic(this, &ASGameModeBase::OnPowerupSpawnQueryCompleted);
}
}
}
void ASGameModeBase::OnPowerupSpawnQueryCompleted(UEnvQueryInstanceBlueprintWrapper* QueryInstance, EEnvQueryStatus::Type QueryStatus)
{
if (QueryStatus != EEnvQueryStatus::Success)
{
UE_LOG(LogTemp, Warning, TEXT("Spawn bot EQS Query Failed!"));
return;
}
TArray<FVector> Locations = QueryInstance->GetResultsAsLocations();
// Keep used locations to easily check distance between points
TArray<FVector> UsedLocations;
int32 SpawnCounter = 0;
// Break out if we reached the desired count or if we have no more potential positions remaining
while (SpawnCounter < DesiredPowerupCount && Locations.Num() > 0)
{
// Pick a random location from remaining points.
int32 RandomLocationIndex = FMath::RandRange(0, Locations.Num() - 1);
FVector PickedLocation = Locations[RandomLocationIndex];
// Remove to avoid picking again
Locations.RemoveAt(RandomLocationIndex);
// Check minimum distance requirement
bool bValidLocation = true;
for (FVector OtherLocation : UsedLocations)
{
float DistanceTo = (PickedLocation - OtherLocation).Size();
if (DistanceTo < RequiredPowerupDistance)
{
// Show skipped locations due to distance
//DrawDebugSphere(GetWorld(), PickedLocation, 50.0f, 20, FColor::Red, false, 10.0f);
// too close, skip to next attempt
bValidLocation = false;
break;
}
}
// Failed the distance test
if (!bValidLocation)
{
continue;
}
// Pick a random powerup-class
int32 RandomClassIndex = FMath::RandRange(0, PowerupClasses.Num() - 1);
TSubclassOf<AActor> RandomPowerupClass = PowerupClasses[RandomClassIndex];
GetWorld()->SpawnActor<AActor>(RandomPowerupClass, PickedLocation, FRotator::ZeroRotator);
// Keep for distance checks
UsedLocations.Add(PickedLocation);
SpawnCounter++;
}
}
void ASGameModeBase::OnActorKilled(AActor* VictimActor, AActor* Killer)
{
UE_LOG(LogTemp, Log, TEXT("OnActorKilled: Victim: %s, Killer: %s"), *GetNameSafe(VictimActor), *GetNameSafe(Killer));
// Respawn Players after delay
ASCharacter* Player = Cast<ASCharacter>(VictimActor);
if (Player)
{
FTimerHandle TimerHandle_RespawnDelay;
FTimerDelegate Delegate;
Delegate.BindUFunction(this, "RespawnPlayerElapsed", Player->GetController());
float RespawnDelay = 2.0f;
GetWorldTimerManager().SetTimer(TimerHandle_RespawnDelay, Delegate, RespawnDelay, false);
}
// Give Credits for kill
APawn* KillerPawn = Cast<APawn>(Killer);
if (KillerPawn)
{
if (ASPlayerState* PS = KillerPawn->GetPlayerState<ASPlayerState>()) // < can cast and check for nullptr within if-statement.
{
PS->AddCredits(CreditsPerKill);
}
}
}
创建两个类
//SPowerup_HealthPotion.h
protected:
UPROPERTY(EditAnywhere, Category = "HealthPotion")
int32 CreditCost;
//SPowerup_HealthPotion.cpp
ASPowerup_HealthPotion::ASPowerup_HealthPotion()
{
CreditCost = 50;
}
void ASPowerup_HealthPotion::Interact_Implementation(APawn* InstigatorPawn)
{
if (!ensure(InstigatorPawn))
{
return;
}
USAttributeComponent* AttributeComp = USAttributeComponent::GetAttributes(InstigatorPawn);
// Check if not already at max health
if (ensure(AttributeComp) && !AttributeComp->IsFullHealth())
{
// Only activate if healed successfully
if (ASPlayerState* PS = InstigatorPawn->GetPlayerState<ASPlayerState>())
{
if (PS->RemoveCredits(CreditCost) && AttributeComp->ApplyHealthChange(this, AttributeComp->GetHealthMax()))
{
// Only activate if healed successfully
HideAndCooldownPowerup();
}
}
}
}
//SPowerup_Credits.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "SPowerupActor.h"
#include "SPowerup_Credits.generated.h"
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API ASPowerup_Credits : public ASPowerupActor
{
GENERATED_BODY()
protected:
UPROPERTY(VisibleAnywhere, Category = "Components")
UStaticMeshComponent* MeshComp;
UPROPERTY(EditAnywhere, Category = "Credits")
int32 CreditsAmount;
public:
void Interact_Implementation(APawn* InstigatorPawn) override;
ASPowerup_Credits();
};
//SPowerup_Credits.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "SPowerup_Credits.h"
#include "SPlayerState.h"
ASPowerup_Credits::ASPowerup_Credits()
{
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>("MeshComp");
// Disable collision, instead we use SphereComp to handle interaction queries
MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
MeshComp->SetupAttachment(RootComponent);
CreditsAmount = 80;
}
void ASPowerup_Credits::Interact_Implementation(APawn* InstigatorPawn)
{
if (!ensure(InstigatorPawn))
{
return;
}
if (ASPlayerState* PS = InstigatorPawn->GetPlayerState<ASPlayerState>())
{
PS->AddCredits(CreditsAmount);
HideAndCooldownPowerup();
}
}
//SPlayerState.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "SPlayerState.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCreditsChanged, ASPlayerState*, PlayerState, int32, NewCredits, int32, Delta);
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API ASPlayerState : public APlayerState
{
GENERATED_BODY()
protected:
int32 Credits;
public:
UFUNCTION(BlueprintCallable, Category = "PlayerState|Credits") // < Category|SubCategory
void AddCredits(int32 Delta);
UFUNCTION(BlueprintCallable, Category = "PlayerState|Credits")
bool RemoveCredits(int32 Delta);
UPROPERTY(BlueprintAssignable, Category = "Events")
FOnCreditsChanged OnCreditsChanged;
};
//SPlayerState.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "SPlayerState.h"
void ASPlayerState::AddCredits(int32 Delta)
{
// Avoid user-error of adding a negative amount or zero
if (!ensure(Delta > 0.0f))
{
return;
}
Credits += Delta;
OnCreditsChanged.Broadcast(this, Credits, Delta);
}
bool ASPlayerState::RemoveCredits(int32 Delta)
{
// Avoid user-error of adding a subtracting negative amount or zero
if (!ensure(Delta > 0.0f))
{
return false;
}
if (Credits < Delta)
{
// Not enough credits available
return false;
}
Credits -= Delta;
OnCreditsChanged.Broadcast(this, Credits, -Delta);
return true;
}
保存和加载游戏
创建一个新的类
//SSaveGame.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "SSaveGame.generated.h"
/**
*
*/
USTRUCT()
struct FActorSaveData
{
GENERATED_BODY()
public:
/* Identifier for which Actor this belongs to */
UPROPERTY()
FString ActorName;
/* For movable Actors, keep location,rotation,scale. */
UPROPERTY()
FTransform Transform;
UPROPERTY()
TArray<uint8> ByteData;
};
UCLASS()
class ACTIONROGUELIKE_API USSaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY()
int32 Credits;
UPROPERTY()
TArray<FActorSaveData> SavedActors;
};
//SSaveGame.cpp
//SGameModeBase.h
protected:
FString SlotName;
UPROPERTY()
USSaveGame* CurrentSaveGame;
public:
void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override;
void HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) override;
UFUNCTION(BlueprintCallable, Category = "SaveGame")
void WriteSaveGame();
void LoadSaveGame();
//SGameModeBase.cpp
ASGameModeBase::ASGameModeBase()
{
SlotName = "SaveGame01";
}
void ASGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
LoadSaveGame();
}
void ASGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
ASPlayerState* PS = NewPlayer->GetPlayerState<ASPlayerState>();
if (PS)
{
PS->LoadPlayerState(CurrentSaveGame);
}
}
void ASGameModeBase::WriteSaveGame()
{
// Iterate all player states, we don't have proper ID to match yet (requires Steam or EOS)
for (int32 i = 0; i < GameState->PlayerArray.Num(); i++)
{
ASPlayerState* PS = Cast<ASPlayerState>(GameState->PlayerArray[i]);
if (PS)
{
PS->SavePlayerState(CurrentSaveGame);
break; // single player only at this point
}
}
CurrentSaveGame->SavedActors.Empty();
// Iterate the entire world of actors
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
// Only interested in our 'gameplay actors'
if (!Actor->Implements<USGameplayInterface>())
{
continue;
}
FActorSaveData ActorData;
ActorData.ActorName = Actor->GetName();
ActorData.Transform = Actor->GetActorTransform();
// Pass the array to fill with data from Actor
FMemoryWriter MemWriter(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MemWriter, true);
// Find only variables with UPROPERTY(SaveGame)
Ar.ArIsSaveGame = true;
// Converts Actor's SaveGame UPROPERTIES into binary array
Actor->Serialize(Ar);
CurrentSaveGame->SavedActors.Add(ActorData);
}
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SlotName, 0);
}
void ASGameModeBase::LoadSaveGame()
{
if (UGameplayStatics::DoesSaveGameExist(SlotName, 0))
{
CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
if (CurrentSaveGame == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("Failed to load SaveGame Data."));
return;
}
UE_LOG(LogTemp, Log, TEXT("Loaded SaveGame Data."));
// Iterate the entire world of actors
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
// Only interested in our 'gameplay actors'
if (!Actor->Implements<USGameplayInterface>())
{
continue;
}
for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
{
if (ActorData.ActorName == Actor->GetName())
{
Actor->SetActorTransform(ActorData.Transform);
FMemoryReader MemReader(ActorData.ByteData);
FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
Ar.ArIsSaveGame = true;
// Convert binary array back into actor's variables
Actor->Serialize(Ar);
ISGameplayInterface::Execute_OnActorLoaded(Actor);
break;
}
}
}
}
else
{
CurrentSaveGame = Cast<USSaveGame>(UGameplayStatics::CreateSaveGameObject(USSaveGame::StaticClass()));
UE_LOG(LogTemp, Log, TEXT("Created New SaveGame Data."));
}
}
//SPlayState.h
public:
UFUNCTION(BlueprintNativeEvent)
void SavePlayerState(USSaveGame* SaveObject);
UFUNCTION(BlueprintNativeEvent)
void LoadPlayerState(USSaveGame* SaveObject);
//SPlayState.cpp
void ASPlayerState::LoadPlayerState_Implementation(USSaveGame* SaveObject)
{
if (SaveObject)
{
Credits = SaveObject->Credits;
}
}
void ASPlayerState::SavePlayerState_Implementation(USSaveGame* SaveObject)
{
if (SaveObject)
{
SaveObject->Credits = Credits;
}
}
//SGameplayInterface.h
public:
UFUNCTION(BlueprintNativeEvent)
void OnActorLoaded();
在PlayControllerBP中设置,当我们按下L键时保存游戏
可以看到我们的游戏被保存了,但是这里并不安全,因为它只有两字节
修改Credits的蓝图,使得Credits在重新载入后数据和保存的相同
//SItemChest.h
public:
void OnActorLoaded_Implementation();
protected:
UPROPERTY(ReplicatedUsing="OnRep_LidOpened", BlueprintReadOnly,SaveGame) // RepNotify
bool bLidOpened;
//SItemChest.cpp
void ASItemChest::OnActorLoaded_Implementation()
{
ISGameplayInterface::OnActorLoaded_Implementation();
OnRep_LidOpened();
}
现在我们如果改变宝箱的位置或者状态,点击保存游戏后,宝箱仍然延续之前的状态
创建以Actor为基类的蓝图
连接蓝图
创建新的变量
拆分之前的功能,创建新的event独立实现
更新我们的蓝图