見出し画像

デバッグ表示を簡単にするC++マクロを紹介【Unreal EngineエンジニアTIPS】

あいさつ

こんにちは
ブラストエッジゲームズでリードエンジニアをしているNARIといいます。
これまで7年近くUnityのエンジニアをしてましたが
3ヶ月前から縁があってUnreal Engineのエンジニアとして
お仕事をしています。
使用言語がC#からC++に変わったため戸惑いながらお仕事しておりますが
C++やUnreal Engineを触ってみて新しく覚えたことなどを
発信していく予定ですのでよろしくお願いします。
今回は、Unreal Engine上で利用できるTIPSについて
当社で開発中のゲームを実例にご紹介します。
紹介するC++のマクロを使ったデバッグ表示の簡略化テクニックは
非常に小さいTIPSなので取り入れしやすいものだと思います。
是非ご参考にしてください。


マクロの概要

C++ではマクロ機能を使うことで通常のC++文法で実現できないことを
行うことができるようになります。
MSDNによるマクロの解説
今回は、画像のような形で変数名をマクロに設定すると
自動で「変数名:変数」の内容を出力するマクロを準備しました。

マクロを呼び出すと
変数名:変数の形で自動出力される

マクロの説明

以下実際に使用しているプログラムを全て記載します。
ヘッダーファイルのみで動作するので
こちらのファイルを配置してincludeすれば動作します。
PRINTVARを呼び出すと変数名を引数に入れることで
FString文字列として「変数名:変数」の形で変換されます。
LOGVARは「任意の文字列+変数名:変数」で変換されます。
詳しくはソースコードのコメントを参照ください。
UnrealEngine5.1で動作確認しています。
また、画像で使用しているIDEはJetBrains Riderになります。

ソースコード

PJ_LogUtils.h

/*
===============================================================================
       BLAST EDGE GAMES Inc. - Utility Header

    説明 (Description):
    - デバッグのログ表示を楽にするためのユーティリティです。
    - A utility designed for easier debug log displays.
    - マクロを使用して変数の名前とその値をログ出力します。
    - Utilizes macros to output both variable names and their values.

    主な利用 (Main Usage):
    - 主にログの表示などでの利用を想定しています。
    - Primarily intended for use in log displays and similar applications.

    許可 (Permissions):
    - このファイルの改変や利用は自由に行っていただいて構いません。
    - Modification and utilization of this file are freely permitted.
    - クレジットの記載は不要です。
    - No credit attribution is required.

    注意: このユーティリティはデバッグをサポートするために設計されていますが、
    使用によるいかなる問題についてもBLAST EDGE GAMES Inc.は責任を負いません。
    Please note that while this utility is designed to aid in debugging, 
    BLAST EDGE GAMES Inc. cannot be held responsible for any issues that arise 
    from its usage.
===============================================================================
*/
#pragma once
#include "CoreMinimal.h"
#include "Internationalization/Regex.h"


// XXXXXX 変数名:変数のような形式で出力 変数増やしたかったら手動で足してください
#define LOGVAR(Log,Var) FString::Printf(TEXT("%s %s"), TEXT(Log), *PRINTVAR(Var))
#define LOGVAR2(Log,Var1,Var2) FString::Printf(TEXT("%s %s, %s"), TEXT(Log), *PRINTVAR(Var1), *PRINTVAR(Var2))
#define LOGVAR3(Log,Var1,Var2,Var3) FString::Printf(TEXT("%s %s, %s, %s"), TEXT(Log), *PRINTVAR(Var1), *PRINTVAR(Var2), *PRINTVAR(Var3))
#define LOGVAR4(Log,Var1,Var2,Var3,Var4) FString::Printf(TEXT("%s %s, %s, %s, %s"), TEXT(Log), *PRINTVAR(Var1), *PRINTVAR(Var2), *PRINTVAR(Var3), *PRINTVAR(Var4))

// 変数名:変数の形式で表示します int32 FString bool float double に対応 AAA.BBBはBBBで表示されます .ToString()があったら除外してます
#define PRINTVAR(Var) FString::Printf(TEXT("%s:%s "), *PJ_LogUtils::GetLastNamePart(TEXT(#Var)), *PJ_LogUtils::ConvertToString(Var))

