Loading

UE新手入坟笔记

输出调试信息

屏幕输出

在蓝图节点中可以通过Print String来在屏幕上绘制信息

在代码中的做法就比较复杂,通过AddOnScreenDebugMessage来实现蓝图中Print String的效果

/**
*	This function will add a debug message to the onscreen message list.
*	It will be displayed for FrameCount frames.
*
*	@param	Key				A unique key to prevent the same message from being added multiple times.
*	@param	TimeToDisplay	How long to display the message, in seconds.
*	@param	DisplayColor	The color to display the text in.
*	@param	DebugMessage	The message to display.
*/
void AddOnScreenDebugMessage(uint64 Key, float TimeToDisplay, FColor DisplayColor, const FString& DebugMessage, 
                             bool bNewerOnTop = true, const FVector2D& TextScale = FVector2D::UnitVector);

AddOnScreenDebugMessage的实现是修改一个优先级队列(或是一种储存Message的数据结构),然后在DrawOnscreenDebugMessages访问该结构并将其讲消息绘制出来

/**
*  Renders warnings about the level that should be addressed prior to shipping
*
*  @param World         The World to render stats about
*  @param Viewport          The viewport to render to
*  @param Canvas        Canvas object to use for rendering
*  @param CanvasObject       Optional canvas object for visualizing properties
*  @param MessageX          X Pos to start drawing at on the Canvas
*  @param MessageY          Y Pos to draw at on the Canvas
*
*  @return The Y position in the canvas after the last drawn string
*/
float DrawOnscreenDebugMessages(UWorld* World, FViewport* Viewport, FCanvas* Canvas, UCanvas* CanvasObject, 
                                float MessageX, float MessageY);

而通过观察源代码可以发现,DrawOnscreenDebugMessages的本质是DrawItem(万物归宗)

// Class - FCanvas
/**
* Draw a CanvasItem at the given coordinates
*
* @param Item			Item to draw
* @param InPosition	Position to draw item
*/
ENGINE_API void DrawItem(FCanvasItem& Item, const FVector2D& InPosition);

实际使用起来是如下的形式

GEngine->AddOnScreenDebugMessage(0, 2, FColor::Black, TEXT("Hello World"));

控制台输出

控制台输出的消息可以在UE Editor的Output窗口中或是Rider中的Debug Output中找到。类似Unity中的Debug.Log

// 输出 LogTemp: Hello World
UE_LOG(LogTemp, Log, TEXT("Hello World"));
UE_LOG(LogTemp, Warning, TEXT("Warning World"));
UE_LOG(LogTemp, Error, TEXT("Error World"));

除了单纯的输出字符之外,还可以搭配变量,其格式与C语言非常相似

UE_LOG(LogTemp, Log, TEXT("Current Health Is %f"), Health);

// %s strings are wanted as TCHAR* by Log, so use *FString()
FString MyMessage = FString("Unreal Engine").Append("Unity");
UE_LOG(LogTemp, Log, TEXT("%s"), *MyMessage);

UE_LOG(LogTemp, Log, TEXT("%s"), *FVector::ZeroVector.ToString());

除了各种类型能通过ToString转换为FString外,FString本身拥有一个静态方法用于转换浮点数FString::SanitizeFloat()

线框绘制

方法非常多,这里就不一一列举了

通过控制台进行调试

在文件首部完成静态变量以及全局变量的创建,然后在~键打开的控制台中进行赋值,就可以“开启”一些特定的功能

static int32 DebugLogTest = 0;
FAutoConsoleVariableRef CVARDebugWeaponDrawing(TEXT("ATestTPCharacter.DebugLogTest"), DebugLogTest, TEXT("Log Something"), ECVF_Cheat);
// 某段代码中
if (DebugLogTest == 1)
{
    UE_LOG(LogTemp, Log, TEXT("Now Log Start"));
}

UPROPERTY

指针与普通变量

为了让我们能在UE Editor的Details面板中都修改一个类中的属性,我们通常会将其标志为EditAnywhere

UPROPERTY(EditAnywhere)
float Health;

