UE Debug学习之路
本文最后更新于212 天前,其中的信息可能已经过时,如有错误请联系作者

尽量不要使用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);

三个输入分别为:

  1. 日志显示的类别名,我们可以为不同模块的日志添加不同的类别名,以便我们使用过滤器快速筛选需要的日志消息 ;
  2. 日志显示的级别,级别越高显示的内容越重要,显示的颜色越鲜艳,相应的输出消息数量也越少;
  3. 输出的内容,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中修改

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