角色状态的常见机制
创建角色状态设置到UI上
- 在MainPlayer.h中新建血量,最大血量,耐力,最大耐力,金币变量,作为角色的状态
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe Stats")
float Health;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe Stats")
float MaxHealth;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe Stats")
float Stamina;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe Stats")
float MaxStamina;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe Stats")
int Coins;
MaxHealth = 100.f;
Health = MaxHealth;
MaxStamina = 200.f;
Stamina = MaxStamina;
Coins = 0;
- 然后我们需要去血量与耐力的UI蓝图中去实时更新present(这个是采用的比例)
- 思路:因为这些UI都是用在MainPlayer中,我们在蓝图中获取MianPlayer的引用,然后实例化,这样我们每次用到这个UI的时候都会去实例化MainPlayer,然后present绑定个函数用来实时同步present值(状态值除以最大状态值)
- Stamina UI也是如此,运行结果
添加状态更新函数
MainPlayer.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MainPlayer.generated.h"
UCLASS()
class UEGAME_API AMainPlayer : public ACharacter
{
GENERATED_BODY()
public:
AMainPlayer();
UPROPERTY(visibleAnywhere,BlueprintReadOnly)
class USpringArmComponent* SpringArm;
UPROPERTY(visibleAnywhere, BlueprintReadOnly)
class UCameraComponent* FollowCamera;
float BaseTurnRate;
float BaseLookUpRate;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
float Health;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
float MaxHealth;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
float Stamina;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
float MaxStamina;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
int Coins;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
void Jump() override;
void MoveForward(float value);
void MoveRight(float value);
void Turn(float Value);
void LookUp(float Value);
void TurnRate(float Rate);
void LookUpRate(float Rate);
void AddHealth(float value);
void AddStamina(float value);
void AddCoin(float value);
};
MainPlayer.cpp
#include "MainPlayer.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/CharacterMovementComponent.h"
AMainPlayer::AMainPlayer()
{
PrimaryActorTick.bCanEverTick = true;
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(GetRootComponent());
SpringArm->TargetArmLength = 600.f;
SpringArm->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(SpringArm, NAME_None);
FollowCamera->bUsePawnControlRotation = false;
GetCapsuleComponent()->SetCapsuleSize(35.f, 100.f);
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.f, 500.f, 0.f);
GetCharacterMovement()->JumpZVelocity = 600.f;
GetCharacterMovement()->AirControl = 0.15f;
BaseTurnRate = 21.f;
BaseLookUpRate = 21.f;
MaxHealth = 100.f;
Health = MaxHealth;
MaxStamina = 200.f;
Stamina = MaxStamina;
Coins = 0;
}
void AMainPlayer::BeginPlay()
{
Super::BeginPlay();
}
void AMainPlayer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AMainPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
check(PlayerInputComponent);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMainPlayer::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
PlayerInputComponent->BindAxis("MoveForward", this, &AMainPlayer::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AMainPlayer::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &AMainPlayer::Turn);
PlayerInputComponent->BindAxis("LookUp", this, &AMainPlayer::LookUp);
PlayerInputComponent->BindAxis("TurnRate", this, &AMainPlayer::TurnRate);
PlayerInputComponent->BindAxis("LookUpRate", this, &AMainPlayer::LookUpRate);
}
void AMainPlayer::Jump()
{
Super::Jump();
}
void AMainPlayer::MoveForward(float value)
{
if (Controller != nullptr && value != 0.f)
{
FRotator Rotation = Controller->GetControlRotation();
FRotator YowRotation = FRotator(0.0f, Rotation.Yaw, 0.0f);
FVector Direction = FRotationMatrix(YowRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, value);
}
}
void AMainPlayer::MoveRight(float value)
{
if (Controller != nullptr && value != 0.f)
{
FRotator Rotation = Controller->GetControlRotation();
FRotator YowRotation = FRotator(0.0f, Rotation.Yaw, 0.0f);
FVector Direction = FRotationMatrix(YowRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Direction, value);
}
}
void AMainPlayer::Turn(float Value)
{
if (Value != 0.f)
{
AddControllerYawInput(Value);
}
}
void AMainPlayer::LookUp(float Value)
{
if (GetControlRotation().Pitch < 270.f && GetControlRotation().Pitch >180.f && Value > 0.f)
{
return;
}
else if (GetControlRotation().Pitch < 180.f && GetControlRotation().Pitch >45.f && Value < 0.f)
{
return;
}
AddControllerPitchInput(Value);
}
void AMainPlayer::TurnRate(float Rate)
{
float Value = Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds();
if (Value != 0.f)
{
AddControllerYawInput(Value);
}
}
void AMainPlayer::LookUpRate(float Rate)
{
float Value = Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds();
if (GetControlRotation().Pitch < 270.f && GetControlRotation().Pitch >180.f && Value > 0.f)
{
return;
}
else if (GetControlRotation().Pitch < 180.f && GetControlRotation().Pitch >45.f && Value < 0.f)
{
return;
}
AddControllerPitchInput(Value);
}
void AMainPlayer::AddHealth(float value)
{
Health = FMath::Clamp(Health + value, 0.f, MaxHealth);
}
void AMainPlayer::AddStamina(float value)
{
Stamina = FMath::Clamp(Stamina + value, 0.f, MaxStamina);
}
void AMainPlayer::AddCoin(float value)
{
Coins += value;
}
创建可交互物体的基类
- 需求:这个可交互物体,应该可以播放粒子特效,所以我们需要粒子系统,然后有静态网格表示这个物体,作为基类,我们可以把所有的要求都写入里面,然后到时候创建子类继承来重写需求
- 定义一个球形的触发器组件,静态网格,粒子组件与粒子系统,声音资源,进行多播委托事件,虚写多播委托函数,方便到时候子类继承重写
- 可以自定义一下这个物体的旋转和碰撞类别
#include "Components/SphereComponent.h"
:球形碰撞器的头文件
#include "Particles/ParticleSystemComponent.h"
:粒子系统的头文件
InteroperableItem.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InteroperableItem.generated.h"
UCLASS()
class UEGAME_API AInteroperableItem : public AActor
{
GENERATED_BODY()
public:
AInteroperableItem();
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
class USphereComponent* TriggerVolume;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
class UStaticMeshComponent* DisplayMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
class UParticleSystemComponent* ParticleEffectsComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interoperable Item|Particles")
class UParticleSystem* Particle;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interoperable Item|Sounds")
class USoundCue* Sound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interoperable Item|Properties")
bool bRotate;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interoperable Item|Properties")
float RotationRate;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
UFUNCTION()
virtual void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
virtual void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
};
InteroperableItem.cpp
#include "InteroperableItem.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Particles/ParticleSystemComponent.h"
AInteroperableItem::AInteroperableItem()
{
PrimaryActorTick.bCanEverTick = true;
TriggerVolume = CreateDefaultSubobject<USphereComponent>(TEXT("TriggerVolume"));
RootComponent = TriggerVolume;
TriggerVolume->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
TriggerVolume->SetCollisionObjectType(ECollisionChannel::ECC_WorldStatic);
TriggerVolume->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
TriggerVolume->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
DisplayMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("DisplayMesh"));
DisplayMesh->SetupAttachment(GetRootComponent());
ParticleEffectsComponent = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("ParticleEffects"));
ParticleEffectsComponent->SetupAttachment(GetRootComponent());
bRotate = true;
RotationRate = 45.f;
}
void AInteroperableItem::BeginPlay()
{
Super::BeginPlay();
TriggerVolume->OnComponentBeginOverlap.AddDynamic(this, &AInteroperableItem::OnOverlapBegin);
TriggerVolume->OnComponentEndOverlap.AddDynamic(this, &AInteroperableItem::OnOverlapEnd);
}
void AInteroperableItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (bRotate)
{
FRotator rotator = GetActorRotation();
rotator.Yaw += RotationRate * DeltaTime;
SetActorRotation(rotator);
}
}
void AInteroperableItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
}
void AInteroperableItem::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}
创建可交互基类的子类爆炸物
- 创建一个继承自Interoperable类的子类Explosive
- 新建一个伤害值,重写OnOverlapBegin与OnOverlapEnd事件函数,在OnOverlapBegin中判断角色是否触发到炸弹,然后生成发射器播放粒子效果与声音,之后销毁
AExplosiveItem::AExplosiveItem()
{
Damage = 20.f;
}
void AExplosiveItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
Super::OnOverlapBegin(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);
if (OtherActor)
{
AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
if (Player)
{
if (Particle)
{
UGameplayStatics::SpawnEmitterAtLocation(this, Particle, GetActorLocation(), FRotator(0.f), true);
}
if (Sound)
{
UGameplayStatics::PlaySound2D(this, Sound);
}
Destroy();
}
}
}
- 运行结果
爆炸物对玩家的伤害施加
- 我们采用UE中内置的直接伤害附加的功能,在Explosive类中调用
UGameplayStatics::ApplyDamage()
函数传递伤害值,在MainPlayer类中重写TakeDamage
方法,进行接收伤害并对玩家施加
UGameplayStatics::ApplyDamage()
:源码函数分析
- 该函数接收五个参数:
- DamagedActor: 受损的 actor。
- BaseDamage: 基础损害值。
- EventInstigator: 事件发起者,通常是施加损害的对象。
- DamageCauser: 直接导致损害的 actor。
- DamageTypeClass: 损害类型的类。
- 该函数首先检查
DamagedActor
是否存在以及 BaseDamage
是否不为零。如果是这样,则继续执行;否则,返回零表示未造成任何损害。 接下来,设置损害事件 (FDamageEvent)
并传入有效的损害类型类 (TSubclassOf 类型)
。如果提供了 DamageTypeClass
参数,则使用该类;否则,默认使用 UDamageType
类。 最后,调用 DamagedActor 的 TakeDamage
方法,将基础损害值(BaseDamage)
和损害事件 (DamageEvent)
传入,并返回造成的总损害。 整个函数最终实现的功能是将基础损害值施加到指定的受损actor
上,并返回造成的总损害。
float UGameplayStatics::ApplyDamage(AActor* DamagedActor, float BaseDamage, AController* EventInstigator, AActor* DamageCauser, TSubclassOf<UDamageType> DamageTypeClass)
{
if ( DamagedActor && (BaseDamage != 0.f) )
{
TSubclassOf<UDamageType> const ValidDamageTypeClass = DamageTypeClass ? DamageTypeClass : TSubclassOf<UDamageType>(UDamageType::StaticClass());
FDamageEvent DamageEvent(ValidDamageTypeClass);
return DamagedActor->TakeDamage(BaseDamage, DamageEvent, EventInstigator, DamageCauser);
}
return 0.f;
}
- TakeDamage():源码分析
- 该方法接收四个参数:
- Damage: 损害值。
- DamageEvent: 损害事件。
- EventInstigator: 事件发起者。
- DamageCauser: 直接导致损害的 actor。
- 方法首先调用
ShouldTakeDamage
成员方法以确定 pawn
是否应该受到损害。如果返回 false
,则方法直接返回零表示未造成任何损害。 然后,方法调用基类 Super::TakeDamage
方法来处理损害,并将传入的所有参数都传递给它。在此过程中,损害值会被修改以反映抗性等因素的影响。 最后,方法响应所受损害。如果 event instigator
存在并且与 pawn
的控制器不同,则更新最后一个击中者为event instigator
。此外,无论是否存在event instigator 或 event instigator
是否与pawn
控制器相同,都会进行进一步处理,以确保 pawn
在遭受损害后能够做出相应的反应(如播放动画或发出声音)。
float APawn::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
if (!ShouldTakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser))
{
return 0.f;
}
const float ActualDamage = Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);
if (ActualDamage != 0.f)
{
if ( EventInstigator && EventInstigator != Controller )
{
LastHitBy = EventInstigator;
}
}
return ActualDamage;
}
void AExplosiveItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
Super::OnOverlapBegin(OverlappedComponent, OtherActor, OtherComp, OtherBodyIndex, bFromSweep, SweepResult);
if (OtherActor)
{
AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
if (Player)
{
if (Particle)
{
UGameplayStatics::SpawnEmitterAtLocation(this, Particle, GetActorLocation(), FRotator(0.f), true);
}
if (Sound)
{
UGameplayStatics::PlaySound2D(this, Sound);
}
UGameplayStatics::ApplyDamage(OtherActor, Damage, nullptr, this, DamageTypeClass);
Destroy();
}
}
}
float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
float AMainPlayer::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
if (Health - Damage <= 0.f)
{
Health = FMath::Clamp(Health - Damage, 0.f, MaxHealth);
}
else
{
Health -= Damage;
}
return Health;
}
- 在蓝图中选择伤害类型为直接伤害
创建可拾取道具的基类(基础自InteroperableItem)
- 基本与Explosive子类差不多,只不过可拾取的基类目前只需要碰撞检测与一个提供拾取的共有接口OnPick(蓝图化),这样到时候需求就可以去蓝图中可视化操作
PickItem.h
#pragma once
#include "CoreMinimal.h"
#include "GamePlay/InteroperableItem.h"
#include "PickItem.generated.h"
UCLASS()
class UEGAME_API APickItem : public AInteroperableItem
{
GENERATED_BODY()
public:
APickItem();
public:
void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;
void OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) override;
UFUNCTION(BlueprintImplementEvent, Category = "Pick")
void OnPick(class AMainPlayer* Palyer);
};
PickItem.cpp
#include "PickItem.h"
#include "Kismet/GamePlayStatics.h"
#include "Characters/Player/MainPlayer.h"
#include "Sound/SoundCue.h"
APickItem::APickItem()
{
}
void APickItem::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor)
{
AMainPlayer* Player = Cast<AMainPlayer>(OtherActor);
if (Player)
{
UGameplayStatics::SpawnEmitterAtLocation(this, Particle, GetActorLocation(), FRotator(0.f), true);
}
if (Sound)
{
UGameplayStatics::PlaySound2D(this, Sound);
}
OnPick(Player);
Destroy();
}
}
void APickItem::OnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
}
创建可拾取道具基类的子类(Blueprint)
- 创建PickItem蓝图进行MainPlayer的函数调用即可
- 注意将之前MainPlayer中AddHealth这些函数加上反射
- MainPlayer.h中的
UFUNCTION(BlueprintCallable,Category="Player|State")
void AddHealth(float value);
UFUNCTION(BlueprintCallable, Category = "Player|State")
void AddStamina(float value);
UFUNCTION(BlueprintCallable, Category = "Player|State")
void AddCoin(float value);
- 蓝图调用C++编写好的逻辑即可,UI会实时检测更新
- 运行结果
冲刺行为需求
- 思路1:当耐力值到达一个精疲力尽的状态时,就停止冲刺,所以我们需要在混合空间1D里面将Axis Setting最大速度值从600上升到900,添加9个关键帧将冲刺添加上去
- 思路2:新建四个变量来标识状态,耐力消耗速率,耐力的精疲力尽的状态,奔跑速度与冲刺速度
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
float Health;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
float MaxHealth;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
float Stamina;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
float MaxStamina;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Playe State")
float StaminaConsumeRate;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Playe State", meta = (ClampMin = 0, ClampMax = 1))
float ExhaustedStamina;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
int Coins;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player State")
float RunningSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player State")
float SprintSpeed;
MaxHealth = 100.f;
Health = MaxHealth;
MaxStamina = 200.f;
Stamina = MaxStamina;
StaminaConsumeRate = 20.f;
ExhaustedStamina = 0.167f;
Coins = 0;
RunningSpeed = 600.f;
SprintSpeed = 900.f;
创建枚举类型进行标识
- 创建枚举用来标识角色的状态
- 枚举的声明规则
UENUM(BlueprintType)
:幻引擎中的一个宏,用于定义带有蓝图支持的枚举类型,使用此宏定义枚举时,它将在蓝图中可用
UMETA(DisplayName = "Normal")
:是一个宏,用于设置枚举值的显示名称为 “Normal”。这是为了使枚举值在蓝图或编辑器界面中更加可读和直观。
UENUM(BlueprintType)
enum class 类名 : 变量类型
{
变量 UMETA(DisplayName = "你调用时想要显示的名字"),
....
变量 UMETA(DisplayName = "你调用时想要显示的名字")
}
UENUM(BlueprintType)
enum class EPlayerMovementStatus :uint8
{
EPMS_Normal UMETA(DisplayName = "Normal"),
EPMS_Sprinting UMETA(DisplayName = "Sprinting"),
EPMS_Dead UMETA(DisplayName = "Dead")
};
UENUM(BlueprintType)
enum class EPlayerStaminaStatus :uint8
{
EPSS_Normal UMETA(DisplayName = "Normal"),
EPSS_Exhausted UMETA(DisplayName = "Exhausted"),
EPSS_ExhaustedRecovering UMETA(DisplayName = "ExhaustedRecovering")
};
UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Player State")
EPlayerMovementStatus MovementStatus;
UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Player State")
EPlayerStaminaStatus StaminaStatus;
MovementStatus = EPlayerMovementStatus::EPMS_Normal;
StaminaStatus = EPlayerStaminaStatus::EPSS_Normal;
使用标识位检测shift状态
- 新建一个bool变量来监测shift是否按下的状态,新建两个函数用来绑定shift事件映射状态用来判断是否按下shift状态。
- MainPlayer.h
bool bLeftShiftDown;
FORCEINLINE void LeftShiftDown() { bLeftShiftDown = true; }
FORCEINLINE void LeftShiftUp() { bLeftShiftDown = false; }
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMainPlayer::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &AMainPlayer::LeftShiftDown);
PlayerInputComponent->BindAction("Sprint", IE_Released, this, &AMainPlayer::LeftShiftUp);
冲刺逻辑编写
- 思路划分:
- 一、首先我们新建一个设置
移动枚举状态的函数
处理冲刺与不冲刺的时候character
的速度,用于到时候逻辑编写时的状态更换
- 二、然后我们在
Tick
中来设计耐力枚举切换的逻辑
- 三、在
EPSS_Normal
中判断是否按下shift
键,然后判断是否进入了精疲力尽区,逻辑(总耐力 -
耐力消耗值 *
DeltaTime<=
最大耐力值 *
耐力消耗速率),这个逻辑就是每帧判断当前的耐力是否到达的精疲力尽区,到达了精疲力尽区首先改变StaminaStatus状态为Exhausted
,没有进入精疲力尽区Stamina要减去每帧的耐力消耗值,这是必定的
,然后移动状态转为Sprint
,判断没有进入精疲力尽区,那就恢复Stamina值(可以与耐力消耗值一样),然后把移动状态恢复成Normal状态
- 四、在
EPSS_Exhausted
中判断是否还按着shift
键,然后判断是否耐力已经到0了
,如果耐力已经为0,那么我们需要内部编码把shift抬起
,此时StaminaStatus状态转换为ExhaustedRecovering状态
。然后设置移动状态为Normal
,如果耐力没有为0,就直接减去当前帧消耗的耐力,没有按着shift键
,那就直接将StaminaStatus状态转换为ExhaustedRecovering状态
,恢复Stamina值,然后把移动状态恢复成Normal状态
- 五、在
EPSS_ExhaustedRecovering
中判断当Stamina
值已经大于精疲力尽区时,逻辑((总耐力 +
耐力消耗值 *
DeltaTime>=
最大耐力值 *
耐力消耗速率),将StaminaStatus
设置为Normal
状态,然后Stamina要加上每帧的耐力消耗值,这是必定的
,然后抬起shift与移动状态设置为Normal。
void SetMovementStatus(EPlayerMovementStatus Status);
void AMainPlayer::SetMovementStatus(EPlayerMovementStatus Status)
{
MovementStatus = Status;
switch (MovementStatus)
{
case EPlayerMovementStatus::EPMS_Sprinting:
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
break;
default:
GetCharacterMovement()->MaxWalkSpeed = RunningSpeed;
break;
}
}
void AMainPlayer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
switch (StaminaStatus)
{
case EPlayerStaminaStatus::EPSS_Normal:
if (bLeftShiftDown)
{
if (Stamina - StaminaConsumeRate * DeltaTime <= MaxStamina * ExhaustedStamina)
{
StaminaStatus = EPlayerStaminaStatus::EPSS_Exhausted;
}
Stamina -= StaminaConsumeRate * DeltaTime;
SetMovementStatus(EPlayerMovementStatus::EPMS_Sprinting);
}
else
{
Stamina = FMath::Clamp(Stamina + StaminaConsumeRate * DeltaTime, 0.f, MaxStamina);
SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
}
break;
case EPlayerStaminaStatus::EPSS_Exhausted:
if (bLeftShiftDown)
{
if (Stamina - StaminaConsumeRate * DeltaTime <= 0.f)
{
LeftShiftUp();
StaminaStatus = EPlayerStaminaStatus::EPSS_ExhaustedRecovering;
SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
}
else
{
Stamina -= StaminaConsumeRate * DeltaTime;
}
}
else
{
StaminaStatus = EPlayerStaminaStatus::EPSS_ExhaustedRecovering;
Stamina = FMath::Clamp(Stamina + StaminaConsumeRate * DeltaTime, 0.f, MaxStamina);
SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
}
break;
case EPlayerStaminaStatus::EPSS_ExhaustedRecovering:
if (Stamina + StaminaConsumeRate * DeltaTime >= MaxStamina * ExhaustedStamina)
{
StaminaStatus = EPlayerStaminaStatus::EPSS_Normal;
}
Stamina += StaminaConsumeRate * DeltaTime;
LeftShiftUp();
SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
break;
default:
break;
}
}
耐力消耗殆尽的变色UI
- 添加一个Appearance的绑定,在蓝图中将编写的Enum进行判断状态然后切换相应颜色
- 运行结果
冲刺转弯时角色抖动问题
- 我们调整一下动画蓝图即可,设置一个区间,比如在0-100走路,500-600奔跑,700-900冲刺,这样设置区别可以避免插值
MainPlayer.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MainPlayer.generated.h"
UENUM(BlueprintType)
enum class EPlayerMovementStatus :uint8
{
EPMS_Normal UMETA(DisplayName = "Normal"),
EPMS_Sprinting UMETA(DisplayName = "Sprinting"),
EPMS_Dead UMETA(DisplayName = "Dead")
};
UENUM(BlueprintType)
enum class EPlayerStaminaStatus :uint8
{
EPSS_Normal UMETA(DisplayName = "Normal"),
EPSS_Exhausted UMETA(DisplayName = "Exhausted"),
EPSS_ExhaustedRecovering UMETA(DisplayName = "ExhaustedRecovering")
};
UCLASS()
class UEGAME_API AMainPlayer : public ACharacter
{
GENERATED_BODY()
public:
AMainPlayer();
UPROPERTY(visibleAnywhere,BlueprintReadOnly)
class USpringArmComponent* SpringArm;
UPROPERTY(visibleAnywhere, BlueprintReadOnly)
class UCameraComponent* FollowCamera;
float BaseTurnRate;
float BaseLookUpRate;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
float Health;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
float MaxHealth;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Playe State")
float Stamina;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
float MaxStamina;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Playe State")
float StaminaConsumeRate;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Playe State", meta = (ClampMin = 0, ClampMax = 1))
float ExhaustedStamina;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playe State")
int Coins;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player State")
float RunningSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player State")
float SprintSpeed;
UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Player State")
EPlayerMovementStatus MovementStatus;
UPROPERTY(VisibleAnywhere,BlueprintReadWrite,Category="Player State")
EPlayerStaminaStatus StaminaStatus;
bool bLeftShiftDown;
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
void Jump() override;
void MoveForward(float value);
void MoveRight(float value);
void Turn(float Value);
void LookUp(float Value);
void TurnRate(float Rate);
void LookUpRate(float Rate);
UFUNCTION(BlueprintCallable,Category="Player|State")
void AddHealth(float value);
UFUNCTION(BlueprintCallable, Category = "Player|State")
void AddStamina(float value);
UFUNCTION(BlueprintCallable, Category = "Player|State")
void AddCoin(float value);
float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
FORCEINLINE void LeftShiftDown() { bLeftShiftDown = true; }
FORCEINLINE void LeftShiftUp() { bLeftShiftDown = false; }
void SetMovementStatus(EPlayerMovementStatus Status);
};
MainPlayer.cpp
#include "MainPlayer.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/CharacterMovementComponent.h"
AMainPlayer::AMainPlayer()
{
PrimaryActorTick.bCanEverTick = true;
SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArm->SetupAttachment(GetRootComponent());
SpringArm->TargetArmLength = 600.f;
SpringArm->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(SpringArm, NAME_None);
FollowCamera->bUsePawnControlRotation = false;
GetCapsuleComponent()->SetCapsuleSize(35.f, 100.f);
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.f, 500.f, 0.f);
GetCharacterMovement()->JumpZVelocity = 600.f;
GetCharacterMovement()->AirControl = 0.15f;
BaseTurnRate = 21.f;
BaseLookUpRate = 21.f;
MaxHealth = 100.f;
Health = MaxHealth;
MaxStamina = 200.f;
Stamina = MaxStamina;
StaminaConsumeRate = 20.f;
ExhaustedStamina = 0.167f;
Coins = 0;
RunningSpeed = 600.f;
SprintSpeed = 900.f;
MovementStatus = EPlayerMovementStatus::EPMS_Normal;
StaminaStatus = EPlayerStaminaStatus::EPSS_Normal;
bLeftShiftDown = false;
}
void AMainPlayer::BeginPlay()
{
Super::BeginPlay();
}
void AMainPlayer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
switch (StaminaStatus)
{
case EPlayerStaminaStatus::EPSS_Normal:
if (bLeftShiftDown)
{
if (Stamina - StaminaConsumeRate * DeltaTime <= MaxStamina * ExhaustedStamina)
{
StaminaStatus = EPlayerStaminaStatus::EPSS_Exhausted;
}
Stamina -= StaminaConsumeRate * DeltaTime;
SetMovementStatus(EPlayerMovementStatus::EPMS_Sprinting);
}
else
{
Stamina = FMath::Clamp(Stamina + StaminaConsumeRate * DeltaTime, 0.f, MaxStamina);
SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
}
break;
case EPlayerStaminaStatus::EPSS_Exhausted:
if (bLeftShiftDown)
{
if (Stamina - StaminaConsumeRate * DeltaTime <= 0.f)
{
LeftShiftUp();
StaminaStatus = EPlayerStaminaStatus::EPSS_ExhaustedRecovering;
SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
}
else
{
Stamina -= StaminaConsumeRate * DeltaTime;
}
}
else
{
StaminaStatus = EPlayerStaminaStatus::EPSS_ExhaustedRecovering;
Stamina = FMath::Clamp(Stamina + StaminaConsumeRate * DeltaTime, 0.f, MaxStamina);
SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
}
break;
case EPlayerStaminaStatus::EPSS_ExhaustedRecovering:
if (Stamina + StaminaConsumeRate * DeltaTime >= MaxStamina * ExhaustedStamina)
{
StaminaStatus = EPlayerStaminaStatus::EPSS_Normal;
}
Stamina += StaminaConsumeRate * DeltaTime;
LeftShiftUp();
SetMovementStatus(EPlayerMovementStatus::EPMS_Normal);
break;
default:
break;
}
}
void AMainPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
check(PlayerInputComponent);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMainPlayer::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &AMainPlayer::LeftShiftDown);
PlayerInputComponent->BindAction("Sprint", IE_Released, this, &AMainPlayer::LeftShiftUp);
PlayerInputComponent->BindAxis("MoveForward", this, &AMainPlayer::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AMainPlayer::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &AMainPlayer::Turn);
PlayerInputComponent->BindAxis("LookUp", this, &AMainPlayer::LookUp);
PlayerInputComponent->BindAxis("TurnRate", this, &AMainPlayer::TurnRate);
PlayerInputComponent->BindAxis("LookUpRate", this, &AMainPlayer::LookUpRate);
}
void AMainPlayer::Jump()
{
Super::Jump();
}
void AMainPlayer::MoveForward(float value)
{
if (Controller != nullptr && value != 0.f)
{
FRotator Rotation = Controller->GetControlRotation();
FRotator YowRotation = FRotator(0.0f, Rotation.Yaw, 0.0f);
FVector Direction = FRotationMatrix(YowRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, value);
}
}
void AMainPlayer::MoveRight(float value)
{
if (Controller != nullptr && value != 0.f)
{
FRotator Rotation = Controller->GetControlRotation();
FRotator YowRotation = FRotator(0.0f, Rotation.Yaw, 0.0f);
FVector Direction = FRotationMatrix(YowRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Direction, value);
}
}
void AMainPlayer::Turn(float Value)
{
if (Value != 0.f)
{
AddControllerYawInput(Value);
}
}
void AMainPlayer::LookUp(float Value)
{
if (GetControlRotation().Pitch < 270.f && GetControlRotation().Pitch >180.f && Value > 0.f)
{
return;
}
else if (GetControlRotation().Pitch < 180.f && GetControlRotation().Pitch >45.f && Value < 0.f)
{
return;
}
AddControllerPitchInput(Value);
}
void AMainPlayer::TurnRate(float Rate)
{
float Value = Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds();
if (Value != 0.f)
{
AddControllerYawInput(Value);
}
}
void AMainPlayer::LookUpRate(float Rate)
{
float Value = Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds();
if (GetControlRotation().Pitch < 270.f && GetControlRotation().Pitch >180.f && Value > 0.f)
{
return;
}
else if (GetControlRotation().Pitch < 180.f && GetControlRotation().Pitch >45.f && Value < 0.f)
{
return;
}
AddControllerPitchInput(Value);
}
void AMainPlayer::AddHealth(float value)
{
Health = FMath::Clamp(Health + value, 0.f, MaxHealth);
}
void AMainPlayer::AddStamina(float value)
{
Stamina = FMath::Clamp(Stamina + value, 0.f, MaxStamina);
}
void AMainPlayer::AddCoin(float value)
{
Coins += value;
}
float AMainPlayer::TakeDamage(float Damage, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
if (Health - Damage <= 0.f)
{
Health = FMath::Clamp(Health - Damage, 0.f, MaxHealth);
}
else
{
Health -= Damage;
}
return Health;
}
void AMainPlayer::SetMovementStatus(EPlayerMovementStatus Status)
{
MovementStatus = Status;
switch (MovementStatus)
{
case EPlayerMovementStatus::EPMS_Sprinting:
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
break;
default:
GetCharacterMovement()->MaxWalkSpeed = RunningSpeed;
break;
}
}