如果我们只是想要显示在面板中,而不能修改,那么可以将其标志为VisibleAnywhere

但是对于指针类型,例如USpringArmComponent*而言,我们在Details面板中修改的是指针所指向的类的数据,而不是修改指针的指向,所以对USpringArmComponent*标记为VisibleAnywhere,也是可以在面板中修改USpringArmComponent的成员属性的

UPROPERTY(VisibleAnywhere)
class USpringArmComponent* CameraBoom;

Anywhere,DefaultOnly,InstanceOnly

这里以float举例

UPROPERTY(EditAnywhere)
float HealthAnywhere;

UPROPERTY(EditDefaultsOnly)
float HealthDefaults;

UPROPERTY(EditInstanceOnly)
float HealthInstance;

顾名思义,在Content中的Blueprint Class,可以看作它是Default的,因此HealthAnywhereHealthDefaults是可见的并可修改的

而当把Blueprint Class拖拽到场景中,或者通过代码将其实例化了,那么可以看作它是一个Instance,因此HealthAnywhereHealthInstance是可见的并可修改的

BlueprintReadWrite

  • BlueprintReadOnly:该属性只能在蓝图节点中被读取
  • BlueprintReadWrite:该属性能够在蓝图节点中被读取或写入

UFUNCTION

A Private/Protected function cannot be a BlueprintImplementableEvent/BlueprintNativeEvent

BlueprintCallable可以是私有的或是保护的

BlueprintCallable

证明该方法暴露在蓝图中,可以通过蓝图节点调用

BlueprintImplementableEvent

证明该方法不能在C++中提供实现,而是在蓝图中实现

BlueprintNativeEvent

UFUNCTION(BlueprintNativeEvent)
void TriggerAction();

virtual void TriggerAction_Implementation();

如果蓝图中对该方法进行了实现,那么在调用时会调用蓝图的版本;如果蓝图中没有实现,那么会调用C++的版本

查看蓝图中是否实现了对应的接口:[UE-接口]https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/GameplayArchitecture/Interfaces/

UE中的字符串

从原本C++中的const char*std::string到现在UE中的FStringFNameFText,甚至是TEXT

FName

FName用于命名。例如插槽的名称,骨骼的名称。一般名称都会用于比较,因此UE特此做出了FName这一优化。FName中重载了operator==,在比较两名称是否相等时,采用的是比较“Hash”的方法,因此时间复杂度是O(1)

  • FName变量一经创建不可修改
  • FName不区分大小写
/** Get the socket we are attached to. */
UFUNCTION(BlueprintCallable, Category="Utilities|Transformation")
FName GetAttachSocketName() const;
/** 
 * Get Bone Name from index
 * @param BoneIndex Index of the bone
 *
 * @return the name of the bone at the specified index 
 */
UFUNCTION(BlueprintCallable, Category="Components|SkinnedMesh")
FName GetBoneName(int32 BoneIndex) const;

FText

FText用于向玩家显示本文,因此也涉及到了本地化。例如UTextBlock的设置文本

/**
* Directly sets the widget text.
* Warning: This will wipe any binding created for the Text property!
* @param InText The text to assign to the widget
*/
UFUNCTION(BlueprintCallable, Category="Widget", meta=(DisplayName="SetText (Text)"))
virtual void SetText(FText InText);

而如何创建本地化文本也是一个知识(这里不详细讲)

#define LOCTEXT_NAMESPACE "MyNamespace"

FText KillText = LOCTEXT("KillInfo", "PlayerA killed PlayerB");
// Codes...

#undef LOCTEXT_NAMESPACE

FString

功能非常完善的字符串类,较为接近C#中的string或C++中的std::string

需要注意的一点是当函数的参数要求是TCHAR类型时,需要使用*转换

FString MyMessage = FString("Unreal Engine");
UE_LOG(LogTemp, Log, TEXT("%s"), *MyMessage);

TEXT宏

目前的理解是使用TEXT包裹字符串能进行某种转换,能避免乱码的发生

