虚幻引擎C++开发学习(三)

这一章,我们要实现一个俯视视角的坦克小游戏,玩家可以操作坦克在地图中移动。敌人是固定的,但是具备一定的AI,可以瞄准玩家,并且在玩家进入攻击范围后,对玩家进行攻击。如果玩家被摧毁,则游戏结束。反之,玩家需要摧毁所有的敌人后,达成游戏胜利的条件。

一、前言介绍

在开始之前,我们先介绍这一章会涉及到的内容:

  • 创建一个坦克,可以在地图中自由移动
  • 解决输入问题(WASD移动,鼠标点击射击,鼠标转动明确攻击方向)
  • 创建一个敌方炮台类
  • 添加开火功能,玩家和炮台都可以开火攻击敌人
  • 添加血条、伤害和破坏效果
  • 添加HUD的胜利和失败界面

关于绑定输入,这里就不多赘述,如下图:

关于素材和地图,这里也不是我们的重点,暂时略过,感兴趣的可以自行寻找。我们直接进入编码步骤。

二、创建BasePawn

1.1 创建BasePawn

由于我们这里会有两个pawn(玩家和敌人),我们可以先创建一个BasePawn类。这将具有坦克和炮塔共享的基本功能。然后我们就可以创建我们的两个子类,炮台和坦克。

我们新建一个C++类:

 但是哪一个才最适合我们想要做的事情?

  • Actor类,可以被放置在世界中,有相应的视觉表现
  • Pawn类,可由控制器拥有,可以处理运动输入
  • Character类,有一些特定于角色的东西,适合双腿的运动模式或类似飞行和游泳运动。

所以,Pawn应该是我们想要的。我们创建对应的C++类即可。

1.2 Component

USceneComponent:

  • has a transform(旋转或位置)
  • supports attachment(这意味着我们可以将其他组件附加到场景组件)
  • no visual representation

UCapsuleComponent:

  • handle collision

UStaticMeshComponent:

  • visual representation

我们讨论组件的目的,是要理清之后的操作思路。我们首先知道我们的Pawn有自己的root component,它的类型是USceneComponent。我们知道他是没有visual representation,是不可见的。

但是我们可以使用其他类型从SceneComponent(场景组件)派生的对象重新assign该根组件。我们知道UCapsuleComponent来自SceneComponent。如果我们创建一个Capsule,我们可以assign这个,作为Root,替换默认的SceneComponent:

RootComponent(UCapsuleComponent) = CapsuleComp(UCapsuleComponent)

具体的思路可以见下图:

蓝图中,我们创建一个Actor蓝图,然后添加Capsule,接着添加静态网格体组件,并选择车身网格体,同理添加炮台。

 结果:

 在C++中,我们先在.h中定义

private:UPROPERTY()class UCapsuleComponent*  CapsuleComp;

然后在.cpp中添加头文件,并将其设置为Root

CapsuleComp = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule Collider"));
RootComponent = CapsuleComp;

这样我们将BasePawn拖入场景中时,会有:

 接下来我们要将BaseMesh和TurretMesh,attach到Root上(和之前的操作相同):

UPROPERTY()
UStaticMeshComponent* BaseMesh;UPROPERTY()
UStaticMeshComponent* TurretMesh;UPROPERTY()
USceneComponent* ProjectileSpawnPoint;
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Base Mesh"));
BaseMesh->SetupAttachment(CapsuleComp);TurretMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Turret Mesh"));
TurretMesh->SetupAttachment(BaseMesh);ProjectileSpawnPoint = CreateDefaultSubobject<USceneComponent>(TEXT("Spawn Point"));
ProjectileSpawnPoint->SetupAttachment(TurretMesh);

接下来我们可以为BasePawn创建一个基于BasePawn的蓝图类,这样我们进入后,可以看见这样的组件结构:

 然后我们还要创建一个蓝图类作为敌人,命名为BP_PawnTurret。

1.3 UPROPERTY

但是有一点要注意的是,当我们选中左边的组件时,右边的细节面板内容不见了。这是因为 C++ 方面的蓝图没有任何内容。我们可以通过UPROPERTY来对齐进行操作。

UPROPERTY Specifiers:

DefaultsInstanceEvent Graph(l蓝图的事件图表)
Read OnlyVisibleAnyWhereBluePrintReadOnly
VisibleDefaultsOnlyVisibleInstanceOnly
Read/Write(set)EditAnyWhereBluePrintReadWrite
EditDefaultOnly

EditInstanceOnly

关于事件图表,我们在C++中可以使用:

UPROPERTY(VisibleAnyWhere,BluePrintReadWrite)
int32 visibleAnywhere = 12;

这样我们在事件图表中搜索visibleAnywhere,就会有两个节点Set和Get。

其次还有一个需要注意的点是,对于private,会有报错(同样对于BluePrintReadOnly):

BluePrintReadWrite should not be used on private members

对于这种情况,我们仍然可以在事件图表中访问私有变量,我们需要添加:

UPROPERTY(VisibleAnyWhere,BluePrintReadWrite,meta = (AllowPrivateAccess = "true"))

