Краткий справочник по программированию
Введение#
В настоящем справочнике вы найдете основную информацию о том, как начать программировать 3D-приложения и видеоигры с помощью UNIGINE. Справочник включает в себя следующие главы:
- Основные объекты сцены
- Система координат
- Ведение журнала и вывод сообщений в консоль
- Сохранение и загрузка мира
- Закрытие приложения
- Создание и удаление объектов во время выполнения
- Создание и настройка камеры
- Создание и настройка источников света
- Создание, применение и удаление материалов во время выполнения
- Управление существующими объектами сцены
- Выполнение базовых преобразований (перемещение, поворот, масштабирование)
- Как сделать игровой процесс независимым от частоты кадров
- Управление пересечениями
- Получение пользовательских вводов и управление ими
- Создание пользовательского интерфейса
- Воспроизведение звука и музыки
- Настройка физики
- Обнаружение объектов с помощью World Trigger
Основные объекты сцены#
В UNIGINE нода (node) — это базовый тип, от которого наследуются все типы объектов сцены. Некоторые из них отображаются визуально: объекты, декали и эффекты — все они имеют поверхности для представления их геометрии (меш), в то время как другие ( Источники света, Персонажи и т.д.) невидимы.
Каждая нода имеет матрицу преобразования, которая кодирует положение, поворот и масштаб ноды в мире.
Все объекты сцены, добавленные в сцену, независимо от их типа, называются нодами.
Дополнительная информация:
- Дополнительные сведения о типах объектов UNIGINE см. в разделе Встроенные типы объектов.
- Для получения дополнительной информации об управлении нодами с помощью API см. раздел Node-классы.
Система координат#
Трехмерное пространство в UNIGINE представлено правой декартовой системой координат: оси X и Y образуют горизонтальную плоскость, ось Z направлена вверх. При экспорте анимации из 3D-редакторов Y считается прямым направлением.
Положительный угол поворота задает вращение против часовой стрелки. Это соответствует правилу правой руки: если вы установите большой палец правой руки вдоль оси, другие обернутые пальцы будут показывать направление вращения.
Дополнительная информация:
- Дополнительные сведения о типах объектов UNIGINE см. в разделе Встроенные типы объектов.
- Для получения дополнительной информации об управлении нодами с помощью API см. раздел Node-классы.
Ведение журнала и вывод сообщений в консоль#
Вывод сообщений в файл журнала и консоль помогает отслеживать общий ход выполнения вашего приложения и сообщать об ошибках, которые могут быть использованы при отладке. Класс Log позволяет печатать форматированные строковые сообщения в файл журнала и консоль. Приведенный ниже код демонстрирует, как выводить различные типы сообщений:
using namespace Unigine;
// auxiliary variables for messages
const char* file_name = "file.txt";
int ID = 10;
// reporting an error message
Log::error("Loading mesh: can't open \"%s\" file\n", file_name);
// reporting a message
Log::message("-> Added %d UI elements.\n", ID);
// reporting a warning message
Log::warning("ID of the \"%s\" file: %d.\n", file_name, ID);
// reporting a fatal error message to the log file and closing the application
Log::fatal("FATAL ERROR reading \"%s\" file!\n", file_name);
Дополнительная информация:
Сохранение и загрузка мира#
Некоторые приложения управляют одним миром, в то время как другие требуют управления несколькими мирами. В любом случае, очень полезно знать, как сохранить наш текущий мир и загрузить какой-нибудь другой. Чтобы решить эту задачу, мы должны использовать класс World, который разработан как синглтон.
#include <UnigineWorld.h>
using namespace Unigine;
/* .. */
// loading world from the my_world.world file
World::loadWorld("my_world");
Мы также можем сделать то же самое через консоль, используя класс Console, который также разработан как синглтон.
#include <UnigineConsole.h>
using namespace Unigine;
/* .. */
// saving current world to the my_world.world file
Console::run("world_save my_world");
// loading world from the my_world.world file
Console::run("world_load my_world");
Дополнительная информация:
- Для получения дополнительной информации об управлении мирами с помощью API см. статью о классе World.
- Для получения дополнительной информации о консоли и доступных командах см. статью Консоль.
- Для получения дополнительной информации об управлении консолью через API см. статью о классе Console.
- Для получения дополнительной информации об управлении мировыми нодами, которые должны быть сохранены с помощью API, см. методы класса Node.
Закрытие приложения#
Любое приложение должно быть закрыто в какой-то момент. Чтобы закрыть ваше приложение, используйте класс Engine.
Чтобы закрыть приложение, необходимо использовать следующий код:
using namespace Unigine;
/* .. */
// closing the application
Engine::get()->quit();
Создание и удаление нод во время выполнения#
Ноды можно создавать и удалять во время выполнения почти так же просто, как в редакторе. Основной набор действий заключается в следующем:
- Создание. Чтобы создать ноду, мы должны объявить интеллектуальный указатель для типа ноды, которую мы собираемся создать, и вызвать конструктор соответствующего класса, при необходимости предоставляя параметры построения.
- Удаление. Чтобы удалить ноду, мы просто вызываем метод deleteLater() для ноды, которую мы собираемся удалить.
// creating a node of the NodeType named nodename
<NodeType>Ptr nodename = <NodeType>::create(<construction_parameters>);
// removing the node
nodename.deleteLater();
Теперь давайте проиллюстрируем процесс создания и удаления ноды на примере простого статического меша (ObjectMeshStatic) с использованием геометрии загружаемой из ассета.
int AppWorldLogic::init()
{
//create an ObjectMeshStatic node from an fbx model imported to the data/fbx/ folder using the Editor
ObjectMeshStaticPtr my_object = ObjectMeshStatic::create("fbx/model.fbx/model.mesh");
// removing the node
my_object.deleteLater();
return 1;
}
Дополнительная информация:
- Вы можете создавать примитивы с помощью кода, используя класс Primitives.
- Для получения дополнительной информации об управлении мировыми нодами см. методы класса Node.
Создание и настройка камеры#
Камера — это окно в мир, без него вы на самом деле ничего не сможете увидеть. Камеры в UNIGINE управляются с помощью Player. Когда вы добавляете нового персонажа, он создает камеру и задает элементы управления, маски, материалы для последующей обработки для этой камеры.
Для того, чтобы установить нового персонажа в качестве активного, мы должны использовать класс Game, который разработан как синглтон.
Следующий код иллюстрирует создание PlayerSpectator и установку его в качестве активной игровой камеры.
#include <UnigineGame.h>
// injecting Unigine namespace to the global namespace
using namespace Unigine;
int AppWorldLogic::init()
{
// creating a new PlayerSpectator instance
PlayerSpectatorPtr playerSpectator = PlayerSpectator::create();
// setting necessary parameters: FOV, ZNear, ZFar, view direction vector and position.
playerSpectator->setFov(90.0f);
playerSpectator->setZNear(0.1f);
playerSpectator->setZFar(10000.0f);
playerSpectator->setViewDirection(Math::vec3(0.0f, 1.0f, 0.0f));
playerSpectator->setWorldPosition(Math::Vec3(-1.6f, -1.7f, 1.7f));
// setting the player as a default one via the Game singleton instance
Game::setPlayer(playerSpectator);
return 1;
}
Дополнительная информация:
- Для получения дополнительной информации об игроках см. статью Персонажи.
- Для получения дополнительной информации об API игроков см. статью Player-классы.
- Для получения дополнительной информации о игровом классе см. статью о классе Game.
Создание и настройка источников света#
Освещение является основой каждой сцены, определяющей цвета и окончательный вид ваших объектов. Источники света в UNIGINE создаются так же, как и все остальные ноды.
Давайте рассмотрим создание мирового источника света в качестве примера:
#include <UnigineLights.h>
// injecting Unigine namespace to the global namespace
using namespace Unigine;
int AppWorldLogic::init()
{
// creating a world light source and setting its color to white
LightWorldPtr sun = LightWorld::create(Math::vec4(1.0f, 1.0f, 1.0f, 1.0f));
// setting light source's parameters (intensity, disable angle, scattering type, name and rotation)
sun->setName("Sun");
sun->setDisableAngle(90.0f);
sun->setIntensity(1.0f);
sun->setScattering(LightWorld::SCATTERING::SCATTERING_SUN);
sun->setWorldRotation(Math::quat(86.0f, 30.0f, 300.0f));
return 1;
}
Дополнительная информация:
- Для получения дополнительной информации об источниках света см. статью Источники света.
- Для получения дополнительной информации об API источников света см. статью Lights-классы.
Создание, применение и удаление материалов во время выполнения#
Материалы, назначенные определенным поверхностям объекта, определяют, как будет отображаться объект. Они реализуют шейдеры и управляют тем, какие опции, состояния, параметры различных типов и текстуры используются для рендеринга объекта во время прохода рендеринга. Для управления материалами мы используем следующие два класса:
- Класс Materials, который представляет интерфейс для управления загруженными материалами.
- Класс Material, который используется для управления каждым отдельным материалом.
Следующий код можно использовать для создания нового материала, унаследованного от материала mesh_base.
#include <UnigineMaterials.h>
#include <UniginePrimitives.h>
// injecting Unigine namespace to the global namespace
using namespace Unigine;
int AppWorldLogic::init()
{
// creating a box (ObjectMeshDynamic node)
ObjectMeshDynamicPtr my_mesh = Primitives::createBox(Math::vec3(1.5f, 1.5f, 1.5f));
// getting the base mesh_base material to inherit from
MaterialPtr mesh_base = Materials::findManualMaterial("Unigine::mesh_base");
// inherit a new child material from it
MaterialPtr my_mesh_base = mesh_base->inherit();
// save it to "materials/my_mesh_base0.mat"
my_mesh_base->createMaterialFile("materials/my_mesh_base0.mat");
// setting the albedo color of the material to red
my_mesh_base->setParameterFloat4("albedo_color", Math::vec4(255, 0, 0, 255));
// assigning a "my_mesh_base0.mat" material to the surface 0 of the my_mesh ObjectMeshDynamic node
my_mesh->setMaterialPath("materials/my_mesh_base0.mat", 0);
// assigning a "my_mesh_base0.mat" material to all surfaces of the my_mesh ObjectMeshDynamic node
my_mesh->setMaterialPath("materials/my_mesh_base0.mat", "*");
return 1;
}
int AppWorldLogic::shutdown()
{
// deleting the material "materials/my_mesh_base0.mat"
Materials::removeMaterial(Materials::findMaterialByPath("materials/my_mesh_base0.mat")->getGUID());
return 1;
}
Дополнительная информация:
- Для получения дополнительной информации о создании и редактировании материалов с помощью API см. статью о классе Material.
- Для получения дополнительной информации об управлении загруженными материалами с помощью API см. статью о классе Materials.
- Для получения дополнительной информации о форматах файлов материалов см. раздел Файлы материалов.
- Для получения дополнительной информации о параметрах материалов см. файлы материалов в папке %UNIGINE_SDK_BROWSER_INSTALLATION_FOLDER%/sdks/%CURRENT_SDK%/data/core/materials/default/.
Управление существующими объектами сцены#
Не весь контент в мире создается во время выполнения, поэтому мы должны иметь возможность работать с уже существующими нодами. Как мы получаем указатели на существующие объекты, чтобы управлять ими? Здесь снова вступает в игру класс World. В принципе, есть два способа, которыми мы можем получить указатель на определенную ноду, используя методы класса World:
- метод getNodeByName() — когда мы знаем имя ноды
- метод getNodeByID() — когда мы знаем идентификатор ноды
Эти методы возвращают значение NodePtr, которое является указателем на базовый класс, но для выполнения операций с определенным объектом (например, ObjectMeshDynamicPtr) нам необходимо выполнить понижающее преобразование (т.е. преобразовать указатель на базовый класс в указатель на производный).
Иногда вам также может потребоваться выполнить upcasting (т.е. преобразовать указатель на производный в указатель на базовый), в этом случае вы можете использовать сам производный класс. Приведенный ниже код демонстрирует моменты, описанные выше.
#include <UnigineWorld.h>
using namespace Unigine;
/* .. */
// find a pointer to node by a given name
NodePtr baseptr = World::getNodeByName("my_meshdynamic");
// cast a pointer-to-derived from pointer-to-base with automatic type checking
ObjectMeshDynamicPtr derivedptr = checked_ptr_cast<ObjectMeshDynamic>(baseptr);
// static cast (pointer-to-derived from pointer-to-base)
ObjectMeshDynamicPtr derivedptr = static_ptr_cast<ObjectMeshDynamic>(World::getNodeByName("my_meshdynamic"));
// upcast to the pointer to the Object class which is a base class for ObjectMeshDynamic
ObjectPtr object = derivedptr;
// upcast to the pointer to the Node class which is a base class for all scene objects
NodePtr node = derivedptr;
Существуют следующие способы получения компонента для ноды:
// get the component assigned to a node by type "MyComponent"
MyComponent* my_component = ComponentBase::getComponent<MyComponent>(node);
// // do the same by using the function of the Component System
MyComponent* my_component = ComponentSystem::get()->getComponent<MyComponent>(color_zone);
Дополнительная информация:
- Дополнительные сведения о работе с умными указателями см. в статье Работа с умными указателями.
- Для получения дополнительной информации см. статью о классе World.
Выполнение базовых преобразований (перемещение, поворот, масштабирование)#
Каждая нода имеет матрицу преобразования, которая кодирует положение, поворот и масштаб ноды в мире. Если нода добавляется как дочерняя для другой ноды, она имеет матрицу преобразования, связанную с ее родительской нодой. Вот почему класс Node имеет разные функции: getTransform(), setTransform() и getWorldTransform(), setWorldTransform(), которые работают с локальными и мировыми матрицами преобразования соответственно. Следующий код иллюстрирует, как выполнять базовые преобразования нод:
// injecting Unigine namespace to the global namespace
using namespace Unigine;
using namespace Unigine::Math;
// move the node by X, Y, Z units along the corresponding axes
node->setWorldPosition(node->getWorldPosition() + Vec3(X, Y, Z));
// move the node by one unit along the Y axis
node->worldTranslate(0.0f, 1.0f, 0.0f);
// rotate the node around the axis (X, Y, Z) by the Alpha angle
node->setWorldRotation(node->getWorldRotation() * quat(Vec3(X, Y, Z), Alpha));
// rotate the node around X, Y, and Z axes by the corresponding angle (angle_X, angle_Y, angle_Z)
node->setWorldRotation(node->getWorldRotation() * quat(angle_X, angle_Y, angle_Z));
// rotate the node by 45 degrees along the Z axis
node->worldRotate(0.0f, 0.0f, 45.0f);
// orient the node using a direction vector and a vector pointing upwards
node->setWorldDirection(vec3(0.5f, 0.5f, 0.0f), vec3_up, AXIS_Y);
// setting node scale to Scale_X, Scale_Y, Scale_Z along the corresponding axes
node->setWorldScale(vec3(Scale_X, Scale_Y, Scale_Z));
// setting new transformation matrix to scale the node 2 times along all axes, rotate it by 45 degrees around the Z-axis and move it by 1 unit along all axes
Mat4 transform = Mat4(translate(vec3(1.0f, 1.0f, 1.0f)) * rotate(quat(0.0f, 0.0f, 1.0f, 45.0f)) * scale(vec3(2.0f)));
// setting node transformation matrix relative to its parent
node->setTransform(transform);
// setting node transformation matrix relative to the world origin
node->setWorldTransform(transform);
Дополнительная информация:
- Для получения дополнительной информации о матричных преобразованиях см. статью Матричные преобразования.
Сделать игровой процесс независимым от частоты кадров#
Поскольку частота кадров нашего приложения может варьироваться (т.е. метод AppWorldLogic::update() будет вызываться более или менее часто) в зависимости от аппаратного обеспечения, мы должны что-то сделать, чтобы гарантировать, что определенные действия выполняются в одни и те же периоды времени независимо от частоты кадров (например, изменять что-то один раз в секунду и т.д.). Чтобы сделать частоту кадров вашей игры независимой, вы можете использовать множитель масштабирования (время в секундах, затраченное на завершение последнего кадра), возвращаемый следующими методами:
- Engine::getIfps() возвращает обратное значение FPS для вашего приложения.
- Game::getIfps() возвращает масштабированное обратное значение FPS. Этот класс предназначен для использования, когда вы хотите ускорить, замедлить или приостановить рендеринг, физику или игровую логику.
Чтобы изменить преобразования, вы можете использовать следующий код:
#include <UnigineGame.h>
// injecting Unigine namespace to the global namespace
using namespace Unigine;
int AppWorldLogic::update()
{
// getting an inverse FPS value (the time in seconds it took to complete the last frame)
float ifps = Game::getIFps();
// moving the node up by 0.3 units every second instead of every frame
node->worldTranslate(Math::Vec3(0.0f, 0.0f, 0.3f * ifps));
return 1;
}
Чтобы выполнить некоторые изменения один раз за определенный промежуток времени, вы можете использовать следующий код:
#include <UnigineGame.h>
// injecting Unigine namespace to the global namespace
using namespace Unigine;
const float INTERVAL_DURATION = 5; // interval duration
float elapsed_time = INTERVAL_DURATION; // current time left to make changes
int AppWorldLogic::update()
{
// getting an inverse FPS value (the time in seconds it took to complete the last frame)
float ifps = Game::getIFps();
// checking if it's time to make changes
if (elapsed_time < 0.0f)
{
/* .. DO SOME CHANGES .. */
// resetting elapsed time counter
elapsed_time = INTERVAL_DURATION;
}
// decreasing elapsed time counter
elapsed_time -= ifps;
return 1;
}
Дополнительная информация:
Управление пересечениями#
Пересечения широко используются в 3D-приложениях. В UNIGINE существует три основных типа пересечений:
- World Intersection — пересечение с объектами и нодами.
- Physics Intersection — пересечение с формами и коллизионными объектами.
- Game Intersection — пересечение с нодами поиска пути, такими как препятствия.
Но есть некоторые условия для обнаружения пересечений с поверхностью:
- Поверхность включена.
- Поверхности присвоен материал.
Для каждой поверхности включен флаг Intersection.
Вы можете установить этот флаг на поверхность объекта с помощью функции Object.setIntersection().
Приведенный ниже код иллюстрирует несколько способов использования мировых пересечений:
- чтобы найти все ноды, пересекаемые ограничивающим прямоугольником;
- чтобы найти все ноды, пересекаемые ограничивающей сферой;
- чтобы найти все ноды, пересекаемые ограничивающим усеченным конусом;
- чтобы найти первый объект, пересеченный лучом.
#include <UnigineGame.h>
#include <UnigineWorld.h>
// injecting Unigine namespace to the global namespace
using namespace Unigine;
int listNodes(Vector<Ptr<Node>>& nodes, const char* intersection_with)
{
Log::message("Total number of nodes intersecting a %s is: %i \n", intersection_with, nodes.size());
for (int i = 0; i < nodes.size(); i++)
{
Log::message("Intersected node: %s \n", nodes.get(i)->getName());
}
// clearing the list of nodes
nodes.clear();
return 1;
}
int AppWorldLogic::update()
{
// getting a player pointer
PlayerPtr player = Game::getPlayer();
// creating a vector to store intersected nodes
Vector<Ptr<Node>> nodes;
//-------------------------- FINDING INTERSECTIONS WITH A BOUNDING BOX -------------------------
// initializing a bounding box with a size of 3 units, located at the World's origin
WorldBoundBox boundBox(Math::Vec3(0.0f), Math::Vec3(3.0f));
// finding nodes intersecting a bounding box and listing them if any
if (World::getIntersection(boundBox, nodes))
listNodes(nodes, "bounding box");
//------------------------- FINDING INTERSECTIONS WITH A BOUNDING SPHERE ------------------------
// initializing a bounding sphere with a radius of 3 units, located at the World's origin
WorldBoundSphere boundSphere(Math::Vec3(0.0f), 3.0f);
// finding nodes intersecting a bounding sphere and listing them if any
if (World::getIntersection(boundSphere, nodes))
listNodes(nodes, "bounding sphere");
//------------------------- FINDING INTERSECTIONS WITH A BOUNDING FRUSTUM -----------------------
// initializing a bounding frustum with a frustum of the player's camera
WorldBoundFrustum boundFrustum(player->getCamera()->getProjection(), player->getCamera()->getModelview());
// finding ObjectMeshStaticNodes intersecting a bounding frustum and listing them if any
if (World::getIntersection(boundFrustum, Node::OBJECT_MESH_STATIC, nodes))
listNodes(nodes, "bounding frustum");
//---------------- FINDING THE FIRST OBJECT INTERSECTED BY A RAY CAST FROM P0 to P1 --------------
// initializing points of the ray from player's position in the direction pointed by the mouse cursor
Math::ivec2 mouse = Input::getMousePosition();
Math::Vec3 p0 = player->getWorldPosition();
Math::Vec3 p1 = p0 + Math::Vec3(player->getDirectionFromMainWindow(mouse.x, mouse.y)) * 100;
//creating a WorldIntersection object to store the information about the intersection
WorldIntersectionPtr intersection = WorldIntersection::create();
// casting a ray from p0 to p1 to find the first intersected object
ObjectPtr obj = World::getIntersection(p0, p1, 1, intersection);
// print the name of the first intersected object and coordinates of intersection point if any
if (obj)
{
Math::Vec3 p = intersection->getPoint();
Log::message("The first object intersected by the ray at point (%f, %f, %f) is: %s \n ", p.x, p.y, p.z, obj->getName());
}
return 1;
}
Дополнительная информация:
- Для получения дополнительной информации о пересечениях см. статью Пересечения.
- Для получения дополнительной информации об управлении пересечениями World с помощью API см. статью о классе World.
- Для получения дополнительной информации об управлении пересечениями Game с помощью API см. статью о классе Game.
- Для получения дополнительной информации об управлении пересечениями Physics с помощью API см. статью о классе Physics.
Получение пользовательских данных и управление ими#
Большинство приложений предназначены для взаимодействия с пользователем. В UNIGINE вы можете управлять вводимыми пользовательскими данными, используя следующие классы:
- Класс Input
- Класс Controls
- Класс ControlsApp
Следующий код иллюстрирует, как использовать класс Input для получения координат мыши в случае, если была нажата правая кнопка мыши, и для закрытия приложения, если была нажата клавиша "q" (игнорируя эту клавишу, если консоль открыта):
#include <UnigineInput.h>
#include <UnigineConsole.h>
int AppWorldLogic::update()
{
// if right mouse button is clicked
if (Input::isMouseButtonDown(Input::MOUSE_BUTTON_RIGHT))
{
Math::ivec2 mouse = Input::getMousePosition();
// report mouse cursor coordinates to the console
Log::message("Right mouse button was clicked at (%d, %d)\n", mouse.x, mouse.y);
}
// closing the application if a 'Q' key is pressed, ignoring the key if the console is opened
if (Input::isKeyDown(Input::KEY_Q) && !Console::isActive())
{
Engine::get()->quit();
}
return 1;
}
Следующий код иллюстрирует, как использовать класс Controls для обработки ввода с клавиатуры:
#include <UnigineGame.h>
int AppWorldLogic::update()
{
// getting current controls
ControlsPtr controls = Game::getPlayer()->getControls();
// checking controls states and reporting which buttons were pressed
if (controls->clearState(Controls::STATE_FORWARD) || controls->clearState(Controls::STATE_TURN_UP))
{
Log::message("FORWARD or UP key pressed\n");
}
else if (controls->clearState(Controls::STATE_BACKWARD) || controls->clearState(Controls::STATE_TURN_DOWN))
{
Log::message("BACKWARD or DOWN key pressed\n");
}
else if (controls->clearState(Controls::STATE_MOVE_LEFT) || controls->clearState(Controls::STATE_TURN_LEFT))
{
Log::message("MOVE_LEFT or TURN_LEFT key pressed\n");
}
else if (controls->clearState(Controls::STATE_MOVE_RIGHT) || controls->clearState(Controls::STATE_TURN_RIGHT))
{
Log::message("MOVE_RIGHT or TURN_RIGHT key pressed\n");
}
return 1;
}
Следующий код иллюстрирует, как использовать класс ControlsApp для сопоставления клавиш и кнопок с состояниями, а затем для обработки пользовательского ввода:
#include <UnigineGame.h>
int AppWorldLogic::init()
{
// remapping states to other keys and buttons
ControlsApp::setStateKey(Controls::STATE_FORWARD, Input::KEY_PGUP);
ControlsApp::setStateKey(Controls::STATE_BACKWARD, Input::KEY_PGDOWN);
ControlsApp::setStateKey(Controls::STATE_MOVE_LEFT, Input::KEY_L);
ControlsApp::setStateKey(Controls::STATE_MOVE_RIGHT, Input::KEY_R);
ControlsApp::setStateMouseButton(Controls::STATE_JUMP, Input::MOUSE_BUTTON_LEFT);
return 1;
}
int AppWorldLogic::update()
{
if (ControlsApp::clearState(Controls::STATE_FORWARD))
{
Log::message("FORWARD key pressed\n");
}
else if (ControlsApp::clearState(Controls::STATE_BACKWARD))
{
Log::message("BACKWARD key pressed\n");
}
else if (ControlsApp::clearState(Controls::STATE_MOVE_LEFT))
{
Log::message("MOVE_LEFT key pressed\n");
}
else if (ControlsApp::clearState(Controls::STATE_MOVE_RIGHT))
{
Log::message("MOVE_RIGHT key pressed\n");
}
else if (ControlsApp::clearState(Controls::STATE_JUMP))
{
Log::message("JUMP button pressed\n");
}
return 1;
}
Дополнительная информация:
- Для получения дополнительной информации об управлении пользовательскими вводами с помощью класса Input см. статью Input class.
- Для получения дополнительной информации об управлении пользовательскими вводами с помощью класса Controls см. статью Controls class.
- Для получения дополнительной информации об управлении пользовательскими вводами с помощью класса ControlsApp см. статью ControlsApp class.
Создание пользовательского интерфейса#
В UNIGINE графический пользовательский интерфейс (GUI) состоит из различных типов виджетов, добавленных к нему. В принципе, существует два способа создания графического интерфейса:
- Путем добавления виджетов в системный графический интерфейс (пользовательский интерфейс UNIGINE), который отображается поверх окна приложения.
- Путем добавления виджетов к объекту GUI, расположенному в мире. В этом случае может быть применен любой фильтр постобработки.
Чтобы добавить элементы в системный графический интерфейс, вы должны использовать класс Gui.
Существует 2 способа создания макета графического интерфейса:
- Непосредственно из кода через GUI-классы
- С помощью Файлов пользовательского интерфейса (UI)
Следующий код демонстрирует, как добавить метку и ползунок в системный графический интерфейс:
#include <UnigineUserInterface.h>
// injecting Unigine namespace to the global namespace
using namespace Unigine;
GuiPtr gui;
int AppWorldLogic::init()
{
// getting a GUI pointer
gui = Gui::getCurrent();
// creating a label widget and setting up its parameters
WidgetLabelPtr widget_label = WidgetLabel::create(gui, "Label text:");
widget_label->setToolTip("This is my label!");
widget_label->arrange();
widget_label->setPosition(10, 10);
// creating a slider widget and setting up its parameters
WidgetSliderPtr widget_slider = WidgetSlider::create(gui, 0, 360, 90);
widget_slider->setToolTip("This is my slider!");
widget_slider->arrange();
widget_slider->setPosition(100, 10);
gui->addChild(widget_label, Gui::ALIGN_OVERLAP | Gui::ALIGN_FIXED);
gui->addChild(widget_slider, Gui::ALIGN_OVERLAP | Gui::ALIGN_FIXED);
return 1;
}
Чтобы использовать элементы графического интерфейса, мы должны указать обработчики для различных событий (щелчок, изменение и т.д.). Следующий код демонстрирует, как настроить обработчики событий:
#include <UnigineUserInterface.h>
// injecting Unigine namespace to the global namespace
using namespace Unigine;
GuiPtr gui;
/// function to be called when button1 is clicked
int onButton1Clicked(const WidgetPtr &button)
{
/* .. */
}
/// method to be called when button2 is clicked
int AppWorldLogic::onButton2Clicked(const WidgetPtr &button)
{
/* .. */
}
/// method to be called when delete button is clicked
int AppWorldLogic::onButtonDelClicked(const WidgetPtr &button)
{
/* .. */
}
/// method to be called when slider position is changed
int AppWorldLogic::onSliderChanged(const WidgetPtr &slider)
{
/* .. */
}
int AppWorldLogic::init()
{
/* .. */
// getting a GUI pointer
gui = Gui::getCurrent();
// subscbribing for the clicked event with the onButton1Clicked function as a handler
buttonwidget1->getEventClicked().connect(onButton1Clicked);
// subscbribing for the clicked event with the onButton2Clicked function as a handler
buttonwidget2->getEventClicked().connect(this, &AppWorldLogic::onButton2Clicked);
buttonwidget1->getEventClicked().connect(this, &AppWorldLogic::onButtonDelClicked);
// setting AppWorldLogic::onSliderChanged method as a changed event handler for a widget_slider
widget_slider->getEventChanged().connect(this, &AppWorldLogic::onSliderChanged);
/* .. */
return 1;
}
Дополнительная информация:
- Для получения дополнительной информации о файлах пользовательского интерфейса см. статью Файлы пользовательского интерфейса.
- Для получения дополнительной информации об API, связанном с графическим интерфейсом, см. раздел GUI-классы.
- Для получения дополнительной информации о классе Gui см. статью Gui класс.
- Для получения дополнительной информации об обработке событий см. статью Обработка событий.
Воспроизведение звука и музыки#
Источник звука SoundSource#
Класс SoundSource используется для создания направленных источников звука. Чтобы создать источник звука, создайте экземпляр класса SoundSource и укажите все необходимые настройки:
// create a new sound source using the given sound sample file
SoundSourcePtr sound = SoundSource::create("sound.mp3");
// disable sound muffling when being occluded
sound->setOcclusion(0);
// set the distance at which the sound gets clear
sound->setMinDistance(10.0f);
// set the distance at which the sound becomes out of audible range
sound->setMaxDistance(100.0f);
// set the gain that result in attenuation of 6 dB
sound->setGain(0.5f);
// loop the sound
sound->setLoop(1);
// start playing the sound sample
sound->play();
Источник AmbientSource#
Для воспроизведения фоновой музыки создайте экземпляр класса AmbientSource, укажите все необходимые параметры и включите его. Убедитесь, что вы импортировали звуковой ассет в проект.
// create a player so that an ambient sound source is played
PlayerSpectatorPtr player = PlayerSpectator::create();
player->setPosition(Vec3(0.0f, -3.401f, 1.5f));
player->setViewDirection(vec3(0.0f, 1.0f, -0.4f));
Game::setPlayer(player);
// create the ambient sound source
AmbientSourcePtr sound = AmbientSource::create("sound.mp3");
// set necessary sound settings
sound->setGain(0.5f);
sound->setPitch(1.0f);
sound->setLoop(1);
sound->play();
Дополнительная информация:
- Для получения дополнительной информации о направленном звуке см. статью о классе SoundSource.
- Для получения дополнительной информации об окружающем звуке см. статью о классе AmbientSource.
Настройка физики#
Объект должен иметь тело и форму, чтобы на него воздействовала сила тяжести и он мог сталкиваться с другими физическими объектами:
// cube
ObjectMeshStaticPtr box = ObjectMeshStatic::create("core/meshes/box.mesh");
// create a body and a shape based on the mesh
BodyRigidPtr bodyBox = BodyRigid::create(box);
ShapeBoxPtr shapeBox = ShapeBox::create(bodyBox, Math::vec3(1.0f));
Обнаружение объектов с помощью World Trigger#
World trigger генерирует события, когда какие-либо ноды (коллайдеры или нет) попадают внутрь или выходят из него. Триггер может обнаружить ноду любого типа по его ограничительной рамке. Триггер реагирует на все ноды (поведение по умолчанию).
Функция-обработчик события World Trigger фактически выполняется только тогда, когда вызывается следующая функция механизма: то есть перед updatePhysics() (в текущем кадре) или перед update() (в следующем кадре) — независимо от того, что будет первым.
Вы можете подписаться на события Enter и Leave, указав функции-обработчики, которые будут выполняться, когда нода входит в World Trigger или выходит из него. Функция-обработчик должна получать ноду в качестве своего первого аргумента.
// implement the enter event handler
void AppWorldLogic::enter_event_handler(const NodePtr &node)
{
Log::message("\nA node named %s has entered the trigger\n", node->getName());
}
WorldTriggerPtr trigger;
int AppWorldLogic::init()
{
// create a world trigger node
trigger = WorldTrigger::create(Math::vec3(3.0f));
// add the enter event handler to be executed when a node enters the world trigger
trigger->getEventEnter().connect(this, &AppWorldLogic::enter_event_handler);
// add the leave event handler to be executed when a node leaves the world trigger
trigger->getEventLeave().connect(this, &AppWorldLogic::leave_event_handler);
return 1;
}