Переход на UNIGINE с Unity: гайд для программистов
Написание игровой логики, запуск скриптов в редакторе, триггеры, ввод, рейкастинг и другое.
Специально для тех, кто ищет полноценный отечественный аналог Unity или Unreal Engine, мы продолжаем цикл статей про безболезненный переход на UNIGINE с зарубежных движков. В третьем выпуске рассмотрим миграцию с Unity с точки зрения программиста.
Общая информация
Традиционно игровая логика в проекте Unity реализуется через пользовательские компоненты — C# классы, унаследованные от MonoBehaviour. Основная логика компонента определена в событийных методах Start (), Update () и так далее.
UNIGINE предлагает очень похожую концепцию — C# Component System — стабильная и высокопроизводительная компонентная система на .NET 5. Компоненты представлены C# классами, унаследованными от Component, их можно назначить любой ноде в сцене. Жизненный цикл каждого компонента определяется набором методов (Init (), Update () и т. д.), вызываемых в основном цикле движка.
Программирование в UNIGINE с использованием C# мало чем отличается от программирования в Unity. Например, давайте сравним, как выполняется вращение объекта в Unity:
//Исходный код (C#)
using UnityEngine;
public class MyComponent : MonoBehaviour
{
public float speed = 90.0f;
void Update()
{
transform.Rotate(0, speed * Time.deltaTime, 0, Space.Self);
}
}
и в UNIGINE:
//Исходный код (C#)
using Unigine;
/* .. */
public class MyComponent : Component
{
public float speed = 90.0f;
void Update()
{
node.Rotate(0, 0, speed * Game.IFps);
}
}
Кнопка для запуска экземпляра приложения в отдельном окне расположена на панели инструментов в UnigineEditor. Также рядом расположены настройки параметров запуска.
Вот как мы заставим колесо вращаться с помощью C# Component System и запустим экземпляр, чтобы немедленно его проверить:
Более того, системная логика приложения на UNIGINE может быть определена в файлах AppWorldLogic.cs, AppSystemLogic.cs и AppEditorLogic.cs в папке source проекта.
Чтобы узнать больше о последовательности выполнения и о том, как создавать компоненты, перейдите по ссылкам ниже:
Для тех, кто предпочитает C++, UNIGINE позволяет создавать приложения C++ с использованием С++ UNIGINE API, и, при необходимости, C++ Component System.
Основные примеры кода
Вывод в консоль
Используйте клавишу ~, чтобы открыть консоль в приложении
Unity | UNIGINE |
|
|
См. также:
Дополнительные типы сообщений в API класса Log
Видеоруководство, демонстрирующее, как выводить пользовательские сообщения в консоль с помощью C# Component System
Доступ к GameObject / Node из компонента
Unity | UNIGINE |
|
|
См. также:
Видеоруководство, демонстрирующее, как получить доступ к нодам из компонентов с помощью C# Component System
Работа с направлениями
В Unity компонент Transform отвечает за позицию, вращение и масштаб Game Object, а также за родительско-дочерние связи. Чтобы получить вектор направления по одной из осей с учетом вращения GameObject в мировых координатах, в Unity используется соответствующее свойство компонента Transform.
В UNIGINE трансформация ноды в пространстве представлена ее матрицей трансформации (mat4), а все основные свойства и операции с иерархией нод доступны при помощи методов и свойств класса Node. Такой же вектор направления в UNIGINE получается с помощью метода Node.GetWorldDirection ():
Unity | UNIGINE |
|
|
См. также:
Более плавный игровой процесс с DeltaTime / IFps
В Unity, чтобы гарантировать, что определенные действия выполняются за одно и то же время независимо от частоты кадров (например, изменение положения один раз в секунду и т. д.), используется множитель Time.deltaTime (время в секундах, которое потребовалось для завершения последнего кадра). То же самое в UNIGINE называется Game.IFps:
Unity | UNIGINE |
|
|
Рисование отладочных данных
Unity:
//Исходный код (C#)
Debug.DrawLine(Vector3.zero, new Vector3(5, 0, 0), Color.white, 2.5f);
Vector3 forward = transform.TransformDirection(Vector3.forward) * 10;
Debug.DrawRay(transform.position, forward, Color.green);
В UNIGINE за вспомогательную отрисовку отвечает синглтон Visualizer:
//Исходный код (C#)
//Включаем вспомогательную визуализацию
/* .. */
Visualizer.Enabled = true;
Visualizer.RenderLine3D(vec3.ZERO, new vec3(5, 0, 0), vec4.ONE);
Visualizer.RenderVector(node.Position, node.GetDirection(MathLib.AXIS.Y) * 10, new vec4(1, 0, 0, 1));
Примечание. Visualizer также можно включить с помощью консольной команды show_visualizer 1.
См. также:
Загрузка сцены
Unity | UNIGINE |
|
|
Доступ к компоненту из GameObject/Node
Unity:
//Исходный код (C#)
MyComponent my_component = gameObject.GetComponent();
UNIGINE:
//Исходный код (C#)
MyComponent my_component = node.GetComponent();
MyComponent my_component = GetComponent(node);
Доступ к стандартным компонентам
Компонентный подход Unity позволяет рассматривать такие стандартные объекты, как MeshRenderer, Rigidbody, Collider, Transform и другие, как обычные компоненты.
В UNIGINE доступ к аналогам этих сущностей осуществляется иначе. Классы всех типов нод являются производными от Node, поэтому чтобы получить доступ к функциональности ноды определенного типа (например, ObjectMeshStatic), необходимо провести понижающее приведение типа (downcasting). Рассмотрим эти самые популярные варианты использования:
Unity:
//Исходный код (C#)
// получение трансформации GameObject
Transform transform_1 = gameObject.GetComponent();
Transform transform_2 = gameObject.transform;
// доступ к компоненту Mesh Renderer
MeshRenderer mesh_renderer = gameObject.GetComponent();
// доступ к компоненту Rigidbody
Rigidbody rigidbody = gameObject.GetComponent();
// доступ к Collider
Collider collider = gameObject.GetComponent();
BoxCollider boxCollider = collider as BoxCollider;
UNIGINE:
//Исходный код (C#)
// получение матрицы трансформации ноды в мировых координатах
mat4 transform = node.WorldTransform;
// получение локальной матрицы трансформации ноды (относительно родителя)
mat4 local_transform = node.Transform;
// приведение экземпляра к типу ObjectMeshStatic с проверкой
ObjectMeshStatic mesh_static = node as ObjectMeshStatic;
// получение BodyRigid, назначенного на объект
Body body = (node as Unigine.Object).Body;
BodyRigid rigid = body as BodyRigid;
// получение всех коллизионных форм типа ShapeBox
for (int i = 0; i < body.NumShapes; i++)
{
Shape shape = body.GetShape(i);
if (shape is ShapeBox shapeBox)
{
...
}
}
Поиск GameObject/Node
Unity:
//Исходный код (C#)
// поиск по имени
GameObject myGameObj = GameObject.Find("My Game Object");
// Поиск "ammo" дочернего к "magazine".
Transform ammo_transform = gameObject.transform.Find("magazine/ammo");
GameObject ammo = ammo_transform.gameObject;
// Поиск компонентов по типу
MyComponent[] components = Object.FindObjectsOfType();
foreach (MyComponent component in components)
{
// ...
}
// Поиск объектов по тегу
GameObject[] taggedGameObjects = GameObject.FindGameObjectsWithTag("MyTag");
foreach (GameObject gameObj in taggedGameObjects)
{
// ...
}
UNIGINE:
//Исходный код (C#)
// Поиск ноды по имени
Node my_node = World.GetNodeByName("my_node");
// Поиск всех нод с этим именем
List nodes = new List();
World.GetNodesByName("my_node");
// Поиск непосредственно дочерней ноды по имени
int index = node.FindChild("child_node");
Node direct_child = node.GetChild(index);
// Рекурсивный поиск ноды по имени среди всех потомков в иерархии
Node child = node.FindNode("child_node", 1);
// Получение всех компонентов в мире по типу
MyComponent[] my_comps = FindComponentsInWorld();
foreach(MyComponent comp in my_comps)
{
Log.Message("{0}\n",comp.node.name);
}
Приведение от типа к типу
Downcasting (приведение от базового типа к производному) выполняется одинаково в обоих движках с использованием родной конструкции C# as:
Unity | UNIGINE |
|
|
Чтобы выполнить Upcasting (приведение от производного типа к базовому), можно как обычно просто использовать сам экземпляр:
Unity | UNIGINE |
|
|
Уничтожение GameObject/Node
Unity | UNIGINE |
|
|
Для выполнения отложенного удаления ноды в UNIGINE можно создать компонент, который будет отвечать за таймер и удаление.
//Исходный код (C#)
// LifetimeController.cs
/* .. */
public class LifetimeController : Component
{
public float lifetime = 5.0f;
void Update()
{
lifetime = lifetime - Game.IFps;
if (lifetime < 0)
{
// уничтожить текущую ноду со всеми компонентами и свойствами
node.DeleteLater();
}
}
}
// MyComponent.cs
/* .. */
public class MyComponent : Component
{
void Update()
{
if (/* пришло время */)
{
LifetimeController lc = node.AddComponent();
lc.lifetime = 2.0f;
}
}
}
Создание экземпляра GameObject / Node Reference
В Unity экземпляр префаба или копия уже существующего в сцене GameObject создается с помощью функции Object.Instantiate:
//Исходный код (C#)
using UnityEngine;
public class MyComponent : MonoBehaviour
{
public GameObject myPrefab;
void Start()
{
Instantiate(myPrefab, new Vector3(0, 0, 0), Quaternion.identity);
}
}
Затем вы должны указать префаб, который будет создан, в параметрах компонента скрипта.
В UNIGINE получить доступ к уже существующей ноде любого типа можно также через параметр компонента, и клонировать ее при помощи Node.Clone ().
Но ассеты не являются нодами, они принадлежат файловой системе. К ассету можно обратиться, используя эти типы параметров:
AssetLink — для любых ассетов,
AssetLinkNode — для ассетов *.node, содержащих иерархию нод, сохраненную как Node Reference (аналог prefab).
В этом случае ссылка на ассет, аналогично Unity, указывается в UnigineEditor:
Также можно использовать функцию World.LoadNode для загрузки иерархии нод вручную, указав виртуальный путь к ассету.
//Исходный код (C#)
/* .. */
public class MyComponent : Component
{
public Node node_to_clone;
public AssetLinkNode node_to_spawn;
private void Init()
{
Node cloned = node_to_clone.Clone();
Node spawned = node_to_spawn.Load(node.WorldPosition, quat.IDENTITY);
Node spawned_manually = World.LoadNode("nodes/node_reference.node");
}
}
Еще один способ загрузить содержимое ассета *.node — создать NodeReference и работать с иерархией нод как с одним объектом. Тип Node Reference имеет ряд внутренних оптимизаций и тонких моментов (кэширование нод, распаковка иерархии и т.д.), поэтому важно учитывать специфику работы с этими объектами.
//Исходный код (C#)
/* .. */
public class MyComponent : Component
{
void Init()
{
NodeReference nodeRef = new NodeReference("nodes/node_reference_0.node");
}
}
Запуск скриптов в редакторе
Unity позволяет расширять функциональность редактора с помощью C# скриптов. Для этого в скриптах поддерживаются специальные атрибуты:
[ExecuteInEditMode] — для выполнения логики скрипта в режиме Edit, когда приложение не запущено.
[ExecuteAlways] — для выполнения логики скрипта как в режиме Play, так и при редактировании.
Например, так выглядит код компонента, который заставляет GameObject ориентироваться на определенную точку в сцене:
//Исходный код (C#)
//C# Example (LookAtPoint.cs)
using UnityEngine;
[ExecuteInEditMode]
public class LookAtPoint : MonoBehaviour
{
public Vector3 lookAtPoint = Vector3.zero;
void Update()
{
transform.LookAt(lookAtPoint);
}
}
UNIGINE не поддерживает выполнение логики C# внутри редактора. Основной способ расширить функциональность редактора — плагины, написанные на C++.
Для быстрого тестирования или автоматизации разработки можно написать логику на UnigineScript. UnigineScript API обладает только базовой функциональностью и ограниченной сферой применения, но доступен для любого проекта на UNIGINE, включая проекты на .NET 5.
Есть два способа добавить скриптовую логику в проект:
Создайте ассет скрипта .usc.
Определите в нем логику. При необходимости добавьте проверку, загружен ли редактор:
//Исходный код (UnigineScript)
#include
vec3 lookAtPoint = vec3_zero;
Node node;
int init() {
node = engine.world.getNodeByName("material_ball");
return 1;
}
int update() {
if(engine.editor.isLoaded())
node.worldLookAt(lookAtPoint);
return 1;
}
Выделите текущий мир и укажите для него сценарий мира. Нажмите Apply и перезагрузите мир.
Проверьте окно консоли на наличие ошибок.
После этого логика скрипта будет выполняться как в редакторе, так и в приложении.
Используя WorldExpression. С той же целью можно использовать ноду WorldExpression, выполняющую логику при добавлении в мир:
Нажмите Create → Logic → Expression и поместите новую ноду WorldExpression в мир.
Напишите логику на UnigineScript в поле Source:
//Исходный код (UnigineScript)
#include
vec3 lookAtPoint = vec3_zero;
Node node;
int init() {
node = engine.world.getNodeByName("material_ball");
return 1;
}
int update() {
if(engine.editor.isLoaded())
node.worldLookAt(lookAtPoint);
return 1;
}
Проверьте окно Console на наличие ошибок.
Логика будет выполнена немедленно.
Триггеры
Помимо обнаружения столкновений, компонент Collider в Unity может быть использован как триггер, который срабатывает, когда другой коллайдер попадает в его объем.
//Исходный код (C#)
public class MyComponent : MonoBehaviour
{
void Start()
{
collider.isTrigger = true;
}
void OnTriggerEnter(Collider other)
{
// ...
}
void OnTriggerExit(Collider other)
{
// ...
}
}
В UNIGINE Trigger — это специальный тип нод, вызывающих события в определенных ситуациях:
Важно! PhysicalTrigger не обрабатывает столкновения, для этого физические тела и сочленения предоставляют свои собственные события.
WorldTrigger — наиболее распространенный тип триггера, который можно использовать в игровой логике:
//Исходный код (C#)
/* .. */
class MyComponent : Component
{
WorldTrigger trigger;
void enter_callback(Node incomer)
{
Log.Message("\n{0} has entered the trigger space\n", incomer.Name);
}
void Init()
{
trigger = node as WorldTrigger;
if(trigger != null)
{
trigger.AddEnterCallback(enter_callback);
trigger.AddLeaveCallback( leaver => Log.Message("{0} has left the trigger space", leaver.Name));
}
}
}
Обработка ввода
Обычный игровой ввод Unity:
//Исходный код (C#)
public class MyPlayerController : MonoBehaviour
{
void Update()
{
if (Input.GetButtonDown("Fire"))
{
// ...
}
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
// ...
}
}
UNIGINE:
//Исходный код (C#)
/* .. */
class MyPlayerController : Component
{
void Update()
{
if(Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT))
{
Log.Message("Left mouse button was clicked at {0}\n", Input.MouseCoord);
}
if (Input.IsKeyDown(Input.KEY.Q) && !Unigine.Console.Activity)
{
Log.Message("Q was pressed and the Console is not active.\n");
App.Exit();
}
}
}
Также можно использовать синглтон ControlsApp для обработки привязок элементов управления к состояниям. Чтобы настроить привязки, откройте настройки Controls:
Исходный код (C#)
/* .. */
class MyPlayerController : Component
{
void Init()
{
// переназначение состояний клавишам и кнопкам вручную
ControlsApp.SetStateKey(Controls.STATE_FORWARD, 'w');
ControlsApp.SetStateKey(Controls.STATE_BACKWARD, 's');
ControlsApp.SetStateKey(Controls.STATE_MOVE_LEFT, 'a');
ControlsApp.SetStateKey(Controls.STATE_MOVE_RIGHT, 'd');
ControlsApp.SetStateButton(Controls.STATE_JUMP, App.BUTTON_LEFT);
}
void Update()
{
if (ControlsApp.ClearState(Controls.STATE_FORWARD) != 0)
{
Log.Message("FORWARD key pressed\n");
}
else if (ControlsApp.ClearState(Controls.STATE_BACKWARD) != 0)
{
Log.Message("BACKWARD key pressed\n");
}
else if (ControlsApp.ClearState(Controls.STATE_MOVE_LEFT) != 0)
{
Log.Message("MOVE_LEFT key pressed\n");
}
else if (ControlsApp.ClearState(Controls.STATE_MOVE_RIGHT) != 0)
{
Log.Message("MOVE_RIGHT key pressed\n");
}
else if (ControlsApp.ClearState(Controls.STATE_JUMP) != 0)
{
Log.Message("JUMP button pressed\n");
}
}
}
Рейкастинг
Для обнаружения пересечений лучей с объектами в Unity используется Physics.Raycast. GameObject должен иметь прикрепленный компонент Collider для участия в рейкастинге:
//Исходный код (C#)
using UnityEngine;
public class ExampleClass : MonoBehaviour
{
public Camera camera;
void Update()
{
// игнорируем 2 слой
int layerMask = 1 << 2;
layerMask = ~layerMask;
RaycastHit hit;
Ray ray = camera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit, Mathf.Infinity, layerMask))
{
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * hit.distance, Color.yellow);
Debug.Log("Did Hit");
}
else
{
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * 1000, Color.white);
Debug.Log("Did not Hit");
}
}
}
В UNIGINE то же самое делается с помощью Intersections:
//Исходный код (C#)
/* .. */
class IntersectionExample : Component
{
void Init()
{
Visualizer.Enabled = true;
}
void Update()
{
ivec2 mouse = Input.MouseCoord;
float length = 100.0f;
vec3 start = Game.Player.WorldPosition;
vec3 end = start + new vec3(Game.Player.GetDirectionFromScreen(mouse.x, mouse.y)) * length;
// игнорируем поверхности мешей с включенными битами маски Intersection
int mask = ~(1 << 2 | 1 << 4);
WorldIntersectionNormal intersection = new WorldIntersectionNormal();
Unigine.Object obj = World.GetIntersection(start, end, mask, intersection);
if (obj)
{
vec3 point = intersection.Point;
vec3 normal = intersection.Normal;
Visualizer.RenderVector(point, point + normal, vec4.ONE);
Log.Message("Hit {0} at {1}\n", obj.Name, point);
}
}
}
* * *
Напоминаем, что получить доступ к бесплатной версии UNIGINE 2 Community можно заполнив форму на нашем сайте.
Все комплектации UNIGINE:
Community — базовая версия для любителей и независимых разработчиков. Достаточна для разработки видеоигр большинства популярных жанров (включая VR).
Engineering — расширенная, специализированная версия. Включает множество заготовок для инженерных задач.
Sim — максимальная версия платформы под масштабные проекты (размеров планеты и даже больше) с готовыми механизмами симуляции.
Подробнее о комплектациях и ценах