当然我们可以为其添加Category,这个在之前有提到过。

在这一步之后,别忘了给坦克和炮台添加网格体,移动project point位置

二 Moving Tank

2.1 Component

要实现坦克的移动和玩家输入,炮台的站立,我们要创建新的派生类:

其次,为了实现坦克的移动,我们首先要保证视角跟随,即坦克的身后有一个摄像机——Camera Component(UCameraComponent)和一个固定距离的Spring arm component(USpingArmComponent)。

在蓝图中:

所以我们回到蓝图中,添加弹簧臂组件到Capsule下:

 然后我们选中弹簧臂组件,再添加一个摄像机组件:

但是我们的主要目标是C++。

在C++中:

我们在.h中创建:

public:ATank();
private:UPROPERTY(VisibleAnyWhere,Category = "Component")class UCameraComponent* Camera;UPROPERTY(VisibleAnyWhere,Category = "Component")class USpringArmComponent* SpringArm;

在.cpp中:

ATank::ATank(){SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("Spring Arm"));SpringArm->SetupAttachment(RootComponent);Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));Camera->SetupAttachment(SpringArm);
}

然后在蓝图中,可以在类设置修改父类:

 这样我们就可以看到刚刚设置的组件:

 还有一个需要注意的是,我们进入游戏后,场景中哪个是我们要操控的?我们可以选中坦克,然后分配player0:

 这样在游戏开始后,我们操控的就是坦克。

2.2 处理输入

2.2.1 Bind Axis Mapping

我们需要在游戏的每帧,获得按键的输入。

我们把BasePawn中的SetupPlayerInputComponent函数移动到Tank中,并且我们定义一个Move函数
 

void Move(float value);

在之前的函数中,加入一句:

PlayerInputComponent->BindAxis(TEXT("MoveForward"),this,&ATank::Move);

然后我们在Move函数中,添加测试的输出:

UE_LOG(LogTemp,Warning,TEXT("The Value is %f"),value);

这样在游戏中的输出日志中,我们可以看到按下W和S的value值(1和-1)。证明我们Bind成功。

2.2.2 Adding the Offset

要想在引擎中移动,就要对其添加位移。但是这里要弄清楚,我们是在Local space还是在World Space。

我们在场景中选中的Actor,有它们自己的Local方向,但是这个不一定和World方向相同。所以我们希望,当按下W键时,坦克能按照它的Local方向进行前进。

我们将使用AddActorLocalOffset来完成这件事情。我们转到它的定义位置:

void AActor::AddActorLocalOffset(FVector DeltaLocation, bool bSweep, FHitResult* OutSweepHitResult, ETeleportType Teleport)
{if(RootComponent){RootComponent->AddLocalOffset(DeltaLocation, bSweep, OutSweepHitResult, Teleport);}else if (OutSweepHitResult){*OutSweepHitResult = FHitResult();}
}

我们可以看到它会检查RootComponent是否为Null,所以这里实际做的是对RootComponent添加位移。

我们可以在BasePawn里进行测试(别忘了测试完成后删除):

void ABasePawn::Tick(float DeltaTime)
{Super::Tick(DeltaTime);FVector DeltLocation(0.f);DeltLocation.X = 2.f;AddActorLocalOffset(DeltLocation);
}

我们在虚幻中进行模拟,可以看到所有的坦克和炮台都朝着自己的X方向进行移动。既然了解了怎么移动,我们可以在Move函数中,进行操作:

FVector DeltLocation = FVector::ZeroVector;
DeltLocation.X = value;
AddActorLocalOffset(DeltLocation);

这样在场景中,我们可以按住W和S控制前进和后退。

2.2.3 Speed 

关于这个的修改,我们在之前的蓝图学习中有提到过,要使用Delta time。

我们先定义一个可以调整的速度参数:

UPROPERTY(EditAnyWhere,Category ="Movement")
float Speed = 200.f;

然后再利用Delta time来做Scale。

DeltLocation.X = value * Speed * UGameplayStatics::GetWorldDeltaSeconds(this);

这样之后也可以在蓝图中调整速度。

2.3 Local Rotation

我们先来介绍一下Sweeping

在游戏编程中,sweeping是引擎完成的一种技术,只要它处于开启。如果我们有一个移动的物体,比如有一个圆代表的一个球体,当这个球体移动时,每一帧,引擎都会执行sweep 检查。

这意味着它正在检查这个特定帧的移动是否会导致两个对象之间的重叠。

假设某一帧,一个球体和长方体重合了。那么此时Sweeping启用的功能是,引擎将检测到这种重叠并将该球体移回该特定帧,以便它永远不会真正穿透对象。

在官方文档中:

Whether we sweep to the destination location, triggering overlaps along the way and stopping short of the target if blocked by something. Only the root component is swept and checked for blocking collision, child components move without sweeping. If collision is off, this has no effect.

注意最后一句,所以要保证碰撞enabled,Sweeping才能正常工作。

所以我们现在代码中加入:

