斯坦福 UE4 C++ ActionRoguelike游戏实例教程 11.认识GAS & 创建自己的能力系统

发布时间 2023-04-17 16:56:24作者: 仇白

斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论

概述

本篇文章对应Lecture 16 - Writing our own Gameplay Ability System alternative,63~65节。

在这篇文章中,将会带你简单认识一下GAS(Game Ability System)的概念,但是我们并不打算深入学习UE中的GAS,我们准备创建自己的“Action System”,作为最轻量级的GAS系统,来更好的理解这一理念。

本篇文章涉及到较多的代码重构,文末我会把相关的代码都贴出来作为参考。

目录

  1. GAS的概念
  2. 创建自己的技能系统框架
    • SurAction.h(UObject)
    • SurActionComponent.h(ActorComponent)
  3. 添加技能(SurAction)
    • Springting冲刺
    • 重构魔法子弹技能(魔法子弹、黑洞、瞬移球)

GAS的概念

在前面的课程学习中,我们已经让角色拥有了三个不同的技能,分别是发射魔发子弹,发射黑洞和发射传送子弹,使用这些技能的逻辑代码统统写在了我们角色的Character类中。这看起来很自然,不是吗?

然而,当我们的游戏越来越丰富的时候,我们想添加更丰富的功能,例如为角色添加冲刺的技能,为自己上BUFF的技能;又或者,我们想让敌对小兵也拥有发射黑洞的技能;再者,我们设计了几十种不同的BUFF效果,比如火属性异常、中毒、爆破、减气等等,难道我们需要在Character类里面预先定义这些效果,等到要触发的时候再通过一个bool变量之类的来触发吗?也许编码起来并不困难,但是一旦涉及到维护,这将会是一场灾难。

也许,我们可以把这些效果当成是一种插件一样的东西,在我们需要这些效果的时候,动态地添加到我们的角色身上。就像荒野之息里初始台地的四个神殿,通关后才为林克分别解锁了四个不同的技能,而使用GAS,就可以轻松的做到这一点。

GAS是什么?课程里是这样介绍他的:

  • 虚幻引擎的针对abilities、buffs、attributes的框架(和Gameplay框架完全分开,所以和GameMode、PlayerStates、playercontroller这些类无关)

  • 强大的RPG游戏功能集、适配广泛的游戏类型(对包含少量actions/abilities的小型游戏可能有些不必要)

  • 陡峭的学习曲线,没有大量的文档

GAS有什么好处呢?最主要的就是可以实现代码分离:

  • 避免单个类包含所有的关于ability、VFX的代码

  • 一种用途的类更易维护(例如课程后面实现的Action_MagicProjectile, Action_Sprint)

  • 更好的协作,尤其是针对蓝图(版本控制中无法合并二进制的资产)

还有其他好处例如优化游戏运行效率、更好的灵活性等等,大家等到真正使用上了GAS,并定会有更深的感悟。

听说随便插一张图也会使文章变得不那么枯燥

课程的老师给出了一些GAS的学习资源,主要是官方文档、github.com/tranek/GASDocumentation(比官方文档详细)

但是这节课我们不用GAS,要把这东西学明白,甚至可以单独拿出来出一套教程。以理解和学习作为目的,我们要做的就是创建一个属于自己的GAS,在课程里我们称为Action Component/Action System。让我们边做边说。

创建自己的“Action System”

SurAction

接下来统称我上述说的那些BUFF、技能等概念为”能力“,对应到代码中就是Action

是的,为了将各种能力从角色的代码中独立出来,我们需要对这些能力做一个抽象。让我们打开编辑器,继承UObject类,创建一个所有能力的基类,我将其命名为SurAction

之所以继承UObject类,是因为在UE的框架中,UObject可以说是UE框架的基础,UE在C++的基础上为其实现了很多神奇的特性,例如反射、垃圾回收等,继承UObject可以说是一脉相承UE的传统。因为能力这种东西不像Actor或者component一样在游戏中实时起着作用,我们让Action类直接继承UObject即可,正所谓大道至简。

作为所有能力的基类,我们目前需要为其定义三个成员:

  1. ActionName,能力的名字,用于之后的标识和查找
  2. StartAction函数,使能力生效,另外,我们还需要知道是谁调用了这个能力,所以函数参数需要有一个调用者
  3. StopAction函数,使能力失效

很好理解,所有的能力都需要这些特性。这三个成员就组成了最基础的能力基类。具体代码实现如下:

//SurAction.h
// UObject要想在蓝图中被继承必须加上Blueprintable宏
UCLASS(Blueprintable)
class FPSPROJECT_API USurAction : public UObject
{
   GENERATED_BODY()
public:
   UPROPERTY(EditDefaultsOnly, Category = "Action")
   FName ActionName; //FName在UE中通常用于标识和查找

   UFUNCTION(BlueprintNativeEvent,Category = "Action")
   void StartAction(AActor* Instigator);
   
   UFUNCTION(BlueprintNativeEvent,Category = "Action")
   void StopAction(AActor* Instigator);
};


