尽量不要使用UE的热重载,即修改保存C++代码后不关闭UE直接使用它的编译功能。因为这个功能不太稳定,比较容易出Bug,就算UE提示编译完成也有几率出错
1. 输出调试信息
这部分将介绍几个在UE开发中输出调试信息的方法,方便日后我们开发时的debug工作。
首先,使用日志是各种计算机开发都必不可少的环节,在UE中可以使用UE_LOG来输出日志。课程给出了英文的wiki,其中对日志做了比较详细的介绍。示例用法如下:
UE_LOG(LogTemp, Log, TEXT("OtherActor is %s, at game time %f"), *GetNameSafe(OtherActor), GetWorld()->TimeSeconds);
三个输入分别为:
- 日志显示的类别名,我们可以为不同模块的日志添加不同的类别名,以便我们使用过滤器快速筛选需要的日志消息 ;
- 日志显示的级别,级别越高显示的内容越重要,显示的颜色越鲜艳,相应的输出消息数量也越少;
- 输出的内容,TEXT()用于将字符串转为UE所需格式,且使用UNICODE编码支持更多符号。GetNameSafe在对象为空时返回空,这样就不需要额外判空,此外它将返回FString类型,要注意字符串类型前要加一个星号。
对于具有图形化界面的UE来说,它不止可以在日志输出打印调试信息,还可以直接在运行关卡时实时打印各种调试信息,如我们之前在打开宝箱时使用过的射线检测、以及魔法攻击时显示的圆形等等。除了直接显示图形,UE也支持显示字符串。这段代码实现了,在魔法粒子击中的地方显示位置信息的字符串:
FString CombStr = FString::Printf(TEXT("Hit at %s"), *Hit.ImpactPoint.ToString());
// 获取世界,位置,打印的内容,需要attach的actor,颜色,持续时间,是否有影子
DrawDebugString(GetWorld(), Hit.ImpactPoint, CombStr, nullptr, FColor::Green, 2.0f, true);
这样保存编译后进入关卡,就可以看见我们的调试信息,其窗口可在 窗口 -> 开发者工具 -> 输出日志 中选择打开。左键控制Gideon攻击后,可以看见我们设置的LogTemp被成功打印出来。
如果是在VS中利用调试打开的UE,同样的日志消息还会输出到VS的输出窗口中
在游戏中,利用DrawDebugString打印的字符串也正常显示了
2. 使用断点
有程序开发经验的朋友应该对打断点都不陌生,断点调试是Debug的一大利器。通过使用断点让程序停在某一个地方,然后在开发者的监视下逐步执行,这对找到程序bug和梳理程序运行流程都有很大帮助。UE开发的断点有两种,一种是普通的利用VS打断点,另一种则是直接在蓝图系统中打断点。
在VS打断点的方法很简单,在任意一行需要中断的代码前点击一下,显示一个红色的圆圈即可,这里我选择在控制炸药桶爆炸的代码处设置断点。
随后在VS中点击调试(或直接按F5),运行关卡,向炸药桶发射魔法粒子。此时会直接跳回VS界面,断点处会显示黄色的小箭头来表示代码执行的位置,此时就可以进行各种调试操作了。如果此时回到UE界面,会发现整个UE都卡住,直到我们在VS点击继续才会继续运行。
3. 使用断言
断言也是在Debug阶段广泛使用的手段,它类似于if判断会验证一个表达式的真假,但区别在于断言不需要像if那样编写大量重复的判断和打印日志代码,一旦断言检查到不符合预期,程序会直接中断甚至抛出异常,利于开发者定位问题;且断言只在debug阶段有效,对于打包好的程序是不会生效的。
以角色的PrimaryAttack作为例子,首先将PrimaryAttack_TimeElapsed函数添加一个判空的逻辑,这完全是出于提高程序健壮性的正确考虑:
void ASCharacter::PrimaryAttack_TimeElapsed() {
if (ProjectileClass) {
// 获取模型右手位置
FVector RightHandLoc = GetMesh()->GetSocketLocation("Muzzle_01");
// 朝向角色方向,在角色的右手位置生成
FTransform SpawnTM = FTransform(GetActorRotation(), RightHandLoc);
// 此处设置碰撞检测规则为:即使碰撞也总是生成
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.Instigator = this;
GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, SpawnParams);
}
}
但这样的写法却可能产生类似这样的问题:在这段代码开发很久之后,你已经遗忘了具体细节,或者这根本就由你的另一个同事开发。偶然情况下,调用这个函数时ProjectileClass为空,此时这段代码不会运行,但程序也没有任何提示,这无疑加大了debug的工作量。
要解决这个问题,一种方法是在else写好错误的输出信息,另一种就是使用UE的断言。只需将if处改为 if (ensure(ProjectileClass)),ensure就是UE中的断言函数。此外还有check,但两者不同之处在于ensure提示错误但不会中断程序,check会直接结束你的关卡测试,所以通常使用ensure。
为了测试ensure的效果,我们在UE中手动指定Player蓝图类的ProjectileClass为空
添加Debug指令
添加下述的代码,我们在游戏中调用控制台就可以恢复血量
//SCharacter.h
public:
UFUNCTION(Exec)
void HealSelf(float Amount = 100);
//SCharacter.cpp
void ASCharacter::HealSelf(float Amount)
{
AttributeComp->ApplyHealthChange(this,Amount);
}
通过下面的代码实现消灭所有敌人
//SGameModeBase.h
public:
UFUNCTION(Exec)
void KillAll();
//SGameModeBase.cpp
void ASGameModeBase::KillAll()
{
for (TActorIterator<ASAICharacter> It(GetWorld()); It; ++It)
{
ASAICharacter* Bot = *It;
USAttributeComponent* AttributeComp = Cast<USAttributeComponent>(Bot->GetComponentByClass(USAttributeComponent::StaticClass()));
if (ensure(AttributeComp) && AttributeComp->IsAlive())
{
AttributeComp->Kill(this);
}
}
}
//SAttributeComponent.h
public:
UFUNCTION(BlueprintCallable)
bool Kill(AActor* InstigatorActor);
//SAttributeComponent.cpp
bool USAttributeComponent::Kill(AActor* InstigatorActor)
{
return ApplyHealthChange(InstigatorActor,-GetHealthMax());
}
通过下面代码,实现God上帝无敌功能
//SAttributeComponent.cpp
bool USAttributeComponent::ApplyHealthChange(AActor* InstigatorActor,float Delta)
{
if(!GetOwner()->CanBeDamaged())
{
return false;
}
float OldHealth = Health;
Health = FMath::Clamp(Health+Delta,0.0f,HealthMax);
float ActualDelta = Health - OldHealth;
OnHealthChanged.Broadcast(InstigatorActor,this,Health,ActualDelta);
return ActualDelta != 0;
}
用于Degug的控制台变量及一些优化
//SGameModeBase.cpp
static TAutoConsoleVariable<bool> CVarSpawnBots(TEXT("su.SpawnBots"), true, TEXT("Enable spawning of bots via timer."), ECVF_Cheat);
void ASGameModeBase::SpawnBotTimerElapsed()
{
if(!CVarSpawnBots.GetValueOnGameThread())
{
UE_LOG(LogTemp, Warning, TEXT("Bot spawning disabled via cvar 'CVarSpawnBots'."));
return;
}
}
//SAttributeComponent.cpp
static TAutoConsoleVariable<float> CVarDamageMultiplier(TEXT("su.DamageMultiplier"), 1.0f, TEXT("Global Damage Modifier for Attribute Component."), ECVF_Cheat);
bool USAttributeComponent::ApplyHealthChange(AActor* InstigatorActor,float Delta)
{
if(Delta < 0.0f)
{
float DamageMultipier = CVarDamageMultiplier.GetValueOnGameThread();
Delta *= DamageMultipier;
}
}
//SInteractionComponent.cpp
static TAutoConsoleVariable<bool> CVarDebugDrawInteraction(TEXT("su.InteractionDebugDraw"), false, TEXT("Enable Debug Lines for Interact Component."), ECVF_Cheat);
void USInteractionComponent::PrimaryInteract()
{
bool bDebugDraw = CVarDebugDrawInteraction.GetValueOnGameThread();
for(FHitResult Hit : Hits)
{
if(bDebugDraw)
{
DrawDebugSphere(GetWorld(),Hit.ImpactPoint,Radius,32,LineColor,false,2.0f);
}
AActor* HitActor = Hit.GetActor();
if(HitActor)
{
if(HitActor->Implements<USGameplayInterface>())
{
APawn* MyPawn = Cast<APawn>(MyOwner);
ISGameplayInterface::Execute_Interact(HitActor,MyPawn);
break;
}
}
}
if(bDebugDraw)
{
DrawDebugLine(GetWorld(),EyeLocation,End,LineColor,false,2.0f,0,2.0f);
}
}
通过上面的代码,我们在控制台中就可以轻松的进行游戏中的调试
su.CVarDebugDrawInteraction 1 //设置为1来开启交互时绘制的线条
su.CVarSpawnBots 0 //设置为0来停止Bots的产生
su.CVarDamageMultiplier 5 //设置为5来提高弹丸的伤害
建立新的类
//SGameplayFunctionLibrary.h
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "SGameplayFunctionLibrary.generated.h"
/**
*
*/
UCLASS()
class ACTIONROGUELIKE_API USGameplayFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable,Category="GamePlay")
static bool ApplyDamage(AActor* DamageCauser,AActor* TargetActor,int DamageAmount);
};
//SGameplayFunctionLibrary.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "SGameplayFunctionLibrary.h"
#include "SAttributeComponent.h"
bool USGameplayFunctionLibrary::ApplyDamage(AActor* DamageCauser, AActor* TargetActor, int DamageAmount)
{
USAttributeComponent* AttributeComp = USAttributeComponent::GetAttributes(TargetActor);
if(AttributeComp)
{
return AttributeComp->ApplyHealthChange(DamageCauser,-DamageAmount);
}
return false;
}
bool USGameplayFunctionLibrary::ApplyDirctionalDamage(AActor* DamageCauser, AActor* TargetActor, int DamageAmount,
const FHitResult& HitResult)
{
if(ApplyDamage(DamageCauser,TargetActor,DamageAmount))
{
UPrimitiveComponent* HitComp = HitResult.GetComponent();
if(HitComp && HitComp->IsSimulatingPhysics(HitResult.BoneName))
{
HitComp->AddImpulseAtLocation(-HitResult.ImpactNormal * 300000.f,HitResult.ImpactPoint,HitResult.BoneName);
}
return true;
}
return false;
}
//SMagicProjectile.cpp
void ASMagicProjectile::OnActorOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if(OtherActor && OtherActor != GetInstigator())
{ if(USGameplayFunctionLibrary::ApplyDirctionalDamage(GetInstigator(),OtherActor,DamageAmount,SweepResult))
{
Destroy();
}
}
}
//SAICharacter.cpp
ASAICharacter::ASAICharacter()
{
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_WorldDynamic,ECR_Ignore);
GetMesh()->SetGenerateOverlapEvents(true);
}
void ASAICharacter::OnHealthChanged(AActor* InstigatorActor, USAttributeComponent* OwningComp, float NewHealth,
float Delta)
{
//ragdoll
GetMesh()->SetAllBodiesSimulatePhysics(true);
GetMesh()->SetCollisionProfileName("Ragdoll");
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GetCharacterMovement()->DisableMovement();
}
//SCharacter.h
virtual FVector GetPawnViewLocation() const override;
//SCharacter.cpp
FVector ASCharacter::GetPawnViewLocation() const
{
return CameraComp->GetComponentLocation();
}
//SAttributeComponent.cpp
bool USAttributeComponent::ApplyHealthChange(AActor* InstigatorActor,float Delta)
{
if(!GetOwner()->CanBeDamaged() && Delta < 0.0f)
{
return false;
}
}
我们把之前SCharater中的蓝图删掉
以Controller为蓝图创建一个PlayerController,将之前在SCharacter的蓝图连接在这里
在GameModeBP中修改