FString MyMessage = FString(TEXT("Unreal Engine"));

相互转化

FString转换至FName时会丢失原始字符串的大小写信息。FText转换为FString会丢失本地化信息。

UE中的委托

目前UE中的委托可以分为

  • 单播委托
  • 动态单播委托
  • 多播委托

单播委托

单播委托可以具有返回值;单播委托在调用时会执行最后一个被绑定的方法

单播委托的payload机制允许提前绑定函数参数,但却不提供类似C++标准库中的std::placeholders

DECLARE_DELEGATE_OneParam(FDamageDelegate, int, data)

class DamageHandler
{
public:
	void operator()(int data, bool isExplore) const
	{
		GEngine->AddOnScreenDebugMessage(0, 2,FColor::Black, TEXT("Handle Damage"));
	}
};
DamageHandler DamageHandler;
FDamageDelegate DamageDelegate;

// payload进行默认参数的绑定
DamageDelegate.BindRaw(&DamageHandler, &DamageHandler::operator(), false);

DamageDelegate.ExecuteIfBound(1);
// 在DamageHandler生命周期结束时需要解绑
DamageDelegate.Unbind();

动态单播委托

动态意味着能够被序列化,即能够在蓝图中使用

由于动态单播委托只能通过函数名称对UFUNCTION进行绑定,在绑定时需要遍历函数名称,因此效率低,使用BindUFunction进行绑定

动态单播委托与单播委托一致,能拥有返回值

多播委托

多播委托不允许有返回值,且执行委托时各个函数的调用顺序与绑定的顺序无关

多播委托可以看作是单播委托维护了一个TArray

动态多播委托

最常用的委托,DECLARE_DYNAMIC_MULTICAST_DELEGATE

UE中的断言

与C++中的静态断言static_asset不同,ensurecheck的作用相当于在条件不成立的时候给当前语句“打上”一个断点。check的关键不同点在于它默认不会在发行版本中运行

UE4官方文档-断言

check

check会在Debug,Development,Test和Shipping Editor(发布编辑器)中运行

checkSlow

DO_GUARD_SLOW宏只在Debug模式下为1,当该宏为1时,checkSlow会运行

如在development编译环境下,checkSlow其实是空宏,因此以下代码可以通过编译

checkSlow(abc);

ensure

与上述两个check系列不同,check系列在不满足条件时会中断引擎,而ensure不会中断,会产生一个堆栈报告

GetClass和StaticClass

GetClass是类UObjectBase中的方法,UObjectBaseUObject的基类

/** Class the object belongs to. */
UClass* ClassPrivate;

/** Returns the UClass that defines the fields of this object */
FORCEINLINE UClass* GetClass() const
{
    return ClassPrivate;
}

StaticClass是静态方法,由UObject的宏生成

设有蓝图BP_TestActor继承自ATestActor,在ATestActor中调用GetClass,获得的是BP_TestActorUClass*;在ATestActor中调用StaticClass,获得的是ATestActorUClass*

ATestActorAActor为例,ATestActorAActor的派生类,可以通过IsA来判断二者的父子关系

ATestActor* TestActor;
AActor* Actor;
// 两种比较方式 true
bool result = TestActor->IsA(Actor->GetClass());
bool resultStatic = TestActor->IsA(AActor::StaticClass());

或通过IsChildOf来比较判断,IsA的底层也是调用到该方法,因此二者的结果是一致的

bool result = TestActor->GetClass()->IsChildOf(Actor->GetClass());
bool resultStatic = TestActor->GetClass()->IsChildOf(AActor::StaticClass());

IsChildOf采用迭代的方式实现

bool UStruct::IsChildOf( const UStruct* SomeBase ) const
{
   if (SomeBase == nullptr)
   {
      return false;
   }

   bool bOldResult = false;
   for ( const UStruct* TempStruct=this; TempStruct; TempStruct=TempStruct->GetSuperStruct() )
   {
      if ( TempStruct == SomeBase )
      {
         bOldResult = true;
         break;
      }
   }
   return bOldResult;
}
  • 同一个类的不同实例,GetClass相同吗?相同
  • 一个类的GetClass和它的StaticClass相同吗?相同