//SurAction.cpp
void USurAction::StartAction_Implementation(AActor* Instigator)
{
	UE_LOG(LogTemp, Log, TEXT("Running: %s"), *GetNameSafe(this));
}

void USurAction::StopAction_Implementation(AActor* Instigator)
{
	UE_LOG(LogTemp, Log, TEXT("Stoping: %s"), *GetNameSafe(this));
}

这部分代码有几个需要注意的点:

  1. 我们需要用Blueprintable描述符来描述我们的SurAction类。在UE编辑器中,直接继承于UObject的类是不能派生蓝图类的。
  2. StartAction和StopAction使用了BlueprintNativeEvent描述符,意思为该函数可以在蓝图类中被重写,同时它拥有一个默认实现,在代码中实现时需要加上_Implementation后缀。
  3. ActionName的类型是FName。在UE中字符串通常有FName,FText,FString三种表示。FString最接近于Std::String ,允许对字符串进行编辑,通常性能也是最差的;FText通常用于在游戏或编辑器中显示字符串,拥有本地化的特性,至少在显示和调试上效率是比较高的;FName则用于标识编辑器内的各种资源,之前用到的骨骼名部位名什么的都是FName,内部使用hash实现,查找和比较的效率最高。因此这里使用FName是最合适的。

SurActionComponent

有了能力,就必须要有使用能力的地方。因为不只有我们玩家角色需要使用能力,敌对AI,多人游戏中的其他玩家,甚至路边一块小石头都可以拥有能力。所以要将使用能力这个功能抽象出来,我们可以定义一个ActorComponent子类,将其作加入Actor组件大家庭,即插即用。这里,我将其命名为SurActionComponent

作为使用能力的组件,我想让他拥有以下几个属性:

  1. Actions(TArray), 一个数组,作为能力的容器。每个SurActionComponent都需要记住它拥有了那些能力
  2. AddAction函数,顾名思义,为这个组件添加新的能力
  3. StartActionByName函数。顾名思义,启动这个能力。由于组件中保存了多个能力,因此我们需要按名查找我们需要的能力
  4. StopActionByName,和上面的意义,停止这个能力

以下是源码:

//USurActionComponent.h

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class FPSPROJECT_API USurActionComponent : public UActorComponent
{
   GENERATED_BODY()

public:    
   USurActionComponent();
//因为能力有时候是其他Actor赋予的,所以这里的权限是Public
   UFUNCTION(BlueprintCallable, Category="Action")
   void AddAction(TSubclassOf<USurAction> ActionClass);

   UFUNCTION(BlueprintCallable, Category="Action")
   bool StartActionByName(AActor* Instigator, FName ActionName);

   UFUNCTION(BlueprintCallable, Category="Action")
   bool StopActionByName(AActor* Instigator, FName ActionName);


protected:
   virtual void BeginPlay() override;
   UPROPERTY()
   TArray<USurAction*> Actions;

public:    
   virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
};


//USurActionComponent.cpp
void USurActionComponent::AddAction(TSubclassOf<USurAction> ActionClass)
{
	if(!ensure(ActionClass))
	{
		return;
	}
	USurAction* NewAction = NewObject<USurAction>(this, ActionClass);
	if(ensure(NewAction))
	{
		Actions.Add(NewAction);
	}
}

bool USurActionComponent::StartActionByName(AActor* Instigator, FName ActionName)
{
	for(USurAction* Action:Actions)
	{
		if(Action && Action->ActionName == ActionName)
		{
			Action->StartAction(Instigator);
			return true;
		}
	}
	return false;
}

bool USurActionComponent::StopActionByName(AActor* Instigator, FName ActionName)
{
	for(USurAction* Action:Actions)
	{
		if(Action && Action->ActionName == ActionName)
		{
			Action->StopAction(Instigator);
			return true;
		}
	}
	return false;
}

同样有一个值得注意的点:

在代码中我使用了NewObject函数来创建一个UObject对象。在UE中,UObject对象总是比较特殊的,它需要传入一个Outer进去,目前可以理解为用来标识是谁创建了这个UObject,这在后面可以用来确定UObject处于哪个World。

到这里为止,一个丐版的能力系统基本框架就搭好了。我们定义了能力的基类,还定义了能够使用这些能力的组件,现在就可以着手添加我们想要的能力了。当然,这个框架还有很大的优化空间,后面的课程还会不断地改进这个框架。

添加技能

那么现在就开始为我们的能力系统添加新东西吧!在这篇文章中,我们要为角色设计四个技能,分别是疾跑(提高移动速度),还有之前实现过的三种魔法子弹的发射。如果不清楚魔发子弹是怎么实现的,可以查看之前的课程以及作业,这里贴出其他同学做的笔记:

实现黑洞子弹https://www.bilibili.com/read/cv19278591?spm_id_from=333.999.0.0

实现传送子弹https://www.bilibili.com/read/cv19371607?spm_id_from=333.999.0.0

