[UE5]Discordにメッセージを送信するプラグインを作ってみた・実装編

下記の記事で紹介しているプラグインの実装方法について解説する記事です。UnrealC++に慣れていない方でも理解しやすいよう、極力かみ砕いて解説していますのでやや冗長な点もあるかもしれませんがご容赦ください。

https://qiita.com/hatosable/items/bb58f0a9ddffe14db5b9

# 前提
UnrealC++を扱うための環境は既に構築できているものとします。詳しくは[こちら](https://dev.epicgames.com/documentation/ja-jp/unreal-engine/setting-up-visual-studio-development-environment-for-cplusplus-projects-in-unreal-engine)を参照してください。

https://dev.epicgames.com/documentation/ja-jp/unreal-engine/setting-up-visual-studio-development-environment-for-cplusplus-projects-in-unreal-engine

# モジュールの作成
まずはプラグイン内で使用するモジュールを定義します。
今回はpublicに`DeveloperSettings`、`HTTP`、`Json`、`JsonUtilitys`を追加しました。

<details><summary>モジュールとは</summary>

モジュールとは、ざっくりいうとコードをグループごとに分けたものです。
UE5ではコードの保守性や再利用性を高めるため、機能をモジュールとして分割し、必要なタイミングでのみ読み込む仕組みになっています。
 **インクルードファイルのようなもの** と考えると分かりやすいかもしれません。
 モジュールの新規作成には[こちらのプラグイン]( https://www.fab.com/ja/listings/f50d125d-52c6-4192-8cd7-99565acd0af9
)が便利です。
 
</details>

~~~DiscordMessenger.build.cs
// Copyright hatosable All Rights Reserved.
using UnrealBuildTool;

public class DiscordMessenger : ModuleRules
{
	public DiscordMessenger(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] 
		{ 
			"CoreUObject",
			"Engine",
			"DeveloperSettings",   // URL設定の追加に必要
            "HTTP",               // Webhookを送信するためのhttp通信に必要
            "Json",              // メッセージをjson形式で送信するため必要
            "JsonUtilities"     // 同上
		});
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				
			});
		
		DynamicallyLoadedModuleNames.AddRange(
			new string[]
			{
				
			});
	}
}
~~~

# プロジェクト設定の追加

メッセージ送信時に逐一URLを引数に書くのも面倒なので、プロジェクト設定を追加します。
historia様の[こちらの記事](https://historia.co.jp/archives/18025/)を参考に、`FString`型を追加しました。

https://historia.co.jp/archives/18025/

~~~WebhookSettings.h

// Copyright hatosable All Rights Reserved.
#pragma once

#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "WebhookSettings.generated.h"

UCLASS(config = Game, defaultconfig)
class DISCORDMESSENGER_API UWebhookSettings : public UDeveloperSettings
{
	GENERATED_BODY()

public:
	
	virtual FName GetCategoryName() const override 
    {
        return("Discord Messenger"); 
    }

# if WITH_EDITOR
	
	virtual FText GetSectionText() const override 
    {
        return( FText::FromString(TEXT("Discord Messenger")) ); 
    }
	
# endif

	UPROPERTY(EditAnywhere, Config, Category = "DiscordMessenger", 
    meta=(ToolTip= "メッセージを送信するURL"))
	FString WebhookURL;
};

~~~

# 構造体の定義

Discordに送信可能な情報は数が多いため、構造体として定義しまとめられるようにします。また、Enbeds(埋め込み情報。リンク貼ると出てくるアレ)は複数まとめて送信することができるため、そういう点でも構造体としてまとまっていた方が良いです。
![スクリーンショット 2025-04-27 185149.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761410/f5e149c1-3df5-424a-9ca7-0f9221ba4064.png)

~~~DiscordMessageContent.h

// Copyright hatosable All Rights Reserved.
#pragma once

#include "DiscordMessageContent.generated.h"

// 埋め込み情報定義
USTRUCT(BlueprintType)
struct DISCORDMESSENGER_API FDiscordMessageEmbeds
{
	GENERATED_BODY()

public:

    // 埋め込み情報の色
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DiscordMessenger",
    meta = (HideAlphaChannel))
	FLinearColor Color = FColor::Cyan;

    // 埋め込み情報のタイトル
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DiscordMessenger")
	FString Title;

    // 埋め込み情報の概要 リンク等はここに貼る
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DiscordMessenger")
	FString Description;
};

// メッセージ情報定義
USTRUCT(BlueprintType)
struct DISCORDMESSENGER_API FDiscordMessageContent
{
	GENERATED_BODY()

public:

