[UE5.7][C++]自宅AI(Ollama)サーバーをUE5のC++プラグインで連携したよ


まず全体全体(おすすめ)

A.別途REST(非ストリーミング)で完了させる

  • UE → AIサーバへHTTP POST
  • AI → 問題JSON
  • UE → UIに反映(UMG)

メリット:実装が簡単/デバッグしやすいプレイ
:とにかくが長いと立つが出る


自宅AI側:エンドポイントの考え方

あなたが今 OpenWebUI を使っているなら、内部のモデル実体はおそらくOllama

  • Ollamaの代表的なAPI(例)
    • 生成:POST /api/generate
    • チャット:POST /api/chat

UEからは「どのURLに投げるか」だけ差し替えできるようにしておくのが正解。


UE5プラグイン設計(ほぼこれを入れる)

1) 設定(プロジェクトの設定を出す)

  • ベースURL(例:http://192.168.1.23:11434
  • モデル名(例:llama3.1
  • タイムアウト秒
  • オプション: API Key(OpenAI互換を使う場合)

UDeveloperSettingsを使うと綺麗。


実装:UE5 C++(RESTで会話させる最小セット)

(1) ビルド.cs

  • "HTTP"、、"Json""JsonUtilities"追加

(2) 送信受信用の構造体(JSON)

  • リクエスト: モデル / プロンプト(またはメッセージ)
  • 応答: 応答(または選択肢)

(3) ブループリント非同期ノード化(UIに優しい)

  • UBlueprintAsyncActionBase
    • OnSuccess
    • OnError
      を吐く

作った全コード

furcraHomeAIServerChat2.Build.cs

// Some copyright should be here...

using UnrealBuildTool;

public class furcraHomeAIServerChat2 : ModuleRules
{
	public furcraHomeAIServerChat2(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
		
		PublicIncludePaths.AddRange(
			new string[] {
				// ... add public include paths required here ...
			}
			);
			
		
		PrivateIncludePaths.AddRange(
			new string[] {
				// ... add other private include paths required here ...
			}
			);
			
		
		PublicDependencyModuleNames.AddRange(
			new string[]
			{
                "Core", "CoreUObject", "Engine", "UMG","HTTP", "Json", "JsonUtilities"
				// ... add other public dependencies that you statically link with here ...
			}
			);
			
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject",
				"Engine",
				"Slate",
				"SlateCore",
				"DeveloperSettings",
				// ... add private dependencies that you statically link with here ...	
			}
			);
		
		
		DynamicallyLoadedModuleNames.AddRange(
			new string[]
			{
				// ... add any modules that your module loads dynamically here ...
			}
			);
	}
}

AiChatTypes.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"

/**
 * 
 */
class FURCRAHOMEAISERVERCHAT2_API AiChatTypes
{
public:
	AiChatTypes();
	~AiChatTypes();
};





#include "AiChatTypes.generated.h"

USTRUCT(BlueprintType)
struct FAiChatMessage
{
	GENERATED_BODY()

	// "system" | "user" | "assistant"
	UPROPERTY(BlueprintReadWrite, EditAnywhere) FString role;
	UPROPERTY(BlueprintReadWrite, EditAnywhere) FString content;
};

USTRUCT()
struct FOllamaChatRequest
{
	GENERATED_BODY()

	UPROPERTY() FString model;
	UPROPERTY() TArray<FAiChatMessage> messages;
	UPROPERTY() bool stream = false;
};

USTRUCT()
struct FOllamaChatMessage
{
	GENERATED_BODY()

	UPROPERTY() FString role;
	UPROPERTY() FString content;
};

USTRUCT()
struct FOllamaChatResponse
{
	GENERATED_BODY()

	UPROPERTY() FOllamaChatMessage message;
	UPROPERTY() bool done = false;
};

AiChatTypes.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "AiChatTypes.h"

AiChatTypes::AiChatTypes()
{
}

AiChatTypes::~AiChatTypes()
{
}



AiLinkAsyncChat.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AiChatTypes.h"
#include "AiLinkAsyncChat.generated.h"

// Forward declarations for HTTP interfaces to avoid including HTTP headers in this public header
class IHttpRequest;
class IHttpResponse;

using FHttpRequestPtr = TSharedPtr<IHttpRequest, ESPMode::ThreadSafe>;
using FHttpResponsePtr = TSharedPtr<IHttpResponse, ESPMode::ThreadSafe>;

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAiChatSuccess, const FString&, AssistantText);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAiChatError, const FString&, Error);

UCLASS()
class FURCRAHOMEAISERVERCHAT2_API UAiLinkAsyncChat : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintAssignable) FAiChatSuccess OnSuccess;
	UPROPERTY(BlueprintAssignable) FAiChatError OnError;

	// Messagesは「system + 履歴 + 今回のuser」を含めて渡す
	UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
	static UAiLinkAsyncChat* SendChat(const TArray<FAiChatMessage>& Messages);

	virtual void Activate() override;