不过,之前都是以“硬编码”的方式讲这些技能写在角色类中,这在我们今天实现的能力系统中都是要进行重构的,让我们最后看一眼原来是什么样子的:

image-20230412182128752

把技能相关的代码塞在角色类中就显得非常繁杂

在之前的实现中,我们把技能的各种属性(子弹类型、动画、开火延迟等)都写在了角色类中,这并不符合权责分离的思想。幸好,我们马上就要和这些代码告别了。

为角色添加ActionComponent

第一步就是为我们的Character添加ActionComponent,就像以前做过的那样,这里就不细说了。

UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
USurActionComponent* ActionComp;
	

ActionComp = CreateDefaultSubobject<USurActionComponent>("ActionComponent");

冲刺能力 sprint

我们要实现的第一个能力就是Sprint冲刺,目标是在按下左shift时,提高角色的移动速度。首先要记得绑定按键。

image-20230412200228031

绑定按键

接下来自然就是在角色类中定义如何使用冲刺技能了。在以往的做法中,我们可能会直接在 SprintStart函数中直接修改角色的移动速度,但是如今引进了能力系统,我们所有使用技能的代码逻辑在角色类中,也只剩下一个StartActionByName了。

//MyCharacter

//.h
//冲刺技能相关,用于按键绑定
void SprintStart();
void SprintStop();


//.cpp
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	........
	//冲刺相关
	PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &AMyCharacter::SprintStart);
	PlayerInputComponent->BindAction("Sprint", IE_Released, this, &AMyCharacter::SprintStop);
	........
}

void AMyCharacter::SprintStart()
{
    //硬编码,必要的时候你也可以将技能名暴露给UE编辑器
   ActionComp->StartActionByName(this, "Sprint");
}

void AMyCharacter::SprintStop()
{
   ActionComp->StopActionByName(this, "Sprint");
}

然后是设计具体的冲刺技能,因为这个功能很简单,这里直接继承SurAction创建一个蓝图类,我们在蓝图中实现。

image-20230412200839373

创建蓝图类

每个技能都得记得修改ActionName。

image-20230412200815161

记得修改ActionName

还记得在SurAction里定义的StartAction吗?我们对它的描述为BlueprintNativeEvent,可以在蓝图中对其进行重载,我们每一个新能力都需要对这个函数进行重载。在左侧的函数栏上,可以找到重载的按钮。

要注意的是,重载的时候就不会调用父类原本的定义了。如果想要先调用父类的逻辑,可以右键节点,选择”将调用添加到父项函数“。虽然父类函数没有写什么东西,但是起码还是有一句LOG的:

image-20230412201523297

调用父类实现

最后的蓝图长这样子,新定义了一个速度变量,逻辑相当简单,这里就不讲解了。

image-20230412202102333

蓝图实现

定义了能力后,最后就是把能力添加到角色身上。这里展现两种添加方法:

方法一

在角色的蓝图中添加这样两个节点,实际上就是在事件开始的时候调用Add Action函数。优点是简单易懂,体现了动态添加技能的思想,和之前 硬编码的方式形成了鲜明的对比。可以展开联想,利用这样的功能,我们可以在打开宝箱的时候赋予角色一个新的能力,想想就很激动。

image-20230412202320692

蓝图实现添加技能
方法二

在SurActionComponent中添加一个默认能力的数组,我们可以预先将角色自带的技能添加到这个数组中,然后在开始游戏的时候逐个添加到能力数组中。重点是我们添加了UPROPERTY(EditDefaultsOnly)描述符,这样我们就可以在蓝图编辑器里面为角色赋予默认能力了。

//SurActionComponent.h
UPROPERTY(EditDefaultsOnly)
TArray<TSubclassOf<USurAction>> DefaultAction;

//.cpp
void USurActionComponent::BeginPlay()
{
	Super::BeginPlay();
	for(TSubclassOf<USurAction> ActionClass: DefaultAction)
	{
		AddAction(ActionClass);
	}
}

image-20230412204112433

在蓝图中为默认能力数组赋初值

这篇文章的后续都会使用这种方法来添加能力。

PS:涉及到修改UPROPERTY宏的代码修改一定要关掉UE编辑器重新编译,笔者这里贪图热重载又出了不少BUG 。qwq

魔法子弹

创建一个继承于SurAction类的SurAction_ProjectileAttack类,这个类将作为我们所有发射魔法子弹相关技能的基类。与之前不同的是,这个基类可以胜任所有发射魔发子弹相关的技能。

类的头文件如下,可以看到是把以前实现的角色类中很多发射子弹相关的成员都搬了过来,包括动画、特效、开火延迟、子弹类型等等,这些东西本来就是应该和角色的实现分离的,将他们放到这里也算是适得其所了。

值得一提的是,相较于原来的代码,新定义的函数中通常都新增了Instigator这么一个参数。这自然是权责分离所带来的代价,我们定义的能力类需要时刻记住自己的主人是谁,之后修改的代码通常也围绕着这个参数展开。

UCLASS()
class FPSPROJECT_API USurAction_ProjectileAttack : public USurAction
{
   GENERATED_BODY()
public:
   USurAction_ProjectileAttack();