    // ユーザー名 省略するとWebhook登録時の名前を使用
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DiscordMessenger")
	FString Username;

    // メッセージ内容
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DiscordMessenger")
	FString Content;
};

~~~

ちなみに埋め込み情報には「fields」というコンテンツを追加することが可能ですが、UEからメッセージを送信する用途では不要と判断しオミットしています。


# 関数の作成

ここまでできたら、いよいよ関数本体を作成します。
どこでも呼び出せる方が便利なのでBlueprintFuinctionLibrary内に作成しました。
まずは関数定義を作成しましょう。

~~~DiscordMessageLibrary.h

// Copyright hatosable All Rights Reserved.
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DiscordMessageContent.h"
#include "DiscordMessageLibrary.generated.h"

UCLASS()
class DISCORDMESSENGER_API UDiscordMessageLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:

	UFUNCTION(BlueprintCallable, Category = "DiscordMessenger", 
    meta =(AutoCreateRefTerm = "Embeds"))
	static void SendMessageToDiscord(const FDiscordMessageContent& Message, 
    TArray<FDiscordMessageEmbeds> Embeds);

private:

	static uint32 LinearColorToBits(FLinearColor Color);
};

~~~

続いて関数本体の実装です。一つづつ解説していきます。

~~~DiscordMessageLibrary.cpp

// Copyright hatosable All Rights Reserved.

#include "DiscordMessageLibrary.h"
#include "WebhookSettings.h"
#include "Http.h"
#include "Logging.h"
#include "Runtime/Json/Public/Dom/JsonObject.h"
#include"Runtime/Json/Public/Serialization/JsonWriter.h"
#include "Runtime/Json/Public/Serialization/JsonSerializer.h"

void UDiscordMessageLibrary::SendMessageToDiscord(const FDiscordMessageContent& Message, TArray<FDiscordMessageEmbeds> Embeds)
{
    // プロジェクト設定からURLを取得
	FString URL = GetDefault<UWebhookSettings>()->WebhookURL;

    // URLが未設定なら処理を打ち切り
	if (URL.IsEmpty())
	{
		UE_LOG(LogDiscordMessenger, Error, TEXT("SendMessageToDiscord: Invalid URL"));

		return;
	}

    // Jsonオブジェクトに送信内容を格納していく
	TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());

	if (!Message.Username.IsEmpty())
	{
		JsonObject->SetStringField("username", Message.Username);
	}

	if (!Message.Content.IsEmpty())
	{
		JsonObject->SetStringField("content", Message.Content);
	}

	if (!Embeds.IsEmpty())
	{
		TArray<TSharedPtr<FJsonValue>> Array;

		TSharedPtr<FJsonObject> Embed;

		for (auto Item : Embeds)
		{
			Embed = MakeShareable(new FJsonObject());
		
			Embed->SetNumberField("color",LinearColorToBits( Item.Color));
	
			Embed->SetStringField("title", Item.Title);

			Embed->SetStringField("description", Item.Description);

			Array.Add(MakeShareable(new FJsonValueObject(Embed.ToSharedRef())));
		}
	
		JsonObject->SetArrayField("embeds", Array);
	}
	
	FString Payload;
	TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Payload);

    // Jsonオブジェクトをシリアライズ
	FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);

    // HTTPリクエストを作成
	TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
	Request->SetURL(URL);
	Request->SetVerb("POST");
	Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	Request->SetContentAsString(Payload);

    // HTTP通信終了時の処理を登録
	Request->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr,
    FHttpResponsePtr Response, bool bWasSuccessful) {
		if (bWasSuccessful && Response->GetResponseCode() == 204)
		{
			UE_LOG(LogDiscordMessenger, Log, TEXT("Message sent to Discord successfully!"));
		}
		else
		{
			UE_LOG(LogDiscordMessenger, Error, TEXT("Failed to send message to Discord: %s"), *Response->GetContentAsString());
		}
	});

    // HTTP通信をリクエスト
	Request->ProcessRequest();
}

uint32 UDiscordMessageLibrary::LinearColorToBits(FLinearColor Color)
{
	uint8 Red = Color.R * 255;
	uint8 Green = Color.G * 255;
	uint8 Blue = Color.B * 255;
	
	return FColor(Red, Green, Blue, 0).Bits;
}

~~~

#### ①プロジェクト設定から値を取得

`GetDefault<プロジェクト設定クラス>()`でプロジェクトに設定されている値を取得できます。取得したいクラスのインクルードをお忘れなく。