AddActorLocalOffset(DeltLocation,true);

然后在蓝图设置中将碰撞预设修改为BlockAllDynamic。

  • 注意:如果无法移动,大概率是因为和地板重合。

这样Sweeping这项功能设置完成。

而添加旋转的方式和之前的类似:

void ATank::Turn(float value){FRotator DeltaRotation = FRotator::ZeroRotator;DeltaRotation.Yaw = value * TurnRate * UGameplayStatics::GetWorldDeltaSeconds(this);AddActorLocalRotation(DeltaRotation,true);
}

但是这样只是完成了整个坦克的旋转,对于坦克的上半身我们没有添加旋转,而这个需要用到鼠标的输入。

我们希望坦克的上身能跟随鼠标的方向进行转动,我们可以得到鼠标的位置,如果我们要从相机直接向光标画一条线并击中世界中的某个物体,我们可以获得该位置并使用该位置来设置坦克炮塔的旋转。

具体应该怎么做,我们还需要了解Casting。

只要对象本身是我们试图转换为的类型,casting 就会将一个指针的类型cast为另一种指针类型。

我们在BeginPlay中加入代码:

PlayerControllerRef = Cast<APlayerController>(GetController());

这样我们获得了获得了对Controller的访问。并且用cast函数从Acontroller* 到APlayerController*。

  • 注释:
  • Casting Quick Start Guide(官方文档)

我们再整理一下思路:

 我们要完成这个操作在每一帧,所以我们需要Tick函数。然后在函数中:

	Super::Tick(DeltaTime);if (PlayerControllerRef){FHitResult HitResult;PlayerControllerRef->GetHitResultUnderCursor(ECollisionChannel::ECC_Visibility,false,HitResult);}

在我们call这个函数后,HitResult会被line trace的数据填充。我们可以获得碰撞事件的point等。

我们现在可以画一个debugsphere来进行测试:

    DrawDebugSphere(GetWorld(),HitResult.ImpactPoint,25.f,12, FColor::Red,false,-1.f);}

我们到引擎中编译,可以看到我们鼠标的位置会有一个球体,准确的说是在鼠标连线之间的物体碰撞位置(仅供测试):

现在我们可以尝试利用鼠标控制炮台的旋转了。关于向量的计算,这里就不多介绍了。

需要注意的一点是,如果我们的鼠标在地面上,会导致炮台不是水平方向上的旋转,会指向地面。这个效果不是我们想要的,我们希望它能水平,也就是说,我们只希望它旋转的部分数值。

我们将代码放置在BasePawn中,这样后续敌方炮台也可以使用。

protected:
//任何放置在这里的函数或变量,只能被它的子类访问。 void RotateTurret(FVector LookAtTarget);
void ABasePawn::RotateTurret(FVector LookAtTarget){FVector ToTarget = LookAtTarget - TurretMesh->GetComponentLocation();FRotator LookAtRotation = FRotator(0.f,ToTarget.Rotation().Yaw,0.f);TurretMesh->SetWorldRotation(LookAtRotation);
}

然后我们回到Tank中,添加。

 RotateTurret(HitResult.ImpactPoint);

现在我们可以移动鼠标来让炮台转动了(转出残影):

 接下来,我们还要保证敌人炮台的转动,让它们一直瞄准坦克(需要对Tick函数overwrite)。

我们先从BasePawn创建一个派生类Tower。

在.h中:

public:virtual void Tick(float DeltaTime) override;protected:virtual void BeginPlay() override;
private:class ATank* Tank;UPROPERTY(EditDefaultsOnly,Category = "Combat")float FireRange = 300.f;

.cpp中:

void ATower::Tick(float DeltaTime){Super::Tick(DeltaTime);//当坦克进入范围后,才进行瞄准//做法:找到坦克的Location,找到炮台的Location,利用FVector::Dist()计算距离。if (Tank){float Distance = FVector::Dist(GetActorLocation(),Tank->GetActorLocation());if (Distance <= FireRange){//转动炮台RotateTurret(Tank->GetActorLocation());}}//查看Tank是否在距离内}void ATower::BeginPlay(){Super::BeginPlay();//获得TankTank = Cast<ATank>(UGameplayStatics::GetPlayerPawn(this, 0));
}

我们可以在蓝图中,将炮台的父类设置为Tower,然后修改Range参数。

三、Fire

3.1 Bind Action Mapping

Axis Mapping的触发像Tick函数,在每一帧触发。传入float值,根据按下的按钮而改变。

而Action Mapping不同,绑定到Action Mapping的回调函数不需要输入参数,所有发生的事情都是在您按下按钮时。不会在每帧触发,只有当按钮按下时触发。

我们绑定Action Mapping和函数,使用:BindAction()。

为了方便后续坦克和敌人的使用,我们在BasePawn里定义Fire函数。

然后我们先在Tank.cpp中添加绑定:

PlayerInputComponent->BindAction(TEXT("Fire"),IE_Pressed,this,&ATank::Fire);

