Unreal Engine Plugin Architect & Developer
- I create tools and solutions for Unreal Engine with a focus on audio processing at solutions.georgy.dev
- Feel free to email me at [email protected] or join Discord for any questions
Unreal Engine Plugin Architect & Developer

I recently put together a demo project that shows how to create fully interactive AI NPCs in Unreal Engine using speech recognition, AI chatbots, text-to-speech, and realistic lip synchronization. The entire system is built with Blueprints and works across Windows, Linux, Mac, iOS, and Android. If you’ve been exploring AI NPC solutions like ConvAI or Charisma.ai, you’ve probably noticed the tradeoffs: metered API costs that scale with your player count, latency from network roundtrips, and dependency on cloud infrastructure. This modular approach gives you more control, run components locally or pick your own cloud providers, avoid per-conversation billing, and keep your players interactions private if needed. You own the pipeline, so you can optimize for what actually matters to your game. Plus, with local inference and direct audio-based lip sync, you can achieve lower latency and more realistic facial animation, check the demo video below to see the difference yourself. ...

Game localization has traditionally been a time-consuming and expensive process. Manual translation work often requires weeks of coordination with external services. AI language models now provide developers with powerful tools to streamline this workflow while they maintain quality and reduce costs. This guide demonstrates how to set up automated AI-powered translation for your Unreal Engine project using the AI Localization Automator plugin . The plugin turns hours of manual work into minutes of automated processing. ...

