Найти в Дзене

Интеграция PostgreSQL в Unigine: динамическая загрузка игровых объектов из базы данных

Цель проекта: Создание системы, динамически загружающей игровые объекты из базы данных PostgreSQL в сцену Unigine. 1. Архитектурные решения Система построена на трёх ключевых компонентах: AppSystemLogic — точка входа, управляющая порядком инициализации. DatabaseManager — синглтон для управления подключением к PostgreSQL. DatabaseSpawnerComponent — компонент Unigine для спавна объектов. Критически важный порядок инициализации в AppSystemLogic::init(): Инициализация ComponentSystem Инициализация DatabaseManager Загрузка мира Unigine 2. Реализация компонентов 2.1. AppSystemLogic.cpp cpp #include <UnigineLog.h>
#include "DatabaseManager.h"
#include "AppSystemLogic.h"
#include <UnigineComponentSystem.h>
using namespace Unigine;
int AppSystemLogic::init()
{
// 1. КРИТИЧЕСКИ ВАЖНО: сначала ComponentSystem
ComponentSystem::get()->initialize();
Log::message("[AppSystemLogic] ComponentSystem initialized.\n");
// 2. Инициализация DatabaseManager
DatabaseManager& db = Data
Оглавление

Цель проекта: Создание системы, динамически загружающей игровые объекты из базы данных PostgreSQL в сцену Unigine.

1. Архитектурные решения

Система построена на трёх ключевых компонентах:

  1. AppSystemLogic — точка входа, управляющая порядком инициализации.
  2. DatabaseManager — синглтон для управления подключением к PostgreSQL.
  3. DatabaseSpawnerComponent — компонент Unigine для спавна объектов.

Критически важный порядок инициализации в AppSystemLogic::init():

  1. Инициализация ComponentSystem
  2. Инициализация DatabaseManager
  3. Загрузка мира Unigine

2. Реализация компонентов

2.1. AppSystemLogic.cpp

cpp

#include <UnigineLog.h>
#include "DatabaseManager.h"
#include "AppSystemLogic.h"
#include <UnigineComponentSystem.h>

using namespace Unigine;

int AppSystemLogic::init()
{
// 1. КРИТИЧЕСКИ ВАЖНО: сначала ComponentSystem
ComponentSystem::get()->initialize();
Log::message("[AppSystemLogic] ComponentSystem initialized.\n");

// 2. Инициализация DatabaseManager
DatabaseManager& db = DatabaseManager::getInstance();
if (!db.initialize("localhost", "5432", "unigine_world",
"unigine_app", "app_password123")) {
Log::error("[AppSystemLogic] Failed to connect to database!\n");
return 0;
// Останавливаем приложение
}
Log::message("[AppSystemLogic] DatabaseManager ready.\n");

// 3. Unigine загрузит мир автоматически
return 1;
}

int AppSystemLogic::shutdown()
{
DatabaseManager::getInstance().shutdown();
return 1;
}

2.2. DatabaseManager.h

cpp

#pragma once
#include <string>
struct pg_conn;
struct pg_result;

class DatabaseManager
{
public:
static DatabaseManager& getInstance();

bool initialize(const std::string& host,
const std::string& port,
const std::string& dbName,
const std::string& user,
const std::string& password);

pg_result* executeQuery(const std::string& sql);
bool isConnected() const;
void shutdown();

private:
DatabaseManager();
~DatabaseManager();

pg_conn* connection_;
std::string connectionInfo_;
bool isInitialized_;
};

2.3. DatabaseManager.cpp

cpp

#include "DatabaseManager.h"
#include <libpq-fe.h>
#include <UnigineLog.h>

DatabaseManager& DatabaseManager::getInstance()
{
static DatabaseManager instance;
return instance;
}

bool DatabaseManager::initialize(const std::string& host,
const std::string& port,
const std::string& dbName,
const std::string& user,
const std::string& password)
{
// Формируем строку подключения
connectionInfo_ = "host='" + host + "' port='" + port +
"' dbname='" + dbName + "' user='" + user +
"' password='" + password + "'";

// Устанавливаем соединение
connection_ = PQconnectdb(connectionInfo_.c_str());

if (PQstatus(connection_) != CONNECTION_OK) {
Log::error("Connection failed: %s\n", PQerrorMessage(connection_));
return false;
}

isInitialized_ = true;
Log::message("Successfully connected to database '%s'\n", dbName.c_str());
return true;
}

pg_result* DatabaseManager::executeQuery(const std::string& sql)
{
if (!isConnected()) {
Log::error("No database connection.\n");
return nullptr;
}

pg_result* result = PQexec(connection_, sql.c_str());
ExecStatusType status = PQresultStatus(result);

if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK) {
Log::error("Query failed: %s\n", PQerrorMessage(connection_));
PQclear(result);
return nullptr;
}

return result;
}