   virtual void StartAction_Implementation(AActor* Instigator) override;

   UFUNCTION()
   void AttackDelay_Elapsed(ACharacter* InstigatorCharacter);

protected:
   UPROPERTY(VisibleAnywhere, Category = "Attack")
   FName HandSocketName;

   //发射时的特效
   UPROPERTY(EditAnywhere, Category = "Attack")
   UParticleSystem* CastingEffect;
   
   UPROPERTY(EditDefaultsOnly, Category = "Attack")
   UAnimMontage* AttackAnim;
   
   UPROPERTY(EditAnywhere, Category = "Attack")
   float FireDelay;

   UPROPERTY(EditDefaultsOnly, Category = "Attack")
   TSubclassOf<AActor> ProjectileClass;
   
   UPROPERTY(EditDefaultsOnly)
   float MaxShootDist;
   
   
private:
   void TurnToFront(ACharacter* Insitigator);
};

最主要的代码自然就是重载StartAction了,实际上代码逻辑还是原来角色类的那一套:

void USurAction_ProjectileAttack::StartAction_Implementation(AActor* Instigator)
{
    //使用Super调用父类实现
   Super::StartAction_Implementation(Instigator);
   
   if(!ensure(AttackAnim))
   {
      return;
   }
   
   ACharacter* Character = Cast<ACharacter>(Instigator);
   if(Character)
   {
      TurnToFront(Character);
      Character->PlayAnimMontage(AttackAnim);
      //生成特效
      UGameplayStatics::SpawnEmitterAttached(CastingEffect, Character->GetMesh(), HandSocketName, FVector::ZeroVector, FRotator::ZeroRotator, EAttachLocation::SnapToTarget);

      FTimerHandle TimerHandle_AttackDelay;
      FTimerDelegate Delegate;
      Delegate.BindUFunction(this, "AttackDelay_Elapsed", Character);
      
      GetWorld()->GetTimerManager().SetTimer(TimerHandle_AttackDelay, Delegate, FireDelay, false);
   }
}

可以看到,和原来的代码相比,仅仅是新增了一个类型转换,以及将函数与Character绑定在一起的委托罢了。

然后是AttackDelay_Elapsed的代码。可以看到,不过是将SpawnProjectile函数里面的代码复制了过来,围绕InstigatorCharacter修改了几处原本是由this指针发挥作用的地方。

void USurAction_ProjectileAttack::AttackDelay_Elapsed(ACharacter* InstigatorCharacter)
{
   if(ensure(ProjectileClass))
   {
      FVector HandLoc = InstigatorCharacter->GetMesh()->GetSocketLocation(HandSocketName);

      FVector TraceStart = InstigatorCharacter->GetPawnViewLocation();
      FVector TraceEnd = TraceStart + (InstigatorCharacter->GetControlRotation().Vector() * MaxShootDist);

      //碰撞检测半径
      FCollisionShape Shape;
      float Radius = 20.f;
      Shape.SetSphere(Radius);

      //不检测自己
      FCollisionQueryParams Params;
      Params.AddIgnoredActor(InstigatorCharacter);

      //设置检测的目标类型
      FCollisionObjectQueryParams ObjParams;
      ObjParams.AddObjectTypesToQuery(ECC_WorldDynamic);
      ObjParams.AddObjectTypesToQuery(ECC_PhysicsBody);
      ObjParams.AddObjectTypesToQuery(ECC_Pawn);

      FHitResult Hit;
      if(GetWorld()->SweepSingleByObjectType(Hit, TraceStart, TraceEnd, FQuat::Identity, ObjParams, Shape, Params))
      {
         TraceEnd = Hit.ImpactPoint;
      }
      bool bDebugDraw = CVarDebugDrawAttack.GetValueOnGameThread();
      if(bDebugDraw)
      {
         DrawDebugSphere(GetWorld(), Hit.ImpactPoint, Radius, 32, FColor::Orange, false, 2.0f);
      }
      

      //计算方向向量,从目标点到手
      FRotator ProjRotation = FRotationMatrix::MakeFromX(TraceEnd - HandLoc).Rotator();
      
      FTransform SpawnTM = FTransform(ProjRotation, HandLoc);
      
      FActorSpawnParameters spawnParams;
      spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;//即使碰撞也始终生成
      spawnParams.Instigator = InstigatorCharacter;
      
      GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, spawnParams);
   }

   StopAction(InstigatorCharacter);
}

有几个值得注意的点:

  1. 代码里有这么一句:InstigatorCharacter->GetPawnViewLocation(),意思是获取角色的视点。这个函数本来是定义于ACharacter类中的抽象函数,课程里将其在自己实现的角色类中重载,现在的作用是获取摄影机的位置。具体的实现可以参考文末。

  2. 在函数最后调用了StopAction。因为发射子弹是点按触发的,因此我们不设置release按键的相关事件。但是为了有始有终,这里还是需要调用 StopAction。

  3. GetWorld()函数。注意,目前这里是UObject::GetWorld(),课程里花了一定篇幅提醒我们要注意重载GetWorld,否则在蓝图编辑器里会出现一些问题。重载的代码如下:

    UWorld* USurAction::GetWorld() const
    {
       // 因为Action是由组件生成的,这里就转化为组件
       //Outer在UObject创建时传入
       UActorComponent* ActorComponent = Cast<UActorComponent>( GetOuter());
       if(ActorComponent)
       {
          return ActorComponent->GetWorld();
       }
       return nullptr;
    }
    