Introduction Still, as of UE 5.5, multi-line editable text boxes lack proper keyboard and gamepad navigation support. Let’s fix this without modifying the engine’s source code. I’ll experiment with a simple 2x2 grid of text boxes to demonstrate how we can implement smooth, unified navigation across both keyboard and gamepad input devices, which you can also recreate for testing purposes. Current Limitations Keyboard Arrow keys only allow navigation within the content of a single text box, and there’s no way to navigate between text boxes themselves. ...
Need to use third-party libraries that depend on spdlog in your Unreal project? Here’s how to simulate the spdlog API using Unreal’s native UE_LOG system, to let you avoid adding extra dependencies while keeping your code clean. We’ll create a logging utility class that matches the spdlog interface, handling type conversions to FString and formatting along the way. Implementation Here’s the code that bridges spdlog and Unreal’s logging system: #pragma once #include "Containers/UnrealString.h" #include "Internationalization/Text.h" #include "Templates/EnableIf.h" #include "Templates/IsIntegral.h" #include "Templates/IsFloatingPoint.h" #include "Logging/LogMacros.h" #include <string> DEFINE_LOG_CATEGORY_STATIC(LogPiperLibrary, Log, All); /** * Class to simulate the spdlog API in Unreal Engine * spdlog is used in the original piper */ class spdlog { private: // Template function for integral types template <typename T, typename TEnableIf<TIsIntegral<T>::Value, bool>::Type = 0> static FString ToFString(T value) { // Most integral types won't be larger than signed 64-bit integer // TODO: Add support for larger integral types supported by UE (uint64) return FString::Printf(TEXT("%lld"), static_cast<int64>(value)); } // Template function for floating-point types template <typename T, typename TEnableIf<TIsFloatingPoint<T>::Value, bool>::Type = 0> static FString ToFString(T value) { return FString::Printf(TEXT("%f"), static_cast<double>(value)); } // Specialize for FString static FString ToFString(const FString& Value) { return Value; } // Specialize for std::string static FString ToFString(const std::string& Value) { return StringCast<TCHAR>(Value.c_str(), Value.size()).Get(); } // Convert the format string to UTF-16 for Unreal Engine static FText ConvertFormatString(const char* FormatString) { // Convert the format string to FText return FText::FromString(StringCast<TCHAR>(FormatString).Get()); } // Convert a single argument to FFormatArgumentValue template <typename T> static FFormatArgumentValue ToFormatArgumentValue(const T& value) { return FFormatArgumentValue(FText::FromString(ToFString(value))); } // Specialize for FString static FFormatArgumentValue ToFormatArgumentValue(const FString& value) { return FFormatArgumentValue(FText::FromString(value)); } // Specialize for std::string static FFormatArgumentValue ToFormatArgumentValue(const std::string& value) { return FFormatArgumentValue(FText::FromString(StringCast<TCHAR>(value.c_str(), value.size()).Get())); } /** * Replace {} placeholders with {0}, {1}, etc * This is necessary because FText::Format does not support {} placeholders * @param FormatString The format string to convert * @return The converted format string */ static FString ConvertBracedFormatString(const FString& FormatString) { FString ConvertedFormatString = FormatString; int32 PlaceholderIndex = 0; while (ConvertedFormatString.Contains(TEXT("{}"))) { FStringBuilderBase Builder; Builder = ConvertedFormatString; Builder.ReplaceAt(ConvertedFormatString.Find(TEXT("{}")), 2, FString::Printf(TEXT("{%d}"), PlaceholderIndex++)); ConvertedFormatString = Builder.ToString(); } return ConvertedFormatString; } // Format log message with provided arguments template <typename... Args> static FString FormatLogMessage(const char* FormatString, Args... args) { // Convert the format string to FText and replace placeholders FText FormatText = ConvertFormatString(FormatString); FText ConvertedFormatText = FText::FromString(ConvertBracedFormatString(FormatText.ToString())); // Create an array to hold the arguments for formatting TArray<FFormatArgumentValue> FormatArgs; (FormatArgs.Add(ToFormatArgumentValue(args)), ...); // Use FText to format the string with arguments FText FormattedText = FText::Format(ConvertedFormatText, FormatArgs); return FormattedText.ToString(); } public: //~ Functions for simulating the spdlog API template <typename... Args> static void error(const char* FormatString, Args... args) { FString LogMessage = FormatLogMessage(FormatString, args...); UE_LOG(LogPiperLibrary, Error, TEXT("%s"), *LogMessage); } template <typename... Args> static void warn(const char* FormatString, Args... args) { FString LogMessage = FormatLogMessage(FormatString, args...); UE_LOG(LogPiperLibrary, Warning, TEXT("%s"), *LogMessage); } template <typename... Args> static void debug(const char* FormatString, Args... args) { FString LogMessage = FormatLogMessage(FormatString, args...); UE_LOG(LogPiperLibrary, Log, TEXT("%s"), *LogMessage); } template <typename... Args> static void info(const char* FormatString, Args... args) { FString LogMessage = FormatLogMessage(FormatString, args...); UE_LOG(LogPiperLibrary, Log, TEXT("%s"), *LogMessage); } // @formatter:off // Dummy functionality to simulate the spdlog API struct level { constexpr static bool debug = false; }; static bool should_log(bool) { return true; } // Add more functions as needed // @formatter:on }; Usage The class lets you use familiar spdlog calls like spdlog::info, spdlog::debug, spdlog::warn, and spdlog::error in your code. Behind the scenes, it routes everything through Unreal’s logging system, so you can avoid adding the spdlog dependency to your project. ...
As of UE 5.4, the Common UI button (UCommonButtonBase) still doesn’t support direct focus settings. This is because UCommonButtonBase is derived from UUserWidget, which supports focusing, but doesn’t direct the focus to the underlying button itself automatically. However, you can still set the focus on the button by performing a “deep” focus on the Slate button. Here’s how you can achieve this: /** * Sets the focus on the button * This function performs the "deep" focus on the Common UI button, which means that it will set the focus on the button itself * This is useful since UCommonButtonBase is derived from UUserWidget, which doesn't support focus when setting it directly */ UFUNCTION(BlueprintCallable, Category = "mod.io|UI|Button", DisplayName = "Set Button Focus (Common UI)") void SetCommonUIButtonFocus() { #if UE_VERSION_OLDER_THAN(5, 3, 0) if (bIsFocusable) #else if (IsFocusable()) #endif { if (TSharedPtr<SButton> SlateButton = GetSlateButton()) { if (SlateButton->SupportsKeyboardFocus()) { FSlateApplication::Get().SetKeyboardFocus(SlateButton, EFocusCause::Mouse); UE_LOG(LogTemp, Log, TEXT("Set focus on button '%s' (extended way)"), *GetName()); } else { UE_LOG(LogTemp, Warning, TEXT("Trying to set focus on button '%s' but the button does not support keyboard focus"), *GetName()); } } else { UE_LOG(LogTemp, Warning, TEXT("Trying to set focus on button '%s' but the slate button could not be found"), *GetName()); } } else { UE_LOG(LogTemp, Warning, TEXT("Trying to set focus on button '%s' but the button is not focusable"), *GetName()); } } /** * Gets the Slate button widget * The button is highly encapsulated and this function tries to scan the widget tree to find the button * @return The Slate button widget if found, nullptr otherwise */ TSharedPtr<SButton> GetSlateButton() const { if (WidgetTree && WidgetTree->RootWidget) { if (UButton* InternalButton = Cast<UButton>(WidgetTree->RootWidget)) { // UCommonButtonInternalBase::RebuildWidget() creates a SBox wrapper for the button if (TSharedPtr<SBox> BoxButtonWrapper = StaticCastSharedPtr<SBox>(TSharedPtr<SWidget>(InternalButton->GetCachedWidget()))) { if (BoxButtonWrapper->GetChildren() && BoxButtonWrapper->GetChildren()->Num() > 0) { if (TSharedPtr<SButton> InternalButtonSlate = StaticCastSharedPtr<SButton>(TSharedPtr<SWidget>(BoxButtonWrapper->GetChildren()->GetChildAt(0)))) { return InternalButtonSlate; } } } // UButton::RebuildWidget() returns the button directly else if (TSharedPtr<SButton> InternalButtonSlate = StaticCastSharedPtr<SButton>(InternalButton->GetCachedWidget())) { return InternalButtonSlate; } else { UE_LOG(LogTemp, Error, TEXT("Could not find the Slate button widget for button '%s'"), *GetName()); } } } return nullptr; }