輸出調試信息
屏幕輸出
在藍圖節點中可以通過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
的,因此HealthAnywhere
和HealthDefaults
是可見的並可修改的
而當把Blueprint Class拖拽到場景中,或者通過代碼將其實例化了,那么可以看作它是一個Instance,因此HealthAnywhere
和HealthInstance
是可見的並可修改的
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中的FString
,FName
,FText
,甚至是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不同,ensure
和check
的作用相當於在條件不成立的時候給當前語句“打上”一個斷點。check
的關鍵不同點在於它默認不會在發行版本中運行
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
中的方法,UObjectBase
是UObject
的基類
/** 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_TestActor
的UClass*
;在ATestActor
中調用StaticClass
,獲得的是ATestActor
的UClass*
以ATestActor
和AActor
為例,ATestActor
是AActor
的派生類,可以通過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_AutonomousProxy
的ACharacter
的Owner是服務器同步過來的APlayerController
;而又因為Client上只能存在一個AController
,因此ROLE_SimulatedProxy
的ACharacter
的Owner是nullptr
在這種情況下,GetOwner
和GetController
的結果都是一樣的,追到底都是APlayerController
那么為什么會一樣呢,因為在PossessedBy
和UnPossessed
的內部會通過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中的Possess
和UnPossess
/** 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后重新運行