Цель проекта: Создание системы, динамически загружающей игровые объекты из базы данных 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 = 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. Важные уроки
- Порядок инициализации в Unigine критичен:cpp// Правильно:
ComponentSystem::get()->initialize(); // 1
DatabaseManager::getInstance().initialize(...); // 2
// Мир загрузится автоматически после return 1 - Внимательность к именам в БД: Опечатка в одном символе полностью блокирует подключение.
- IDE ≠ Production: Успешная работа в Visual Studio не гарантирует работу standalone-сборки.
- Безопасность подключения: Всегда используйте выделенного пользователя БД вместо 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 успешно реализована. Система динамически загружает объекты из базы данных, обеспечивая гибкость контент-менеджмента без пересборки проекта. Основные сложности были связаны не с кодом, а с настройкой окружения и управлением зависимостями.