// ログ出力を助けする系のクラス
class PJ_LogUtils
{

public:
	// 変数名で不要な部分をトリミングします 例:AAA.BBBをBBBのみにします .ToString()がいたら削除しますそれ以外は.XXX()も含めてます
	static FString GetLastNamePart(const FString& FullName)
	{
		// ".ToString" を探し、そのインデックスを取得
		const int32 ToStringIndex = FullName.Find(TEXT(".ToString()"));
    
		// ".ToString" が存在する場合、その部分を削除
		FString ModifiedName = ToStringIndex != INDEX_NONE ? FullName.Left(ToStringIndex) : FullName;

		// 末尾に .XXX() パターンがある場合にマッチ
		const FRegexPattern MyPattern(TEXT("\\.[A-Za-z]+\\(\\)$"));
		FRegexMatcher MyMatcher(MyPattern, ModifiedName);

		// 最後の '.' のインデックスを探す
		int32 LastDotIndex;
		if (ModifiedName.FindLastChar('.', LastDotIndex))
		{
			if (MyMatcher.FindNext()) // .XXX() パターンが末尾に存在する場合
			{
				// 1つ前の.を取得してAAA.XXX()のような出力になるようにする
				FString BeforeLastDot = ModifiedName.Left(LastDotIndex);
				int32 SecondLastDotIndex;
				if (BeforeLastDot.FindLastChar('.', SecondLastDotIndex))
				{
					return ModifiedName.Mid(SecondLastDotIndex + 1);
				}
				// ()がなかったらAAAだけ返す
				return BeforeLastDot;
			}
			return ModifiedName.Mid(LastDotIndex + 1);
		}
		return ModifiedName;
	}

	// intやFStringやfloatの変換用関数
	template<typename T>
	static FString ConvertToString(const T& Value);

	template<>
	static FString ConvertToString<int32>(const int32& Value)
	{
		return FString::Printf(TEXT("%d"), Value);
	}

	template<>
	static FString ConvertToString<float>(const float& Value)
	{
		return FString::Printf(TEXT("%.3f"), Value);
	}

	template<>
	static FString ConvertToString<FString>(const FString& Value)
	{
		return Value;
	}

	template<>
	static FString ConvertToString<double>(const double& Value)
	{
		return FString::Printf(TEXT("%.3f"), Value);
	}
	template<>
	static FString ConvertToString<bool>(const bool& Value)
	{
		return FString::Printf(TEXT("%s"), Value ? TEXT("TRUE") : TEXT("FALSE"));
	}
};

解説する点は大きく2つあります。

解説その1 変数名の表示の取得方法

#define PRINTVAR(Var) FString::Printf(TEXT("%s:%s "), *PJ_LogUtils::GetLastNamePart(TEXT(#Var)), *PJ_LogUtils::ConvertToString(Var))

この処理で重要なポイントは#Varの記述になります。
変数の先頭に#をつけると
変数自体の名前を文字列として表示するための記法になります。
これを活用することでコンパイル前のプリプロセスでマクロとして
変数名と変数内容の形でコードを展開するようにしています。
この実装にあたり以下のサイトを参考にしました。
Unreal Engineで使えるように一部の設定などを置き換えさせて頂いてます。
マクロにしかできないこと 〜C++でマクロを使うべきな場面〜

このdefineを組みわせて最初に記述している
#define LOGVAR(文字列+末尾に変数名の列挙のマクロ)
が作られています。

解説その2 変数の内容について

intboolFStringなどの情報をそのまま表示することは
マクロだけではできないので
staticなクラスを作り実現しています。
それがclass PJ_LogUtilsの内容になります。
このクラスでは大きく2つのことをしています。

1つ目:変数名の文言を調整する
そのまま#VarTEXTで表示すると余計な情報が含まれて
表示されることがあります。
インスタンスの変数名だけ表示したいのに
インスタンス名+変数名の表示になったり
などのケースが発生します。
例えばTest.Name.ToString()でマクロ呼び出しすると
ログの方も「Test.Name.ToString():HogeHoge」
と表示されてしまいます。
この場合、Nameだけ表示してほしいので
それ以外の変数名の表示を調整して表示するようにしています。

static FString GetLastNamePart(const FString& FullName)