#### ②Jsonオブジェクトに送信内容を格納

Jsonオブジェクトの取り扱いは @fumittu 様の[こちらの記事](https://qiita.com/fumittu/items/f9f9e2e7c056b76a3d60)が参考になります。

https://qiita.com/fumittu/items/f9f9e2e7c056b76a3d60

文字列であれば`JsonObject->SetStringField("フィールド名", 値)`でJsonに値を登録することが可能です。
Embedsの構造はややこしいので整理してみます。

~~~
    "embeds" : [
    {
      "color" : ff0000,      
      "title" : "title",
      "description": "description"
    },
     {
      "color" : 0000ff,      
      "title" : "title",
      "description": "description"
    }]
~~~

先ほど構造体として定義した内容を配列に格納していることが分かるかと思います。そして、構造体の内容はJsonオブジェクトとして解釈することもできそうですね。

配列として構造体の情報をまとめる`TArray<TSharedPtr<FJsonValue>>`を用意し、引数として渡された埋め込み情報の配列をforeach文でJsonオブジェクトに変換・格納します。

~~~
for (auto Item : Embeds)
		{
			Embed = MakeShareable(new FJsonObject());
		
			Embed->SetNumberField("color",LinearColorToBits( Item.Color));
	
			Embed->SetStringField("title", Item.Title);

			Embed->SetStringField("description", Item.Description);

			Array.Add(MakeShareable(new FJsonValueObject(Embed.ToSharedRef())));
		}
~~~

そういえば埋め込み情報の色指定には16進数を使用しますが、構造体には`FLinearColor`を設定しました。FLinearColorはグラフ内でカラーピッカーが使えるため、`FColor`よりも直感的に設定できるからです。しかしFLinearColorを16進数に変換することはできません。どうしましょうね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3761410/59e23add-75bd-4e62-8ebb-0ecfb5a3ca55.png)

#### ④FLinearColorを16進数に変換する(<font color=red>超ゴリ押し</font>)

FlinearColorから16進数への変換をゴリ押しで行っていきます。ほかにいい方法あったら教えてください。

~~~
uint32 UDiscordMessageLibrary::LinearColorToBits(FLinearColor Color)
{
	uint8 Red = Color.R * 255;
	uint8 Green = Color.G * 255;
	uint8 Blue = Color.B * 255;
	
	return FColor(Red, Green, Blue, 0).Bits;
}
~~~

FLinearColorではRGBAの値を0～1で定義します。対してFColorでは各値0～255で定義しています。
つまり**FLiniarColorの各値に255を掛けてやればFColorに変換できるのです。** もっとエレガントな方法無かったんですかね。
FColorに変換できれば`FColor.Bits`で色を16進数で表現した値を取得することができます。
なお、Discord Webhookでは色のアルファ値は設定できないので0を入れておきましょう。

#### ⑤HTTPリクエストの送信

HTTP通信の実装は[こちらの記事](https://dev.classmethod.jp/articles/unrealengine5-http-api-call/)を参考に行いました。

https://dev.classmethod.jp/articles/unrealengine5-http-api-call/

`FHttpModule::Get().CreateRequest()`でHTTP通信リクエストを作成し、必要な情報を格納していきます。
処理を非同期にするなら、ラムダ式内でコールバックを呼び出すと良いでしょう(方法に関しては趣旨から外れるのでここでは割愛します)。

~~~
// HTTPリクエストを作成
	TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
	Request->SetURL(URL);
	Request->SetVerb("POST");
	Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	Request->SetContentAsString(Payload);

    // HTTP通信終了時の処理を登録
	Request->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr,
    FHttpResponsePtr Response, bool bWasSuccessful) {
		if (bWasSuccessful && Response->GetResponseCode() == 204)
		{
			UE_LOG(LogDiscordMessenger, Log, TEXT("Message sent to Discord successfully!"));
		}
		else
		{
			UE_LOG(LogDiscordMessenger, Error, TEXT("Failed to send message to Discord: %s"), *Response->GetContentAsString());
		}
	});

    // HTTP通信をリクエスト
	Request->ProcessRequest();
~~~

以上で実装は完了です。お疲れ様でした。

# 終わりに
いかがだったでしょうか。
この記事をきっかけにUnrealC++やプラグイン作成に挑戦してくれる方が一人でも増えれば幸いです。
情報をどこまでカットしていいのか分からずやや長めの記事となってしまいました。最後までお付き合いいただきありがとうございます。
ちなみに次に作るものは未定です。