然后对Fire函数,加入测试:

	FVector ProjectileSpawnPointLocation = ProjectileSpawnPoint->GetComponentLocation();DrawDebugSphere(GetWorld(),ProjectileSpawnPointLocation,25.f,12,FColor::Red,false,3.f);

我们进入编译查看结果,在我们鼠标点击的位置会有测试结果:

 接下来我们把它也应用到Tower中。我们这里会用到Timers。

.h中:

	//2秒的等待时间//查看我们是否可以开火FTimerHandle FireRateTimerHandle;float FireRate = 2.f;void CheckFireCondition();

.cpp中,首先在BeginPlay中:

 GetWorldTimerManger().SetTimer(FireRateTimerHandle,this,&ATower::CheckFireCondition,FireRate,true);
void ATower::CheckFireCondition(){if (Tank){float Distance = FVector::Dist(GetActorLocation(),Tank->GetActorLocation());if (Distance <= FireRange){//转动炮台Fire();}}
}

我们测试结果:

 可以看到,当我们靠近炮台时,Fire函数被使用。关于重构的部分暂时略过。

3.2 发射子弹

我们创建一个Projectile Class,过程和之前类似(Actor),这里简单说一下:

创建Projectile Class,创建UStaticMeshComponent,并设置为RootComponent,创建基于它的蓝图,设置网格体。

接下来我们就要在场景中生成(spawn)子弹,这需要SpawnActor函数。

在这之前,我们先学习TSubclassOf:

TSubclassOfhttp://tsubclassof/在BasePawn中添加

	UPROPERTY(EditDefaultsOnly,Category = "Combat")TSubclassOf<class AProjectile> ProjectileClass;

然后回到BP_PawnTank蓝图中,右边的细节面板就有了选项:

我们选择蓝图,是因为TSubClassof允许我们设置这个Projectile class为一个特定的type,基于Projectile。这样选择之后,Projectile class被设置为BP Projectile 类型。        

那为什么要设置这个?我们要了解SpawnActor的怎样工作的。

SpawnActor是属于UWorld class的函数,Spawn actor 可以在游戏运行时在运行时调用,它可以创建 actor。

SpawnActor:关于SpawnActor<>(),如果我们想生成子弹,我们需要首先传入C++ class type在<>里Aprojectile。

我们在BasePawn中的fire函数生成子弹:

void ABasePawn::Fire(){FVector Location = ProjectileSpawnPoint->GetComponentLocation();FRotator Rotation = ProjectileSpawnPoint->GetComponentRotation();GetWorld()->SpawnActor<AProjectile>(ProjectileClass,Location,Rotation);
}

进入编译器查看结果:

 但是子弹没有移动,我们接下来设置子弹的移动。想要完成子弹的移动有几个方法:

  • 1 设置子弹的方向和距离,这需要每帧更新
  • 2 添加Impulse,引擎进行物理模拟
  • 3 使用MoveMent组件

我们使用Projectile Movement Component:UProjectileMovementComponenthttps://docs.unrealengine.com/4.27/en-US/API/Runtime/Engine/GameFramework/UProjectileMovementComponent/

我们添加组件斌,并且对炮台也分配子弹网格体:

	UPROPERTY(VisibleAnyWhere,Category = "MoveMent")class UProjectileMovementComponent* ProjectileMoveMent;
	ProjectileMoveMent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("Projectile Movement Component"));ProjectileMoveMent->MaxSpeed = 1300.f;ProjectileMoveMent->InitialSpeed = 1300.f;

进入编译器,现在我们和炮台都可以发射子弹了:

四 Damage

4.1 Hit Event

首先我们要确定碰撞事件,即子弹的碰撞事件,这样我们可以摧毁子弹。

在.h中

UFUNCTION()void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,FVector NormalImpulse,const FHitResult& Hit);

在BeginPlay中:

ProjectileMesh->OnComponentHit.AddDynamic(this, &AProjectile::OnHit);

关于delegates(代理委托):
虚幻4:代理委托基础(delegate) - 知乎

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Delegates/

关于AddDynamic:

it's a macro usually used to bind a function to an event

https://docs.unrealengine.com/4.26/zh-CN/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Delegates/Dynamic/

void AProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,FVector NormalImpulse,const FHitResult& Hit){UE_LOG(LogTemp, Warning, TEXT("OnHit"));UE_LOG(LogTemp, Warning, TEXT("HitComp: %s"), *HitComp->GetName());UE_LOG(LogTemp, Warning, TEXT("OtherActor: %s"), *OtherActor->GetName());UE_LOG(LogTemp, Warning, TEXT("OtherComp: %s"), *OtherComp->GetName());}

我们在输出日志中,输出碰撞事件的结果:

4.2 Damage/Health Class

既然要造成伤害,那就需要血条或生命值。我们在之前了解过USceneComponent,它derived from UActorComponent:

  • UActorComponent:No transform,No attachment
  • USceneComponent:Has transform,Support attachment

由于我们要处理伤害和健康,我们不需要多余的东西,UActorComponent足够了。

了解了这些,我们新建Actor组件的C++类——HealthComponent。在里面定义一些简单的变量:

private:UPROPERY(EditAnywhere)float MaxHealth = 100.f;float Health=0.f;

在BeginPlay中

Health = MaxHealth;

然后我们在坦克和炮台的蓝图中加入Health组件。

接下来:

UFUNCTION()//因为我们要将其bind到delegate,它需要正确的输入参数适合那个delegate。对于要bind到OntakeAnyDamage的函数,输入参数列表如下://受到伤害的Actor,伤害数值//我们有这个输入参数的原因是虚幻引擎有DamageType的概念。我们可以创建具有额外数据的自定义伤害类型,这些数据可以通知你执行不同的操作,具体取决于你可能具有火焰伤害、毒药伤害、爆炸伤害等的伤害类型。//An instigator is the controller responsible for the damage.//This is the actual actor causing the damage.这是子弹本身void DamageTaken(AActor *DamagedActor, float Damage, const UDamageType *DamageType, class AController *Instigator, AActor *DamageCauser)
GetOwner()->OnTakeAnyDamage.AddDynamic(this,&UHealthComponent::DamageTaken);

这样当我们产生damage事件时,我们都会从该委托中获得广播,这将导致调用damage taken函数。我们接着要使用ApplyDamage,这需要一些输入:

我们先在Projectile里定义一个Damge数值,然后在BasePawn-fire中

auto Projectile = GetWorld()->SpawnActor<AProjectile>(ProjectileClass,Location,Rotation);
Projectile->SetOwner(this);

这样我们就可以访问最新生成的子弹;然后当pawn生成子弹时,它会设置那个子弹的owner,这样我们再使用GetOwner时,我们会获得那个拥有子弹的class的实例。

我们在projectile-Onhit中:

auto MyOwner = GetOwner();if(MyOwner == nullptr) return;auto MyOwnerInstigator = MyOwner->GetInstigatorController();auto DamageTypeClass = UDamageType::StaticClass();if (OtherActor&& OtherActor != this && OtherActor != MyOwner){UGameplayStatics::ApplyDamage(OtherActor, Damage, MyOwnerInstigator, this, DamageTypeClass);Destory();}

接着回到DamageTaken中:

if(Damage<=0.f) return;Health -= Damage;UE_LOG(LogTemp,Warning,TEXT("Health: %f"),Health);

4.3 Death

我们使用GameMode来确定游戏的开始和结束,我们首先创建一个GameMode的C++类:

然后再创建一个基于他的蓝图:

 然后再项目设置中,将其设置为默认游戏模式。并在蓝图中修改默认pawn类为坦克蓝图:

 接下来为了实现death,我们有如下思路:

  1. 创建HandleDestruction(BasePawn)
  2. 创建ActorDied函数(GameMode)
  3. Call HandleDestruction in ActorDied
  4. Call ActorDied  health成为0时

第一步:我们先定义HandleDestruction函数,然后进入Tower中。对于炮台,我们先进行简单的摧毁。

void ATower::HandleDestruction(){Super::HandleDestruction();Destroy();
}

对于坦克,我们现阶段希望其能隐藏。

void ATank::HandleDestruction(){Super::HandleDestruction();SetActorHiddenInGame(true);//禁用TickSetActorTickEnabled(false);
}

第二步+第三步:

public:void ActorDied(AActor* DeadActor);
void AToonTanksGameMode::ActorDied(AActor* DeadActor){//如果坦克被摧毁了if(DeadActor == Tank){Tank->HandleDestruction();if(Tank->GetTankPlayerController()){//禁止输入按键响应Tank->DisableInput(Tank->GetTankPlayerController());//确保Mouse cursor不显示Tank->GetTankPlayerController()->bShowMouseCursor = false;}}else if(ATower* DestoryedTower = Cast<ATower>(DeadActor)){DestoryedTower->HandleDestruction();}
}

第四步,我们首先要在HealthComponent中获得GameMode:

class AtonTanksGameMode* ToonTanksGameMode;

在BeginPlay中:

ToonTanksGameMode = Cast<AToonTanksGameMode>(UGameplayStatics::GetGameMode(this));

然后在DamageTaken中:

	if(Health <= 0.f &&ToonTanksGameMode){ToonTanksGameMode->ActorDied(DamagedActor);}

现在我们就可以在场景中消灭炮台,并被消灭。但是我们还需要添加音效,特效,胜利及失败界面等内容。

五、游戏特效及输赢界面

5.1 custom player Controller

我们首先创建一个PlayerController的c++类,和一个基于它的蓝图类。我们需要设置这个蓝图类为默认Player Controller。只要在GameMode蓝图中设置一下即可:

 然后添加代码:

void AToonTanksPlayerController::SetPlayerEnabledState(bool bPlayerEnabled){if(bPlayerEnabled){GetPawn()->EnableInput(this);}else{GetPawn()->DisableInput(this);}bShowMouseCursor = bPlayerEnabled;
}

这样我们可以将之前ActorDied中的部分代码用这个函数替换掉。

我们再进入该蓝图中,设置默认鼠标光标为十字准星: 