bool DatabaseManager::isConnected() const
{
return isInitialized_ && connection_ &&
PQstatus(connection_) == CONNECTION_OK;
}

void DatabaseManager::shutdown()
{
if (connection_) {
PQfinish(connection_);
connection_ = nullptr;
}
isInitialized_ = false;
}

2.4. DatabaseSpawnerComponent.h

cpp

#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineNode.h>

class DatabaseSpawnerComponent : public Unigine::ComponentBase {
public:
COMPONENT_DEFINE(DatabaseSpawnerComponent, ComponentBase);
COMPONENT_INIT(initialize);

PROP_PARAM(File, spawn_prefab, "Spawn Prefab", "");

protected:
void initialize();

private:
void spawnObjectFromDB(int id, const char* name, double x, double y, double z);
Unigine::PropertyPtr prop;
Unigine::NodePtr spawn_prefab_node;
};

2.5. DatabaseSpawnerComponent.cpp

cpp

#include "DatabaseSpawnerComponent.h"
#include "DatabaseManager.h"
#include <UnigineWorld.h>
#include <UnigineLog.h>
#include <libpq-fe.h>

REGISTER_COMPONENT(DatabaseSpawnerComponent);

void DatabaseSpawnerComponent::initialize()
{
// 1. Получаем свойство компонента
prop = getProperty();
if (!prop) {
Log::error("Could not get component property.\n");
return;
}

// 2. Загружаем префаб
Unigine::String prefab_path = prop->getParameterPtr("spawn_prefab")->getValueString();
spawn_prefab_node = Unigine::World::loadNode(prefab_path);
if (spawn_prefab_node.isNull()) {
Log::error("Could not load prefab from: %s\n", prefab_path.get());
return;
}

// 3. Проверяем подключение к БД
DatabaseManager& db = DatabaseManager::getInstance();
if (!db.isConnected()) {
Log::error("No connection to database.\n");
return;
}

// 4. Выполняем SQL-запрос
const char* sql = "SELECT id, name, pos_x, pos_y, pos_z FROM game_objects;";
pg_result* result = db.executeQuery(sql);
if (!result) {
Log::error("Query failed or returned no data.\n");
return;
}

// 5. Обрабатываем результаты
int rows = PQntuples(result);
for (int i = 0; i < rows; ++i) {
int id = atoi(PQgetvalue(result, i, 0));
const char* name = PQgetvalue(result, i, 1);
double x = atof(PQgetvalue(result, i, 2));
double y = atof(PQgetvalue(result, i, 3));
double z = atof(PQgetvalue(result, i, 4));

spawnObjectFromDB(id, name, x, y, z);
}

// 6. Освобождаем память
PQclear(result);
}

void DatabaseSpawnerComponent::spawnObjectFromDB(int id, const char* name,
double x, double y, double z)
{
// Клонируем префаб
Unigine::NodePtr newObject = spawn_prefab_node->clone();

// Устанавливаем позицию
newObject->setWorldPosition(Unigine::Math::Vec3(x, y, z));

// Даём имя
newObject->setName(Unigine::String::format("DB_Obj_%d_%s", id, name));

// Добавляем в сцену
newObject->setParent(node);
}