由于UObject是UE中最基本的类,它本身是不会带有关卡的任何信息的,因此它需要通过Outer,也就是创建UObject时传进去的那个参数,才能得知自己属于谁,属于哪一个世界。如果你翻看源码,会发现UObject::GetWorld()的实现和这里的重载几乎是一样的,都是通过Outer获得World的信息。区别这里的重载有一个显式的类型转换,将Outer转化为了UActorComponent。

为什么要转化呢?笔者目前不知道其背后的原理,但从结果来说,在原先的实现中,蓝图编辑器里如果要调用GetWorld()之后的函数,比如通道检测,获取timer之类的功能,是无法正常查找的。差别可以从下面两张图看出,只有在重载了GetWorld后,才能找到sweep相关的函数。

至少我们得出一个结论,在继承了UObject之后,尽量重载GetWorld函数吧。

image-20230413172240819

没有重载GetWorld的情况

image-20230413172318506

重载了GetWorld的情况

经过各种查漏补缺后,终于写好了发射子弹的能力。最后一步就是创建一个子类蓝图,将其命名为Action_MagicProjectile

image-20230413172841935

创建Action_MagicProjectile

然后将里面各个属性填进去。

image-20230413174121201

别忘了修改ActionName

别忘了在角色类中添加调用能力的代码,和冲刺技能一样,只有短短一行代码。

void AMyCharacter::PrimaryAttack()
{
   ActionComponent->StartActionByName(this, "PrimaryAttack");
}

进入游戏,顺利运行。

可以看到输出日志也输出了我们想看到的东西。

image-20230413174745502

下方的输出日志也正常的输出了信息

最后,就是把原本角色类中没用的代码统统删掉,酣畅淋漓。

image-20230413174956125

删除冗余代码总是有着别样的快感

黑洞子弹& 瞬移子弹

剩下这两个技能就更简单了。直接复制刚创建的Action_MagicProjectile蓝图类,是的,我们不需要单独创建一个子类,直接复制即可。我们定义的是发射子弹的能力,每种子弹能力的区别只有特效和子弹类型之类的属性,因此我们不用大费周章地单独设计每一个子弹相关的能力,我觉得这是很酷的一点。

将其改名,按照自己的需要修改属性,然后依葫芦画瓢地绑定给角色即可。

image-20230413175746799

本节课创建的四个技能全家福

image-20230413175850213

添加到默认技能数组中

最终效果&总结

本篇文章介绍了GAS,并且自己实现了一个简化版的能力系统,新增了冲刺技能,并重构了之前实现的三种子弹技能。学完了这一节,想必各位对如何实现随游戏进程解锁能力,拾取强力BUFF等操作有了一个直观的实现方案,现在马上动手设计自己的能力体系难道不是一件很让人兴奋的事情吗?

目前技能系统还存在一个问题,那就是角色可以在释放技能的同时释放其他技能,通俗的话来说,就是技能没有后摇。虽然这样使用技能很爽快,但作为游戏开发者,我们希望能够自由地控制哪些技能可以被打断,在下节课中,我们将介绍游戏标签(TAG)系统,以让我们方便得知游戏里的各种状态。

参考链接

UE4中的字符串类:FName、FText和FStringhttps://blog.csdn.net/weixin_43405546/article/details/95978408

斯坦福UE4C++课程P63-P65游戏能力系统GAShttps://www.bilibili.com/read/cv19665705

全部相关代码

SurAction

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "SurAction.generated.h"

/**
 * 
 */
// UObject要想在蓝图中被继承必须加上Blueprintable宏
UCLASS(Blueprintable)
class FPSPROJECT_API USurAction : public UObject
{
   GENERATED_BODY()
public:
   UPROPERTY(EditDefaultsOnly, Category = "Action")
   FName ActionName; //FName在UE中通常用于标识和查找

   UFUNCTION(BlueprintNativeEvent,Category = "Action")
   void StartAction(AActor* Instigator);
   
   UFUNCTION(BlueprintNativeEvent,Category = "Action")
   void StopAction(AActor* Instigator);