当然现阶段我们还不能看到,因为我们还没有显示鼠标。

5.2 Starting the Game

我们回到GameMode中,在private:

	//过多久游戏可以开始,并接受玩家的输入float StartDelay = 3.f;//函数void HandleGameStart();

在cpp中:

void AToonTanksGameMode::HandleGameStart(){//移动之前的BeginPlay代码Tank = Cast<ATank>(UGameplayStatics::GetPlayerPawn(this, 0));ToonTanksPlayerController =Cast<AToonTanksPlayerController>(UGameplayStatics::GetPlayerController(this,0));if(ToonTanksPlayerController){ToonTanksPlayerController->SetPlayerEnabledState(false);FTimerHandle PlayerEnabledTimerHandle;FTimerDelegate PlayerEnabledTimerDelegate = FTimerDelegate::CreateUObject(ToonTanksPlayerController,&AToonTanksPlayerController::SetPlayerEnabledState,true);GetWorldTimerManager().SetTimer(PlayerEnabledTimerHandle,PlayerEnabledTimerDelegate,StartDelay,false);}}

这样我们进入游戏后,3秒不能接受玩家的输入,且我们的鼠标变为了十字瞄准。

接下来我们要在屏幕上显示这些信息,这需要用到蓝图的implementable event。我们首先在GameMode的protected下:

	//我们不需要在C++中为其提供body,虚幻会希望我们在蓝图中完成它的实现UFUNCTION(BlueprintImplementableEvent)void StartGame();

然后在HandleGameStart函数中加入StartGame,进入引擎编译,然后进入GameMode蓝图中。

我们可以在蓝图中加入StartGame事件:

 接着我们创建一个新的控件蓝图(这部分我们之前有提到过):

 并在里面加入一个简单的文本框:

 我们接下来要尝试将其加入到屏幕中,回到蓝图:

这只是个简单的实现,我们要做的是在屏幕上显示倒计时,这部分主要用蓝图实现,和之前的内容多有重复,就不多做记录,只放入最后的蓝图。

 5.3 获胜和失败界面

我们可以创建一个Gameover函数来完成这个目标,在GameMode中:

	UFUNCTION(BlueprintImplementableEvent)void GameOver(bool bWonGame);

并在ActorDied中加入函数,false:

GameOver(false);

在Towerdead中,加入计数,如果敌人被全部消灭,则为true。为了完成计数,我们创建:

	int32 TargetTower = 0;int32 GetTargetTowerCount();

我们可以使用GetAllActorsOfClass:

int32 AToonTanksGameMode::GetTargetTowerCount(){TArray<AActor*> Toewers;UGameplayStatics::GetAllActorsOfClass(this,ATower::StaticClass(),Toewers);return Toewers.Num();
}

并在HandleGameStart中获取TargetTower,我们还要保证其更新:

        --TargetTower;if(TargetTower == 0){GameOver(true);}

这样我们可以在GameMode蓝图中创建GameOver事件,并将其添加到ViewPort:

 我们先将之前的StartGame复制一份,命名为EndGame,删除除了DisPlay文本之外的所有蓝图和变量,然后再GameMode中添加蓝图:

 这样我们的胜利和失败条件和界面就完成了。

5.4 特殊效果

我们需要回到之前的Projectile中添加

	UPROPERTY(EditAnyWhere,Category = "Combat")class UParticleSystem* HitParticles; 

然后回到子弹蓝图中添加对应效果(可以自行选择免费素材)。然后我们需要在子弹击中后生成该粒子效果。我们在Projectile的OnHit中添加:

void AProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,FVector NormalImpulse,const FHitResult& Hit){auto MyOwner = GetOwner();if(MyOwner == nullptr) {Destroy();return;}auto MyOwnerInstigator = MyOwner->GetInstigatorController();auto DamageTypeClass = UDamageType::StaticClass();if (OtherActor&& OtherActor != this && OtherActor != MyOwner){UGameplayStatics::ApplyDamage(OtherActor, Damage, MyOwnerInstigator, this, DamageTypeClass);if(HitParticles){UGameplayStatics::SpawnEmitterAtLocation(this,HitParticles,GetActorLocation(),GetActorRotation());} }Destroy();}

然后我们编译进入游戏,查看效果,这是子弹击中的效果:

接下来我们再添加跟随子弹的粒子系统,我们需要添加组件来完成这件事。我们在Projectile添加:

	UPROPERTY(EditAnyWhere,Category = "Combat")class UparticleSystemComponent* TrailPatticles;
	TrailPatticles = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Smoke Trail"));TrailPatticles->SetupAttachment(RootComponent);

然后我们进入蓝图分配资源即可,查看效果:

 接下来在BasePawn中我们添加死亡的特效:

	UPROPERTY(EditAnywhere,Category = "Combat")class UParticleSystem* DeathParticles;

并在HandleDestruction中:

void ABasePawn::HandleDestruction(){if(DeathParticles){UGameplayStatics::SpawnEmitterAtLocation(this,DeathParticles,GetActorLocation(),GetActorRotation());}
}