3. Настройка базы данных PostgreSQL

3.1. Создание базы данных и пользователя

sql

-- Создание базы данных
CREATE DATABASE unigine_world;

-- Создание выделенного пользователя
CREATE USER unigine_app WITH PASSWORD 'app_password123';
GRANT ALL PRIVILEGES ON DATABASE unigine_world TO unigine_app;

3.2. Структура таблиц (реальная схема из проекта)

sql

-- Таблица game_objects
CREATE TABLE game_objects (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
object_type VARCHAR,
pos_x REAL DEFAULT 0.0,
pos_y REAL DEFAULT 0.0,
pos_z REAL DEFAULT 0.0,
health INTEGER DEFAULT 100,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Таблица users (для будущей аутентификации)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR NOT NULL UNIQUE,
password_hash VARCHAR NOT NULL,
email VARCHAR,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
is_active BOOLEAN DEFAULT true
);

-- Тестовые данные
INSERT INTO game_objects (name, object_type, pos_x, pos_y, pos_z) VALUES
('Большой дуб', 'tree', 15.5, 0.0, 10.0),
('Гранитная скала', 'rock', -8.0, 0.0, 5.5),
('Сундук с амуницией', 'chest', 0.0, 0.5, 0.0);

4. Решённые проблемы

4.1. Опечатка в имени базы данных

Проблема: При создании БД допущена опечатка: unigine_wotld вместо unigine_world
Решение: Переименование базы данных через pgAdmin:

sql

ALTER DATABASE unigine_wotld RENAME TO unigine_world;

4.2. Проблема с запуском из UNIGINE SDK Browser

Проблема: При запуске скомпилированного приложения из SDK Browser возникала ошибка:

text

Не удалось найти: libpq.dll, libintl-9.dll, libssl-3-x64.dll

Причина: Visual Studio автоматически добавляет пути к библиотекам, но standalone .exe ищет DLL только в своей папке.
Решение: Ручное копирование необходимых DLL из C:\Program Files\PostgreSQL\18\bin\ в папку сборки проекта \bin\.

Необходимые библиотеки PostgreSQL:

  • libpq.dll
  • libintl-9.dll
  • libssl-3-x64.dll
  • libcrypto-3-x64.dll

5. Важные уроки

  1. Порядок инициализации в Unigine критичен:cpp// Правильно:
    ComponentSystem::get()->initialize();
    // 1
    DatabaseManager::getInstance().initialize(...);
    // 2
    // Мир загрузится автоматически после return 1
  2. Внимательность к именам в БД: Опечатка в одном символе полностью блокирует подключение.
  3. IDE ≠ Production: Успешная работа в Visual Studio не гарантирует работу standalone-сборки.
  4. Безопасность подключения: Всегда используйте выделенного пользователя БД вместо postgres.

6. Текущий статус проекта

Работает:

  • Подключение к PostgreSQL установлено
  • Объекты динамически загружаются из таблицы game_objects
  • Спавн объектов на сцене в правильных координатах
  • Работает как из Visual Studio, так и из UNIGINE SDK Browser

8. Технические детали

Версии ПО:

  • Unigine 2.19.1.2 (Community Edition)
  • PostgreSQL 18
  • Visual Studio 2022
  • Windows 11

Архитектура:

text

AppSystemLogic → DatabaseManager (синглтон) → PostgreSQL

DatabaseSpawnerComponent (в сцене Unigine)

Спавн объектов на основе данных из БД

Ключевые файлы проекта:

  • source/AppSystemLogic.cpp/h — точка входа
  • source/DatabaseManager.cpp/h — управление БД
  • source/DatabaseSpawnerComponent.cpp/h — компонент спавна
  • data/properties/DatabaseSpawnerComponent.prop — свойства компонента

Заключение: Интеграция PostgreSQL с Unigine успешно реализована. Система динамически загружает объекты из базы данных, обеспечивая гибкость контент-менеджмента без пересборки проекта. Основные сложности были связаны не с кодом, а с настройкой окружения и управлением зависимостями.