上記が該当の処理になります。
処理の内容としてはToString()があれば除去して
それ以外AAA.BBB.CCCと並んでいればCCCだけになるように
正規表現で取得し直しています。
元々、Test.NameNameだけ取得したいため
それに適した関数名にしていたのですが
Test.Name.ToString();のようなケースを後から対応する必要があったため
微妙に関数名が実装内容と一致してないのですがご了承ください(汗

他のクラス関数(Test.Player.GetActionName()など)も
対応するべきかなと思ったのですが
この場合は残しておいたほうが意図がわかりやすいため
関数名をそのまま表示するようにしています。

つ目:変数内容の表示をする
最初にも説明した通り、マクロだけではintboolFStringなどの
値を表示できないためそれを表示するための変換関数を通しています。

// intやFStringやfloatの変換用関数
template<typename T>
static FString ConvertToString(const T& Value);

となっているところ以降が該当のコードです。
細かい解説は、しませんが基本的にtempalate定義を行い
ConvertToStringを変換したい型ごとにすることで
変数の内容を表示しています。
基本的にFVectorFNameなどのUnreal Engine専用のクラスも
ToString()をすれば表示できるため
intなどの基本的な型とFStringの変換関数だけ用意して対応しています。
足りない場合は必要な型を足していく運用を想定しています。

使用する際の注意点

  • シンタックスエラーになった際にマクロが展開された状態でエラーが出るため原因が追いづらくなります。

  • 別のライブラリなどで同じ名前のマクロがあった場合競合します。その際はほかの名前に変えるなどしてください。

  • 色々なところで使用するとコードサイズが大きくなっていきます。今回のマクロもデバッグ表示などのケースで使う形にして本番に表示されるような使い方は避けましょう。

上記注意して注意して使用していきましょう。
特にマクロは、コンパイル前にソースコードとして
展開し直されるという特徴をよく理解して使いましょう。
またマクロは使い方によっては、他にも注意する点はあるので
調べてから実装しましょう。

画像のように変換できない関数を指定してもIDE上ではシンタックスエラーになりません
しかしビルドの際にエラーが発生してしまいよく調べないとどの場所でエラーが発生してるか
分かりづらくなります

なぜ作ったのか

ゲームの紹介

当社では、「GOMAN -stuck in the avici hell」(以下GOMANと呼称)
というゲームを自社で絶賛開発しております。
簡単に紹介しますと3Dの横スクロールアクションゲームで
規模としましては10名以下の少人数で開発している
オリジナルタイトルのインディゲームになります。
ゲーム部分の詳しい解説などは、開発日誌の形で
今後ご紹介する予定ですのでよろしくお願いします。

9月開催のTOKYO GAME SHOW 2023に出展予定ですので是非遊びに来てください!

マクロの開発使用例

今回、上記のゲームを実例にTIPSの使用例を簡単にさせていただきます。
Unreal Engineに限らずですがゲーム開発をしていると
ゲームの内部状態を表示したいなどの需要が出てくると思います。
GOMANでも以下のように敵1体に対しての
AIの状態や再生しているアニメーションなどの
様々な情報を表示しています。
GOMAN開発は1年以上経過進んでおり、上記のようなデバッグ機能で
表示するパラメーターもかなり増えていた状態になっております。
自分がプロジェクト配属時は以下のようなコードとなっておりました。

この状態だと1つの処理の中で全てのデバッグ変数を呼び出しているため保守が非常が難しい

今後を考えると以下の課題を治す必要があります。

  • パラメーターの追加が難しい

  • パラメーターの表示順序を後から変更できない

この2つの問題を直すために今回のマクロを使い以下のように直しました。

上記によりデバッグの項目を追加をしやすく、表示順の変更がしやすくなりました。

また、もう一つ使用例を紹介させていただきます。
Unreal Engine上でアセットの設定やパラメーターの設定を行いますが
設定漏れがあった際のエラー表示が必要になります。
そうした場合にも今回のマクロは非常に役に立ちます。
例えば設定漏れのエラーを表示したい場合は通常、以下のように記載すると思います。

これもマクロを使うと以下のようになります。
上記のようにエラーログなどでエラーになった設定値を出力する際も冗長なコードがなくなり
変数名の名前を間違えたりすることが減ります。

基本的に自分はマクロ関数を積極的に使うのは
コードの肥大化やコンパイルエラーの追跡が困難になるので
推奨していませんがデバッグ表示の活用では便利かなと思って
今回紹介させていただきました。
デメリットも理解して
今後も活用できる所があれば使っていきたいと思います。

以上で今回のTIPSの紹介を終わります。
今回の知識が少しでも皆さんのゲーム開発に役立てばと思います。

それでは、皆さん良いゲーム開発を!

この記事が気に入ったらサポートをしてみませんか?