関連ブログ
- [UE4][UE5]開発環境の容量を少しでも減らす 2024.08.14UE
- [UE5] PushModel型のReplicationを使い、ネットワーク最適化を図る 2024.05.29UE
- [UE5]マテリアルでメッシュをスケールする方法 2024.01.17UE
CATEGORY
2022.06.29UE4UE/ C++UE/ Debug
執筆バージョン: Unreal Engine 4.27 |
皆さんこんにちは、エンジニアの森です
今回は、C++ から Message Log ウィンドウにログを出す方法と、 FTokenizedMessage の使い方について紹介します
Message Log ではなく Output Log ウィンドウにログを出すには以前の記事や公式ドキュメントを参照してください
Message Log ウィンドウとは
PIE ログ出力
C++へのリンク
ブループリントノードへのリンク
ログリストを追加する
ブループリント Call Stack の表示
Build 実行時にエラーチェックしてログを出す
プロジェクトダウンロード
Message Log は UE のメニューから[Window] >[Developer Tools] >[MessageLog]と選んだときに表示されるウィンドウです
主にブループリント内で無効なオブジェクトへのアクセスや配列の範囲外参照をしたときなどは、ここにエラーが表示されます
今回は、Blueprint Function Library を使用して Message Log ウィンドウへのログ出力を制御する仕組みを作ります
また、このウィンドウ自体はエディター専用のものです
あらかじめランタイムモジュールを切り分けた上、ソースコード中では WITH_EDITOR マクロの中で利用するようにしています
早速ログ出力処理を作りましょう
ログ出力には FMessageLog::AddMessage か FMessageLog::Info を使います
ソースコードを書いていきますが、今回はあらかじめ「MyMessageLogRuntime」モジュールを作成しておきました
プロジェクト名は「MyMessageLog」です
本ブログ記事の最後にプロジェクトを載せているので、手元で結果を確認したい方はそちらからお願いします
1 2 3 4 5 6 7 8 9 10 11 |
// 中略 if (Target.bBuildEditor) { PublicDependencyModuleNames.AddRange( new string[] { "UnrealEd", "MessageLog", } ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "Logging/LogVerbosity.h" #include "Logging/TokenizedMessage.h" #include "MyMessageLogFunctionLibrary.generated.h" UCLASS() class MYMESSAGELOGRUNTIME_API UMyMessageLogFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: template <typename FmtType, typename... ArgTypes> static void OutPIELog(EMessageSeverity::Type Severity, const FmtType& Fmt, ArgTypes... Args) { const FString Message = FString::Printf(Fmt, Args...); OutPIELogInternal(Severity, Message); } UFUNCTION(BlueprintCallable, meta = (DisplayName = "Out PIE Log")) static void OutPIELogBP(FString Message); protected: static void OutPIELogInternal(EMessageSeverity::Type Severity, const FString& Message); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Fill out your copyright notice in the Description page of Project Settings. #include "MyMessageLogRuntime/Public/MyMessageLogFunctionLibrary.h" #if WITH_EDITOR #include "Engine/Engine.h" #include "Logging/MessageLog.h" #include "Logging/TokenizedMessage.h" #include "Templates/SharedPointer.h" #endif void UMyMessageLogFunctionLibrary::OutPIELogBP(FString Message) { OutPIELogInternal(EMessageSeverity::Info, Message); } void UMyMessageLogFunctionLibrary::OutPIELogInternal(EMessageSeverity::Type Severity, const FString& Message) { #if WITH_EDITOR // 出力方法 : FMessageLog::AddMessage を使用 { TSharedRef<FTokenizedMessage> TokenizedMessage = FTokenizedMessage::Create(Severity); TokenizedMessage->AddToken(FTextToken::Create(FText::FromString(Message))); FMessageLog(TEXT("PIE")).AddMessage(TokenizedMessage); } // 出力方法 : FMessageLog::Info を使用 { TSharedRef<FTokenizedMessage> TokenizedMessage = FMessageLog(TEXT("PIE")).Info(); TokenizedMessage->AddToken(FTextToken::Create(FText::FromString(Message))); } #endif } |
適当な BP アクターを作って、これを呼び出しました
すると、このようにログが出てきます
FMessageLog::AddMessage を使うと、Output Log にも同じ内容が出力されます
一方で、 FMessageLog::Info を使う場合は、Output Log への出力は行われないようです
他の、FMessageLog::Warning や FMessageLog::Message でも同様に Output Log へは出力されませんでした
このとき FMessageLog::CriticalError を使ったり、EMessageSeverity::CriticalError を使ってメッセージ表示しようとするとプログラムにブレークがかかります
C++ からログ出力するためのマクロを作りましょう
マクロの名前は「MY_LOG」とします
実装には FSourceCodeNavigation::OpenSourceFile を使います
マクロの中身で OnMessageTokenActivated を使用して、ログを出力した行へのリンクを仕込みます
エラーと警告は画面にも表示します
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "MyMessageLogRuntime/Public/MyMessageLogFunctionLibrary.h" #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) #define EXPAND_MYPROJECT_LOG(CategoryName, Verbosity, FileName, LineNumber, Format, ...) \ UE_LOG(CategoryName, Verbosity, Format, ##__VA_ARGS__); \ UMyMessageLogFunctionLibrary::OutPIELog(ELogVerbosity::Verbosity, TEXT("PIE"), FileName, LineNumber, Format, ##__VA_ARGS__); #else #define EXPAND_MYPROJECT_LOG(...) #endif #define MYPROJECT_LOG(CategoryName, Verbosity, Format, ...) \ EXPAND_MYPROJECT_LOG(CategoryName, Verbosity, TEXT(__FILE__), __LINE__, Format, ##__VA_ARGS__) #define MY_LOG(CategoryName, Verbosity, Format, ...) MYPROJECT_LOG(CategoryName, Verbosity, Format, ##__VA_ARGS__) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class MYMESSAGELOGRUNTIME_API UMyMessageLogFunctionLibrary : public UBlueprintFunctionLibrary { // 追加 template <typename FmtType, typename... ArgTypes> static void OutPIELog(ELogVerbosity::Type Verbosity, const FName& LogName, const FString& File, int32 Line, const FmtType& Fmt, ArgTypes... Args) { const FString Message = FString::Printf(Fmt, Args...); OutPIELogInternal(Verbosity, LogName, File, Line, Message); } protected: // 追加 static void OutPIELogInternal(ELogVerbosity::Type Verbosity, const FName& LogName, const FString& File, int32 Line, const FString& Message); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
// Fill out your copyright notice in the Description page of Project Settings. #include "MyMessageLogRuntime/Public/MyMessageLogFunctionLibrary.h" #if WITH_EDITOR #include "Engine/Engine.h" #include "Logging/MessageLog.h" #include "Logging/TokenizedMessage.h" #include "Templates/SharedPointer.h" #include "SourceCodeNavigation.h" // C++へのリンクのため追加 #endif class MyMessageLogHelper { public: static void VisitCode(const TSharedRef<IMessageToken>& Token, FString AbsolutePath, int32 Line); static bool GetLogSeverity(ELogVerbosity::Type Verbosity, EMessageSeverity::Type& OutSeverity); static bool OutScreenLog(EMessageSeverity::Type& Severity, const FString& Message); }; void MyMessageLogHelper::VisitCode(const TSharedRef<IMessageToken>& Token, FString AbsolutePath, int32 Line) { #if WITH_EDITOR FSourceCodeNavigation::OpenSourceFile(AbsolutePath, Line); #endif } bool MyMessageLogHelper::GetLogSeverity(ELogVerbosity::Type Verbosity, EMessageSeverity::Type& OutSeverity) { #if WITH_EDITOR const TMap<ELogVerbosity::Type, EMessageSeverity::Type> VerbosityToSeverity = { {ELogVerbosity::Warning, EMessageSeverity::Warning}, {ELogVerbosity::Error, EMessageSeverity::Error}, {ELogVerbosity::Fatal, EMessageSeverity::CriticalError}, }; const EMessageSeverity::Type* Found = VerbosityToSeverity.Find(Verbosity); if (Found) { OutSeverity = *Found; return true; } #endif return false; } bool MyMessageLogHelper::OutScreenLog(EMessageSeverity::Type& Severity, const FString& Message) { if (GEngine) { if (Severity == ELogVerbosity::Warning) { GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, Message); } else if (Severity == ELogVerbosity::Error) { GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, Message); } } } TSharedPtr<FTokenizedMessage> UMyMessageLogFunctionLibrary::OutMessageLogInternal(ELogVerbosity::Type Verbosity, const FName& LogName, const FString& File, int32 Line, const FString& Message) { EMessageSeverity::Type Severity = EMessageSeverity::Info; MyMessageLogHelper::GetLogSeverity(Verbosity, Severity); MyMessageLogHelper::OutScreenLog(Severity, Message); #if WITH_EDITOR if (LogName != NAME_None) { UMyMessageLogFunctionLibrary::RegisterLogListing(LogName, FText::FromName(LogName)); TSharedRef<FTokenizedMessage> TokenizedMessage = FMessageLog(LogName).Message(Severity); TokenizedMessage->AddToken(FTextToken::Create(FText::FromString(Message)) ->OnMessageTokenActivated(FOnMessageTokenActivated::CreateStatic(&MyMessageLogHelper::VisitCode, File, Line)) ); return TokenizedMessage; } #endif return nullptr; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "MyLogActor.generated.h" UCLASS() class MYMESSAGELOG_API AMyLogActor : public AActor { GENERATED_BODY() public: UFUNCTION(BlueprintCallable) void OutPIELog(FString Message); }; |
1 2 3 4 5 6 7 8 9 10 11 |
// Fill out your copyright notice in the Description page of Project Settings. #include "MyMessageLog/MyLogActor.h" #include "MyMessageLogRuntime/Public/MyMessageLogMacros.h" void AMyLogActor::OutPIELog(FString Message) { //UE_LOG(LogTemp, Error, TEXT("%s"), *Message); MY_LOG(LogTemp, Error, TEXT("%s"), *Message); } |
このように動作します↓
これで MY_LOG マクロを書いた行までジャンプする機能ができました
自分でエラーを出力すると、実行終了時の見慣れたダイアログも出てきます
今度は先ほど作成した BP ノードに対して同じものを作ります
(ノードを呼び出したグラフへのリンクを仕込みます)
以下、そのためのソースコードです
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "Logging/LogVerbosity.h" #include "Logging/TokenizedMessage.h" #include "MyMessageLogFunctionLibrary.generated.h" UENUM() enum EMessageSeverityType { /* CriticalError = EMessageSeverity::CriticalError, */ Error = EMessageSeverity::Error, PerformanceWarning = EMessageSeverity::PerformanceWarning, Warning = EMessageSeverity::Warning, Info = EMessageSeverity::Info, }; UCLASS() class MYMESSAGELOGRUNTIME_API UMyMessageLogFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: template <typename FmtType, typename... ArgTypes> static TSharedPtr<FTokenizedMessage> OutMessageLog(ELogVerbosity::Type Verbosity, const FName& LogName, const FString& File, int32 Line, const FmtType& Fmt, ArgTypes... Args) { const FString Message = FString::Printf(Fmt, Args...); return OutMessageLogInternal(Verbosity, LogName, File, Line, Message); } UFUNCTION(BlueprintCallable, CustomThunk, meta = (DisplayName = "Out PIE Log")) static void OutPIELogBP(EMessageSeverityType Severity = EMessageSeverityType::Info, FString Message = FString(TEXT(""))); protected: static TSharedPtr<FTokenizedMessage> OutMessageLogInternal(ELogVerbosity::Type Verbosity, const FName& LogName, const FString& File, int32 Line, const FString& Message); static TSharedPtr<FTokenizedMessage> OutMessageLogBP(EMessageSeverity::Type Severity, const FName& LogName, const UObject* ActiveObject, const FFrame& StackFrame, const FString& Message); DECLARE_FUNCTION(execOutPIELogBP); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
// Fill out your copyright notice in the Description page of Project Settings. #include "MyMessageLogRuntime/Public/MyMessageLogFunctionLibrary.h" #if WITH_EDITOR #include "Engine/Engine.h" #include "Logging/MessageLog.h" #include "Templates/SharedPointer.h" #include "SourceCodeNavigation.h" #include "Misc/UObjectToken.h" #include "Kismet2/KismetEditorUtilities.h" #include "Kismet2/KismetDebugUtilities.h" #endif class MyMessageLogHelper { public: static void VisitCode(const TSharedRef<IMessageToken>& Token, FString AbsolutePath, int32 Line); static void VisitObject(const TSharedRef<IMessageToken>& Token); static bool GetLogSeverity(ELogVerbosity::Type Verbosity, EMessageSeverity::Type& OutSeverity); static void OutLog(EMessageSeverity::Type& Severity, const FString& Message); static void OutScreenLog(EMessageSeverity::Type& Severity, const FString& Message); }; void MyMessageLogHelper::VisitCode(const TSharedRef<IMessageToken>& Token, FString AbsolutePath, int32 Line) { #if WITH_EDITOR FSourceCodeNavigation::OpenSourceFile(AbsolutePath, Line); #endif } void MyMessageLogHelper::VisitObject(const TSharedRef<IMessageToken>& Token) { #if WITH_EDITOR if (Token->GetType() == EMessageToken::Object) { const TSharedRef<FUObjectToken> UObjectToken = StaticCastSharedRef<FUObjectToken>(Token); if (UObjectToken->GetObject().IsValid()) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(UObjectToken->GetObject().Get()); } } #endif } bool MyMessageLogHelper::GetLogSeverity(ELogVerbosity::Type Verbosity, EMessageSeverity::Type& OutSeverity) { const TMap<ELogVerbosity::Type, EMessageSeverity::Type> VerbosityToSeverity = { {ELogVerbosity::Warning, EMessageSeverity::Warning}, {ELogVerbosity::Error, EMessageSeverity::Error}, {ELogVerbosity::Fatal, EMessageSeverity::CriticalError}, }; const EMessageSeverity::Type* Found = VerbosityToSeverity.Find(Verbosity); if (Found) { OutSeverity = *Found; return true; } return false; } void MyMessageLogHelper::OutLog(EMessageSeverity::Type& Severity, const FString& Message) { switch (Severity) { case EMessageSeverity::CriticalError: UE_LOG(LogTemp, Fatal, TEXT("%s"), *Message); break; case EMessageSeverity::Error: UE_LOG(LogTemp, Error, TEXT("%s"), *Message); break; case EMessageSeverity::PerformanceWarning: case EMessageSeverity::Warning: UE_LOG(LogTemp, Warning, TEXT("%s"), *Message); break; case EMessageSeverity::Info: UE_LOG(LogTemp, Log, TEXT("%s"), *Message); break; default: break; } } void MyMessageLogHelper::OutScreenLog(EMessageSeverity::Type& Severity, const FString& Message) { if (GEngine) { if (Severity == ELogVerbosity::Warning) { GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, Message); } else if (Severity == ELogVerbosity::Error) { GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, Message); } } } //-------------------------------------------------------------------------------------------------------------------------------- void UMyMessageLogFunctionLibrary::OutMessageLogInternal(ELogVerbosity::Type Verbosity, const FName& LogName, const FString& File, int32 Line, const FString& Message) { EMessageSeverity::Type Severity = EMessageSeverity::Info; MyMessageLogHelper::GetLogSeverity(Verbosity, Severity); MyMessageLogHelper::OutScreenLog(Severity, Message); #if WITH_EDITOR TSharedRef<FTokenizedMessage> TokenizedMessage = FMessageLog(LogName).Message(Severity); TokenizedMessage->AddToken(FTextToken::Create(FText::FromString(Message)) ->OnMessageTokenActivated(FOnMessageTokenActivated::CreateStatic(&MyMessageLogHelper::VisitCode, File, Line)) ); #endif } TSharedPtr<FTokenizedMessage> UMyMessageLogFunctionLibrary::OutMessageLogBP(EMessageSeverity::Type Severity, const FName& LogName, const UObject* ActiveObject, const FFrame& StackFrame, const FString& Message) { MyMessageLogHelper::OutLog(Severity, Message); MyMessageLogHelper::OutScreenLog(Severity, Message); #if WITH_EDITOR && WITH_EDITORONLY_DATA // to protect access to GeneratedClass->DebugData if (LogName != NAME_None) { UMyMessageLogFunctionLibrary::RegisterLogListing(LogName, FText::FromName(LogName)); UClass* ClassContainingCode = FKismetDebugUtilities::FindClassForNode(ActiveObject, StackFrame.Node); UBlueprint* BlueprintObj = (ClassContainingCode ? Cast<UBlueprint>(ClassContainingCode->ClassGeneratedBy) : nullptr); if (BlueprintObj) { UObject* ObjectBeingDebugged = BlueprintObj->GetObjectBeingDebugged(); const FString& PathToDebug = BlueprintObj->GetObjectPathToDebug(); if (ObjectBeingDebugged == nullptr) { // Check if we need to update the object being debugged UObject* ObjectToDebug = FindObjectSafe<UObject>(nullptr, *PathToDebug); if (ObjectToDebug) { // If the path to debug matches a newly-spawned object, set the hard reference now ObjectBeingDebugged = ObjectToDebug; BlueprintObj->SetObjectBeingDebugged(ObjectBeingDebugged); } } const int32 BreakpointOffset = StackFrame.Code - StackFrame.Node->Script.GetData() - 1; if (GIsEditor && GIsPlayInEditorWorld) { TSharedRef<FTokenizedMessage> TokenizedMessage = FMessageLog(LogName).Message(Severity); UBlueprintGeneratedClass* GeneratedClass = Cast<UBlueprintGeneratedClass>(ClassContainingCode); if ((GeneratedClass != nullptr) && GeneratedClass->DebugData.IsValid()) { UEdGraphNode* BlueprintNode = GeneratedClass->DebugData.FindSourceNodeFromCodeLocation(StackFrame.Node, BreakpointOffset, true); if (BlueprintNode != nullptr) { TokenizedMessage ->AddToken(FTextToken::Create(BlueprintNode->GetNodeTitle(ENodeTitleType::ListView))) ->AddToken(FTextToken::Create(FText::FromString(TEXT(":")))) ->AddToken(FUObjectToken::Create(BlueprintNode, FText::FromString(Message)) ->OnMessageTokenActivated(FOnMessageTokenActivated::CreateStatic(&MyMessageLogHelper::VisitObject))); } } return TokenizedMessage; } } } #endif return nullptr; } DEFINE_FUNCTION(UMyMessageLogFunctionLibrary::execOutPIELogBP) { P_GET_PROPERTY(FByteProperty, Severity); P_GET_PROPERTY(FStrProperty, Message); P_FINISH; P_NATIVE_BEGIN; UMyMessageLogFunctionLibrary::OutMessageLogBP(EMessageSeverity::Type(Severity), FName(TEXT("PIE")), P_THIS, Stack, Message); P_NATIVE_END; } |
関数を CustomThunk にして、FFrame を取得し、FKismetDebugUtilities::FindClassForNode でノード情報を取得しています
このあたりのソースコードはエンジンのデフォルト実装を参考にしています(Kismet2/KismetDebugUtilities.h)
動作はこのようになります↓
デリゲートを使用しているので、他にも任意のアクションを仕込むことができます
詳しくは公式ドキュメントのこちらやデリゲートの項目を参照してください
PIE 以外のログリストを追加してみます
追加には、 FMessageLogModule::RegisterLogListing を使用します
この関数は、既にリストが存在する場合には、それを「上書き」するようになっているので、先に FMessageLogModule::IsRegisteredLogListing で存在チェックが必要です
追加時に指定するパラメーターはこちらを参考にしてください
1 2 |
UFUNCTION(BlueprintCallable) static void RegisterLogListing(FName LogName, FText LogDisplayName); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#if WITH_EDITOR #include "Modules/ModuleManager.h" #include "MessageLogModule.h" #endif void UMyMessageLogFunctionLibrary::RegisterLogListing(FName LogName, FText LogDisplayName) { #if WITH_EDITOR if (LogName != NAME_None) { FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog"); if (!MessageLogModule.IsRegisteredLogListing(LogName)) { FMessageLogInitializationOptions InitOptions; InitOptions.bShowPages = true; InitOptions.bAllowClear = true; InitOptions.bShowInLogWindow = true; InitOptions.bShowFilters = true; MessageLogModule.RegisterLogListing(LogName, LogDisplayName, InitOptions); } } #endif } |
BP のミスにより無限ループを起こしたときなどには、ログ内にコールスタックが表示されます
自作関数でもこのような UI 込みでコールスタックにログを出すものを作ってみます
1 2 3 4 5 6 7 8 |
PublicDependencyModuleNames.AddRange( new string[] { // Slate への依存関係を追加追加 "Slate", "SlateCore", "EditorStyle", } |
1 2 3 4 5 6 7 8 9 10 |
UCLASS() class MYMESSAGELOGRUNTIME_API UMyMessageLogFunctionLibrary : public UBlueprintFunctionLibrary { // 中略 UFUNCTION(BlueprintCallable, CustomThunk, meta = (DisplayName = "Out Log with Call Stack")) static void OutLogCallStackBP(EMessageSeverityType Severity = EMessageSeverityType::Info, FName LogName = TEXT("PIE"), FString Message = FString(TEXT(""))); DECLARE_FUNCTION(execOutLogCallStackBP); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
#if WITH_EDITOR #include "EditorStyleSet.h" #include "Styling/SlateBrush.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Text/SMultiLineEditableText.h" #endif DEFINE_FUNCTION(UMyMessageLogFunctionLibrary::execOutLogCallStackBP) { P_GET_PROPERTY(FByteProperty, Severity); P_GET_PROPERTY(FNameProperty, LogName); P_GET_PROPERTY(FStrProperty, Message); P_FINISH; P_NATIVE_BEGIN; #if WITH_EDITOR TSharedPtr<FTokenizedMessage> TokenizedMessage = UMyMessageLogFunctionLibrary::OutMessageLogBP(EMessageSeverity::Type(Severity), LogName, P_THIS, Stack, Message); if (TokenizedMessage.IsValid()) { TSharedRef<FTokenizedMessage> TokenizedMessageRef = TokenizedMessage.ToSharedRef(); auto DisplayCallStackLambda = [](const FText CallStack) { TSharedPtr<SMultiLineEditableText> TextBlock; TSharedRef<SWidget> DisplayWidget = SNew(SBox) .MaxDesiredHeight(512) .MaxDesiredWidth(512) .Content() [ SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SScrollBox) + SScrollBox::Slot() [ SAssignNew(TextBlock, SMultiLineEditableText) .AutoWrapText(true) .IsReadOnly(true) .Text(CallStack) ] ] ]; FSlateApplication::Get().PushMenu( FSlateApplication::Get().GetActiveTopLevelWindow().ToSharedRef(), FWidgetPath(), DisplayWidget, FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup) ); FSlateApplication::Get().SetKeyboardFocus(TextBlock); }; // Display a pop-up that will display the complete script call stack TokenizedMessageRef->AddToken( FActionToken::Create( FText::FromString(TEXT("Call Stack")), FText::FromString(TEXT("Displays the underlying call stack")), FOnActionTokenExecuted::CreateStatic(DisplayCallStackLambda, FText::FromString(Stack.GetStackTrace()))) ); } #endif P_NATIVE_END; } |
↓動作
例えば、通常の SkeletalMeshActor をワールドに置いて、Skeletal Mesh 欄に None をセットすると、ビルド時に警告が出ます (メニューを開いて Map Check のみ実行しても確認できます)
このとき、アクター名をクリックすればワールド上でどこにあるアクターなのか分かります
Ctrl+Alt でドキュメントのプレビューも出てきます(文字化けしてますが…)
Ctrl+Alt ではなく単に Docs の部分をクリックすると、FMapErrorToken により以下のような形式で URL が生成され、そこに飛ばされます (実装は FDocumentationLink::ToUrl)
↑アクセスすると公式ドキュメントに飛びます
この機能を使うのは簡単で、AActor::CheckForErrors をオーバーライドするだけです
AActor::CheckForErrors をオーバーライドしましょう
オーバーライド関数の中で警告かエラーを出力すれば UE によってカウントされ、Map Check の結果としてユーザーに届きます
また、この関数は BP エディター上でコンパイルを走らせた時にも呼ばれていて、その場合もコンパイル中の Map Check ログとして出てくるようです
ログに出す内容としては FMapErrorToken はあえて使わず、アクターの情報 + 任意の URL に飛ばすのみにします
1 2 3 4 5 6 7 8 9 10 |
UCLASS() class MYMESSAGELOG_API AMyMapCheckActor : public AActor { GENERATED_BODY() #if WITH_EDITOR virtual void CheckForErrors() override; #endif }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include "MyMessageLog/MyMapCheckActor.h" #if WITH_EDITOR #include "CoreGlobals.h" #include "Logging/TokenizedMessage.h" #include "Logging/MessageLog.h" #include "Misc/UObjectToken.h" #include "Misc/MapErrors.h" #endif #if WITH_EDITOR void AMyMapCheckActor::CheckForErrors() { Super::CheckForErrors(); if (TestValue == 1) { FString HistoriaBlogURL; if (GConfig && GConfig->GetString(TEXT("historiaInc.URLs"), TEXT("HistoriaBlogURL"), HistoriaBlogURL, GEditorIni)) { const FString DocsURL = TEXT("Shared/CoreUObject/UObject/UObject"); FMessageLog MapCheck("MapCheck"); MapCheck.Warning() ->AddToken(FUObjectToken::Create(this)) ->AddToken(FTextToken::Create(FText::FromString(TEXT("TestValue is 1")))) ->AddToken(FActionToken::Create(FText::FromString(TEXT("Blog")), FText::FromString(TEXT("Historia Blog")), FOnActionTokenExecuted::CreateStatic([](FString InURL)->void { FPlatformProcess::LaunchURL(*InURL, NULL, NULL); }, HistoriaBlogURL))) // ->AddToken(FMapErrorToken::Create(FName(TEXT("AMyMapCheckActor::CheckForErrors")))) ; } } } #endif |
1 2 |
[historiaInc.URLs] HistoriaBlogURL="https://historia.co.jp/archives/" |
↓動作
というわけで、今回は Message Log ウィンドウにログ出力する方法を見ていきました
主にデバッグ目的で使用する方法を取り扱いましたが、IMessageToken の継承先を見るとまだまだ紹介できていない機能がありそうです
余裕がある方はさらに詳しく調べてみてください
ありがとうございました
今回作ったプロジェクトをダウンロードできます
UE バージョンは 4.27.2 です