Написание игровой логики, запуск скриптов в редакторе, триггеры, ввод, рейкастинг и другое.
Специально для тех, кто ищет полноценный отечественный аналог 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 |
|
|
См. также:
Система координат в 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.
См. также:
Все типы визуализаций в API класса Visualizer.
Загрузка сцены
Unity |
UNIGINE |
|
|
Доступ к компоненту из 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 |
|
|
Чтобы выполнить 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<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.
Есть два способа добавить скриптовую логику в проект:
Создав скрипт мира:
Создайте ассет скрипта .usc.
Определите в нем логику. При необходимости добавьте проверку, загружен ли редактор:
//Исходный код (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;
}
Выделите текущий мир и укажите для него сценарий мира. Нажмите Apply и перезагрузите мир.
Проверьте окно консоли на наличие ошибок.
После этого логика скрипта будет выполняться как в редакторе, так и в приложении.
Используя WorldExpression. С той же целью можно использовать ноду WorldExpression, выполняющую логику при добавлении в мир:
Нажмите Create -> Logic -> Expression и поместите новую ноду WorldExpression в мир.
Напишите логику на UnigineScript в поле Source:
//Исходный код (UnigineScript)
{
vec3 lookAtPoint = vec3_zero;
Node node = engine.world.getNodeByName("my_node");
node.worldLookAt(lookAtPoint);
}
Проверьте окно Console на наличие ошибок.
Логика будет выполнена немедленно.
Триггеры
Помимо обнаружения столкновений, компонент 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 — максимальная версия платформы под масштабные проекты (размеров планеты и даже больше) с готовыми механизмами симуляции.
Комментарии (5)
Ma0oo
19.05.2022 09:38У юнити появился новый visual element, благодаря которому сделать кастомный редактор для чего угодно стало не в пример проще. Хоть свои кастомные поля для int, string, object можно запилить, создав класс от visual element.
А как в unigine обстоят дела с созданием редактора? Просто ли добавить части инспектора в окно scene view? Можно ли создавать свои 3д handler для сцены?
Unigine Автор
20.05.2022 11:43Здравствуйте! В редакторе есть API для плагинов на C++ и C#, можно добавлять свои элементы в UI, так же можно использовать сторонние GUI (например, Dear ImGUI) для быстрых итераций.
red-cat-fat
Код при таком форматировании очень больно читать...
red-cat-fat
Во, теперь замечательное форматирование, спасибо!
Unigine Автор
Спасибо, что обратили на это внимание и помогли вести блог чуточку лучше :)