代码中的各种Get

对于初学者而言,总是会傻傻的分不清

GetOwner

追本溯源,这个方法来自类AActor

/** Get the owner of this Actor, used primarily for network replication. */
UFUNCTION(BlueprintCallable, Category=Actor)
AActor* GetOwner() const;

ACharacter

假设一个Listen-server的场景,场景中有两个ACharacter,代表两个玩家

那么在Server端,两个ACharacter的Owner分别是它们的APlayerController,代表它们的两个控制器

那么在Client端,ROLE_AutonomousProxyACharacter的Owner是服务器同步过来的APlayerController;而又因为Client上只能存在一个AController,因此ROLE_SimulatedProxyACharacter的Owner是nullptr

在这种情况下,GetOwnerGetController的结果都是一样的,追到底都是APlayerController

那么为什么会一样呢,因为在PossessedByUnPossessed的内部会通过SetOwner来将结果设置为AController

void APawn::PossessedBy(AController* NewController)
{
    SetOwner(NewController);
    AController* const OldController = Controller;
    Controller = NewController;
    // Codes...
}

void APawn::UnPossessed()
{
    // Codes...
    SetOwner(nullptr);
    Controller = nullptr;
    // Codes...
}

AActor

那么如果我只是简单的在场景中创建一个AActor,那么默认它的Owner就是nullptr,这个时候我们需要SetOwner来对齐指定一个Owner。例如一把枪,它的Owner就是持枪的人,也就是一个ACharacter,而ACharacter的Owner,理所应当的,就是AController(注意,这里持枪的人可能是玩家的控制的也可能是AI控制的,因此既有可能是APlayerController也有可能是AAIController

UActorComponent

虽然它是继承自UObject的,但是它自己内部也实现了一个GetOwner。以UStaticMeshComponent为例,它的Owner就是他所挂载的AActor。以一把抢举例子,它肯定挂载了不少组件,不管这些组件的父子层级如何,它们的Owner都是这把枪

GetController

追本溯源,这个方法来自类APawn,目的是返回控制该APawn的AController,拓展的来说,这对应到AController中的PossessUnPossess

/** Returns controller for this actor. */
UFUNCTION(BlueprintCallable, Category=Pawn)
AController* GetController() const;

GetPawn

APlayerState中和AController中都有这个方法,懂的都懂,不必多说

/** Return the pawn controlled by this Player State. */
APawn* GetPawn() const { return PawnPrivate; }
/** Getter for Pawn */
FORCEINLINE APawn* GetPawn() const { return Pawn; }

GetInstigator

追本溯源,这个方法来自类AActor

/** Pawn responsible for damage and other gameplay events caused by this actor. */
UPROPERTY(BlueprintReadWrite, ReplicatedUsing=OnRep_Instigator, meta=(ExposeOnSpawn=true, AllowPrivateAccess=true), Category=Actor)
class APawn* Instigator;

/** Returns the instigator for this actor, or nullptr if there is none. */
UFUNCTION(BlueprintCallable, meta=(BlueprintProtected = "true"), Category="Game")
APawn* GetInstigator() const;

ACharacter

还是假设一个Listen-server的场景,场景中有两个ACharacter,代表两个玩家。

那么不管在Server还是在Client,ACharacter的Instigator都是它们自己。请看源码

void APawn::PreInitializeComponents()
{
    Super::PreInitializeComponents();
    if (GetInstigator() == nullptr)
    {
        SetInstigator(this);
    }
    // Codes...
}

AActor

Instigator可以翻译为煽动者。比如当一名玩家中弹时,伤害的来源是这颗子弹,但是煽动者是开枪的玩家。

在ShooterGame模板中,当实例化一颗子弹的时候,会通过调用子弹的SetInstigator和武器的GetInstigator来将开枪的玩家设置为子弹的Instigator

void AShooterWeapon_Projectile::ServerFireProjectile_Implementation(FVector Origin, FVector_NetQuantizeNormal ShootDir)
{
    FTransform SpawnTM(ShootDir.Rotation(), Origin);
    AShooterProjectile* Projectile = Cast<AShooterProjectile>
        (UGameplayStatics::BeginDeferredActorSpawnFromClass(this, ProjectileConfig.ProjectileClass, SpawnTM));
    if (Projectile)
    {
        Projectile->SetInstigator(GetInstigator());
        Projectile->SetOwner(this);
        Projectile->InitVelocity(ShootDir);

        UGameplayStatics::FinishSpawningActor(Projectile, SpawnTM);
    }
}

意会环节

Instigator本身的目的可能就是为了储存一个引用以供日后调用,只是它在AActor中被规定为是APawn*类型,毕竟UE是以FPS游戏起家的,那么如此的设计看来也不是完全没有道理

An Actor is going to be a lower in the hierarchy, meaning you could have just about any gameplay class as the instigator. As in, anything that inherits from Actor can be the instigator, such as Pawn, Character, HUD, GameMode, Controller, AI Controller, PlayerController, etc...

This includes anything you've created from Actor, such as weapons, projectiles, and so on.

Instigator is just a way to store a reference so you can then understand where it originated. If you wanted to have another reference, you can create your own in the class you're working with and use that instead of the instigator, which could fix your issue with being limited to Pawn.

GetInstigateController

都是封装

AController* AActor::GetInstigatorController() const
{
   return Instigator ? Instigator->Controller : nullptr;
}

该方法返回的结果,常见的一种用法是在ApplyDamage

/** Hurts the specified actor with generic damage.
* @param DamagedActor - Actor that will be damaged.
* @param BaseDamage - The base damage to apply.
* @param EventInstigator - Controller that was responsible for causing this damage (e.g. player who shot the weapon)
* @param DamageCauser - Actor that actually caused the damage (e.g. the grenade that exploded)
* @param DamageTypeClass - Class that describes the damage that was done.
* @return Actual damage the ended up being applied to the actor.
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="Game|Damage")
static float ApplyDamage(AActor* DamagedActor, float BaseDamage, AController* EventInstigator, AActor* DamageCauser, 
                         TSubclassOf<class UDamageType> DamageTypeClass);

UI与C++

作为一篇入门笔记,这里仅介绍UMG与C++的搭配使用,以及如何将UMG添加到HUD上。这种处理方式和Unity较为相似,在Editor中对UI进行摆放,在Code中编写逻辑

创建并添加

首先应该创建一个C++ Widget基类,以供WBP继承。这里使用属性宏,使指针在初始化阶段会自动和WBP中的同名控件进行绑定,省去了手动查找的麻烦

UCLASS()
class SHOOTERGAME_API UKillInformationWidget : public UUserWidget
{
	GENERATED_BODY()

	UPROPERTY(Meta = (BindWidget))
	UTextBlock* KillerName;
	
	UPROPERTY(Meta = (BindWidget))
	UTextBlock* VictimName;
	
	UPROPERTY(Meta = (BindWidget))
	UImage* WeaponIcon;
};

假设这个UI是需要一开始就显示在游戏中的,那么可以在HUD的BeginPlay中创建并添加UI

// ShooterHUD.h
UPROPERTY(EditAnywhere, Category = "Widgets")
TSubclassOf<class UKillInformationWidget> KillInformationWidget;
// ShooterHUD.cpp
void AShooterHUD::BeginPlay()
{
	Super::BeginPlay();
	UKillInformationWidget* Widget = CreateWidget<UKillInformationWidget>(GetWorld()->GetGameInstance(), KillInformationWidget);
	if (Widget)
	{
		Widget->AddToViewport();
        
        // 销毁则调用
        // Widget->RemoveFromViewport();
	}
}
// 当然也可以通过代码读取资源的方式
static ConstructorHelpers::FClassFinder<UKillInformationWidget> WidgetClass(TEXT("/Game/Blueprints/Widget/WBP_KillInformationWidget"));
// 通过WidgetClass.Class获取TSubclassOf<T>类型

Widget的生命周期

以上述添加代码为例,会以此调用如下的函数

  • CreateWidget
  • Initialize
  • NativeOnInitialized
  • AddToScreen
  • NativePreConstruct
  • NativeConstruct
  • NativeTick
  • NativeDestruct(销毁的时候调用)

UPROPERTY(Meta = (BindWidget))是在Initialize中完成绑定的(在NativeOnInitialized执行前就完成了),因此对事件的绑定放在NativeOnInitialized中。

如果不想通过宏绑定,那么也可以手动绑定

bool UUHealthBar::Initialize()
{
	const bool Result = Super::Initialize();
	PlayerName = Cast<UTextBlock>(GetWidgetFromName(TEXT("PlayerName")));
    if (!PlayerName)
    {
        return false;
    }
	return Result;
}

Animation与C++

UAnimInstance

UUserWidget和UMG一样,我们同样可以使用UAnimInstance来搭配动画蓝图使用

UCLASS()
class SHOOTERGAME_API UShooterTPPAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
protected:
	virtual void NativeUninitializeAnimation() override;
	
	virtual void NativeInitializeAnimation() override;
	
	virtual void NativeBeginPlay() override;

	virtual void NativeUpdateAnimation(float DeltaSeconds) override;
	
	virtual void NativePostEvaluateAnimation() override;
};

这里简单介绍一下UAnimInstance初始化时的生命周期

在正常的游戏流程中

  • NativeUninitializeAnimation
  • NativeInitializeAnimation
  • NativeBeginPlay
  • 进入游戏循环 以下两个函数反复调用
  • NativeUpdateAnimation
  • NativePostEvaluateAnimation

相反的,如果只是编译成功或进行动画加载,那么会只会执行BeginPlay之前的函数

  • NativeUninitializeAnimation
  • NativeInitializeAnimation

如果是在动画蓝图界面中 那么会进入Tick循环,注意此时BeginPlay并不会被调用,BeginPlay只会在开始游戏进程的时候才会被调用

  • NativeUpdateAnimation
  • NativePostEvaluateAnimation

那么如此看来我们就可以在NativeInitializeAnimation()中进行一些属性的初始化,然后在NativeUpdateAnimation()去每帧更新

动画蓝图常用节点及属性

State Transition面板-Transition

  • Priority Order:数值越高,代表优先级越高。再多个切换条件同时满足的时候,会选择优先级高的Transition

  • Bidirectional:勾选后使Transition变为双向(共用一个条件?)

  • Blend Logic:当SourcePose是一个混合空间的时候,Transition在执行Blend的时候SourcePose很有可能会发生变化,导致混合结果的混乱

    • Standard Blend:Evaluate both SourcePose and TargetPose
    • Inertialization:惯性插值,Evaluate only TargetPose
  • Automatic Rule Based on Sequence Player in State:勾选上代表当动画播放完成时才会进行动画的切换

State Transition面板-Blend Settings

  • Duration:状态间切换的混合时间
  • Mode:混合所使用的曲线,具体可按Ctrl+Alt查看曲线函数图像
  • Custom Blend Curve:自定义混合曲线
  • Blend Profile:混合配置,在配置中可以指定混合过程中骨骼的权重,权重越大,混合时候变化越快

State Transition条件常用节点

  • CurrentTime
  • CurrentStateTime
  • GetRelevantAnimTimeRemaining
  • GetRelevantAnimTime
  • StateWeight

UE4中的坐标系

UE4是左手坐标系。往前是X,往上是Z,往右是Y

  • 绕X轴旋转称作Roll,翻滚。(歪头)其中往右歪头数值变大,往左歪头数值变小
  • 绕Y轴旋转称作Pitch,俯仰(点头)其中抬头数值变大,低头数值变小
  • 绕Z轴旋转称作Yaw,偏航角(转头)往右转头数值变大,往左转头数值变小

在UE4 Editor中,Rotation的X代表Roll,Y代表Pitch,Z代表Yaw,与上文对应

在C++的FRotator构造函数中,一定要注意构造的顺序,与编辑器面板不同,构造函数的顺序可以看作是Y-Z-X

/**
* Constructor.
*
* @param InPitch Pitch in degrees.
* @param InYaw Yaw in degrees.
* @param InRoll Roll in degrees.
*/
FORCEINLINE FRotator(float InPitch, float InYaw, float InRoll);

脚步IK

由图可得,通过人物碰撞体的1/4高减去射线击中障碍物的距离,可以得到脚步需要抬高的数值。将这个数值应用在IK节点上就可以实现抬脚的效果

但是一般情况下由于人物胶囊碰撞体比较大,所以会出现角色处在台阶半空中,使其中一只脚悬空的状况。此时我们可以对比左右两脚需要抬起的高度,选择一个绝对值最大值作为BodyOffSet,然后让人物整体骨骼下降一个BodyOffSet。此时情况就与上图相同了

最后若人物处在斜面上,自然也需要计算出斜面的坡度,然后引用到脚部的旋转量上。如图所示,将斜面的法向量进行分解,然后得到它在X和Z方向上的距离,此时进行一个arctan计算,就可以求得夹角(x / z = tanθ)

这里进行反正切求出了角的数值,但是以X轴正方向来说,还需要计算出旋转角的正负。这里假设角色是往X轴正方向看的,那么也就代表了脚部需要往下看,所以是负角度

但是3D游戏中斜面只计算Pitch是不够的,还需要计算Roll,同理有下图。此时X轴方向朝屏幕向内,人物向前的方向也是朝屏幕向内。那么人物的脚部为了贴合地面,需要往左歪头。往左歪头的角度理应是负数,但是由于图中对normal分解后得到的是-y方向上的值,因此负负得正后求得旋转角是正角度

因此我们就可以写出这样的C++代码

float UShooterTPPAnimInstance::FootIKTrace(FName SocketName, float TraceDistance, FRotator& OutIKRotator)
{
	const FVector SocketLocation = OwnerCharacter->GetMesh()->GetSocketLocation(SocketName);
	FHitResult IKResult;
	const float HalfHeight = OwnerCharacter->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
	const float StartZ = OwnerCharacter->GetActorLocation().Z - HalfHeight / 2;
	const float EndZ = OwnerCharacter->GetActorLocation().Z - HalfHeight - TraceDistance;
	const FVector StartLocation(SocketLocation.X, SocketLocation.Y, StartZ);
	const FVector EndLocation(SocketLocation.X, SocketLocation.Y, EndZ);
	FCollisionObjectQueryParams ObjectQueryParams;
	ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldStatic);

	FCollisionQueryParams QueryParams;
	QueryParams.bTraceComplex = true;
	if (!GetWorld()->LineTraceSingleByObjectType(IKResult, StartLocation, EndLocation, ObjectQueryParams, QueryParams))
	{
		OutIKRotator = FRotator::ZeroRotator;
		return 0;
	}

	// 弧度转角度
	const float Pitch = FMath::RadiansToDegrees(-FMath::Atan2(IKResult.Normal.X, IKResult.Normal.Z));
	const float Row = FMath::RadiansToDegrees(FMath::Atan2(IKResult.Normal.Y, IKResult.Normal.Z));
	OutIKRotator = FRotator(Pitch, 0, Row);
	
	return HalfHeight / 2 - IKResult.Distance;
}

