Чи коли-небудь ви думали, як круто було б мати реальну людську бесіду в вашій грі? Уявіть, що ви створюєте систему діалогів з NPC, яка виглядає справді природно, живого оповідача, що відповідає на дії гравців, або просто інструмент у грі, який може генерувати унікальний контент на ходу. Звучить чудово, чи не так? Незалежно від того, чи ви створюєте однокористувацьку гру чи багатокористувацький досвід, додавання генеративного ШІ може зробити гру набагато цікавішою і динамічнішою. У цьому дописі я покажу вам крок за кроком, як зробити це можливим!
Haiku, робот
Я вивчаю Креативні медіа та ігрові технології в Університеті прикладних наук Бреди і наразі знаходжусь на другому році програми за спеціальністю програмування. На другому блоці я створив плагін генеративного ШІ, який дозволяє генерувати діалоги з NPC. Тепер я радий поділитися тим, чого навчився. Це буде керівництво для початківців, тому я поясню все якнайдокладніше, щоб допомогти вам уникнути тих проблем, з якими я зіткнувся під час своєї роботи. Я проведу вас через процес створення плагіну для Unreal Engine за допомогою C++, який може інтегрувати сторонню бібліотеку (наприклад, модель генеративного ШІ).
Спочатку ми детальніше поговоримо про генеративний ШІ. Потім я покажу, як налаштувати плагін для Unreal Engine, щоб додати сторонню бібліотеку для офлайн-генерації ШІ. Як бонус, я також розгляну впровадження для онлайн-генерації ШІ. Плагін може працювати як онлайн, так і офлайн, що означає, що він може бути гнучким і корисним для всіх видів ігор та проектів!
Але чому варто використовувати офлайн-генерацію ШІ?
З розвитком генеративних ШІ-систем, таких як OpenAI та інших платформ, все більше програм використовують ці системи, щоб залишатися на гребені тренду. Проте не всі говорять про проблеми, які виникають при використанні онлайн-генеративного ШІ. Наприклад, це коштує грошей. Щоб надсилати запити до OpenAI, потрібна підписка або баланс на вашому рахунку. Залежно від кількості токенів у відповіді, яку він надсилає, вам доведеться платити. Це також потребує підключення до інтернету та акаунта на певному сайті. Не кажучи вже про те, що він може бути вразливим до атак і викривати чутливі дані.
Офлайн-моделі генеративного ШІ, з іншого боку, можна використовувати безкоштовно. Вони не потребують акаунта або інтернет-з'єднання, лише потужний процесор і, можливо, графічний процесор. Звісно, офлайн-ШІ має свої недоліки, наприклад, необхідність великої обчислювальної потужності навіть для генерації простих відповідей, і для цього потрібна велика LLM (Large Language Model) для створення відповідей. Однак іноді приємно експериментувати і подивитися, який результат вийде!
Почнемо створювати офлайн-генеративний ШІ для Unreal Engine!
Пошук бібліотеки
По-перше, ми маємо зрозуміти, що будемо працювати з C++. Тому нам потрібно знайти бібліотеку, яка надає API для взаємодії з генеративним ШІ. Для цієї задачі ми будемо використовувати llama.cpp.
Звісно, існують й інші бібліотеки, але llama.cpp виділяється тим, що вона написана на C++, працює з лише однією бібліотекою і не потребує додаткових залежностей. Це велика перевага при підключенні до Unreal Engine. Крім того, у неї є активна спільнота, яка постійно оновлює та виправляє проблеми (і ми не хочемо зіткнутися з проблемами при використанні інших бібліотек). Єдине, що може бути мінусом, — репозиторій на GitHub не надто дружелюбний до користувачів, і бібліотека вимагає від користувача вже мати достатні знання в C++ та генеративному ШІ (але не хвилюйтесь, тому ви читаєте це керівництво! 😄).
Побудова бібліотеки
Почнемо з побудови бібліотеки llama.cpp. За замовчуванням немає готових бібліотек або файлів, які можна просто завантажити та підключити. Ми повинні побудувати її за допомогою CMake.
Простими словами, CMake — це інструмент, який допомагає генерувати необхідні файли або бібліотеки, які будуть працювати на вашому поточному комп'ютері.
Це дозволяє уникнути створення окремих збірок для кожної різної машини і замість цього мати одну збірку, яку можна компілювати для різних платформ за допомогою CMake.
Тепер, коли ми знаємо, що таке CMake, можемо завантажити його з офіційного сайту і використовувати для побудови llama.cpp (не забудьте також встановити GitHub!). Перейдіть до папки, де ви хочете побудувати бібліотеку, і введіть ці команди в консолі:
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
Після цього ви можете побудувати бібліотеку за допомогою CMake, набравши наступні команди:
cmake -B build -DBUILD_SHARED_LIBS=OFF
cmake --build build --config Release
Додаючи рядок -DBUILDSHAREDLIBS=OFF, ми говоримо CMake, що ми хочемо побудувати llama.cpp без .dll файлів (тобто тільки .lib файли). Якщо ви застрягли або хочете побудувати його по-іншому, будь ласка, ознайомтесь з репозиторієм на GitHub для llama.cpp
Тепер ми згенерували всі необхідні файли для llama.cpp! Але тепер справжнє питання: які файли нам потрібні? CMake згенерував багато папок. Але не хвилюйтесь! В основному нам потрібні ці файли: llama.lib (llama.cpp/build/src/release), ggml.lib та ggml-base.lib (llama.cpp/build/ggml/src/Release) і ggml-cpu.lib (llama.cpp/build/ggml/src/ggml-cpu/Release). Нам також знадобиться заголовочний файл llama.h (lama.cpp/include) та всі заголовочні файли всередині папки ggml.h, ggml-alloc.h, тощо (llama.cpp/ggml/include).
Створення порожнього плагіна для Unreal Engine
По-перше, вам потрібно мати встановлений Visual Studio 2022 разом з усіма необхідними інструментами. Якщо ви не знаєте, як налаштувати Visual Studio для розробки в Unreal Engine, будь ласка, ознайомтесь з офіційним посібником: Налаштування Visual Studio.
Тепер перейдіть до вашого проекту Unreal (або створіть новий порожній проект) і перейдіть до меню “Edit”. Виберіть “Plugins” зі списку. Ви побачите кнопку “+ Add”. Клацніть на неї, щоб створити ваш порожній плагін. Назвемо його “AwesomeAIPlugin”, і він автоматично відкриє Visual Studio для вас.
Ось як має виглядати кінцева структура плагіна (BlogExample — це мій проект Unreal Engine)
Перейдіть до YourPluginName.uplugin і там ви знайдете два основних налаштування всередині нього (рядки після “Modules”). Ми не будемо змінювати їх, але корисно знати, що вони означають:
1. Type: визначає, як модуль використовується в engine. Ось кілька типових значень:
- Runtime: Використовується під час реального ігрового процесу. Це найбільш поширений тип для функціональності, з якою взаємодіють гравці.
- Editor: Використовується для інструментів або функцій, які працюють тільки в редакторі (не в грі, можливо додавання нового UI для редактора Unreal).
- RuntimeAndEditor: Використовується як в грі, так і в редакторі.
- Developer: Використовується для інструментів для відлагодження або розробки (не включається у фінальну гру).
2. LoadingPhase: контролює коли модуль завантажується. Ось кілька типових фаз:
- Default: Завантажується, коли engine потребує цього, зазвичай на самому початку запуску.
- PreDefault: Завантажується ще раніше, до більшості інших модулів.
- PostDefault: Завантажується пізніше за стандартну фазу.
- PreLoadingScreen: Завантажується перед появою екрану завантаження.
- PostEngineInit: Завантажується після того, як engine закінчить ініціалізацію.
- None: Модуль ніколи не завантажується автоматично (вам потрібно завантажити його вручну).
Після цього ми можемо перейти до наступного кроку, а саме:
Імпортування llama.cpp в Unreal Engine
По-перше, нам потрібно додати необхідні бібліотеки до Unreal Engine, щоб він міг їх використовувати.
Для цього нам потрібно помістити всі файли в наш проект разом з “AwesomeAIPlugin”. Перейдімо до ExampleProject/Plugins/YourPluginName/Source/YourPluginName і додамо папку з назвою “ThirdParty”. Відкрийте цю папку і помістіть файли, які ми отримали з CMake. Я організую їх дещо інакше, щоб виглядало акуратніше. Я залишу файли .lib в папці “ThirdParty”, але також створю нову папку під назвою “headers” для всіх заголовочних файлів. Отже, у нас будуть всі .lib файли в “ThirdParty” і всі заголовки в ThirdParty/headers.
Добре, тепер у нас є всі файли для нашої бібліотеки, але Unreal Engine все ще не розпізнає їх. Звичайні C++ модулі Unreal Engine налаштовуються за допомогою файлу YourPluginName.Build.cs, і сторонні бібліотеки не є винятком. Тому нам потрібно перейти до YourPluginName.Build.cs (краще відкрити Visual Studio .sln файли в папці вашого ExampleProject і перейти до ExampleProject->Plugins->Source->YourPluginName->YourPluginName.Build.Cs) і змінити деякі рядки коду на такі:
// Copyright Epic Games, Inc. All Rights Reserved.
using System.IO;
using UnrealBuildTool;
public class AwesomeAIPlugin : ModuleRules
{
public AwesomeAIPlugin(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... додайте публічні шляхи для включень тут ...
Path.Combine(ModuleDirectory, "ThirdParty", "headers") // Додаємо директорію заголовків
}
);
// ...
// Включаємо бібліотеки llama.cpp
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty", "llama.lib"));
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ggml.lib"));
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ggml-base.lib"));
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "ThirdParty", "ggml-cpu.lib"));
}
}
Повний код має виглядати ось так: цей. Ми майже закінчили! Тепер нам потрібно знову згенерувати файли рішення, щоб Visual Studio могла розпізнати нові файли. Для цього перейдіть до вашої папки ExampleProject, клацніть правою кнопкою миші на ExampleProject.uproject (для мене це BlogExample.uproject) і виберіть Генерувати файли проекту Visual Studio:
Після цього в тій самій папці відкрийте ExampleProject.sln, потім клацніть правою кнопкою миші на модуль ExampleProject (для мене це BlogExample) і виберіть Rebuild solution.
І все! Це був важкий шлях, але тепер у нас є наша бібліотека llama.cpp в Unreal Engine! Ви можете спробувати запустити Unreal Engine через Visual Studio і перевірити, чи все працює, натиснувши Local Windows Debugger, але переконайтесь, що ви використовуєте режим Development Editor, який було виділено жовтим кольором на попередньому знімку екрану.
Перші взаємодії з офлайн-генеративним ШІ!
Добре, ми нарешті налаштували Unreal Engine! Тепер давайте насправді згенеруємо відповідь за допомогою llama.cpp. Однак перед тим, як почати, я хочу пояснити деякі базові терміни про LLM та їх роботу:
- LLM (Large Language Model) — LLM це тип ШІ, який тренується на величезній кількості текстів (як книги, статті та вебсайти). Він вивчає шаблони мови, щоб відповідати на запитання, писати історії або генерувати код. Можна вважати це надпотужним автозаповненням: ви вводите щось, а він передбачає, що буде далі. LLM може бути попередньо навченим, що означає, що він вже знає загальні речі про мову та популярні теми. Якщо ви хочете, щоб він спеціалізувався на чомусь конкретному (наприклад, на взаємодії, схожій на людську), ви можете дати йому додаткове навчання, яке називається fine-tuning.
- Токени — Токени це будівельні блоки тексту для LLM.
Токен може бути словом, частиною слова або навіть просто однією літерою чи символом (залежить від моделі). LLM думає токенами, а не цілими реченнями. Коли генерується текст, модель передбачає наступний токен крок за кроком, як розв'язання головоломки по частинах. - Temperature — Це параметр, що контролює, наскільки “креативними” чи “випадковими” будуть відповіді моделі. Низька температура (зазвичай число з плаваючою комою 0.2) змушує модель обирати найбільш безпечні, передбачувані відповіді. Висока температура (зазвичай число з плаваючою комою між 0.8 і 1.0) робить модель більш креативною або різноманітною у своїх відповідях.
Сподіваюсь, ці визначення допоможуть вам краще зрозуміти, як працюють LLM. Але де ж взяти LLM? Насправді, llama.cpp використовує LLM з розширеннями .gguf, і їх дуже багато! Просто переконайтесь, що завантажили fine-tuned версію моделі, оскільки в іншому випадку генерування тексту може бути дуже дивним. Я буду використовувати Mistral-7B-Instruct-v0.3-GGUF (Mistral-7B-Instruct-v0.3.Q6K.gguf)_, оскільки це потужна модель, яка перевершує інші навіть з меншою кількістю параметрів (це як “клітини мозку” LLM. Чим більше параметрів у моделі, тим “розумнішою” вона може бути). Завантажте будь-яку модель, яка вам подобається, і збережіть її на вашому комп'ютері.
Для простоти ми будемо писати код у файлі YourPluginName.cpp. У наведеному нижче коді ви побачите, як ми налаштовуємо ці початкові параметри та готуємо базові дані для взаємодії з ШІ:
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AwesomeAIPlugin.h"
#include
#include
#include "llama.h"
#define LOCTEXT_NAMESPACE "FAwesomeAIPluginModule"
void FAwesomeAIPluginModule::StartupModule()
{
// Цей код буде виконуватись після того, як ваш модуль буде завантажено в пам'ять; точний час визначається у файлі .uplugin для кожного модуля
// Встановлюємо роль помічника в контексті
std::string assistantRole = "Helpful assistant";
std::string messageToAI = "Hey, who are you?";
// Вставте шлях до локальної моделі ШІ
std::string model_path = "C:/CPlusPlus/LlamaCppAi/assets/aiModels/Mistral-7B-Instruct-v0.3.Q6_K.gguf";
// ...
Після налаштування основних параметрів, нам потрібно визначити кілька ключових змінних llama.cpp, які будуть обробляти взаємодію між ШІ та користувачем. Це включає історію чатів, саму модель, контекст (ctx, який виступає як “робочий простір” ШІ) і семплер для управління тим, як ШІ генерує відповіді. Буфер використовується для форматованих запитів, щоб все було правильно підготовлено перед тим, як передавати їх моделі (щоб модель зрозуміла запит):
std::vector messages; // Зберігає контекст розмови
llama_model* model = nullptr; // Екземпляр моделі
llama_context* ctx = nullptr; // Екземпляр контексту
llama_sampler* sampler = nullptr; // Екземпляр семплера
std::vector formatted; // Буфер для форматованих запитів
Далі ми ініціалізуємо модель ШІ та налаштовуємо її контекст і методи семплінгу:
// --- Ініціалізація моделі ---
llama_model_params model_params = llama_model_default_params();
model = llama_load_model_from_file(model_path.c_str(), model_params);
if (!model) {
UE_LOG(LogTemp, Error, TEXT("Не вдалося завантажити модель."));
return;
}
llama_context_params ctx_params = llama_context_default_params();
ctx_params.n_ctx = 2048; // Стандартний розмір контексту (розмір вікна контексту, скільки токенів (слів, частин слів, пунктуаційних знаків) модель може врахувати одночасно.
(Якщо ви перевищите цей ліміт, модель почне забувати старі частини розмови.)
ctx = llama_new_context_with_model(model, ctx_params); // Фактично створює робочий простір (пам'ять)
if (!ctx) {
UE_LOG(LogTemp, Error, TEXT("Не вдалося створити контекст."));
return;
}
// Ініціалізація семплера (Семплер у контексті мовних моделей визначає, як вибирається наступний токен (слово, частина слова чи символ) під час генерації тексту.)
sampler = llama_sampler_chain_init(llama_sampler_chain_default_params());
// Додаємо специфічні техніки семплінгу
llama_sampler_chain_add(sampler, llama_sampler_init_min_p(0.1f, 1)); // забезпечує, що менш ймовірні токени не вибираються занадто часто.
llama_sampler_chain_add(sampler, llama_sampler_init_top_p(0.90f, 1)); // обмежує пул токенів до топ 90% за ймовірністю, роблячи вихід більш зосередженим.
llama_sampler_chain_add(sampler, llama_sampler_init_temp(0.7)); // контролює випадковість; менші значення роблять відповіді ШІ більш передбачуваними, вищі — більш креативними.
llama_sampler_chain_add(sampler, llama_sampler_init_dist(LLAMA_DEFAULT_SEED));
formatted.resize(llama_n_ctx(ctx));
// -----------------------------
Тепер додаємо роль ШІ та введення користувача до історії розмови (messages змінна). Таким чином, модель знає, хто що сказав. Після цього встановлюємо ліміт слів для відповіді ШІ (у цьому випадку 50 слів). Потім ми форматуємо запит з оновленими повідомленнями, переконуючись, що розмова правильно структурована. Якщо форматований запит занадто довгий, ми змінюємо розмір буфера, щоб він підійшов. Якщо щось йде не так під час форматування, ми реєструємо помилку. Нарешті, ми конвертуємо відформатовані дані в рядок, який можна передати моделі.
// Додаємо роль для моделі ШІ та введення користувача до повідомлень
messages.push_back({ "system", _strdup(assistantRole.c_str()) });
messages.push_back({ "user", _strdup(messageToAI.c_str()) });
int word_limit = 50;
// Форматуємо запит з оновленими повідомленнями
int new_len = llama_chat_apply_template(model, nullptr, messages.data(), messages.size(), true, formatted.data(), formatted.size());
if (new_len > static_cast<int>(formatted.size())) {
formatted.resize(new_len);
new_len = llama_chat_apply_template(model, nullptr, messages.data(), messages.size(), true, formatted.data(), formatted.size());
}
if (new_len < 0) {
UE_LOG(LogTemp, Error, TEXT("Не вдалося відформатувати шаблон чату."));
return;
}
std::string prompt(formatted.begin(), formatted.begin() + new_len);
Тепер настає головна частина, коли ШІ генерує відповідь. Починаємо з токенізації запиту (розбиття на менші частини, які модель може зрозуміти). Якщо є проблема з токенізацією, ми повертаємо помилку. Далі ми входимо в цикл, де модель генерує один токен за раз. Ми відслідковуємо кількість згенерованих слів і зупиняємося, коли досягаємо ліміту слів або модель завершить (llamatokeniseog_). Кожен токен перетворюється назад у текст і додається до відповіді. Після генерації відповіді ми виводимо її і додаємо до історії розмови.
Тепер ми звільняємо ресурси, щоб уникнути витоків пам'яті.
// Генерація відповіді (Основний цикл)
auto generate = [&](const std::string& prompt) {
const int n_prompt_tokens = -llama_tokenize(model, prompt.c_str(), prompt.size(), NULL, 0, true, true);
std::vector prompt_tokens(n_prompt_tokens);
if (llama_tokenize(model, prompt.c_str(), prompt.size(), prompt_tokens.data(), prompt_tokens.size(), llama_get_kv_cache_used_cells(ctx) == 0, true) < 0) {
return std::string("Помилка: Не вдалося токенізувати запит.");
}
llama_batch batch = llama_batch_get_one(prompt_tokens.data(), prompt_tokens.size());
llama_token new_token_id;
std::ostringstream response_stream;
FString UELOG = "";
int word_count = 0; // Лічильник слів для обмеження кількості слів у відповіді
while (true) {
if (llama_decode(ctx, batch)) {
return std::string("Помилка: Не вдалося декодувати.");
}
new_token_id = llama_sampler_sample(sampler, ctx, -1);
if (llama_token_is_eog(model, new_token_id) || word_count >= word_limit) {
break;
}
char buf[256];
int n = llama_token_to_piece(model, new_token_id, buf, sizeof(buf), 0, true);
if (n < 0) {
return std::string("Помилка: Не вдалося перетворити токен на фрагмент.");
}
// Виведення кожного слова по мірі його генерації
std::string word_piece(buf, n);
UELOG += word_piece.c_str();
response_stream << word_piece; // Додаємо слово до відповіді
word_count++; // Збільшуємо лічильник слів
//UE_LOG(LogTemp, Log, TEXT("%s"), *UELOG); // Розкоментуйте, щоб вивести токен за токеном генерацію.
batch = llama_batch_get_one(&new_token_id, 1);
}
std::cout << std::endl;
return response_stream.str();
};
std::string response = generate(prompt);
UE_LOG(LogTemp, Warning, TEXT("Згенерована відповідь: %s"), *FString(response.c_str()));
// Додаємо відповідь до історії розмови
messages.push_back({ "assistant", _strdup(response.c_str()) });
// звільняємо ресурси
llama_sampler_free(sampler);
llama_free(ctx);
llama_free_model(model);
Повний код можна знайти тут. Якщо ви не хочете видаляти історію повідомлень, не обов'язково звільняти ресурси в кінці. Розумію, що це може бути складно і заплутано, але код був переважно змінений з офіційного прикладу llama.cpp. Я намагався зробити його якомога простішим. Тепер ви можете спілкуватися зі своєю локальною моделлю! (А якщо щось піде не так, ви будете знати, де саме це сталося.)
Результат ви зможете побачити, запустивши свій проект в Unreal Engine та відкривши журнал виводу:
Створення онлайн-режиму для нашого генеративного ШІ.
Оскільки це бонусна частина, я не буду вдаватися в деталі. Unreal Engine надає можливість надсилати запити через HTTP, і ми будемо використовувати це для відправки запитів до OpenAI. Крім того, нам потрібно буде використовувати JSON для налаштування запиту.
Спочатку нам потрібно створити акаунт на OpenAI та отримати секретний ключ. Після того, як ми його отримаємо, потрібно додати нову публічну залежність у YourPluginName.Build.cs:
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"HTTP", "Json", "JsonUtilities" // Для здійснення HTTP-запитів; Для обробки JSON об'єктів; Для спрощення серіалізації та десеріалізації JSON
}
);
Далі ми налаштовуємо підключення до онлайн-ШІ, використовуючи API від моделей OpenAI GPT. Спочатку визначаємо роль асистента, потім повідомлення, яке потрібно відправити, і секретний API-ключ для аутентифікації. Потім створюємо JSON-вантаж, який містить усі необхідні дані для запиту, такі як ім'я моделі, температура та історія повідомлень (системні та користувацькі повідомлення).
Також ми готуємо HTTP-запит, вказуючи URL API, заголовки (включаючи секретний API-ключ) та контент (JSON-рядок, який ми створили).
Коли все налаштовано, ми відправляємо запит до API OpenAI. Коли відповідь приходить, перевіряємо, чи вона є валідною. Якщо так, ми парсимо JSON-відповідь, щоб витягнути відповідь ШІ та записати її в журнал. Якщо виникає помилка, ми також її записуємо (сподіваємось, що нам не доведеться її побачити😊):
// Copyright Epic Games, Inc. All Rights Reserved.
// AwesomeAIPlugin.cpp
#include "AwesomeAIPlugin.h"
#include
#include
#include "llama.h"
// Для онлайн AI
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "JsonObjectConverter.h"
#define LOCTEXT_NAMESPACE "FAwesomeAIPluginModule"
void FAwesomeAIPluginModule::StartupModule()
{
std::string assistantRole = "Допоміжний асистент";
std::string messageToAI = "Привіт, хто ти?";
std::string modelName = "gpt-3.5-turbo";
std::string secretKey = ""; // ВСТАВТЕ СЮДИ СВІЙ СЕКРЕТНИЙ КОД
// Підготовка JSON-даних (реальні дані, що надсилаються у запиті або повідомленні, часто як частина HTTP-запиту або мережевої комунікації)
TSharedPtr JsonPayload = MakeShareable(new FJsonObject);
JsonPayload->SetStringField("model", FString(modelName.c_str()));
JsonPayload->SetNumberField("max_tokens", 4096);
JsonPayload->SetNumberField("temperature", 0.7);
TArray<TSharedPtr<FJsonValue>> Messages;
TSharedPtr<FJsonObject> SystemMessageObject = MakeShareable(new FJsonObject); // Створення "system" повідомлення
SystemMessageObject->SetStringField("role", "system");
SystemMessageObject->SetStringField("content", FString(assistantRole.c_str()));
// Обгортка об'єкта в FJsonValueObject
Messages.Add(MakeShareable(new FJsonValueObject(SystemMessageObject)));
// Створення нового "user" повідомлення
TSharedPtr<FJsonObject> UserMessageObject = MakeShareable(new FJsonObject);
UserMessageObject->SetStringField("role", "user");
UserMessageObject->SetStringField("content", FString(messageToAI.c_str()));
Messages.Add(MakeShareable(new FJsonValueObject(UserMessageObject))); // Обгортка об'єкта в FJsonValueObject
JsonPayload->SetArrayField("messages", Messages);
FString JsonString;
TSharedRef<TJsonWriter<TCHAR>> Writer = TJsonWriterFactory<TCHAR>::Create(&JsonString);
FJsonSerializer::Serialize(JsonPayload.ToSharedRef(), Writer);
// Налаштування HTTP-запиту
TSharedRef<IHttpRequest> HttpRequest = FHttpModule::Get().CreateRequest();
HttpRequest->SetURL("https://api.openai.com/v1/chat/completions");
HttpRequest->SetVerb("POST");
HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
HttpRequest->SetHeader(TEXT("Authorization"), FString(secretKey.c_str()));
HttpRequest->SetContentAsString(JsonString);
UE_LOG(LogTemp, Log, TEXT("Контент запиту: %s \n"), *JsonString);
// Створення callback-функції для завершення запиту
HttpRequest->OnProcessRequestComplete().BindLambda(
[](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode()))
{
// Логування всього JSON-відповіді для налагодження
FString RawResponse = Response->GetContentAsString();
UE_LOG(LogTemp, Log, TEXT("Повна відповідь від OpenAI: %s"), *RawResponse);
// Парсинг відповіді
TSharedPtr<FJsonObject> JsonResponse;
TSharedRef<TJsonReader<TCHAR>> Reader = TJsonReaderFactory<TCHAR>::Create(Response->GetContentAsString());
if (FJsonSerializer::Deserialize(Reader, JsonResponse) && JsonResponse.IsValid())
{
const TArray<TSharedPtr<FJsonValue>>* Choices;
if (JsonResponse->TryGetArrayField("choices", Choices) && Choices->Num() > 0)
{
TSharedPtr<FJsonObject> Choice = (*Choices)[0]->AsObject();
if (Choice.IsValid())
{
TSharedPtr<FJsonObject> Message = Choice->GetObjectField("message");
if (Message.IsValid())
{
const FString AIResponse = Message->GetStringField("content");
UE_LOG(LogTemp, Warning, TEXT("Відповідь AI: %s"), *AIResponse);
}
}
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("Не вдалося розпарсити JSON-відповідь."));
}
}
else
{
if (Response.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("HTTP-запит не вдався: %s"), *Response->GetContentAsString());
}
else
{
UE_LOG(LogTemp, Error, TEXT("HTTP-запит не вдався: Невірна відповідь."));
}
}
});
// Відправка запиту
HttpRequest->ProcessRequest();
}
Повний код можна знайти тут. І він працює! 🎉:
Майбутні покращення та мої думки
Нам вдалося згенерувати відповідь від AI, але що далі? Ну, є ще багато чого, що можна дослідити! Потрібно додати історію чату для онлайн-режиму, робити асинхронні виклики до офлайн-AI, тестувати інші моделі, реалізувати зручний інтерфейс для налаштувань, створювати нові функції для blueprint, і багато іншого.
Все залежить від вашої ідеї!
Наприклад, у моєму другому проекті на курсі BUAS я зміг створити повноцінну систему діалогів для NPC з простим у використанні API (використовуючи Blueprints), налаштуваннями, кастомними UAsset та компонентами. Нижче наведено відео приклад того, як це працює в моєму проекті:
Висновок
Сподіваюся, що цей блог був корисним для розуміння того, як: налаштувати плагін для Unreal Engine, працювати з llama.cpp, зрозуміти, як працює LLM, і з’єднати все це разом! Це демонструє, що можна створити і використовувати як локальний, так і онлайн генеративний AI у вашій грі.
Це був дійсно цікавий досвід для мене, і я щиро сподіваюся, що в майбутньому більше ігор впровадять генеративний AI для створення реалістичних світів, де гравці зможуть взаємодіяти з кожним NPC.
Дякую за те, що прочитали цей блог! 🫶
Перекладено з: Unlock the Future: Build a Generative AI Plugin for Unreal Engine with C++ (Offline and Online)