private:
	TArray<FAiChatMessage> MessagesInternal;
	void HandleResponse(FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bOk);
};

AiLinkAsyncChat.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "AiLinkAsyncChat.h"

#include "UAiLinkSettings.h"
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "JsonObjectConverter.h"

UAiLinkAsyncChat* UAiLinkAsyncChat::SendChat(const TArray<FAiChatMessage>& Messages)
{
	auto* Node = NewObject<UAiLinkAsyncChat>();
	Node->MessagesInternal = Messages;
	return Node;
}

void UAiLinkAsyncChat::Activate()
{
	const UAiLinkSettings* S = GetDefault<UAiLinkSettings>();
	if (!S)
	{
		OnError.Broadcast(TEXT("Settings not found"));
		return;
	}

	const FString Url = S->BaseUrl / TEXT("api/chat");

	FOllamaChatRequest Body;
	Body.model = S->Model;
	Body.messages = MessagesInternal;
	Body.stream = false;

	FString JsonStr;
	if (!FJsonObjectConverter::UStructToJsonObjectString(Body, JsonStr))
	{
		OnError.Broadcast(TEXT("Failed to serialize request JSON"));
		return;
	}

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Req = FHttpModule::Get().CreateRequest();
	Req->SetURL(Url);
	Req->SetVerb(TEXT("POST"));
	Req->SetHeader(TEXT("Content-Type"), TEXT("application/json; charset=utf-8"));
	Req->SetTimeout(S->TimeoutSeconds);
	Req->SetContentAsString(JsonStr);

	Req->OnProcessRequestComplete().BindUObject(this, &UAiLinkAsyncChat::HandleResponse);
	Req->ProcessRequest();

	UE_LOG(LogTemp, Warning, TEXT("[AI] Activate() called"));
	UE_LOG(LogTemp, Warning, TEXT("[AI] URL: %s"), *Req->GetURL());
	UE_LOG(LogTemp, Warning, TEXT("[AI] Payload: %s"), *JsonStr);
}

void UAiLinkAsyncChat::HandleResponse(FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bOk)
{
	if (!bOk || !Resp.IsValid())
	{
		OnError.Broadcast(TEXT("HTTP request failed (no response)"));
		return;
	}

	const int32 Code = Resp->GetResponseCode();
	const FString Content = Resp->GetContentAsString();
	UE_LOG(LogTemp, Warning, TEXT("[AI] HTTP %d"), Code);
	UE_LOG(LogTemp, Warning, TEXT("[AI] Body: %s"), *Content);

	UE_LOG(LogTemp, Warning, TEXT("[AI] HandleResponse() called. bOk=%d RespValid=%d"),
		bOk ? 1 : 0, Resp.IsValid() ? 1 : 0);

	if (Resp.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("[AI] HTTP %d"), Resp->GetResponseCode());
		UE_LOG(LogTemp, Warning, TEXT("[AI] Body: %s"), *Resp->GetContentAsString());
	}



	if (Code < 200 || Code >= 300)
	{
		OnError.Broadcast(FString::Printf(TEXT("HTTP %d: %s"), Code, *Content));
		return;
	}

	FOllamaChatResponse Parsed;
	if (!FJsonObjectConverter::JsonObjectStringToUStruct(Content, &Parsed, 0, 0))
	{
		OnError.Broadcast(FString::Printf(TEXT("Failed to parse JSON: %s"), *Content));
		return;
	}

	UE_LOG(LogTemp, Warning, TEXT("[AI] Broadcasting Success: %s"), *Parsed.message.content);
	OnSuccess.Broadcast(Parsed.message.content);

}

AiLinkAsyncGenerate.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AiLinkRequestTypes.h"
#include "AiLinkAsyncGenerate.generated.h"

// Forward declarations for HTTP interfaces to avoid including HTTP headers in this public header
class IHttpRequest;
class IHttpResponse;

using FHttpRequestPtr = TSharedPtr<IHttpRequest, ESPMode::ThreadSafe>;
using FHttpResponsePtr = TSharedPtr<IHttpResponse, ESPMode::ThreadSafe>;

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAiGenerateSuccess, const FString&, Text);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAiGenerateError, const FString&, Error);

UCLASS()
class FURCRAHOMEAISERVERCHAT2_API UAiLinkAsyncGenerate : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintAssignable) FAiGenerateSuccess OnSuccess;
	UPROPERTY(BlueprintAssignable) FAiGenerateError OnError;

	UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
	static UAiLinkAsyncGenerate* GenerateText(const FString& Prompt);

	virtual void Activate() override;

private:
	FString PromptInternal;
	void HandleResponse(FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bOk);
};

AiLinkAsyncGenerate.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "AiLinkAsyncGenerate.h"