然后同样的,我们进入蓝图进行设置。

 接下来我们再为其添加声音,首先创建对应变量,然后进入蓝图进行设置:
在Projectile:

	UPROPERTY(EditAnyWhere,Category = "Combat")class USoundBase* LaunchSound; UPROPERTY(EditAnyWhere,Category = "Combat")USoundBase* HitSound;

BasePawn:

	UPROPERTY(EditAnywhere,Category = "Combat")class UParticleSystem* DeathParticles;

 设置完成后,我们就要播放这些音效。进入Projectile的OnHit中:

if(HitSound){UGameplayStatics::PlaySoundAtLocation(this,HitSound,GetActorLocation());}

BeginPlay:

if(LaunchSound){UGameplayStatics::PlaySoundAtLocation(this,LaunchSound,GetActorLocation());}

同样的对Basepawn。

六、结尾

我们对游戏进行最后的优化:

  • 使摄像机移动更加平滑
  • 解决玩家死亡后,敌人仍在射击的问题

首先我们进入坦克并选中SpringArm,勾选下面两项:

 同样我们也可以通过调整下面的参数来调整,使摄像机的移动更加平滑。

关于下一个问题,我们进入Tank,创建一个bool变量:

bool bAlive = true;

然后进入HandleDestruction中,将其设置为false。然后进入Tower中

void ATower::CheckFireCondition(){if(Tank == nullptr){return;}if (InFireRange() && Tank->bAlive){Fire();}
}

到此为止,这个小游戏就完成了。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在虚幻引擎中创建大气的HIMIL电影作品

今天瑞云渲染小编给大家带来了关于电影制片人Tiziano Fioriti展示了《H I M I L》项目背后的工作流程&#xff0c;解释了人工智能是如何用于细节的&#xff0c;并谈到了设置火光的问题。 介绍 大家好&#xff0c;我叫Tiziano Fioriti&#xff0c;是来自意大利的自由电影制作人…

颠覆游戏开发,虚幻引擎 UE5 正式发布

整理 | 章雨铭 责编 | 屠敏 出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09; 4月6日&#xff0c;UE5正式发布&#xff01; 体验完UE5的新功能后&#xff0c;3D游戏狂热爱好者感叹&#xff1a;“惊呆了&#xff0c;老铁&#xff01;”美工直呼&#xff1a;”工作量…

虚幻4引擎将至!从虚幻看游戏引擎发展

3D游戏引擎是个什么玩意 泡泡网显卡频道5月23日 在某游戏中的一个场景中&#xff0c;玩家控制的角色躲藏在屋子里&#xff0c;敌人正在屋子外面搜索玩家。突然&#xff0c;玩家控制的是一个穿迷彩服的士兵&#xff0c;突然碰倒了桌子上的一个杯子&#xff0c;杯子坠地发出破碎…

UE DTMqtt 虚幻引擎 Mqtt 客户端插件说明

目录 CreateMqttClient Connect Subscribe UnSubscribe Publish Disconnect BindConnectedDelegate BindConnectionLostDelegate BindMessageDelegate CreateMqttClient 创建一个Mqtt客户端对象 Connect 链接Mqtt服务器Subscribe 订阅消息频道UnSubscribe 取消订阅频道…

Unreal Engine 虚幻引擎 接入第三方SDK

前言 虚幻引擎对SDK接入有着一套专门的方式&#xff0c;本文主要描述了如何给使用虚幻引擎开发的项目接入第三方SDK,并分享了接入微信SDK的分享等基础功能的实践过程&#xff0c;还涉及到调试和提高整合SDK效率的一些方法。 SDK意义 SDK 广义是指Software Development Kit 即 应…

虚幻引擎UE4中的Tick事件

虚幻引擎UE4中的Tick事件是程序运行中每一帧刷新一次&#xff0c;例如在通常每秒60帧的游戏运行中&#xff0c;Tick事件就每秒执行了60次。这个事件相当于Unity中的Update和LateUpdate函数一样的性质&#xff0c;&#xff08;同理在JS中就是animate()函数&#xff09;。 在UE4里…

虚幻与Unity引擎 之 ✨ 为什么国内那么多开发者喜欢用虚幻或者Unity?

为什么国内那么多开发者喜欢用虚幻或者Unity? 虚幻第一版1998年问世,Unity第一版2005年面世 简单介绍 首先,使用哪一款游戏引擎去制作一款游戏,并不是根据使用者的喜好来制定的。 开发者们的喜好千奇百怪,但最终能在技术选型中影响到结果的,往往是技术积累以及引擎本身的…

【唐老狮】Unity和UE4两大游戏引擎,你该如何选择?

经常被想进入游戏行业的同学问这样一个问题&#xff1a;Unity和UE4学哪个更好&#xff1f;当我面对这样的问题&#xff0c;往往都会先问清楚对方对哪个更感兴趣&#xff0c;然后就引导他学习哪个&#xff0c;投其所好的回答对方的问题&#xff01; 你心里肯定在想&#xff0c;你…