因为左右脚骨骼朝向原因,所以需要在最终结果给右脚的位置取个反

// 需要往上抬的距离
const float RightFootMoveUpDistance = FootIKTrace(RightFootBoneName, FootTraceDistance, LeftFootIKRotator);
const float LeftFootMoveUpDistance = FootIKTrace(LeftFootBoneName, FootTraceDistance, RightFootIKRotator);
BodyOffSet = FVector(0, 0, FMath::Min(RightFootMoveUpDistance, LeftFootMoveUpDistance));


RightFootIKLocation = FVector((RightFootMoveUpDistance - BodyOffSet.Z) * -1, 0, 0);
LeftFootIKLocation = FVector(LeftFootMoveUpDistance - BodyOffSet.Z, 0, 0);

最终将数据应用到IK节点上

脚步IK:Effector以自身的骨骼空间为参考系,Joint以父节点作为参考系

脚步Rotation:上文中Rotation是以世界坐标系完成计算的,所以Rotation Space选择World Space,以叠加的形式应用到脚步中

UE中的网络知识

基础RPC以及Replicated上手还是相当容易的,这里仅记录一些重要的,易错的知识点

RPC

调用相关

If the RPC is being called from Server to be executed on a Client, only the Client who actually owns that Actor will execute the function.

If the RPC is being called from Client to be executed on the Server, the Client must own the Actor that the RPC is being called on.

