Доступ к нодам и файлам через свойства (Property)
Каждый ассет ( asset), используемый в вашем проекте, будь то нода, меш, материал, текстура или любой другой, имеет уникальный идентификатор (GUID). Идентификатор GUID определяет путь к ассету (т.е. местоположение ассета в проекте). Идентификаторы GUID используются для сохранения всех связей и зависимостей между ассетами, независимо от их имени и местоположения в проекте (при изменении имени или местоположения ассета в проекте его идентификатор GUID не изменяется).
Использование идентификаторов GUID для связывания ваших ассетов безопаснее, чем использование имен файлов, поскольку вам не нужно беспокоиться о том, что ваш материал потеряет текстуру при изменении его имени. Однако прямое управление идентификаторами GUID довольно запутанно.
Свойство позволяет привязывать определенные ассеты к ноде с помощью GUID, даже не задумываясь о них, предоставляя вам легкий доступ к этим ассетам. Существует ряд типов параметров свойств, делающих это возможным:
- material - для материалов
- property - для свойств
- file - для всех остальных файлов (текстур, сеток, звуков и т.д.)
Тип параметра свойства node позволяет связать одну ноду с другой аналогичным образом, используя идентификатор.
Художники и программисты, разрабатывающие проект, должны иметь возможность работать независимо: художники готовят контент (текстуры, материалы, модели и т.д.), в то время как программисты пишут код, реализующий логику, которая выполняет определенные операции с контентом.
Использование свойств упрощает весь процесс:
- Художники могут безопасно перемещать или переименовывать файлы и ноды. Программисты всегда работают со свойствами: создают их, устанавливают и считывают значения параметров (которые могут представлять ссылки на различные ассеты). Художники тоже могут устанавливать параметры свойств, они делают это через редактор.
- Ни художники, ни программисты не должны работать с идентификаторами нод или идентификаторами GUID и запоминать их. Программист всегда имеет под рукой переменную (параметр свойства), обеспечивающую доступ к любой необходимой ноде или файлу.
Общий подход, основанный на свойствах, понятен и прост. Существует два основных случая, в зависимости от логики вашего проекта:
Общий подход#
Общий подход для всех проектов, которые не используют Систему компонентов C++, должен быть следующим:
-
Сначала мы создаем свойство для хранения ссылок на все ноды и ассеты, которые нам нужны, и сохраняем его в папке data нашего проекта. Например, свойство может быть таким:
<?xml version="1.0" encoding="utf-8"?> <property version="2.16.0.2" name="my_property" parent_name="node_base" manual="1" editable="1"> <parameter name="some_float" type="float">30.5</parameter> <parameter name="some_string" type="string">Hello from my_property!</parameter> <parameter name="some_node" type="node">0</parameter> <parameter name="some_material" type="material"></parameter> <parameter name="some_mesh" type="file"></parameter> <parameter name="some_file" type="file"></parameter> </property>
-
Затем откройте UnigineEditor, выберите нужную ноду, нажмите Add new property и перетащите .prop файл в новое поле свойств, затем перетащите все необходимые ассеты и ноды в соответствующие поля свойства (см. видео ниже).
Привязка нод и ассетов к свойству -
Поскольку мы не используем компоненты, нам придется привязываться к имени ноды, которой назначено свойство со ссылками на ассеты. Итак, в методе init() класса WorldLogic мы получаем ноду по ее имени:
int AppWorldLogic::init() { /* ... */ NodePtr node = World::getNodeByName("node_name"); /* ... */ return 1; }
-
Затем мы получаем свойство, присвоенное ему:
PropertyPtr property = node->getProperty();
-
Теперь мы можем использовать это свойство для получения доступа к нодам и файлам:
-
чтобы получить материал, мы можем просто использовать соответствующий параметр ноды:
property->getParameterPtr("node_param_name")->getValueMaterial();
-
чтобы получить путь к файлу, мы можем просто использовать:
As we have a path to our file, we can use it, for example:const char *path = property->getParameterPtr("file_param_name")->getValueFile();
// to create a node reference NodeReferencePtr node_ref = NodeReference::create(path_to_node_file); // to load a sound source SoundSourcePtr sound = SoundSource::create(path_to_sound_file);
-
Давайте воспользуемся примером, чтобы проиллюстрировать этот подход.
Пример использования#
В этом примере мы будем работать с нодами и ассетами, связанными с определенной нодой, используя свойство с помощью C++ и C#.
Давайте создадим простой объект MeshStatic с именем my_object, унаследуем материал от mesh_base для назначения поверхностям нашего объекта и добавим какой-нибудь аудиофайл (*.mp3 или *.oga) в наш проект.
Итак, мы связываем файл *.mesh, материал, ноду material_ball из мира по умолчанию и аудиофайл, используя .prop файл описанный выше.
В нашем коде мы будем:
- Вращать привязанную ноду.
- Изменять привязанную материал и сохранять изменения.
- Создавать новый объект, используя привязанный меш.
- Проигрывать привязанный аудиофайл.
Реализация на C++#
Ниже вы найдете реализацию описанного выше примера на C++. Вы можете скопировать и вставить код в файл AppWorldLogic.cpp вашего проекта.
AppWorldLogic.cpp
#include "AppWorldLogic.h"
#include <UnigineMaterials.h>
#include <UnigineSounds.h>
#include <UnigineGame.h>
#include <UnigineWorld.h>
#include <UnigineFileSystem.h>
using namespace Unigine;
using namespace Math;
NodePtr my_node; // node to which a property with links is assigned
PropertyPtr property; // property with all necessary links
MaterialPtr material; // linked material
NodePtr param_node; // linked node
SoundSourcePtr sound; // sound source to be played
ObjectMeshStaticPtr generated_obj; // object to be generated using the mesh
AppWorldLogic::AppWorldLogic() {
}
AppWorldLogic::~AppWorldLogic() {
}
int AppWorldLogic::init() {
// getting the node to which a property with links to assets is assigned
my_node = World::getNodeByName("my_object");
// getting the property, that will be used to access all necessary files
property = my_node->getProperty();
// using access to property parameters to perform desired actions
if (property) {
// getting a material from the corresponding property parameter
material = property->getParameterPtr("some_material")->getValueMaterial();
// getting the path to the mesh file from the corresponding property parameter
const char *mesh_file_name = property->getParameterPtr("some_mesh")->getValueFile();
// creating the object by using the mesh
generated_obj = ObjectMeshStatic::create(mesh_file_name);
// setting the object position relative to another node position
generated_obj->setWorldPosition(my_node->getWorldPosition());
generated_obj->translate(vec3(-1.0f, 0.0f, 0.0f));
// getting the path to the sound file from the corresponding property parameter
const char *sound_file_name = property->getParameterPtr("some_file")->getValueFile();
// getting a node from the corresponding property parameter
param_node = property->getParameterPtr("some_node")->getValueNode();
// creating and playing a sound from the file
sound = SoundSource::create(sound_file_name);
sound->setMaxDistance(100.0f);
sound->setLoop(1);
sound->play();
// reporting results to the console
Log::message("Path to mesh file: %s\nPath to sound file: %s\nNode ID: %d\n", mesh_file_name, sound_file_name, param_node->getID());
}
return 1;
}
// start of the main loop
int AppWorldLogic::update() {
// Write here code to be called before updating each render frame: specify all graphics-related functions you want to be called every frame while your application executes.
float ifps = Game::getIFps();
// changing the material
material->setParameterFloat4("albedo_color", vec4(Game::getRandomFloat(0.0f, 1.0f), Game::getRandomFloat(0.0f, 1.0f), Game::getRandomFloat(0.0f, 1.0f), 1.0f));
// rotate linked node
param_node->setRotation(param_node->getRotation() * quat(0, 0, 30.0f * ifps));
return 1;
}
int AppWorldLogic::postUpdate() {
// The engine calls this function before rendering each render frame: correct behavior after the state of the node has been updated.
return 1;
}
int AppWorldLogic::updatePhysics() {
// Write here code to be called before updating each physics frame: control physics in your application and put non-rendering calculations.
// The engine calls updatePhysics() with the fixed rate (60 times per second by default) regardless of the FPS value.
// WARNING: do not create, delete or change transformations of nodes here, because rendering is already in progress.
return 1;
}
// end of the main loop
int AppWorldLogic::shutdown() {
// Write here code to be called on world shutdown: delete resources that were created during world script execution to avoid memory leaks.
// saving current material color (check it in the UnigineEditor to see that it was modified)
material->save();
return 1;
}
int AppWorldLogic::save(const Unigine::StreamPtr &stream) {
// Write here code to be called when the world is saving its state: save custom user data to a file.
UNIGINE_UNUSED(stream);
return 1;
}
int AppWorldLogic::restore(const Unigine::StreamPtr &stream) {
// Write here code to be called when the world is restoring its state: restore custom user data to a file here.
UNIGINE_UNUSED(stream);
return 1;
}
Реализация на C##
Ниже вы найдете реализацию описанного выше примера на C#. Вы можете скопировать и вставить код в файл AppWorldLogic.cs вашего проекта.
AppWorldLogic.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Unigine;
namespace UnigineApp
{
class AppWorldLogic : WorldLogic
{
// World logic, it takes effect only when the world is loaded.
// These methods are called right after corresponding world script's (UnigineScript) methods.
Node my_node; // node to which a property with links is assigned
Property property; // property with all necessary links
Material material; // linked material
Node param_node; // linked node
SoundSource sound; // sound source to be played
ObjectMeshStatic generated_obj; // object to be generated using the mesh
public AppWorldLogic()
{
}
public override bool Init()
{
// getting the node to which a property with links to assets is assigned
my_node = World.GetNodeByName("my_object");
// getting the property, that will be used to access all necessary files
property = my_node.GetProperty();
// using access to property parameters to perform desired actions
if (property) {
// getting a material from the corresponding property parameter
material = property.GetParameterPtr("some_material").ValueMaterial;
// getting the path to the mesh file from the corresponding property parameter
String mesh_file_name = property.GetParameterPtr("some_mesh").ValueFile;
// creating the object by using the mesh
generated_obj = new ObjectMeshStatic(mesh_file_name);
// setting the object position relative to another node position
generated_obj.WorldPosition = my_node.WorldPosition;
generated_obj.Translate(-1.0f, 0.0f, 0.0f);
// getting the path to the sound file from the corresponding property parameter
String sound_file_name = property.GetParameterPtr("some_file").ValueFile;
// getting a node from the corresponding property parameter
param_node = property.GetParameterPtr("some_node").ValueNode;
// creating and playing a sound from the file
sound = new SoundSource(sound_file_name);
sound.MaxDistance = 100.0f;
sound.Loop = 1;
sound.Play();
// reporting results to the console
Log.Message("Path to mesh file: {0}\nPath to sound file: {1}\nNode ID: {2}\n", mesh_file_name, sound_file_name, param_node.ID);
}
return true;
}
// start of the main loop
public override bool Update()
{
// Write here code to be called before updating each render frame: specify all graphics-related functions you want to be called every frame while your application executes.
float ifps = Game.IFps;
// changing the material
material.SetParameterFloat4("albedo_color", new vec4(Game.GetRandomFloat(0.0f, 1.0f), Game.GetRandomFloat(0.0f, 1.0f), Game.GetRandomFloat(0.0f, 1.0f), 1.0f));
// rotate linked node
param_node.SetRotation(param_node.GetRotation() * new quat(0, 0, 30.0f * ifps));
return true;
}
public override bool PostUpdate()
{
// The engine calls this function before rendering each render frame: correct behavior after the state of the node has been updated.
return true;
}
// end of the main loop
public override bool Shutdown()
{
// Write here code to be called on world shutdown: delete resources that were created during world script execution to avoid memory leaks.
// saving current material color (check it in the UnigineEditor to see that it was modified)
material.Save();
return true;
}
public override bool Save(Stream stream)
{
// Write here code to be called when the world is saving its state: save custom user data to a file.
return true;
}
public override bool Restore(Stream stream)
{
// Write here code to be called when the world is restoring its state: restore custom user data to a file here.
return true;
}
}
}
Подход с использованием системы компонентов C++#
Если вы используете Компонентную систему C++ в своем проекте, рекомендуется следующая последовательность действий:
- Создайте компонент, унаследовав класс от ComponentBase. Шаблон этого класса доступен в заголовке UnigineComponentSystem.h.
- Добавьте поля для хранения ссылок на все необходимые ноды и файлы, материалы, меши и т.д. (с помощью макросов PROP_PARAM).
- Создайте файл *.prop для этого класса (путем компиляции и запуска приложения).
- Откройте свой мир в UnigineEditor и назначьте свойство сгенерированного компонента нужным нодам.
-
Укажите все ноды, материалы, текстуры, меши, другие файлы, которые будут использоваться, перетащив их из Asset Browser непосредственно в соответствующее поле свойства в окне Parameters.
Привязка нод и ассетов к свойству. - Экземпляр компонента создается при запуске приложения. Этот экземпляр имеет переменные, обеспечивающие доступ ко всем используемым ассетам.
Для получения более подробной информации об использовании компонентной системы C++ см. Пример использования компонентной системы C++.
Глобальные свойства для нескольких миров#
Иногда вам может понадобиться иметь свойство со ссылками на ассеты (аналогичное описанному выше), которое вы хотите использовать в нескольких мирах, своего рода глобальное самодостаточное свойство, не назначенное ни одной ноде. Такое свойство может быть использовано, например, для хранения настроек для определенного типа оружия (FBX-модель, звуки стрельбы, ноды с системами частиц для визуальных эффектов и т.д.), которые будут использоваться глобально на различных уровнях игры.
Процедура здесь выглядит следующим образом:
-
Сначала мы создаем свойство для хранения ссылок на все ноды и ассеты, которые нам нужны, и сохраняем его в папке data нашего проекта. Например, свойство может быть таким:
<?xml version="1.0" encoding="utf-8"?> <property version="2.16.0.2" name="my_property" parent_name="node_base" manual="1" editable="1"> <parameter name="some_float" type="float">30.5</parameter> <parameter name="some_string" type="string">Hello from my_property!</parameter> <parameter name="some_node" type="node">0</parameter> <parameter name="some_material" type="material"></parameter> <parameter name="some_mesh" type="file"></parameter> <parameter name="some_file" type="file"></parameter> </property>
-
Затем откройте UnigineEditor, найдите созданное свойство в окне Properties, щелкните по нему правой кнопкой мыши и выберите Create Child. Должно быть создано свойство с именем my_property_0 (вы можете переименовать его, если хотите).
Создайте дочернее свойство -
Дизайнеры уровней подготавливают нужные настройки, перетаскивая необходимые ассеты и ноды в соответствующие поля дочернего свойства my_property_0, а также устанавливают другие параметры (если таковые имеются).
Привязка нод и ассетов к свойству -
Программисты могут получить доступ к любому из этих ассетов через это глобальное свойство из любого мира. Поскольку мы не используем компоненты, нам нужно будет найти свойство со ссылками на ассеты по его имени (my_property_0). Итак, в методе init() класса WorldLogic мы делаем следующее:
int AppWorldLogic::init() { /* ... */ PropertyPtr property = Properties::findManualProperty("my_property_0"); /* ... */ return 1; }
-
Теперь мы можем использовать это свойство для получения доступа к нодам и файлам:
-
чтобы получить материал, мы можем просто использовать соответствующий параметр ноды:
property->getParameterPtr("node_param_name")->getValueMaterial();
-
чтобы получить путь к файлу, мы можем просто использовать:
const char *path = property->getParameterPtr("file_param_name")->getValueFile();
Поскольку у нас есть путь до нашего файла мы можем, например, использовать его чтобы:
// to create a node reference NodeReferencePtr node_ref = NodeReference::create(path_to_node_file); // to load a sound source SoundSourcePtr sound = SoundSource::create(path_to_sound_file);
-
Реализация на C++#
Итак, реализация примера на C++ (описанная выше) для глобального свойства будет переписана, как показано ниже. Вы можете скопировать и вставить код в файл AppWorldLogic.cpp вашего проекта.
AppWorldLogic.cpp
#include "AppWorldLogic.h"
#include <UnigineMaterials.h>
#include <UnigineSounds.h>
#include <UnigineGame.h>
#include <UnigineWorld.h>
#include <UnigineFileSystem.h>
using namespace Unigine;
using namespace Math;
PropertyPtr property; // property with all necessary links
MaterialPtr material; // linked material
NodePtr param_node; // linked node
SoundSourcePtr sound; // sound source to be played
ObjectMeshStaticPtr generated_obj; // object to be generated using the mesh
AppWorldLogic::AppWorldLogic() {
}
AppWorldLogic::~AppWorldLogic() {
}
int AppWorldLogic::init() {
// getting the property that will be used to access all necessary files using its name
property = Properties::findManualProperty("my_property_0");
// using access to property parameters to perform desired actions
if (property) {
// getting a material from the corresponding property parameter
material = property->getParameterPtr("some_material")->getValueMaterial();
// getting the path to the mesh file from the corresponding property parameter
const char *mesh_file_name = property->getParameterPtr("some_mesh")->getValueFile();
// creating the object by using the mesh
generated_obj = ObjectMeshStatic::create(mesh_file_name);
// setting the object position relative to another node position
generated_obj->setWorldPosition(my_node->getWorldPosition());
generated_obj->translate(vec3(-1.0f, 0.0f, 0.0f));
// getting the path to the sound file from the corresponding property parameter
const char *sound_file_name = property->getParameterPtr("some_file")->getValueFile();
// getting a node from the corresponding property parameter
param_node = property->getParameterPtr("some_node")->getValueNode();
// creating and playing a sound from the file
sound = SoundSource::create(sound_file_name);
sound->setMaxDistance(100.0f);
sound->setLoop(1);
sound->play();
// reporting results to the console
Log::message("Path to mesh file: %s\nPath to sound file: %s\nNode ID: %d\n", mesh_file_name, sound_file_name, param_node->getID());
}
return 1;
}
// start of the main loop
int AppWorldLogic::update() {
// Write here code to be called before updating each render frame: specify all graphics-related functions you want to be called every frame while your application executes.
float ifps = Game::getIFps();
// changing the material
material->setParameterFloat4("albedo_color", vec4(Game::getRandomFloat(0.0f, 1.0f), Game::getRandomFloat(0.0f, 1.0f), Game::getRandomFloat(0.0f, 1.0f), 1.0f));
// rotate linked node
param_node->setRotation(param_node->getRotation() * quat(0, 0, 30.0f * ifps));
return 1;
}
int AppWorldLogic::postUpdate() {
// The engine calls this function before rendering each render frame: correct behavior after the state of the node has been updated.
return 1;
}
int AppWorldLogic::updatePhysics() {
// Write here code to be called before updating each physics frame: control physics in your application and put non-rendering calculations.
// The engine calls updatePhysics() with the fixed rate (60 times per second by default) regardless of the FPS value.
// WARNING: do not create, delete or change transformations of nodes here, because rendering is already in progress.
return 1;
}
// end of the main loop
int AppWorldLogic::shutdown() {
// Write here code to be called on world shutdown: delete resources that were created during world script execution to avoid memory leaks.
// saving current material color (check it in the UnigineEditor to see that it was modified)
material->save();
return 1;
}
int AppWorldLogic::save(const Unigine::StreamPtr &stream) {
// Write here code to be called when the world is saving its state: save custom user data to a file.
UNIGINE_UNUSED(stream);
return 1;
}
int AppWorldLogic::restore(const Unigine::StreamPtr &stream) {
// Write here code to be called when the world is restoring its state: restore custom user data to a file here.
UNIGINE_UNUSED(stream);
return 1;
}
Реализация на C##
Реализация примера на C# (описанная выше) для глобального свойства будет переписана, как показано ниже. Вы можете скопировать и вставить код в файл AppWorldLogic.cs вашего проекта.
AppWorldLogic.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Unigine;
namespace UnigineApp
{
class AppWorldLogic : WorldLogic
{
// World logic, it takes effect only when the world is loaded.
// These methods are called right after corresponding world script's (UnigineScript) methods.
Property property; // property with all necessary links
Material material; // linked material
Node param_node; // linked node
SoundSource sound; // sound source to be played
ObjectMeshStatic generated_obj; // object to be generated using the mesh
public AppWorldLogic()
{
}
public override bool Init()
{
// getting the property that will be used to access all necessary files using its name
property = Properties.FindManualProperty("my_property_0");
// using access to property parameters to perform desired actions
if (property) {
// getting a material from the corresponding property parameter
material = property.GetParameterPtr("some_material").ValueMaterial;
// getting the path to the mesh file from the corresponding property parameter
String mesh_file_name = property.GetParameterPtr("some_mesh").ValueFile;
// creating the object by using the mesh
generated_obj = new ObjectMeshStatic(mesh_file_name);
// setting the object position relative to another node position
generated_obj.WorldPosition = my_node.WorldPosition;
generated_obj.Translate(-1.0f, 0.0f, 0.0f);
// getting the path to the sound file from the corresponding property parameter
String sound_file_name = property.GetParameterPtr("some_file").ValueFile;
// getting a node from the corresponding property parameter
param_node = property.GetParameterPtr("some_node").ValueNode;
// creating and playing a sound from the file
sound = new SoundSource(sound_file_name);
sound.MaxDistance = 100.0f;
sound.Loop = 1;
sound.Play();
// reporting results to the console
Log.Message("Path to mesh file: {0}\nPath to sound file: {1}\nNode ID: {2}\n", mesh_file_name, sound_file_name, param_node.ID);
}
return true;
}
// start of the main loop
public override bool Update()
{
// Write here code to be called before updating each render frame: specify all graphics-related functions you want to be called every frame while your application executes.
float ifps = Game.IFps;
// changing the material
material.SetParameterFloat4("albedo_color", new vec4(Game.GetRandomFloat(0.0f, 1.0f), Game.GetRandomFloat(0.0f, 1.0f), Game.GetRandomFloat(0.0f, 1.0f), 1.0f));
// rotate linked node
param_node.SetRotation(param_node.GetRotation() * new quat(0, 0, 30.0f * ifps));
return true;
}
public override bool PostUpdate()
{
// The engine calls this function before rendering each render frame: correct behavior after the state of the node has been updated.
return true;
}
// end of the main loop
public override bool Shutdown()
{
// Write here code to be called on world shutdown: delete resources that were created during world script execution to avoid memory leaks.
// saving current material color (check it in the UnigineEditor to see that it was modified)
material.Save();
return true;
}
public override bool Save(Stream stream)
{
// Write here code to be called when the world is saving its state: save custom user data to a file.
return true;
}
public override bool Restore(Stream stream)
{
// Write here code to be called when the world is restoring its state: restore custom user data to a file here.
return true;
}
}
}