#include "UAiLinkSettings.h"	
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "JsonObjectConverter.h"

UAiLinkAsyncGenerate* UAiLinkAsyncGenerate::GenerateText(const FString& Prompt)
{
	auto* Node = NewObject<UAiLinkAsyncGenerate>();
	Node->PromptInternal = Prompt;
	return Node;
}

void UAiLinkAsyncGenerate::Activate()
{
	const UAiLinkSettings* S = GetDefault<UAiLinkSettings>();
	if (!S)
	{
		OnError.Broadcast(TEXT("Settings not found"));
		return;
	}

	const FString Url = S->BaseUrl / TEXT("api/generate");

	FOllamaGenerateRequest Body;
	Body.model = S->Model;
	Body.prompt = PromptInternal;
	Body.stream = false;

	FString JsonStr;
	if (!FJsonObjectConverter::UStructToJsonObjectString(Body, JsonStr))
	{
		OnError.Broadcast(TEXT("Failed to serialize request JSON"));
		return;
	}

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Req = FHttpModule::Get().CreateRequest();
	Req->SetURL(Url);
	Req->SetVerb(TEXT("POST"));
	Req->SetHeader(TEXT("Content-Type"), TEXT("application/json; charset=utf-8"));
	Req->SetTimeout(S->TimeoutSeconds);
	Req->SetContentAsString(JsonStr);

	Req->OnProcessRequestComplete().BindUObject(this, &UAiLinkAsyncGenerate::HandleResponse);
	Req->ProcessRequest();
}

void UAiLinkAsyncGenerate::HandleResponse(FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bOk)
{
	if (!bOk || !Resp.IsValid())
	{
		OnError.Broadcast(TEXT("HTTP request failed (no response)"));
		return;
	}

	const int32 Code = Resp->GetResponseCode();
	const FString Content = Resp->GetContentAsString();

	if (Code < 200 || Code >= 300)
	{
		OnError.Broadcast(FString::Printf(TEXT("HTTP %d: %s"), Code, *Content));
		return;
	}

	FOllamaGenerateResponse Parsed;
	if (!FJsonObjectConverter::JsonObjectStringToUStruct(Content, &Parsed, 0, 0))
	{
		OnError.Broadcast(FString::Printf(TEXT("Failed to parse JSON: %s"), *Content));
		return;
	}

	OnSuccess.Broadcast(Parsed.response);
}

AiLinkRequestTypes.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"

/**
 * 
 */
class FURCRAHOMEAISERVERCHAT2_API AiLinkRequestTypes
{
public:
	AiLinkRequestTypes();
	~AiLinkRequestTypes();
};


//#pragma once
//#include "CoreMinimal.h"
#include "AiLinkRequestTypes.generated.h"

USTRUCT()
struct FOllamaGenerateRequest
{
	GENERATED_BODY()

	UPROPERTY() FString model;
	UPROPERTY() FString prompt;
	UPROPERTY() bool stream = false;
};

USTRUCT()
struct FOllamaGenerateResponse
{
	GENERATED_BODY()

	UPROPERTY() FString response;
	UPROPERTY() bool done = false;
};

AiLinkRequestTypes.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "AiLinkRequestTypes.h"

AiLinkRequestTypes::AiLinkRequestTypes()
{
}

AiLinkRequestTypes::~AiLinkRequestTypes()
{
}

UAiLinkSettings.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "Home AI Chat"))
class FURCRAHOMEAISERVERCHAT2_API UAiLinkSettings : public UDeveloperSettings
{
	GENERATED_BODY()

public:
	UPROPERTY(Config, EditAnywhere, Category = "AI")
	FString BaseUrl = TEXT("http://192.168.1.23:11434");

	UPROPERTY(Config, EditAnywhere, Category = "AI")
	//FString Model = TEXT("llama3.1"); 
	//FString Model = TEXT("deepseek-r1:32b");llama3.1:8b
	//FString Model = TEXT("llama3.1:70b");//llama3.1:8b
	FString Model = TEXT("llama3.1:8b");//
	UPROPERTY(Config, EditAnywhere, Category = "AI")
	float TimeoutSeconds = 120.0f;

	// 例: "You are NPC assistant in my game..."
	UPROPERTY(Config, EditAnywhere, Category = "AI")
	FString SystemPrompt = TEXT("You are a helpful in-game NPC. Keep replies concise.");
};

UAiLinkSettings.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "UAiLinkSettings.h"

BluePrintはこんなかんじ

以上。

[UE5][CPP]Failed to compile plugin source code. See output log for more information.

Failed to compile plugin source code. See output log for more information.

プラグインのソースコードをコンパイルできませんでした。詳細は出力ログを参照してください。