第一点很好理解,定向传输。在多人游戏中,一定会有多个Client,每个Client中又有多个ACharacter(Autonomous或Simulated)。而在XXCharacter中发起的Client的RPC只会发送给正在操纵XXCharacter的Client上的XXCharacter

第二点,假设在一个Listen-Server环境下,场景中拥有一个Replicated的炮台(假设这个炮台不被任何人所控制)。那么这个炮台在Server上是ROLE_Authority,在Client上是ROLE_SimulatedProxy。也就是说Client并没有拥有这个炮台,那么也无法在Client上通过RPC请求Server做某些事情了。但是当Client上请求Possess炮台且成功后,经过AController的同步,Client对炮台的控制权变为了ROLE_AutonomousProxy,那么此时可是是做Client拥有了炮台,能调用RPC了

再举一个例子,场景中的门。或许你会认为Client开门需要在门中调用OpenDoorOnServer这样一个RPC函数,其实这么做是错误的,因为Client并没有拥有这个门。其中一种可行的做法是通过在APlayerController中调用RPC,然后服务器接收之后进行一次Overlap,最后对门进行一系列的操作

执行条件

在Server上调用,注意NetMulticast不仅会在Server上运行,还会广播到其他Client

在Client上调用

Replicated

一切Replicated都是从Server同步到Client,没有逆向的过程

