Переход на 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# Component System

  • C# UNIGINE API

Для тех, кто предпочитает C++, UNIGINE позволяет создавать приложения C++ с использованием С++ UNIGINE API, и, при необходимости, C++ Component System.

Основные примеры кода

Вывод в консоль

Используйте клавишу ~, чтобы открыть консоль в приложении

Unity

UNIGINE

Исходный код (C#)

Debug.Log("Text: " + text);

Debug.LogFormat("Formatted text: {0}", text);

Исходный код (C#)

Log.Message("Debug info:" + text + "\n");

Log.Message("Debug info: {0}\n", new vec3(1, 2, 3));

См. также:

  • Дополнительные типы сообщений в API класса Log

  • Видеоруководство, демонстрирующее, как выводить пользовательские сообщения в консоль с помощью C# Component System

Доступ к GameObject / Node из компонента

Unity

UNIGINE

Исходный код (C#)

GameObject this_go = gameObject;

string n = gameObject.name;

Исходный код (C#)

Node this_node = node;

string n = node.Name;

См. также:

  • Видеоруководство, демонстрирующее, как получить доступ к нодам из компонентов с помощью C# Component System

Работа с направлениями

В Unity компонент Transform отвечает за позицию, вращение и масштаб Game Object, а также за родительско-дочерние связи. Чтобы получить вектор направления по одной из осей с учетом вращения GameObject в мировых координатах, в Unity используется соответствующее свойство компонента Transform.

В UNIGINE трансформация ноды в пространстве представлена ее матрицей трансформации (mat4), а все основные свойства и операции с иерархией нод доступны при помощи методов и свойств класса Node. Такой же вектор направления в UNIGINE получается с помощью метода Node.GetWorldDirection():

Unity

UNIGINE

Исходный код (C#)

Vector3 forward = transform.forward;

Vector3 right = transform.right;

Vector3 up = transform.up;

transform.Translate(forward * speed * Time.deltaTime);

Исходный код (C#)

vec3 forward = node.GetWorldDirection(MathLib.AXIS.Y);

vec3 right = node.GetWorldDirection(MathLib.AXIS.X);

vec3 up = node.GetWorldDirection(MathLib.AXIS.Z);

node.Translate(forward * speed * Game.IFps);

См. также:

  • Система координат в UNIGINE

Более плавный игровой процесс с DeltaTime / IFps

В Unity, чтобы гарантировать, что определенные действия выполняются за одно и то же время независимо от частоты кадров (например, изменение положения один раз в секунду и т. д.), используется множитель Time.deltaTime (время в секундах, которое потребовалось для завершения последнего кадра). То же самое в UNIGINE называется Game.IFps:

Unity

UNIGINE

Исходный код (C#)

transform.Rotate(0, speed *  Time.deltaTime, 0, Space.Self);

Исходный код (C#)

node.Rotate(0, 0, speed * Game.IFps);

Рисование отладочных данных

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.

См. также:

  • Все типы визуализаций в API класса Visualizer.

Загрузка сцены

Unity

UNIGINE

Исходный код (C#)

SceneManager.LoadScene("YourSceneName",LoadSceneMode.Single);

Исходный код (C#)

World.LoadWorld("YourSceneName");

Доступ к компоненту из GameObject/Node

Unity:

Исходный код (C#)

MyComponent my_component = gameObject.GetComponent<MyComponent>();

UNIGINE:

Исходный код (C#)

MyComponent my_component = node.GetComponent<MyComponent>();

MyComponent my_component = GetComponent<MyComponent>(node);

Доступ к стандартным компонентам

Компонентный подход Unity позволяет рассматривать такие стандартные объекты, как MeshRenderer, Rigidbody, Collider, Transform и другие, как обычные компоненты.

В UNIGINE доступ к аналогам этих сущностей осуществляется иначе. Классы всех типов нод являются производными от Node, поэтому чтобы получить доступ к функциональности ноды определенного типа (например, ObjectMeshStatic), необходимо провести понижающее приведение типа (downcasting). Рассмотрим эти самые популярные варианты использования:

Unity:

Исходный код (C#)

// получение трансформации GameObject

Transform transform_1 = gameObject.GetComponent<Transform>();

Transform transform_2 = gameObject.transform;

// доступ к компоненту Mesh Renderer

MeshRenderer mesh_renderer = gameObject.GetComponent<MeshRenderer>();

// доступ к компоненту Rigidbody

Rigidbody rigidbody = gameObject.GetComponent<Rigidbody>();

// доступ к Collider

Collider collider = gameObject.GetComponent<Collider>();

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<MyComponent>();

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<Node> nodes = new List<Node>();

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<MyComponent>();

foreach(MyComponent comp in my_comps)

{

    Log.Message("{0}\n",comp.node.name);

}

Приведение от типа к типу

Downcasting (приведение от базового типа к производному) выполняется одинаково в обоих движках с использованием родной конструкции C# as:

Unity

UNIGINE

Исходный код (C#)

Collider collider = gameObject.GetComponent<Collider>;

BoxCollider boxCollider = collider as BoxCollider;

Исходный код (C#)

Node node = World.GetNodeByName("my_mesh");

ObjectMeshStatic mesh = node as ObjectMeshStatic;

Чтобы выполнить Upcasting (приведение от производного типа к базовому), можно как обычно просто использовать сам экземпляр:

Unity

UNIGINE

Исходный код (C#)

Collider collider = gameObject.GetComponent<Collider>;

BoxCollider boxCollider = collider as BoxCollider;

Collider coll = boxCollider;

Исходный код (C#)

Node node = World.GetNodeByName("my_mesh");

ObjectMeshStatic mesh = node as ObjectMeshStatic;

Unigine.Object obj = mesh;

Уничтожение GameObject/Node

Unity

UNIGINE

Исходный код (C#)

Destroy(myGameObject);

// уничтожить объект с задержкой в 1 секунду

Destroy(myGameObject, 1);

Исходный код (C#)

node.DeleteLater(); // рекомендуемый вариант

// нода уничтожается после текущего кадра

node.DeleteForce(); // форсировать уничтожение ноды в данный момент, что не всегда безопасно

Для выполнения отложенного удаления ноды в 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<LifetimeController>();

            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.

Есть два способа добавить скриптовую логику в проект:

  • Создав скрипт мира:

  1. Создайте ассет скрипта .usc.


  1. Определите в нем логику. При необходимости добавьте проверку, загружен ли редактор:

Исходный код (UnigineScript)

#include <core/unigine.h>

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;

}

  1. Выделите текущий мир и укажите для него сценарий мира. Нажмите Apply и перезагрузите мир.

  1. Проверьте окно консоли на наличие ошибок.

После этого логика скрипта будет выполняться как в редакторе, так и в приложении.

  • Используя WorldExpression. С той же целью можно использовать ноду WorldExpression, выполняющую логику при добавлении в мир:

  1. Нажмите Create -> Logic -> Expression и поместите новую ноду WorldExpression в мир.

  2. Напишите логику на UnigineScript в поле Source:

Исходный код (UnigineScript)

{

vec3 lookAtPoint = vec3_zero;

Node node = engine.world.getNodeByName("my_node");

node.worldLookAt(lookAtPoint);

}

  1. Проверьте окно Console на наличие ошибок.

  2. Логика будет выполнена немедленно.

Триггеры

Помимо обнаружения столкновений, компонент Collider в Unity может быть использован как триггер, который срабатывает, когда другой коллайдер попадает в его объем.

Исходный код (C#)

public class MyComponent : MonoBehaviour

{

    void Start()

    {

        collider.isTrigger = true;

    }

    void OnTriggerEnter(Collider other)

    {

        // ...

    }

    void OnTriggerExit(Collider other)

    {

        // ...

    }

}

В UNIGINE Trigger — это специальный тип нод, вызывающих события в определенных ситуациях:

  • NodeTrigger вызывает коллбэк при изменении состояния включен/выключен и позиции самой ноды.

  • WorldTrigger вызывает коллбэк, когда какая-либо нода (независимо от типа) попадает внутрь или за его пределы.

  • PhysicalTrigger вызывает коллбэк, когда физические объекты попадают внутрь или наружу его пределов.

Важно! 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 — максимальная версия платформы под масштабные проекты (размеров планеты и даже больше) с готовыми механизмами симуляции.

Подробнее о комплектациях и ценах

Источник: https://habr.com/ru/company/unigine/blog/665886/


Интересные статьи

Интересные статьи

Другой мир - другие правила.Текст в играх далеко не всегда нужен. Особенно, когда геймплей понятен без слов и игра не вещает историю. В этом аркадном прототипе о переключ...
Субботний вечер омрачен скандалом - сайт не работает, провайдер негодяй, админы - не специалисты, а сервера - решето. Вызов принят, или почему при всей нелюбви к 1С-Битри...
Всем привет, на связи Александр Панов, техлид из Pixonic. В компании я отвечаю за межпроектные решения и околопроектную периферию и сегодня хочу поделиться своим опытом и наработками. Плат...
Несмотря на то, что “в коробке” с Битриксом уже идут модули как для SOAP (модуль “Веб сервисы” в редакции “Бизнес” и старше), так и для REST (модуль “Rest API” во всех редакциях, начиная с...
В статье описаны необходимые параметры сервера для оптимальной работы сайта на платформе 1С-Битрикс.