‘furcraeaUEOpenAI2’ already defines a member called ‘furcraeaUEOpenAI2’ with the same parameter types
Expecting to find a type to be declared in a target rules named ‘furcraeaUEOpenAI2EditorTarget’. This type must derive from the ‘TargetRules’ type defined by UnrealBuildTool.

‘furcraeaUEOpenAI2’ は既に同じパラメータ型を持つ ‘furcraeaUEOpenAI2’ というメンバーを定義しています。
‘furcraeaUEOpenAI2EditorTarget’ というターゲットルールで宣言される型が見つかることを期待しています。この型は、UnrealBuildTool で定義された ‘TargetRules’ 型から派生している必要があります。

[UE5.7][VisualStudio]C++プラグインを作る方法 2025/12/13再度検証済み

ポジTAさんのこのぺーじをvs2022でやる

https://zenn.dev/posita33/books/ue5_starter_cpp_and_bp_001/viewer/chap_01_vs2019_setup

C++Pluginの作り方

C++ Pluginの作成

プロジェクトを作成する際に「C++」でプロジェクトを作成します。

[Edit] > [Plugins]を選択します。

独自のPluginを作成するには左上の「Add」ボタンをクリックします。

プラグインの作成手順は以下の手順になります。

  1. Pluginのテンプレートを選択する
  2. Plugin名を入力する
  3. Plugin情報を入力する(任意)
  4. Create Pluginをクリック

Author制作者名
DescriptionPluginの説明
Author URL制作者のPlugin説明用のURLを書いておくとPluginの制作者名をクリックするとURLに移動する
Is Beta Versionチェックボックスを有効にすると、Plugin名の横にBetaが表示される

Create Plugin 」を押す。2分ぐらいかかる?

Pluginのウィンドウで作成したPlugin名を検索すると一覧に登録されています。

Editボタンで(Plugin名).pluginファイルで設定をより詳細に変更できます。

色々設定して128×128のアイコン設定して。。

ContentsBrowserやVisualStudioに追加されたPluginが表示されます。

以下の感じで吐き出される

furcraeaBluePrintLib.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Modules/ModuleManager.h"

class FfurcraeaBluePrintLibModule : public IModuleInterface
{
public:

	/** IModuleInterface implementation */
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
};

furcraeaBluePrintLib.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "furcraeaBluePrintLib.h"

#define LOCTEXT_NAMESPACE "FfurcraeaBluePrintLibModule"

void FfurcraeaBluePrintLibModule::StartupModule()
{
	// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
	
}

void FfurcraeaBluePrintLibModule::ShutdownModule()
{
	// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,
	// we call this function before unloading the module.
	
}

#undef LOCTEXT_NAMESPACE
	
IMPLEMENT_MODULE(FfurcraeaBluePrintLibModule, furcraeaBluePrintLib)

furcraeaBluePrintLibBPLibrary.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "Kismet/BlueprintFunctionLibrary.h"
#include "furcraeaBluePrintLibBPLibrary.generated.h"

/* 
*	Function library class.
*	Each function in it is expected to be static and represents blueprint node that can be called in any blueprint.
*
*	When declaring function you can define metadata for the node. Key function specifiers will be BlueprintPure and BlueprintCallable.
*	BlueprintPure - means the function does not affect the owning object in any way and thus creates a node without Exec pins.
*	BlueprintCallable - makes a function which can be executed in Blueprints - Thus it has Exec pins.
*	DisplayName - full name of the node, shown when you mouse over the node and in the blueprint drop down menu.
*				Its lets you name the node using characters not allowed in C++ function names.
*	CompactNodeTitle - the word(s) that appear on the node.
*	Keywords -	the list of keywords that helps you to find node when you search for it using Blueprint drop-down menu. 
*				Good example is "Print String" node which you can find also by using keyword "log".
*	Category -	the category your node will be under in the Blueprint drop-down menu.
*
*	For more info on custom blueprint nodes visit documentation:
*	https://wiki.unrealengine.com/Custom_Blueprint_Node_Creation
*/
UCLASS()
class UfurcraeaBluePrintLibBPLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_UCLASS_BODY()

	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Execute Sample function", Keywords = "furcraeaBluePrintLib sample test testing"), Category = "furcraeaBluePrintLibTesting")
	static float furcraeaBluePrintLibSampleFunction(float Param);
};

furcraeaBluePrintLibBPLibrary.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "furcraeaBluePrintLibBPLibrary.h"
#include "furcraeaBluePrintLib.h"

UfurcraeaBluePrintLibBPLibrary::UfurcraeaBluePrintLibBPLibrary(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{

}

float UfurcraeaBluePrintLibBPLibrary::furcraeaBluePrintLibSampleFunction(float Param)
{
	return -1;
}

参考URL

全部ポジTAさんですありがとうございます。

https://zenn.dev/posita33/books/ue5_posilab_ue_research_and_development/viewer/category_350_pulugin_start