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后重新運行


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM