虚幻引擎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:
| Defaults | Instance | Event Graph(l藍圖的事件圖表) | |
| Read Only | VisibleAnyWhere | BluePrintReadOnly | |
| VisibleDefaultsOnly | VisibleInstanceOnly | ||
| Read/Write(set) | EditAnyWhere | BluePrintReadWrite | |
| 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函數
?
在之前的函數中,加入一句:
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,我們有如下思路:
第一步:我們先定義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:
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();} }到此為止,這個小游戲就完成了。
總結
以上是生活随笔為你收集整理的虚幻引擎C++开发学习(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 运行时读取PAK文件
- 下一篇: AtCoder - 2365 Came