Rider中断点技巧

对一个断点点击右键,可以进入高级选项面板

  • Enabled代表断点断点是否启用,取消勾选则代表忽略这个断点

  • Suspend代表当程序运行断点处时,程序是否需要暂停。取消勾选代表程序不暂停,继续运行,此时断点变为黄色

通过条件判断执行断点

假设我们有一个循环语句,我想当特定条件发生时,断点才会被“启用”

for (int i = 0; i < 10; i++)
{
    // 假设这一行Log语句被设置了一个断点
    UE_LOG(LogTemp, Log, TEXT("%d"), i);
}

那么我们可以这么干,让循环执行到i == 5时,断点才会被“启用”。并且由于我们勾选了"Breakpoint hit" message,因此我们可以在Console窗口中找到断点信息。勾选Stack trace则会让调用栈信息输出到Console窗口中,这里就不演示了

自定义Console输出

有图可得,我令这个断点不会执行中断,并为其添加了一定的条件,最后自定义了输出。因此在Console窗口中首先会输出一段字符串,然后i的值被评估,“计算”得出i的类型以及结果

举一反三的来说,如果输入的是bool表达式,那么Rider将会计算这个表达式的真假,并对其进行输出,例如

// Console
"Is i == 8 ?"; i == 8 = (bool) false

以及

最后Remove once hit代表这个断点是一次性的,当它被中断一次后,它将被移除(需要勾选Suspend才会中断)

坑点总结

  • 蓝图类中的初始值可能会和C++中构造函数相冲突,导致C++中修改的信息不会更新到蓝图类中。这种情况可以在蓝图类中"类默认值"标签页进行属性还原或修改,或重新创建蓝图类,或者右键蓝图类并点击Reload
  • 当遇到无法解决的编译问题或代码更改不生效时,可以尝试关闭UE Edior后重新运行
posted @ 2021-10-27 21:33  _FeiFei  阅读(2537)  评论(0编辑  收藏  举报