   //如果不重载这个函数的话,在蓝图中调用该函数可能不会出现应有的函数关联
   virtual UWorld* GetWorld() const override;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "SurAction.h"

void USurAction::StartAction_Implementation(AActor* Instigator)
{
   UE_LOG(LogTemp, Log, TEXT("Running: %s"), *GetNameSafe(this));
}

void USurAction::StopAction_Implementation(AActor* Instigator)
{
   UE_LOG(LogTemp, Log, TEXT("Stoping: %s"), *GetNameSafe(this));
}

UWorld* USurAction::GetWorld() const
{
   // 因为Action是由组件生成的,这里就转化为组件
   //Outer在UObject创建时传入
   UActorComponent* ActorComponent = Cast<UActorComponent>( GetOuter());
   if(ActorComponent)
   {
      return ActorComponent->GetWorld();
   }
   return nullptr;
}

SurActionComponent

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SurActionComponent.generated.h"


class USurAction;
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class FPSPROJECT_API USurActionComponent : public UActorComponent
{
   GENERATED_BODY()

public:    
   USurActionComponent();
   //因为能力有时候是其他Actor赋予的,所以这里的权限是Public
   UFUNCTION(BlueprintCallable, Category="Action")
   void AddAction(TSubclassOf<USurAction> ActionClass);

   UFUNCTION(BlueprintCallable, Category="Action")
   bool StartActionByName(AActor* Instigator, FName ActionName);

   UFUNCTION(BlueprintCallable, Category="Action")
   bool StopActionByName(AActor* Instigator, FName ActionName);

   UPROPERTY(EditDefaultsOnly)
   TArray<TSubclassOf<USurAction>> DefaultAction;
   
protected:
   virtual void BeginPlay() override;
   UPROPERTY()
   TArray<USurAction*> Actions;

public:    
   virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

      
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "SurActionComponent.h"
#include "SurAction.h"
// Sets default values for this component's properties
USurActionComponent::USurActionComponent()
{
   PrimaryComponentTick.bCanEverTick = true;
}

void USurActionComponent::AddAction(TSubclassOf<USurAction> ActionClass)
{
   if(!ensure(ActionClass))
   {
      return;
   }
   USurAction* NewAction = NewObject<USurAction>(this, ActionClass);
   if(ensure(NewAction))
   {
      Actions.Add(NewAction);
   }
}

bool USurActionComponent::StartActionByName(AActor* Instigator, FName ActionName)
{
   for(USurAction* Action:Actions)
   {
      if(Action && Action->ActionName == ActionName)
      {
         Action->StartAction(Instigator);
         return true;
      }
   }
   return false;
}

bool USurActionComponent::StopActionByName(AActor* Instigator, FName ActionName)
{
   for(USurAction* Action:Actions)
   {
      if(Action && Action->ActionName == ActionName)
      {
         Action->StopAction(Instigator);
         return true;
      }
   }
   return false;
}


// Called when the game starts
void USurActionComponent::BeginPlay()
{
   Super::BeginPlay();
   for(TSubclassOf<USurAction> ActionClass: DefaultAction)
   {
      AddAction(ActionClass);
   }
}

void USurActionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
   Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

}

MyCharacter

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"

class USurActionComponent;
class USurInteractionComponent;
class UCameraComponent;
class USpringArmComponent;
class USurAttributeComponent;
class USurAction;
UCLASS()
class FPSPROJECT_API AMyCharacter : public ACharacter
{
   GENERATED_BODY()

public:
   // Sets default values for this character's properties
   AMyCharacter();

protected:
   UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")
   USurAttributeComponent* AttributeComponent;

   UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
   USurActionComponent* ActionComponent;
   
   // Called when the game starts or when spawned
   virtual void BeginPlay() override;

   //弹簧臂组件
   UPROPERTY(VisibleAnywhere)
   USpringArmComponent* SpringArmComp;

   UPROPERTY(VisibleAnywhere)
   UCameraComponent* CameraComp;

   UPROPERTY(VisibleAnywhere)
   USurInteractionComponent* InteractComp;
   

   void MoveForward(float value);
   void MoveRight(float value);

   void StartJump();
   void StopJump();
   void PrimaryInteract();

   //主攻击
   void PrimaryAttack();

   //次要攻击Q(黑洞)
   void SecondarySkill();
   //三技能F(瞬移)
   void ThirdSkill();
   
   //冲刺技能相关,用于按键绑定
   void SprintStart();
   void SprintStop();
   
   UFUNCTION()
   void OnHealthChanged(AActor* InstigatorActor, USurAttributeComponent* OwningComp, float NewHealth, float Delta);
   
   virtual void PostInitializeComponents() override;

   virtual FVector GetPawnViewLocation() const override;
public:
   

   // Called every frame
   virtual void Tick(float DeltaTime) override;

   // Called to bind functionality to input
   virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