UE4虚幻引擎开发手机游戏

该文章来自用户转载 点击阅读原文 UE4虚幻引擎作为游戏开发引擎之一&#xff0c;在很多项目中都会使用到。下面就给大家分享下如何使用UE4开发高品质的手机游戏。 作者介绍 大纲 •介绍 •UE4 针对3A级手游的渲染管线和特征集 •使用ES3.1&#xff0c; Vulkan&#xff0c; Meta…

虚幻引擎在游戏之外可以做什么呢?

虚幻引擎 4 的跨界表演获得了“家装”引擎&#xff0c;VR 引擎&#xff0c;电影引擎等众多称号&#xff0c;那么在这些新领域的正确玩法是什么&#xff1f; 不玩不知道&#xff0c;一玩吓一跳。 -------又玩到新玩具的老陶 一个趁手的工具在开发者手里&#xff0c;你永远不会猜…

学习虚幻引擎UMG的基础

内容概括 对于UMG&#xff0c;之前我没有相关的经验&#xff0c;今天学习了下其基础知识。因此记录了一些自己感兴趣的内容。 本篇主要参考的教程是&#xff1a;UMG UI设计器快速入门 | 虚幻引擎文档。 但是我排除了一些关联度不高的、过于基础的、重复的内容。增添了一些自己…

虚幻引擎4中的实时GI技术

video&#xff1a; http://v.youku.com/v_show/id_XNDEwMDEwNTg4.html UE4中的realtime GI是基于这个researcher&#xff1a; http://blog.icare3d.org/ 主要的paper&#xff1a; http://perso.telecom-paristech.fr/~eisemann/publications/Crassin2011VoxelGlobalPG/Vox…

用 UE 虚幻引擎做个捏脸小功能~~

最近在学习 UE 相关的使用&#xff0c;正好看到一篇文章讲解用 Control Rig 实现简单捏脸功能&#xff0c;这种小而美的完整案例挺适合来练手的&#xff0c;涉及到了 UI、蓝图、动画、骨骼等方面&#xff0c;值得推荐一下。 从这个小功能出发&#xff0c;在学习其他内容时如果有…

Unreal Engine 虚幻引擎,性能分析,优化(一)

目录 一、Frame 帧 二、理想情况下&#xff0c;做性能分析时&#xff0c;当寻找游戏存在的瓶颈时&#xff0c;游戏的运行环境越接近目标硬件和目标平台&#xff0c;获得的数据越准确。 三、线程 四、Analysis of a frame &#xff0c;“CPU”线程&#xff0c;“Game”线程 怎…

UE4 虚幻引擎 GitSourceControl源码解析

效果功能图 背景与目标 资产的工作量很大&#xff0c;并不是个人在短时间内可以独自完成的&#xff0c;需要大量美术人员长时间的进行团队合作完成的&#xff0c;并且完成一版之后&#xff0c;后续也还有更新维护的需求。所以UE的Editor也提供了对资产版本管理的功能&#xff…

虚幻4与Unity: 哪个游戏引擎是开发者的最爱?

转自: http://www.narkii.com/club/thread-335184-1.html 虚幻4与Unity&#xff1a; 哪个游戏引擎是开发者的最爱&#xff1f; 虚幻4 &#xff08;UE4&#xff09; 和Unity是目前市场上最热门的游戏引擎&#xff0c;也各自拥有为数众多的开发者&#xff0c;虽然一些大型的游戏公…

【UE4】虚幻引擎运行流程

前言 当前版本&#xff1a;4.26.2 本文原创 说明&#xff1a;有些名词保持英文可能更容易理解 目录 游戏流程总览 各个平台启动引擎的入口函数 引擎入口函数 GuardedMain() EnginePreInit 引擎预初始化 EngineInit 引擎初始化 EngineLoop 引擎主循环 EngineExit 引擎退…

【虚幻引擎】UE4/UE5插件

一、插件分类 插件分为七大类 Blank&#xff1a;空白插件&#xff0c;可以从头开始自己定义想要的插件风格和内容&#xff0c;用此模板创建的插件不会有注册或者菜单输入。BlueprintLibrary&#xff1a;创建一个含有蓝图函数库的插件&#xff0c;此模板函数都是静态全局函数&…

虚幻引擎5 C++游戏开发教程

虚幻引擎5 C游戏开发教程 与 Epic Games 合作创建。从基础开始学习 C&#xff0c;同时在 Unreal 中制作您的前 5 个视频游戏 课程英文名&#xff1a;Unreal Engine 5 C Developer Learn C & Make Video Games 此视频教程共5.5小时&#xff0c;中英双语字幕&#xff0c;画…

【虚幻引擎】带你了解UE4

行业前景 越来越多的开发者&#xff0c;开发商投入到UE4中&#xff0c;简单来说就是使用UE4的人越来越多&#xff0c;许多独立游戏&#xff0c;大型的游戏&#xff0c;大公司例如“腾讯&#xff0c;网易”都在使用UE4来开发&#xff0c;加上UE4对开发者来说使用完全免费&#…