   UFUNCTION(Exec)
   void HealSelf(float Amount = 100);
   
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyCharacter.h"

#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "FPSPeojectile.h"
#include "SurAttributeComponent.h"
#include "SurInteractionComponent.h"
#include "DrawDebugHelpers.h"
#include "SurActionComponent.h"
#include "SurDashProjectile.h"
// Sets default values
AMyCharacter::AMyCharacter()
{
   // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
   PrimaryActorTick.bCanEverTick = true;

   SpringArmComp = CreateDefaultSubobject<USpringArmComponent>("MySpringArmComp");
   CameraComp = CreateDefaultSubobject<UCameraComponent>("MyCamera");
   SpringArmComp->SetupAttachment(RootComponent);

   CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
   
   InteractComp = CreateDefaultSubobject<USurInteractionComponent>("SurInteractionComp");

   AttributeComponent = CreateDefaultSubobject<USurAttributeComponent>("SurAttributeComp");

   ActionComponent = CreateDefaultSubobject<USurActionComponent>("ActionComponent");
   // 关闭“使用控制器旋转Yaw”
   bUseControllerRotationYaw = false;
   // 开启“使用Pawn控制旋转”
   SpringArmComp->bUsePawnControlRotation = true;
   // 获取“角色移动”组件,然后开启“将旋转朝向运动”
   GetCharacterMovement()->bOrientRotationToMovement = true;

}

void AMyCharacter::PostInitializeComponents()
{
   Super::PostInitializeComponents();
   AttributeComponent->OnHealthChanged.AddDynamic(this, &AMyCharacter::OnHealthChanged);  
}

FVector AMyCharacter::GetPawnViewLocation() const
{
   //后面的偏移量是为了防止视角卡在奇怪的地方
   return CameraComp->GetComponentLocation() + CameraComp->GetComponentRotation().Vector() * 10.f;
}


// Called when the game starts or when spawned
void AMyCharacter::BeginPlay()
{
   Super::BeginPlay();

   
}

void AMyCharacter::MoveForward(float value)
{
   AController* controller = GetController();
   FRotator rotator =  controller->GetControlRotation();
   rotator.Pitch = 0.f;
   rotator.Roll = 0.f;
   AddMovementInput(rotator.Vector(), value);
}

void AMyCharacter::MoveRight(float value)
{
   AController* controller = GetController();
   FRotator rotator =  controller->GetControlRotation();
   rotator.Pitch = 0;
   rotator.Roll = 0;
   // 获取相机(鼠标控制器)的朝向,转向右侧,并朝这个方向移动;传入的Y表示右侧
   FVector rightVector = FRotationMatrix(rotator).GetScaledAxis(EAxis::Y); 
   AddMovementInput(rightVector, value);
}

void AMyCharacter::StartJump()
{
   bPressedJump = true;
}

void AMyCharacter::StopJump()
{
   bPressedJump = false;
}

void AMyCharacter::PrimaryInteract()
{
   InteractComp->PrimaryInteract();
   
}

void AMyCharacter::PrimaryAttack()
{
   ActionComponent->StartActionByName(this, "PrimaryAttack");
}

void AMyCharacter::SecondarySkill()
{
   ActionComponent->StartActionByName(this, "BlackHoleProjectile");
}

void AMyCharacter::ThirdSkill()
{
   ActionComponent->StartActionByName(this, "DashProjectile");
}


void AMyCharacter::SprintStart()
{
   ActionComponent->StartActionByName(this, "Sprint");
}

void AMyCharacter::SprintStop()
{
   ActionComponent->StopActionByName(this, "Sprint");
}


//如果生命值小于0,则禁止输入
void AMyCharacter::OnHealthChanged(AActor* InstigatorActor, USurAttributeComponent* OwningComp, float NewHealth,
   float Delta)
{
   if(NewHealth <= 0.f && Delta < 0)
   {
      APlayerController* PC = Cast<APlayerController>(GetController());
      DisableInput(PC);
   }
}



// Called every frame
void AMyCharacter::Tick(float DeltaTime)
{
   Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
   Super::SetupPlayerInputComponent(PlayerInputComponent);
   //绑定输入事件
   PlayerInputComponent->BindAxis("MoveForward", this , &AMyCharacter::MoveForward);
   PlayerInputComponent->BindAxis("MoveRight", this , &AMyCharacter::MoveRight);
   PlayerInputComponent->BindAxis("Turn", this, &AMyCharacter::AddControllerYawInput);
   PlayerInputComponent->BindAxis("LookUp", this, &AMyCharacter::AddControllerPitchInput);
   // 设置"操作"绑定。
   PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMyCharacter::Jump);
   PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AMyCharacter::PrimaryAttack);
   PlayerInputComponent->BindAction("MainSkill", IE_Pressed, this, &AMyCharacter::SecondarySkill);
   PlayerInputComponent->BindAction("SecondSkill", IE_Pressed, this, &AMyCharacter::ThirdSkill);

   PlayerInputComponent->BindAction("PrimaryInteract", IE_Pressed, this, &AMyCharacter::PrimaryInteract);
   
   //冲刺相关
   PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &AMyCharacter::SprintStart);
   PlayerInputComponent->BindAction("Sprint", IE_Released, this, &AMyCharacter::SprintStop);
   
   
}

void AMyCharacter::HealSelf(float Amount)
{
   AttributeComponent->ApplyHealthChanges(this, Amount);
}

SurAction_ProjectileAttack

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "SurAction.h"
#include "SurAction_ProjectileAttack.generated.h"

/**
 * 
 */
class UAnimMontage;
class UParticleSystem;
UCLASS()
class FPSPROJECT_API USurAction_ProjectileAttack : public USurAction
{
   GENERATED_BODY()
public:
   USurAction_ProjectileAttack();

   virtual void StartAction_Implementation(AActor* Instigator) override;

   UFUNCTION()
   void AttackDelay_Elapsed(ACharacter* InstigatorCharacter);

protected:
   UPROPERTY(VisibleAnywhere, Category = "Attack")
   FName HandSocketName;

   //发射时的特效
   UPROPERTY(EditAnywhere, Category = "Attack")
   UParticleSystem* CastingEffect;
   
   UPROPERTY(EditDefaultsOnly, Category = "Attack")
   UAnimMontage* AttackAnim;
   
   UPROPERTY(EditAnywhere, Category = "Attack")
   float FireDelay;

   UPROPERTY(EditDefaultsOnly, Category = "Attack")
   TSubclassOf<AActor> ProjectileClass;
   
   UPROPERTY(EditDefaultsOnly)
   float MaxShootDist;
   
   FTimerHandle TimerHandle_PrimaryAttack;
   
private:
   void TurnToFront(ACharacter* Insitigator);
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "SurAction_ProjectileAttack.h"

#include "DrawDebugHelpers.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/Character.h"
#include "Kismet/GameplayStatics.h"

//是否开启交互debug显示
static  TAutoConsoleVariable<bool> CVarDebugDrawAttack(TEXT("su.AttackDebugDraw"), false, TEXT("Enable Debug Sphere for Attack Component."), ECVF_Cheat);



USurAction_ProjectileAttack::USurAction_ProjectileAttack()
{
   HandSocketName = "Muzzle_01";
   FireDelay = 0.2f;
   MaxShootDist = 10000.f;
}

void USurAction_ProjectileAttack::StartAction_Implementation(AActor* Instigator)
{
   Super::StartAction_Implementation(Instigator);
   
   if(!ensure(AttackAnim))
   {
      return;
   }
   
   ACharacter* Character = Cast<ACharacter>(Instigator);
   if(Character)
   {
      TurnToFront(Character);
      Character->PlayAnimMontage(AttackAnim);
      //生成特效
      UGameplayStatics::SpawnEmitterAttached(CastingEffect, Character->GetMesh(), HandSocketName, FVector::ZeroVector, FRotator::ZeroRotator, EAttachLocation::SnapToTarget);

      FTimerHandle TimerHandle_AttackDelay;
      FTimerDelegate Delegate;
      Delegate.BindUFunction(this, "AttackDelay_Elapsed", Character);
      
      GetWorld()->GetTimerManager().SetTimer(TimerHandle_PrimaryAttack, Delegate, FireDelay, false);
   }
}

void USurAction_ProjectileAttack::AttackDelay_Elapsed(ACharacter* InstigatorCharacter)
{
   if(ensure(ProjectileClass))
   {
      FVector HandLoc = InstigatorCharacter->GetMesh()->GetSocketLocation(HandSocketName);
      
      FVector TraceStart = InstigatorCharacter->GetPawnViewLocation();
      FVector TraceEnd = TraceStart + (InstigatorCharacter->GetControlRotation().Vector() * MaxShootDist);

      //碰撞检测半径
      FCollisionShape Shape;
      float Radius = 20.f;
      Shape.SetSphere(Radius);

      //不检测自己
      FCollisionQueryParams Params;
      Params.AddIgnoredActor(InstigatorCharacter);

      //设置检测的目标类型
      FCollisionObjectQueryParams ObjParams;
      ObjParams.AddObjectTypesToQuery(ECC_WorldDynamic);
      ObjParams.AddObjectTypesToQuery(ECC_PhysicsBody);
      ObjParams.AddObjectTypesToQuery(ECC_Pawn);

      FHitResult Hit;
      if(GetWorld()->SweepSingleByObjectType(Hit, TraceStart, TraceEnd, FQuat::Identity, ObjParams, Shape, Params))
      {
         TraceEnd = Hit.ImpactPoint;
      }
      bool bDebugDraw = CVarDebugDrawAttack.GetValueOnGameThread();
      if(bDebugDraw)
      {
         DrawDebugSphere(GetWorld(), Hit.ImpactPoint, Radius, 32, FColor::Orange, false, 2.0f);
      }
      

      //计算方向向量,从目标点到手
      FRotator ProjRotation = FRotationMatrix::MakeFromX(TraceEnd - HandLoc).Rotator();
      
      FTransform SpawnTM = FTransform(ProjRotation, HandLoc);
      
      FActorSpawnParameters spawnParams;
      spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;//即使碰撞也始终生成
      spawnParams.Instigator = InstigatorCharacter;
      
      GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, spawnParams);
   }

   StopAction(InstigatorCharacter);
}


void USurAction_ProjectileAttack::TurnToFront(ACharacter* Insitigator)
{
   AController* controller = Insitigator->GetController();
   FRotator rotator =  controller->GetControlRotation();
   rotator.Pitch = 0;
   rotator.Roll = 0;
   Insitigator->SetActorRotation(rotator);
}