Русский
Русский
English
Статистика
Реклама

Unity

Разделяй и властвуй Использование FSM в Unity

23.04.2021 22:09:05 | Автор: admin

Грамотная архитектура играет ключевую роль при разработке любого программного продукта. Корни большинства распространенных проблем с производительностью, расширяемостью или понятностью кода растут именно из ее отсутствия. Отсутствие строго определенной структуры проекта лишает разработчиков возможности мыслить абстракциями, понимать написанный коллегой код с первого взгляда и предугадывать место возникновения ошибки. А в некоторых случаях человек может запутаться даже в своем собственном коде, перенасыщенном сущностями и компонентами. Но практически каждый программист рано или поздно, сам ли или с помощью умной книжки, знакомится с решениями, которые хороши вне зависимости от контекста. Они настолько эффективны и универсальны, что находят место в решении множества задач, и... Да, знаю, можно не продолжать, все уже и так поняли, что я о шаблонах проектирования. Одни на них молятся, другие находили среди них свои велосипеды. Некоторые утверждают на собеседовании, что изучили их вдоль и поперек и уличили в полной бесполезности. Но все, так или иначе, слышали о них. Сегодня речь пойдет об одном из паттернов - "Состояние". А точнее, о конечных автоматах. Даже если вы относитесь к последней из перечисленных выше групп, вы наверняка сталкивались со следующим инструментом:

Минимальный аниматор главного героя в платформереМинимальный аниматор главного героя в платформере

Аниматоры в Unity построены как раз на конечных автоматах. Каждая анимация группы объектов представлена в виде состояния. Условия и порядок переходов между ними определяется в аниматоре, который является конечным автоматом. Также, неоднократно поднималась тема использования конечных автоматов для описания логики работы объектов со сложным поведением. AI ботов, управление главным героем, вот это все.

Я бы хотел сделать акцент на менее изъезженной теме. Конечный автомат позволяет эффективно управлять состоянием игры в целом. Более того, с его помощью можно однажды описать бизнес-логику абстрактной игры, и отталкиваться в дальнейшем от этой заготовки при разработке прототипов. Так, многие ГК игры, построенные по такому принципу могут отличаться друг от друга на полсотни строк и структуру сцены, реализуя всю логику на автомате. А в сложнейших и запутанных проектах новый функционал может быть внедрен максимально просто путем добавления в существующий автомат нового состояния и добавления переходов в него, исключая возможность возникновения багов и неожиданного поведения. Также, среди плюсов построения бизнес-логики приложения на автоматах можно выделить следующее:

  • Каждое состояние описывает свою логику максимально просто. Нет никаких условных блоков, каждое состояние представляет собой последовательность действий на входе и выходе и список сигналов, которые автомат обрабатывает в текущем состоянии. Таким образом, мы отображаем джойстик на входе в состояние Play и скрываем на выходе. Но даже если каким-то чудом игрок увидел джойстик на экране паузы, он не сможет двигать персонажа. И не потому, что активен еще один флаг isPaused, а просто потому, что в состоянии паузы не предусмотрено принимать сигналы с джойстика.

  • В каждый момент времени однозначно видно, в каком состоянии мы сейчас находимся. Логируя переходы между состояниями и сигналы, посылаемые в автомат мы видим четкую последовательность событий, и состояние, в котором мы находимся.

  • В дополнение к предыдущему пункту, следует отметить, что это также очень удобно в контексте дебага. Посмотрев логи, мы сразу видим, что бот сломался потому, что был инициализирован не после старта уровня, а во время генерации сцены, когда еще не был готов контекст для работы его AI.

  • Многие баги становятся просто невозможны, потому что мы строго определяем условия переходов. Мы точно не попадем в состояние Play, пока состояние WaitMatch не получит сигнал "match_ready", а если мы захотим вернуться в лобби, мы сначала отправим серверу команду об этом, и только после сигнала "room_left" выполним переход.

  • Сама идея логики на автоматах максимально прозрачна. Видя ТЗ мы сразу понимаем, каким будет список состояний, логика, реализованная в каждом из них и граф переходов. Причем, как было отмечено выше, "внешняя" часть графа в большинстве игр будет оставаться неизменной.

Но давайте по порядку. А начну я пожалуй с одного важного принципа, встречающегося в программировании на всех уровнях. Разделение реализации и интерфейса. Логики и визуализации. Разница между серверной архитектурой и клиентской. На каждом уровне мы изолируем сложность внутренней системы, обеспечивая корректную ее работу и предоставляя удобный интерфейс для работы с ней.

Теория автоматов - довольно крупный раздел дискретной математики. Автоматов бывает много, хороших и разных. Так что попытаемся сформулировать наши требования к абстрактному автомату, который будет лежать в основе архитектуры приложения и будет наилучшим образом реализовывать алгоритм его работы.

Определим, что из себя представляет наш конечный автомат. Для этого опишем его структуру:

FSM

AState

- public FSM(AState initState)

- public void Signal(string name, object data = null)

- private void ChangeState(AState newState)

- void Enter()

- void Exit()

- AState Signal()

Итак, мы имеем 2 сущности:

Конечный автомат. Интерфейс у него невелик. Начиная работу в некотором состоянии, он может принимать сигналы извне, передавая их в текущее состояние. И если оно в ответ на сигнал вернуло новое состояние, автомат выполняет переход в него. В момент перехода мы выполняем Exit для последнего состояния и Enter для следующего. Таким образом, у нас получается следующая конструкция:

 public class FSM {   private AState currentState;   public FSM(AState initState) => ChangeState(initState);      private void ChangeState(AState newState)   {     if (newState == null) return;     currentState?.Exit();     currentState = newState;     currentState.Enter();   }   public void Signal(string name, object arg = null)   {     var result = currentState.Signal(name, arg);     ChangeState(result);   } }

Абстрактное состояние. Из соображений удобства и микрооптимизации его методы имеют нехитрую реализацию, которая наследуется потомками, в которых оные не определены.

public class AState{  public virtual void Enter() => null;  public virtual void Exit() => null;  public virtual AState Signal(string name, object arg) => null;}

А в самих состояниях мы просто описываем логику

public class SLoad : AState{    public override void Enter()    {        Game.Data.Set("loader_visible",true);        var load = SceneManager.LoadSceneAsync("SceneGameplay");        load.completed+=a=>Game.Fsm.Signal("scene_loaded");    }    public override void Exit()    {        Game.Data.Set("loader_visible",false);    }        public override AState Signal(string name, object arg)    {        if (name == "scene_loaded")            return new SLobby();        return null;    }    }

Как вы могли заметить, я описал логику переходов между состояниями, но даже вскользь не затронул тему хранения и изменения их списка. Дело в том, что в этом нет смысла, мы можем просто создавать новые состояния при переходе. Экземпляр состояния в большинстве случаев представляет собой 3 ссылки на функции, хранящиеся статически. И переходы между состояниями - не такое частое действие, чтобы дать сколько-то существенный оверхед. Зато мы получаем гору синтаксического сахара и статический анализ в подарок! При желании, проблема аллокаций решается очень легко, но это будет не так удобно. Кроме того, вот пример того, как такое состояние можно легко параметризовать, не усложняя при этом общий интерфейс

public class SMessage : AState{    private string msgText;    private AState next;    public SMessage(string messageText, AState nextState)    {        msgText = messageText;        btnText = buttonText;        next = nextState;    }        public override void Enter()    {        Game.Data.Set("message_text", msgText);        Game.Data.Set("window_message_visible",true);    }    public override void Exit()    {        Game.Data.Set("window_message_visible",false);    }        public override AState Signal(string name, object arg)    {        if (name == "message_btn_ok")             return next;        return null;    }}

Таким образом вместо того, чтобы делать усложненную систему переходов c аргументами, мы передаем их в конструктор и создаем экземпляр состояния с желаемыми параметрами.

...case "iap_ok":return new SMessage("Item purchased! Going back to store.", new SStore());...

В этой статье я не буду задерживаться на устройстве структуры Game.Data, используемой в состояниях, достаточно просто отметить, что это словарь, в элементах которого реализован шаблон "Наблюдатель". Объекты сцены, например, элементы UI, могут подписываться на изменения в нем и отображать гарантированно актуальные данные. Она играет ключевую роль в связи между визуализацией и логикой в моих примерах, но не является необходимым условием для работы с конечными автоматами. Сигналы в автомат же можно отправлять откуда угодно, но одним из самых универсальных примеров будет следующий скрипт.

public class ButtonFSM : MonoBehaviour, IPointerClickHandler{    public string key;        public override void OnPointerClick(PointerEventData eventData)    {        Game.Fsm.Signal(key);    }}

Иными словами, мы при клике по кнопке(на самом деле, любому CanvasRenderer) передаем соответствующий сигнал в автомат. При переходе между состояниями мы можем любым удобным нам способом включать и выключать разные Canvas, менять маски, используемые в Physics.Raycast и даже иногда менять Time.timeScale! Как бы ужасно и бескультурно это ни казалось на первый взгляд, пока сделанное в Enter отменяется в Exit, оно гарантированно не может доставить каких-либо неудобств, так что вперед! Главное - не переусердствуйте.

Подробнее..

Немного о графиках, сплайнах и генерации ландшафта

26.04.2021 18:04:48 | Автор: admin

Всем привет! Недавно я решил написать свой алгоритм генерации ландшафта для своих игр на игровом движке Unity 3D. На самом деле мой алгоритм вполне подойдет и для любых других движков и не только движков, так как использует только чистый C#. Делать это с помощью шума мне показалось неинтересным, и я решил реализовать все с помощью интерполяции. Конечно все скажут зачем изобретать велосипед, но это еще и хорошая практика, а в жизни пригодится все. Если вам не понравится моя реализация через интерполяцию, я в конце напишу алгоритм для генерации с помощью шума Перлина(Perlin Noise). Итак, приступим.

1. Кривые Безье.

Первый способ реализации я решил сделать через формулу кривых Безье. Формула для n-го количества точек в пространстве:

, где B базисные функции кривой Безье, по-другому полиномы Бернштейна. Их формула -

.[1]

Реализовать это в коде совсем просто, так что приступим.

1) Создадим структуру Point, которая будет иметь два параметра координаты x и y и переопределим для него некоторые операторы(+,-,*,/).

[Serializable]public struct Point { public float x, y; public Point(float x, float y) { this.x = x; this.y = y; } public static Point operator +(Point a, Point b) => new Point(a.x + b.x, a.y + b.x); public static Point operator -(Point a, float d) => new Point(a.x - d, a.y - d); public static Point operator -(Point a, Point b) => new Point(a.x - b.x, a.y - b.y); public static Point operator *(float d, Point a) => new Point(a.x * d, a.y * d); public static Point operator *(Point a, float d) => new Point(a.x * d, a.y * d); public static Point operator *(Point a, Point b) => new Point(a.x * b.x, a.x * b.y); public static Point operator /(Point a, float d) => new Point(a.x / d, a.y / d); public static Point operator /(Point a, Point b) => new Point(a.x / b.x, a.y / b.y); public static bool operator ==(Point lhs, Point rhs) => lhs.x == rhs.x && lhs.y == rhs.y; public static bool operator !=(Point lhs, Point rhs) => lhs.x != rhs.x || lhs.y != rhs.y; }

2) Давайте теперь напишем сам метод для получения точки по параметру t. Еще нам нужно будет создать функцию для вычисления факториала.

int factorial(int n) { int f = 1; for (int i = 1; i < n; i++) { f *= i; } return f; } Point curveBezier(Point[] points, float t) { Point curve = new Point(0, 0); for (int i = 0; i < points.Length; i++) curve += points[i] * factorial(points.Length - 1) / (factorial(i) * factorial(points.Length - 1 - i)) * (float)Math.Pow(t, i) * (float)Math.Pow(1 - t, points.Length - 1 - i); return curve; }

Теперь возникает проблемка, если наши точки будут расположены не через одинаковый шаг, то значения будут получаться некорректно. Теперь нам нужно реализовать метод, который будет находить значение t для нашей функции, которое соответствует нужному x. Для этого я решил воспользоваться методом Ньютона, с помощью которого можно найти корни функции.[2] Для этого нам нужно найти производную для нашей функции Безье. Делается это очень просто, так как каждый член преобразуется из c_n * x ^ n в c_n * n * x ^ (n-1). Член с нулевой степенью пропадает.

3) Теперь реализуем получение производной.

Point derivative(Point[] points, float t) { Point curve = new Point(0, 0); for (int i = 0; i < points.Length; i++) { Point c = points[i] * factorial(points.Length - 1) / (factorial(i) * factorial(points.Length - 1 - i)); if (i > 1) { curve += c * i * (float)Math.Pow(t, i - 1) * (float)Math.Pow(1 - t, points.Length - 1 - i); } if (points.Length - 1 - i > 1) { curve -= c * (float)Math.Pow(t, i) * (points.Length - 1 - i) * (float)Math.Pow(1 - t, points.Length - 2 - i); } } return curve; }

4) А так же получение параметра t по методу Ньютона.

float timeBezier(Point[] points, float x, float e = 0.0001f) { float t = 0.5f; float h = (curveBezier(points, t).x - x) / (derivative(points, t).x - 1); while (Math.Abs(h) >= e) { h = (curveBezier(points, t).x - x) / (derivative(points, t).x - 1); t -= h; } return t; }

В итоге у нас готовы все функции для построения кривой Безье с равномерным шагом. Для генерации лучше использовать шаг по оси x, а y мы будем получать. Давайте напишем код который будет генерировать ландшафт, пока что только по одной из осей. Этот код полностью подойдет только для движка Unity, в остальных нужно немного его изменить.

public Point[] points;public GameObject prefab;public int length;private void Start() { for(int i = 0; i < length; i++) { GameObject block = Instantiate(prefab) as GameObject; float t = timeBezier(points, points[0] + (points[points.Length-1].x  points[0].x) * i / length); block.name = i.ToString(); block.transform.parent = transform; block.transform.position = transform.position + new Vector3(curveBezier(points, t).x, curveBezier(points, t).y, 0); } }

Осталось сделать такую же генерацию по оси z и потом вычислять не для x, а для обоих осей.

public Point[] px, pz;public GameObject prefab;public int length;private void Start(){ for(int i = 0; i < length; i++) { for(int j = 0; j < length; j++) { GameObject block = Instantiate(prefab) as GameObject;  float tx = timeBezier(points, px[0] + (px[px.Length-1].x  px[0].x) * i / length);  float tz = timeBezier(points, pz[0] + (pz[pz.Length-1].x  pz[0].x) * i / length); block.name = i.ToString() +   + j.ToString(); block.transform.parent = transform; block.transform.position = transform.position + new Vector3(curveBezier(px, tx).x, (curveBezier(px, tx).y + curveBezier(pz, tz).y), curveBezier(pz, tz).x);  } }}

Итак, теперь у нас готов простой генератор, который создает ровный ландшафт по заданным точкам. Еще можно написать функцию для генерирования рандомных точек, а генерировать их в зависимости от сида, для этого легко использовать класс Random() из пространства имен System. Главное при создании этого класса не забыть в скобках написать сид, иначе будет рандомные значения, а не определенные. Самое лучшее пользоваться методом NextDouble(), просто преобразуя его в float, тогда у вас все значения будут в диапазоне от 0 до 1 включительно.

2. Сплайн Лагранжа
Давайте теперь попробуем реализовать генерацию ландшафта с помощью сплайна Лагранжа[3]. Его формулу еще легче реализовать, так как там в качестве параметра выступает x, а не t.

1) Напишем функцию которая будет получать позицию по x и y по одному x.

Point curveLagrange(Point points, float x) {  Vector2 curve = new Vector2(x, 0); for(int i = 0; i < points.Length; i++) { float dx = points[i].y; for (int k = 0; k < points.Length; k++) if (k != i) dx *= (x - points[k].x) / (points[i].x - points[k].x); curve.y += dx; } return curve;  }

2) Осталось в методе Start() заменить код на немного другой.

for(int i = 0; i < length; i++) { GameObject block = Instantiate(prefab) as GameObject; block.name = i.ToString(); block.transform.parent = transform; block.transform.position = transform.position + new Vector3(curveLagrange(points, points[0].x + (points[points.Length - 1].x - points[0].x) * (float)i / (float)(length - 1)).x, curveLagrange(points, points[0].x + (points[points.Length - 1].x - points[0].x) * (float)i / (float)(length - 1)).y); }

Для генерации по двум осям нужно проделать все то же самое, что и с кривыми Безье.

Теперь я покажу как генерировать ландшафт с помощью шума Перлина(только для Unity).

for(int i = 0; i < size_x; i++){ for(int j = 0; j < size_z; j++) { GameObject block = Instantiate(prefab) as GameObject; block.transform.parent = transform; block.transform.position = transform.position + new Vector3(i, Mathf.PerlinNoise(i, j), j); }}

Как итог, у нас есть два разных простеньких генератора ландшафта, которые вполне рабочие. Для более хорошего ландшафта можно брать среднее значение среди этих двух функций или рассчитывать несколько кривых по разным сидам, а потом объединять. Ну а на этом я заканчиваю, дальше все зависит от вас самих.

Ссылки:

[1] https://en.wikipedia.org/wiki/B%C3%A9zier_curve

[2] https://en.wikipedia.org/wiki/Newton%27s_method

[3] https://en.wikipedia.org/wiki/Lagrange_polynomial

Подробнее..

Ускоряем ваш Unity проект. ECS для MonoBehavior разработчиков

08.05.2021 16:18:41 | Автор: admin

Магия вне Хогвартса

Привет, Хабр!

На обложке демо-игра Megacity. Она содержит 4,5 млн элементов Mesh Renderer, 5000 динамических транспортных средств, 200 000 уникальных строительных объектов и 100 000 уникальных аудиоисточников. Но самое удивительное, что вся эта мощь запустилась на Iphone X при 60 кадрах в секунду . Как все это возможно?

Пару лет назад компания Unity представила свой стекDOTS, на котором и построен проект Megacity. Это некий список технологий, которые в совокупности позволяют колдовать и ускорять ваш проект в десятки раз. В корне всей магии лежат 2 простых заклинания:

  • Если правильно управлять данными, процессору будет легче их обрабатывать, а если их легче будет обрабатывать, то игрокам будет легче жить.

  • Количество ядер процессора растет, но код среднестатистического программиста не использует все ядра процессора. А значит игрокам все же живется туго. На их устройствах можно жарить яичницу, а фреймрейт ведет себя как Джокер - непредсказуемо.

Для того, чтобы юным Unity волшебникам удалось осуществить вышеописанные постулаты, компания выпустила дополнительные пакеты:

Дары смерти от Unity Technologies
  • Job System - Пишите многопоточный код, даже если вы не знаете что такое "Race condition" или "Context switch".

  • Burst Compiler - Ускорьте код в 10 раз, просто добавив атрибут и отказавшись от ссылочных типов.

  • Unity Mathematics - Специальная математика, адаптированная под компилятор Burst.

  • Native Container - Обертка над неуправляемой памятью. Это такие же List, Array, Queue, но живут в мире без мусора. Мусор - это ваш код. Мусор - это то, что могут оставлять после себя мертвые ссылочные типы. Сборкой мусора в проекте занимается некий дворник - Garbage Collector, но он очень медленный и злой дядька . Да и Greenpeace советует убирать после себя трупы, не правда ли?

А сердцем DOTS является:

  • Entities - архитектурный паттерн Entity Component System (ECS), который ставит во главу угла данные, а не объекты, и тем самым переворачивает привычное представление о программировании среди ценителей ООП . Подобный подход развивает идею композиции над наследованием и позволяет легко адаптироваться под динамичные потребности гейм-дизайнера.

    ECS стоит рассматривать в первую очередь как архитектурное решение. Скорость - это бонус.

На текущий момент DOTS выглядит скорее проклятьем Unity Technologies, чем волшебной палочкой. Стек до сих пор не вышел в официальный релиз. И неизвестно когда Хогвартс распахнет свои двери для новых волшебников. Сейчас это крайне нестабильная штука, которую точно не стоит использовать в Production проекте.

Поэтому, давайте начнем колдовать вне Хогвартса.

Производительность! Бесплатно и без регистрации

Entity Component System - это сердце нового подхода от Unity. Но одновременно и самая нестабильная часть всего стека DOTS . Давайте попробуем применить эту технологию самим, без пакета от Unity Technologies.

Из чего состоит ECS?

Entity - это все, что вас окружает. Ваша кошка, сын маминой подруги, пицца - это все entity.

Component - это то, что делает ваш Entity особенным. У кошки есть хвост, у сына маминой подруги - мозги, у пиццы - кетчуп. Это все - компоненты.

System - логика, которая управляет всеми entity, у которых есть тот или иной набор компонентов. Хвост помогает всем кошкам балансировать в пространстве, мозги помогают всем сыновьям маминой подруги быть круче вас, а кетчуп позволяет любой пицце быть вкусной.

Если перенести аналогию на игровые объекты, то ваш персонаж в игре - это Entity. Компонент физики - Rigidbody, а система - это то, что будет управлять всей физикой на сцене, включая вашего персонажа в игре.

Согласно официальной статье , снижение количества Update функций и переход на чистый c# автоматически добавит скорости в вашу игру. А Job System, Burst Compiler можно применять к любой архитектуре. И если у команды Unity пакет Entities пока что официально не выпущен, давайте обратимся к кулинарным профессионалам из сообщества и посмотрим, как же они предлагают нам приготовить этот самым ECS.

Рецепт ECS под соусом Leo

Поиск Гугл по рецептам приводит нас к самому вкусному результату:

Leo ECS - очень легкий и быстрый ECS фреймворк, который не требует какого-то специфичного игрового движка. Он прост, быстр и не форсирует вашу интеграцию с Unity Engine.

Другие ECS рецепты, которые могут вас заинтересовать

Entitas - аппетитный фреймворк с самым большим комьюнити, но вся его проблема в том, что он работает на классах. Добавлять этот ингредиент сейчас не модно, повара предпочитают использовать структуры на компонентах. Классы почти в 2 раза медленней структур в синтетических тестах, т.к хранят в памяти ссылку. А данные тесты демонстрируют отставание по скорости готовки до 10 раз.

ECS.ME - потрясающий ECS фреймворк для Unity, приготовленный для сетевых игр с элементом отката (RollBack). Эдакий некоммерческий аналог Quantum от Exit Games. Рецепт очень-очень крутой и вкусный. Но чтобы rollback не дал трещину, нужно четко следовать правилам данного фреймворка.

EgoECS - ECS фреймворк с сильнейшей интеграцией в Unity. С его помощью можно готовить MonoBehavior в качестве компонентов. Если вам не важна скорость работы, EgoECS выглядит весьма хорошим рецептом. Каждая система - это всего 1 фильтр, кому-то такой подход может скручивать руки при написании кода.

Svelto ECS - очень амбициозный рецепт, с очень сложным API. Но я должен был его упомянуть. Скорее всего он не подходит новичкам, но если вы заинтересовались, то на habr существует перевод Wiki данного фреймворка.

Actors - изначально разрабатывался как набор утилит. В рецепте присутствуют реактивные переменные, которые могут нарушить порядок вызовов функций. Реактивщина в ECS может сбить вас с толку при поиске багов / ошибок в вашей игре.

Morpeh - очень хороший Фреймворк, который предлагает вам не просто приготовить ECS архитектуру, но включить очень много сторонних блюд к вашему столу. Внутренности содержат много кода, которые никак не относятся к самому паттерну, а призваны ускорить в целом разработку проекта. Кому-то это может нравиться, я же считаю, что рецепт должен содержать только ингредиенты для целевого блюда.

Вот мы и определились. Чистый C# и соус Leo. Что может быть круче?

Превращаем GameObject в Entity

Если Leo ECS по умолчанию конвертирует игровые объекты из Unity Engine, то как интегрировать его с этим движком? Можно ли как-нибудь очень-очень легко превращать наши игровые объекты в Entity, чтобы процесс готовки был простым и понятным.

В этом нам поможет моя собственная библиотека UniLeo . Я написал ее для того, чтобы сохранить привычный flow работы с игровым движком Unity.

UniLeo автоматически конвертирует ваши игровые объекты в Entity и позволяет настраивать компоненты прямо из инспектора.

Перейдем наконец-то к коду

Подключаем пакеты через Unity Package Manager

Добавьте ссылку в Packages/manifest.json

"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git","com.voody.UniLeo": "https://github.com/voody2506/UniLeo.git",

Или Unity Editor -> Window -> Package Manager

Компонент в Leo ECS - это обычная структура, но мы, благодаря UniLeo можем управлять ее содержимым в рамках редактора Unity.

Не забываем пространство имен
using Leopotam.Ecs;using Voody.UniLeo;
Создаем первый компонент
[Serializable] // <- Данный атрибут необходим, чтобы управлять компонентом из редактораpublic struct PlayerComponent {     public float health; }
Пример компонента с элементами Unity
[Serializable]public struct UnityExampleComponent {     public Rigidbody rigidBody;  public Transform transform public GameObject unityObject }

То есть, если в проекте нужно управлять физикой, необходимо создать компонент с переменной RigidBody. Таким образом можно управлять абсолютно любым элементом игрового движка Unity.

Теперь нам необходимо как-нибудь прикрепить наш компонент к игровому объекту, но как это сделать? Компоненты - это структуры. А движок Unity позволяет крепить к игровым объектам только классы, которые наследуется от MonoBehavior .

Но в UniLeo можно создать класс-проводник, который должен наследоваться MonoProvider и его можно крепить к игровым объектам.

Создаем класс-проводник
public sealed class PlayerComponentProvider : MonoProvider<PlayerComponent> { }

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

Отлично, теперь мы можем без лишних проблем настроить наш компонент прямо из редактора Unity.

Настало время написать первую систему. Для этого необходимо просто создать класс и реализовать один из готовых интерфейсов, чаще всего используются только:

IEcsInitSystem // Срабатывает 1 раз при инициализации

IEcsRunSystem // Срабтаывает на Update или Fixed Update метод

Пишем первую систему
class PlayerHealthSystem : IEcsInitSystem, IEcsRunSystem {    // Переменная _world автоматически инициализируется    EcsWorld _world = null;    // В фильтре просто описываем с каким компонентом     будет работать система     EcsFilter<PlayerComponent> _filter = null;    public void Init () {    // Сработает на старте    }    public void Run () {        foreach (var i in _filter) {            // entity которые содержат PlayerComponent.            ref var entity = ref _filter.GetEntity (i);             // Get1 вернет ссылку на "PlayerComponent".            ref var player = ref _filter.Get1 (i);            player.helth = player.helth - 10;         }    }}

Когда наша первая система создана, мы должны как-то запустить ее: для этого создадим стартап код ECS, это простой класс, который наследуется от MonoBehavior .

Запускаем ECS
class Startup : MonoBehaviour {    EcsWorld _world;    EcsSystems _systems;    void Start () {        // create ecs environment.        _world = new EcsWorld ();        _systems = new EcsSystems (_world)            .ConvertScene() // Этот метод сконвертирует GO в Entity            .Add (new PlayerComponent ());        _systems.Init ();    }        void Update () {        // process all dependent systems.        _systems.Run ();    }    void OnDestroy () {        // destroy systems logical group.        _systems.Destroy ();        // destroy world.        _world.Destroy ();    }}

Обратите внимания, что к вашему игровому объекты автоматически добавится компонент Convert To Entity , который позволяет выбрать предпочтительный метод конвертации ваших игровых объектов.

Выбираем метод конвертации

Convert And Inject - Создаст entity на основе компонентов, повешанных на игровой объект.

Convert And Destroy - После превращения GO в Entity , объект автоматически удалится. Может быть полезно, когда необходимо просто добавить определенные компоненты в ECS мир..

После запуска игровой объект автоматически сконвертируется в Entity. Система начнет отрабатывать свои методы. Мы успешно интегрировали Leo ECS в наш проект. Поздравляю!

Наверное у вас еще остались вопросы, я попробую ответить на них.

Вопрос-Ответ

Как работать с Prefab?

Вы можете порождать prefab любым известным вам способом, конвертация пройдет автоматически.

GameObject.Instantiate(gameObject, position, rotation, _world);PhotonNetwork.Instantiate <- рабоатет и в сторонних библиотеках
Как работать с ивентами в ECS?

В ECS всё - данные . Ивенты тоже, необходимо просто создать компонент и запустить его в нужный момент. Ну и создать систему, которая бы реагировала на этот ивент.

// MonoBehavior void OnCollisionEnter(Collision collision){// Создаем новый entity и добавляем компонент    // Переменная _world хранится в Startup классе    EcsEntity entity = _world.NewEntity ();     var event = new EventComponent ();entity.Replace (event);    // Теперь просто можно перехватить этот ивент в любой системе}

В ECS очень важно не нарушать порядок, при котором выполняются системы, асинхронное срабатывание может привести к большим проблемам в будущем.

Очень часто ивент-компоненты нужно очищать в конце кадра, для этого в LeoECS есть готовый механизм.

void Start () {    var world = new EcsWorld ();    _update = new EcsSystems (world);    _update        .Add (new CalculateSystem ())        .Add (new UpdateSystem ())        .OneFrame<EventComponent> () // Этот компонент удалится          самостоятельно в этот момент времени        .Init ();}
А что насчет многопоточности?

У Leo ECS есть готовая интеграция , которая позволяет запускать системы в разных потоках. Она не использует Job System и Burst Compiler , а использует стандартную библиотеку System.Threading.

Но вы вполне можете использовать и пакеты от компании Unity, они дадут намного больше производительности, но интеграция с ними не так легка.

На хабре уже существует отличный пост про Job систему. Вам необходимо вызвать ее внутри ваших Run методов в LeoECS.

Я пишу сетевую игру, как мне работать с ECS

Готовой интеграции для LeoECS с популярными сетевыми библиотеками на данный момент не существует, но передача данных осуществляется через ивенты, а как работать с ивентами я уже отвечал.

В любом случается, на данный момент вам придется самостоятельно придумать как лучше всего интегрировать то или иное решение с ECS.

P.S

Да, возможно сторонние ECS решения не такие быстрые, как подход DOTS, но они позволяют существенно увеличить производительность относительно классического MonoBehavior подхода. Теперь вы знаете с чего начать свой Megacity проект!

Спасибо, что дочитали до конца!

Подробнее..

Как обновить все сцены Unity-проекта в один клик

30.05.2021 12:21:38 | Автор: admin
Танюшка - автор канала IT DIVA и данной статьи, кофеголик и любитель автоматизировать рутинуТанюшка - автор канала IT DIVA и данной статьи, кофеголик и любитель автоматизировать рутину

Unity - это один из популярнейших игровых движков, который используется, как инди-разработчиками для создания игр и мобильных приложений, так и компаниями, чьи продукты используют технологии виртуальной и дополненной реальности.

На этом движке, не смотря на его недостатки, создано большое количество крупных проектов с кучей сцен, которые приходится модифицировать со временем по мере развития проекта. И чем больше этих сцен, тем дольше может быть процесс их обновления.

В этой статье мне хотелось бы поделиться своими наработками, которые помогают мне быстро добавлять новые компоненты на сцены, а также менять существующие.

Для этого мы с вами сегодня напишем простой инструмент, который позволит автоматизировать обновление сцен в ваших Unity-проектах и поможет вам сэкономить в совокупности несколько минут или даже часов ручной работы.

А далее вы уже сможете использовать и модифицировать приведённый в примере код под свои нужды.

Зачем нужен такой инструмент

Представьте, что у вас есть 3 уровня в вашем проекте. Вы добавили новые скрипты, изменили интерфейс, обновили используемые 3D-модели. Но только в рамках одной сцены.

В итоге, вы вручную переносите проделанные изменения на две другие сцены, чтобы сохранить порядок в проекте. Когда нужно обновить только 3 сцены - это довольно несложно.

Но что будет, когда их станет 10? 20? 50?

Уверена, вы сразу вспомните все ругательства, которые знаете. Да ещё и можете забыть обновить какой-то из компонентов. В итоге придётся снова проверять все сцены на наличие нужных вам обновлений. А это ваше время.

Как эту проблему решить?

На самом деле, довольно просто!

Можно поступить несколькими способами. Например, использовать метод OnValidate() в классах, которые уже присутствуют на сцене. Но для этого нужно запустить каждую сцену и сохранить её вручную. Более того, не весь функционал изменения сцен нам будет доступен через OnValidate(), поскольку данный метод есть только у объектов, наследованных от MonoBehaviour.

Нам такой вариант не подходит. Но знать о нём тоже полезно.

Поэтому мы пойдём другим путём и напишем скрипт, который автоматически обновит каждую сцену и выполнит любые необходимые изменения. Написав такой скрипт один раз, вы точно сэкономите себе уйму времени в будущем.

Чтобы это сделать, мы создадим новый класс в папке Editor:

Пример возможной иерархии для расширений движкаПример возможной иерархии для расширений движка

Обращаю внимание, на то, что название некоторых папок в Unity играет роль. В данном случае, в папке "Editor" мы будем хранить все скрипты, которые помогают нам расширить базовый функционал редактора Unity и, например, создавать диалоговые окна.

Далее добавим необходимые пространства имён, а также укажем, что наследоваться будем от Editor Window (а не от MonoBehaviour, как происходит по умолчанию):

using UnityEngine;using UnityEditor;public class SceneUpdater : EditorWindow{    [MenuItem("Custom Tools/Scene Updater")]    public static void ShowWindow()    {        GetWindow(typeof(SceneUpdater));    }        private void OnGUI()    {        if (GUILayout.Button("Update scenes"))    Debug.Log("Updating")    }}

С помощью атрибута [MenuItem("Custom Tools/Scene Updater")] мы создадим элемент меню с заданной иерархией в самом движке. Таким образом мы будем вызывать диалоговое окно будущего инструмента:

Новый элемент меню, через который мы будем использовать наш инструментНовый элемент меню, через который мы будем использовать наш инструмент

Далее мы добавим кнопку, с помощью которой будем запускать наш дальнейший код:

using UnityEngine;using UnityEditor;public class SceneUpdater : EditorWindow{    [MenuItem("Custom Tools/Scene Updater")]    public static void ShowWindow()    {        GetWindow(typeof(SceneUpdater));    }        private void OnGUI()    {        if (GUILayout.Button("Update scenes"))    Debug.Log("Updating")    }}

А теперь напишем несколько полезных функций, которые лично я довольно часто использую.

Быстрое добавление компонентов к объектам

Для добавления компонентов к объектам с уникальными именами можно написать вот такую функцию:

/// <summary>/// Добавление компонента к объекту с уникальным названием/// </summary>/// <param name="objectName"> название объекта </param>/// <typeparam name="T"> тип компонента </typeparam>private void AddComponentToObject<T>(string objectName) where T : Component{    GameObject.Find(objectName)?.gameObject.AddComponent<T>();}

Использовать её можно вот так:

AddComponentToObject<BoxCollider>("Plane");AddComponentToObject<SampleClass>("EventSystem");

Это немного быстрее, чем каждый раз прописывать всё самостоятельно.

Быстрое удаление объектов по имени

Аналогично можно сделать и для удаления объектов:

/// <summary>/// Уничтожение объекта с уникальным названием/// </summary>/// <param name="objectName"> название объекта </param>private void DestroyObjectWithName(string objectName){    DestroyImmediate(GameObject.Find(objectName)?.gameObject);}

И использовать так:

DestroyObjectWithName("Sphere");

Перенос позиции, поворота и размера между объектами

Для компонентов Transform и RectTransform можно создать функции, с помощью которых будет происходить копирование локальной позиции, поворота и размера объекта (например, если нужно заменить старый объект новым или изменить настройки интерфейса):

/// <summary>/// Копирование позиции, поворота и размера с компонента Transform у одного объекта/// на такой же компонент другого объекта./// Для корректного переноса координат у parent root объеков должны быть нулевые координаты/// </summary>/// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>/// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>/// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>/// <param name="copeRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>/// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>private static void CopyTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,     bool copyPosition = true, bool copeRotation = true, bool copyScale = true){    var newTransform = objectToCopyFrom.GetComponent<Transform>();    var currentTransform = objectToPasteTo.GetComponent<Transform>();            if (copyPosition) currentTransform.localPosition = newTransform.localPosition;    if (copeRotation) currentTransform.localRotation = newTransform.localRotation;    if (copyScale) currentTransform.localScale = newTransform.localScale;}    /// <summary>/// Копирование позиции, поворота и размера с компонента RectTransform у UI-панели одного объекта/// на такой же компонент другого объекта. Не копируется размер самой панели (для этого использовать sizeDelta)/// Для корректного переноса координат у parent root объеков должны быть нулевые координаты/// </summary>/// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>/// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>/// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>/// <param name="copeRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>/// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>private static void CopyRectTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,    bool copyPosition = true, bool copeRotation = true, bool copyScale = true){    var newTransform = objectToCopyFrom.GetComponent<RectTransform>();    var currentTransform = objectToPasteTo.GetComponent<RectTransform>();            if (copyPosition) currentTransform.localPosition = newTransform.localPosition;    if (copeRotation) currentTransform.localRotation = newTransform.localRotation;    if (copyScale) currentTransform.localScale = newTransform.localScale;}

Причём, благодаря тому, что есть переменные-условия, мы сможем контролировать, какие параметры мы хотим скопировать:

var plane = GameObject.Find("Plane");var cube = GameObject.Find("Cube");CopyTransformPositionRotationScale(plane, cube, copyScale:false);

Изменение UI-компонентов

Для работы с интерфейсом могут быть полезны функции, позволяющие быстро настроить Canvas, TextMeshPro и RectTransform:

/// <summary>/// Изменение отображения Canvas/// </summary>/// <param name="canvasGameObject"> объект, в компонентам которого будет производиться обращение </param>/// <param name="renderMode"> способ отображения </param>/// <param name="scaleMode"> способ изменения масштаба </param>private void ChangeCanvasSettings(GameObject canvasGameObject, RenderMode renderMode, CanvasScaler.ScaleMode scaleMode){    canvasGameObject.GetComponentInChildren<Canvas>().renderMode = renderMode;    var canvasScaler = canvasGameObject.GetComponentInChildren<CanvasScaler>();    canvasScaler.uiScaleMode = scaleMode;    // выставление стандартного разрешения    if (scaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize)    {        canvasScaler.referenceResolution = new Vector2(720f, 1280f);        canvasScaler.matchWidthOrHeight = 1f;    }} /// <summary>/// Изменение настроек для TextMeshPro/// </summary>/// <param name="textMeshPro"> тестовый элемент </param>/// <param name="fontSizeMin"> минимальный размер шрифта </param>/// <param name="fontSizeMax"> максимальный размер шрифта </param>/// <param name="textAlignmentOption"> выравнивание текста </param>private void ChangeTMPSettings(TextMeshProUGUI textMeshPro, int fontSizeMin, int fontSizeMax, TextAlignmentOptions textAlignmentOption = TextAlignmentOptions.Center){    // замена стандартного шрифта    textMeshPro.font = (TMP_FontAsset) AssetDatabase.LoadAssetAtPath("Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset", typeof(TMP_FontAsset));    textMeshPro.enableAutoSizing = true;    textMeshPro.fontSizeMin = fontSizeMin;    textMeshPro.fontSizeMax = fontSizeMax;    textMeshPro.alignment = textAlignmentOption;}/// <summary>/// Изменение параметров RectTransform/// </summary>/// <param name="rectTransform"> изменяемый элемент </param>/// <param name="alignment"> выравнивание </param>/// <param name="position"> позиция в 3D-пространстве </param>/// <param name="size"> размер </param>private void ChangeRectTransformSettings(RectTransform rectTransform, AnchorPresets alignment, Vector3 position, Vector2 size){    rectTransform.anchoredPosition3D = position;    rectTransform.sizeDelta = size;    rectTransform.SetAnchor(alignment);}

Замечу, что для RectTransform я использую расширение самого класса, найденное когда-то давно на форумах по Unity. С его помощью очень удобно настраивать Anchor и Pivot. Такие расширения рекомендуется складывать в папку Utils:

Пример возможной иерархии для расширений стандартных классов Пример возможной иерархии для расширений стандартных классов

Код данного расширения оставляю для вас в спойлере:

RectTransformExtension.cs
using UnityEngine;public enum AnchorPresets{    TopLeft,    TopCenter,    TopRight,    MiddleLeft,    MiddleCenter,    MiddleRight,    BottomLeft,    BottomCenter,    BottomRight,    VertStretchLeft,    VertStretchRight,    VertStretchCenter,    HorStretchTop,    HorStretchMiddle,    HorStretchBottom,    StretchAll}public enum PivotPresets{    TopLeft,    TopCenter,    TopRight,    MiddleLeft,    MiddleCenter,    MiddleRight,    BottomLeft,    BottomCenter,    BottomRight,}/// <summary>/// Расширение возможностей работы с RectTransform/// </summary>public static class RectTransformExtension{    /// <summary>    /// Изменение якоря    /// </summary>    /// <param name="source"> компонент, свойства которого требуется изменить </param>    /// <param name="align"> способ выравнивания </param>    /// <param name="offsetX"> смещение по оси X </param>    /// <param name="offsetY"> смещение по оси Y </param>    public static void SetAnchor(this RectTransform source, AnchorPresets align, int offsetX = 0, int offsetY = 0)    {        source.anchoredPosition = new Vector3(offsetX, offsetY, 0);        switch (align)        {            case (AnchorPresets.TopLeft):            {                source.anchorMin = new Vector2(0, 1);                source.anchorMax = new Vector2(0, 1);                break;            }            case (AnchorPresets.TopCenter):            {                source.anchorMin = new Vector2(0.5f, 1);                source.anchorMax = new Vector2(0.5f, 1);                break;            }            case (AnchorPresets.TopRight):            {                source.anchorMin = new Vector2(1, 1);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.MiddleLeft):            {                source.anchorMin = new Vector2(0, 0.5f);                source.anchorMax = new Vector2(0, 0.5f);                break;            }            case (AnchorPresets.MiddleCenter):            {                source.anchorMin = new Vector2(0.5f, 0.5f);                source.anchorMax = new Vector2(0.5f, 0.5f);                break;            }            case (AnchorPresets.MiddleRight):            {                source.anchorMin = new Vector2(1, 0.5f);                source.anchorMax = new Vector2(1, 0.5f);                break;            }            case (AnchorPresets.BottomLeft):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(0, 0);                break;            }            case (AnchorPresets.BottomCenter):            {                source.anchorMin = new Vector2(0.5f, 0);                source.anchorMax = new Vector2(0.5f, 0);                break;            }            case (AnchorPresets.BottomRight):            {                source.anchorMin = new Vector2(1, 0);                source.anchorMax = new Vector2(1, 0);                break;            }            case (AnchorPresets.HorStretchTop):            {                source.anchorMin = new Vector2(0, 1);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.HorStretchMiddle):            {                source.anchorMin = new Vector2(0, 0.5f);                source.anchorMax = new Vector2(1, 0.5f);                break;            }            case (AnchorPresets.HorStretchBottom):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(1, 0);                break;            }            case (AnchorPresets.VertStretchLeft):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(0, 1);                break;            }            case (AnchorPresets.VertStretchCenter):            {                source.anchorMin = new Vector2(0.5f, 0);                source.anchorMax = new Vector2(0.5f, 1);                break;            }            case (AnchorPresets.VertStretchRight):            {                source.anchorMin = new Vector2(1, 0);                source.anchorMax = new Vector2(1, 1);                break;            }            case (AnchorPresets.StretchAll):            {                source.anchorMin = new Vector2(0, 0);                source.anchorMax = new Vector2(1, 1);                break;            }        }    }    /// <summary>    /// Изменение pivot    /// </summary>    /// <param name="source"> компонент, свойства которого требуется изменить </param>    /// <param name="preset"> способ выравнивания </param>    public static void SetPivot(this RectTransform source, PivotPresets preset)    {        switch (preset)        {            case (PivotPresets.TopLeft):            {                source.pivot = new Vector2(0, 1);                break;            }            case (PivotPresets.TopCenter):            {                source.pivot = new Vector2(0.5f, 1);                break;            }            case (PivotPresets.TopRight):            {                source.pivot = new Vector2(1, 1);                break;            }            case (PivotPresets.MiddleLeft):            {                source.pivot = new Vector2(0, 0.5f);                break;            }            case (PivotPresets.MiddleCenter):            {                source.pivot = new Vector2(0.5f, 0.5f);                break;            }            case (PivotPresets.MiddleRight):            {                source.pivot = new Vector2(1, 0.5f);                break;            }            case (PivotPresets.BottomLeft):            {                source.pivot = new Vector2(0, 0);                break;            }            case (PivotPresets.BottomCenter):            {                source.pivot = new Vector2(0.5f, 0);                break;            }            case (PivotPresets.BottomRight):            {                source.pivot = new Vector2(1, 0);                break;            }        }    }}

Использовать данные функции можно так:

// изменение настроек отображения Canvasvar canvas = GameObject.Find("Canvas");ChangeCanvasSettings(canvas, RenderMode.ScreenSpaceOverlay, CanvasScaler.ScaleMode.ScaleWithScreenSize);// изменение настроек шрифтаvar tmp = canvas.GetComponentInChildren<TextMeshProUGUI>();ChangeTMPSettings(tmp, 36, 72, TextAlignmentOptions.BottomRight);// изменение RectTransformChangeRectTransformSettings(tmp.GetComponent<RectTransform>(), AnchorPresets.MiddleCenter, Vector3.zero, new Vector2(100f, 20f));

Аналогично, может пригодиться расширение для класса Transform для поиска дочернего элемента (при наличии сложной иерархии):

TransformExtension.cs
using UnityEngine;/// <summary>/// Расширение возможностей работы с Transform/// </summary>public static class TransformExtension{    /// <summary>    /// Рекурсивный поиск дочернего элемента с определённым именем    /// </summary>    /// <param name="parent"> родительский элемент </param>    /// <param name="childName"> название искомого дочернего элемента </param>    /// <returns> null - если элемент не найден,    ///           Transform элемента, если элемент найден    /// </returns>    public static Transform FindChildWithName(this Transform parent, string childName)    {        foreach (Transform child in parent)        {            if (child.name == childName)                return child;            var result = child.FindChildWithName(childName);            if (result)                return result;        }        return null;    }}

Для тех, кому хочется иметь возможность видеть событие OnClick() на кнопке в инспекторе - может быть полезна вот такая функция:

/// <summary>/// Добавление обработчика события на кнопку (чтобы было видно в инспекторе)/// </summary>/// <param name="uiButton"> кнопка </param>/// <param name="action"> требуемое действие </param>private static void AddPersistentListenerToButton(Button uiButton, UnityAction action){    try    {        // сработает, если уже есть пустое событие        if (uiButton.onClick.GetPersistentTarget(0) == null)            UnityEventTools.RegisterPersistentListener(uiButton.onClick, 0, action);    }    catch (ArgumentException)    {        UnityEventTools.AddPersistentListener(uiButton.onClick, action);    }}

То есть, если написать следующее:

// добавление события на кнопкуAddPersistentListenerToButton(canvas.GetComponentInChildren<Button>(), FindObjectOfType<SampleClass>().QuitApp);

То результат работы в движке будет таким:

Результат работы AddPersistentListenerРезультат работы AddPersistentListener

Добавление новых объектов и изменение иерархии на сцене

Для тех, кому важно поддерживать порядок на сцене, могут быть полезны функции изменения слоя объекта, а также создания префаба на сцене с возможностью присоединения его к родительскому элементу и установке в определённом месте иерархии:

/// <summary>/// Изменение слоя объекта по названию слоя/// </summary>/// <param name="gameObject"> объект </param>/// <param name="layerName"> название слоя </param>private void ChangeObjectLayer(GameObject gameObject, string layerName){    gameObject.layer = LayerMask.NameToLayer(layerName);}/// <summary>/// Добавление префаба на сцену с возможностью определения родительского элемента и порядка в иерархии/// </summary>/// <param name="prefabPath"> путь к префабу </param>/// <param name="parentGameObject"> родительский объект </param>/// <param name="hierarchyIndex"> порядок в иерархии родительского элемента </param>private void InstantiateNewGameObject(string prefabPath, GameObject parentGameObject, int hierarchyIndex = 0){    if (parentGameObject)    {        var newGameObject = Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)), parentGameObject.transform);                    // изменение порядка в иерархии сцены внутри родительского элемента        newGameObject.transform.SetSiblingIndex(hierarchyIndex);    }    else        Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)));}

Таким образом, при выполнении следующего кода:

// изменение тэга и слоя объектаvar cube = GameObject.Find("Cube");cube.tag = "Player";ChangeObjectLayer(cube, "MainLayer");               // создание нового объекта на сцене и добавление его в иерархию к существующемуInstantiateNewGameObject("Assets/Prefabs/Capsule.prefab", cube, 1);

Элемент встанет не в конец иерархии, а на заданное место:

Цикл обновления сцен

И наконец, самое главное - функция, с помощью которой происходит вся дальнейшая автоматизация открывания-изменения-сохранения сцен, добавленных в File ->Build Settings:

/// <summary>/// Запускает цикл обновления сцен в Build Settings/// </summary>/// <param name="onSceneLoaded"> действие при открытии сцены </param>private void RunSceneUpdateCycle(UnityAction onSceneLoaded){    // получение путей к сценам для дальнейшего открытия    var scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToList();    foreach (var scene in scenes)    {        // открытие сцены        EditorSceneManager.OpenScene(scene);                    // пометка для сохранения, что на сцене были произведены изменения        EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());                    // проведение изменений        onSceneLoaded?.Invoke();                    // сохранение        EditorApplication.SaveScene();                    Debug.Log($"UPDATED {scene}");    }}

А теперь соединим всё вместе, чтобы запускать цикл обновления сцен по клику на кнопку:

Полный код SceneUpdater.cs
#if UNITY_EDITORusing System;using UnityEditor.Events;using TMPro;using UnityEngine.UI;using System.Collections.Generic;using UnityEngine.SceneManagement;using UnityEditor;using UnityEditor.SceneManagement;using System.Linq;using UnityEngine;using UnityEngine.Events;/// <summary>/// Класс для обновления сцен, включённых в список BuildSettings (активные и неактивные)/// </summary>public class SceneUpdater : EditorWindow{    [MenuItem("Custom Tools/Scene Updater")]    public static void ShowWindow()    {        GetWindow(typeof(SceneUpdater));    }    private void OnGUI()    {        // пример использования        if (GUILayout.Button("Update scenes"))            RunSceneUpdateCycle((() =>            {                // изменение тэга и слоя объекта                var cube = GameObject.Find("Cube");                cube.tag = "Player";                ChangeObjectLayer(cube, "MainLayer");                                // добавление компонента к объекту с уникальным названием                AddComponentToObject<BoxCollider>("Plane");                                // удаление объекта с уникальным названием                DestroyObjectWithName("Sphere");                                // создание нового объекта на сцене и добавление его в иерархию к существующему                InstantiateNewGameObject("Assets/Prefabs/Capsule.prefab", cube, 1);                // изменение настроек отображения Canvas                var canvas = GameObject.Find("Canvas");                ChangeCanvasSettings(canvas, RenderMode.ScreenSpaceOverlay, CanvasScaler.ScaleMode.ScaleWithScreenSize);                // изменение настроек шрифта                var tmp = canvas.GetComponentInChildren<TextMeshProUGUI>();                ChangeTMPSettings(tmp, 36, 72, TextAlignmentOptions.BottomRight);                // изменение RectTransform                ChangeRectTransformSettings(tmp.GetComponent<RectTransform>(), AnchorPresets.MiddleCenter, Vector3.zero, new Vector2(100f, 20f));                                // добавление события на кнопку                AddPersistentListenerToButton(canvas.GetComponentInChildren<Button>(), FindObjectOfType<SampleClass>().QuitApp);                // копирование настроек компонента                CopyTransformPositionRotationScale(GameObject.Find("Plane"), cube, copyScale:false);            }));    }    /// <summary>    /// Запускает цикл обновления сцен в Build Settings    /// </summary>    /// <param name="onSceneLoaded"> действие при открытии сцены </param>    private void RunSceneUpdateCycle(UnityAction onSceneLoaded)    {        // получение путей к сценам для дальнейшего открытия        var scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToList();        foreach (var scene in scenes)        {            // открытие сцены            EditorSceneManager.OpenScene(scene);                        // пометка для сохранения, что на сцене были произведены изменения            EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());                        // проведение изменений            onSceneLoaded?.Invoke();                        // сохранение            EditorApplication.SaveScene();                        Debug.Log($"UPDATED {scene}");        }    }    /// <summary>    /// Добавление обработчика события на кнопку (чтобы было видно в инспекторе)    /// </summary>    /// <param name="uiButton"> кнопка </param>    /// <param name="action"> требуемое действие </param>    private static void AddPersistentListenerToButton(Button uiButton, UnityAction action)    {        try        {            // сработает, если уже есть пустое событие            if (uiButton.onClick.GetPersistentTarget(0) == null)                UnityEventTools.RegisterPersistentListener(uiButton.onClick, 0, action);        }        catch (ArgumentException)        {            UnityEventTools.AddPersistentListener(uiButton.onClick, action);        }    }    /// <summary>    /// Изменение параметров RectTransform    /// </summary>    /// <param name="rectTransform"> изменяемый элемент </param>    /// <param name="alignment"> выравнивание </param>    /// <param name="position"> позиция в 3D-пространстве </param>    /// <param name="size"> размер </param>    private void ChangeRectTransformSettings(RectTransform rectTransform, AnchorPresets alignment, Vector3 position, Vector2 size)    {        rectTransform.anchoredPosition3D = position;        rectTransform.sizeDelta = size;        rectTransform.SetAnchor(alignment);    }    /// <summary>    /// Изменение настроек для TextMeshPro    /// </summary>    /// <param name="textMeshPro"> тестовый элемент </param>    /// <param name="fontSizeMin"> минимальный размер шрифта </param>    /// <param name="fontSizeMax"> максимальный размер шрифта </param>    /// <param name="textAlignmentOption"> выравнивание текста </param>    private void ChangeTMPSettings(TextMeshProUGUI textMeshPro, int fontSizeMin, int fontSizeMax, TextAlignmentOptions textAlignmentOption = TextAlignmentOptions.Center)    {        // замена стандартного шрифта        textMeshPro.font = (TMP_FontAsset) AssetDatabase.LoadAssetAtPath("Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset", typeof(TMP_FontAsset));        textMeshPro.enableAutoSizing = true;        textMeshPro.fontSizeMin = fontSizeMin;        textMeshPro.fontSizeMax = fontSizeMax;        textMeshPro.alignment = textAlignmentOption;    }    /// <summary>    /// Изменение отображения Canvas    /// </summary>    /// <param name="canvasGameObject"> объект, в компонентам которого будет производиться обращение </param>    /// <param name="renderMode"> способ отображения </param>    /// <param name="scaleMode"> способ изменения масштаба </param>    private void ChangeCanvasSettings(GameObject canvasGameObject, RenderMode renderMode, CanvasScaler.ScaleMode scaleMode)    {        canvasGameObject.GetComponentInChildren<Canvas>().renderMode = renderMode;        var canvasScaler = canvasGameObject.GetComponentInChildren<CanvasScaler>();        canvasScaler.uiScaleMode = scaleMode;        // выставление стандартного разрешения        if (scaleMode == CanvasScaler.ScaleMode.ScaleWithScreenSize)        {            canvasScaler.referenceResolution = new Vector2(720f, 1280f);            canvasScaler.matchWidthOrHeight = 1f;        }    }         /// <summary>    /// Получение всех верхних дочерних элементов    /// </summary>    /// <param name="parentGameObject"> родительский элемент </param>    /// <returns> список дочерних элементов </returns>    private static List<GameObject> GetAllChildren(GameObject parentGameObject)    {        var children = new List<GameObject>();                for (int i = 0; i< parentGameObject.transform.childCount; i++)            children.Add(parentGameObject.transform.GetChild(i).gameObject);                return children;    }    /// <summary>    /// Копирование позиции, поворота и размера с компонента Transform у одного объекта    /// на такой же компонент другого объекта.    /// Для корректного переноса координат у parent root объеков должны быть нулевые координаты    /// </summary>    /// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>    /// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>    /// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>    /// <param name="copyRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>    /// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>    private static void CopyTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,         bool copyPosition = true, bool copyRotation = true, bool copyScale = true)    {        var newTransform = objectToCopyFrom.GetComponent<Transform>();        var currentTransform = objectToPasteTo.GetComponent<Transform>();                if (copyPosition) currentTransform.localPosition = newTransform.localPosition;        if (copyRotation) currentTransform.localRotation = newTransform.localRotation;        if (copyScale) currentTransform.localScale = newTransform.localScale;    }        /// <summary>    /// Копирование позиции, поворота и размера с компонента RectTransform у UI-панели одного объекта    /// на такой же компонент другого объекта. Не копируется размер самой панели (для этого использовать sizeDelta)    /// Для корректного переноса координат у parent root объеков должны быть нулевые координаты    /// </summary>    /// <param name="objectToCopyFrom"> объект, с которого копируются части компонента </param>    /// <param name="objectToPasteTo"> объект, на который вставляются части компонента </param>    /// <param name="copyPosition"> по умолчанию позиция копируется, с помощью данного параметра это можно отключить </param>    /// <param name="copyRotation"> по умолчанию поворот копируется, с помощью данного параметра это можно отключить </param>    /// <param name="copyScale"> по умолчанию размер копируется, с помощью данного параметра это можно отключить </param>    private static void CopyRectTransformPositionRotationScale(GameObject objectToCopyFrom, GameObject objectToPasteTo,        bool copyPosition = true, bool copyRotation = true, bool copyScale = true)    {        var newTransform = objectToCopyFrom.GetComponent<RectTransform>();        var currentTransform = objectToPasteTo.GetComponent<RectTransform>();                if (copyPosition) currentTransform.localPosition = newTransform.localPosition;        if (copyRotation) currentTransform.localRotation = newTransform.localRotation;        if (copyScale) currentTransform.localScale = newTransform.localScale;    }    /// <summary>    /// Уничтожение объекта с уникальным названием    /// </summary>    /// <param name="objectName"> название объекта </param>    private void DestroyObjectWithName(string objectName)    {        DestroyImmediate(GameObject.Find(objectName)?.gameObject);    }    /// <summary>    /// Добавление компонента к объекту с уникальным названием    /// </summary>    /// <param name="objectName"> название объекта </param>    /// <typeparam name="T"> тип компонента </typeparam>    private void AddComponentToObject<T>(string objectName) where T : Component    {        GameObject.Find(objectName)?.gameObject.AddComponent<T>();    }    /// <summary>    /// Изменение слоя объекта по названию слоя    /// </summary>    /// <param name="gameObject"> объект </param>    /// <param name="layerName"> название слоя </param>    private void ChangeObjectLayer(GameObject gameObject, string layerName)    {        gameObject.layer = LayerMask.NameToLayer(layerName);    }    /// <summary>    /// Добавление префаба на сцену с возможностью определения родительского элемента и порядка в иерархии    /// </summary>    /// <param name="prefabPath"> путь к префабу </param>    /// <param name="parentGameObject"> родительский объект </param>    /// <param name="hierarchyIndex"> порядок в иерархии родительского элемента </param>    private void InstantiateNewGameObject(string prefabPath, GameObject parentGameObject, int hierarchyIndex = 0)    {        if (parentGameObject)        {            var newGameObject = Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)), parentGameObject.transform);                        // изменение порядка в иерархии сцены внутри родительского элемента            newGameObject.transform.SetSiblingIndex(hierarchyIndex);        }        else            Instantiate((GameObject) AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)));    }}#endif

Таким образом, всего за один клик все сцены нашего проекта автоматически обновятся, а мы тем временем сможем пойти выпить кофе и просто понаслаждаться проделанной работой:

Волшебная кнопкаВолшебная кнопка

Заключение

Имея в своём распоряжении такой инструмент, вы сможете делать всё, что вам угодно за считанные клики: сериализовать поля, менять иерархию на сценах, настраивать Fuse/IClone/DAZ и других персонажей, а также менять Build Pipeline, но об этом как-нибудь в другой раз.

Главное, не забывайте использовать систему контроля версий и проверять запуск ваших модификаций сперва на одной сцене (т.е. без использования RunSceneUpdateCycle).

Запустить тестовый проект и получить полный код можно на моём GitHub.

Кстати, тех, кто планирует строить карьеру в IT, ябуду рада видеть на своёмYouTube-канале IT DIVA. Там вы сможете найти видео по тому, как оформлять GitHub, проходить собеседования, получать повышение, справляться с профессиональным выгоранием, управлять разработкой и т.д.

Спасибо за внимание и до новых встреч!

Подробнее..

Подпишись, чтобы не пропустить События

16.06.2021 20:20:54 | Автор: admin

Событийно-ориентированный подход к разработке позволяет естественным изолировать реализацию бизнес-логики приложения от пользовательского интерфейса. Как в большей степени Unity-разработчик, я буду демонстрировать все именно в контексте геймдева. В прошлой статье я показал, как конечные автоматы позволяют организовать бизнес-логику в виде системы состояний со строго обозначенным поведением. Сегодня мне бы хотелось уделить внимание тому, как именно автомат должен взаимодействовать с игровыми объектами и управлять состоянием сцены.

В примерах в статье про автоматы я использовал следующую конструкцию:

Game.Event.Invoke("joystick_updated", input);

По структуре вызова функции можно понять, что есть некоторый статический класс Game, предоставляющий глобальный доступ к своим полям, что очень удобно при небольших размерах проекта. Вот часть его содержимого:

public static class Game{    public static FSM Fsm = new FSM();    public static EventManager Event = new EventManager();       public static ObservableData Data = new ObservableData();...

В этих примерах можно увидеть некоторые вольности в деталях реализации. При масштабировании проекта, например, придется отказаться от статического контекста и на основе класса Game реализовать компоненты, назовем их претенциозно MonoBehaviourPro с подобной структурой для сложных подсистем, и передавать ее в качестве контекста автомату и компонентам этих подсистем. Я намеренно сглаживаю эти углы для большей наглядности примера. Сегодня мы рассмотрим класс с многострадальным названием EventManager, так как он является зависимостью ObservableData и без него мы не сможем двинуться дальше. По ссылке можно увидеть полную реализацию класса EventManager, принцип его работы предельно прост. Мы храним список делегатов c произвольной сигнатурой, подписанных на события со строковым ключом.

Важно, что мы работаем с Generic-структурой, поэтому следует помнить о Type safety. Тип аргумента при отправке события должен соответствовать сигнатурам функций, подписанных на него. Также, можно заметить, что EventManager отдельно хранит binds и binds_global и имеет отдельный интерфейс для работы с ними. Это реализация, специфичная для Unity. Дело в том, что там существует система сцен, позволяющая подгружать или выгружать сцены и объекты. И разница между этими двумя словарями в том, что первый очищается при выгрузке сцены. В идеальном мире мы всегда подписываем объект в Awake и отписываем его в OnDestroy. В таком случае можно было бы обойтись одним binds, не очищая его никогда. Каждый объект подписывается и отписывается в рамках своего жизненного цикла и разве что при переходе между сценами происходило бы немного лишней работы над поштучным отписыванием выгружаемых объектов. Но такой подход не прощает ошибок, выгруженный подписчик в лучшем случае сразу сломает вызов делегата и будет найдена, а в худшем - станет причиной утечки памяти. Так что, в качестве "защиты от дурака" лучше при переходе явно отписывать все, что не было обозначено как Global.

Итак, интерфейс EventManager cводится к 5 методам:

        public void Bind<T>(string name, Action<T> ev)        public void BindGlobal<T>(string name, Action<T> ev)        public void Unbind<T>(string name, Action<T> ev)        public void UnbindGlobal<T>(string name, Action<T> ev)                  public void Bind(string name, Action ev)        public void BindGlobal(string name, Action ev)        public void Unbind(string name, Action ev)        public void UnbindGlobal(string name, Action ev)                  public void Invoke<T>(string name, T arg)                  public void Invoke(string name)

Мы можем подписываться на события и отправлять их. И все это с аргументом произвольного типа. В примере из статьи про FSM мы передавали ввод с джойстика в автомат и, если состояние предусматривает такую возможность, передавали в EventManager событие изменения положения джойстика , на которое может подписаться компонент, управляющий положением игрока(Или потомок MonoBehaviourPro, какой нибудь PlayerController, который передаст информацию о вводе в свой автомат, и если игрок в состоянии SPlayerDriving , будет передавать ввод с джойстика уже автомобилю, за рулем которого он сидит, а если в SPlayerClimbing, джойстик будет двигать игрока перпендикулярно нормали плоскости, по которой он движется, с соответствующей анимацией. Но это уже более сложные примеры, не будем на этом задерживаться). Или же, на входе в состояние игры SWin мы можем отправить событие level_done, а на него подписать анимацию экрана победы, конфетти, и чего там еще ваш ГД придумает.

Некоторые события, не влияющие на игровой процесс, такие как включение-выключение звука, вполне можно обрабатывать в обход автомата, отправляя событие напрямую с кнопки, и принимая его классом-оберткой PlayerPrefs, как бы он ни был устроен. Тут я призываю вас руководствоваться здравым смыслом и собственным чувством прекрасного.

Таким образом, автомат в процессе своей работы может отправлять события подписавшимся на них объектам через EventManager. В следующей статье я расскажу о возможностях, предоставляемых классом ObservableData в связке с описанными инструментами.

Эта статья - вторая в серии:
- Разделяй и властвуй Использование FSM в Unity
- Подпишись, чтобы не пропустить События

Подробнее..

Недельный геймдев 20 30 мая, 2021

02.06.2021 02:11:21 | Автор: admin

На этой неделе: вышла альфа-версия Unreal Engine 5, а AMD обновили драйвер для работы с UE5, Chaos релизнули V-Ray glTF Viewer, Unity выпустили ArtEngine 2021.5, вышла первая публичная бетка OctaneRender 2021.1, Epic Games новый конкурс организовали: Twinmotion Community Challenge #6.

Из интересностей: Episode 1 : Salad Mug DYNAMO DREAM (видео делалось 3 года), как создать объёмные облака в Unreal Engine 4.26, чуть подробнее про Sua, кратко про VFX в Shadow and Bone, полезная статья о том, как создавать доступные игры для людей с ограниченными возможностями и несколько интересных туториалов по работе с шейдерами в Unity.

Обновления/релизы/новости

Вышел долгожданный Unreal Engine 5

Epic Games провела 26 мая презентацию Unreal Engine 5, на которой показала новые возможности движка, включая системы по работе с ассетами, светом, анимациями и звуком. Старые системы и инструменты тоже получили множество улучшений.

Уже можно скачать альфу UE5 и семпл проект Valley of the Ancient.

Краткий обзор по ходу презентации можно почитать тут.

Chaos выпустили V-Ray glTF Viewer

Бесплатную коллекцию скриптов на Python для рендеринга моделей в glTF формате с использованием своего модуля рендеринга V-Ray.

Легковесный формат файлов для 3D-ресурсов glTF становится всё более популярным в реалтайм приложениях и теперь поддерживается в DCC софте, включая Blender и Nvidias Omniverse.

Инструмент запускается из командной строки, работает с .gltf и .glb, поддерживает ключевые характеристики PBR материалов спецификации glTF 2.0.

Плагин TearKnit FX для Maya для анимации разрыва ткани

Вместо использования симуляции плагин воссоздаёт структуру нитей ткани процедурно, ускоряя вычисления и делая вывод более согласованным.

Стоимость однопользовательской лицензии 30 долларов. Лицензия на несколько юзеров от 45 до 490 долларов за неограниченное число.

Unity выпустили ArtEngine 2021.5

Новую версию инструмента для обработки материалов с помощью искусственного интеллекта. Обновление включает возможность экспортировать проекты ArtEngine и делиться ими с другими художниками.

Unity также выпустили набор примеров использования тулзы, чтобы помочь новым пользователям изучить возможности. Пользователи Studio-редакции получили новый интерфейс для командной строки, позволяющий запускать задачи ArtEngine в фоновом режиме, не открывая само приложение.

AMD в тесном сотрудничестве с Epic Games выпустила драйвер Radeon Software Adrenalin, настроенный для поддержки разработки Unreal Engine 5

Если хотите опробовать UE5, обязательно загрузите его для обеспечения оптимальной производительности, если в вашей системе используется видеокарта AMD Radeon.

Chaos выпустили Vantage 1.3

Новую версию инструмента для работы с большими сценами V-Ray в реальном времени.

Добавлена поддержка параметров из импортированного .vrscene для VRay2SidedMtl, двустороннего материала V-Ray, а также анимированной камеры. Кроме того, Vantage теперь включает в себя шумоподавитель OptiX, который также интегрирован в сам V-Ray.

Вышла первая публичная бетка OctaneRender 2021.1

В этом выпуске улучшена производительность на текущих графических процессоров Nvidia Ampere, добавлен новый булевый клипинг материал, переработана работа с AOV.

К тому же это первая версия OctaneRender доступная только под Windows и Linux, поддержка macOS теперь полностью перенесена в новую версию Octane X.

Полный список изменений/улучшений лучше на сайте посмотреть.

Вышел ZBrush 2021.6.6

Этот выпуск обеспечивает равенство функций с ZBrushCore и ZBrushCoreMini. Обновление бесплатно для текущих пользователей.

Халява/раздачи/бандлы/курсы/конкурсы

Epic Games новый конкурс организовали: Twinmotion Community Challenge #6

Тема: Смешать старый и новый мир. Хотят увидеть, как участники сочетают современный футуристический архитектурный дизайн с контекстом старого мира. Призом будет $500.

Для участия нужно отправить изображение. Дедлайн 23 июня.

Интересные статьи/видео

Как создать объёмные облака в Unreal Engine 4.26

Уильям Фаучер поделился подробным видео-туториалом. Дефолтные весьма ограничены, но с помощью блюпринтов можно их неплохо твикнуть.

Чуть подробнее про Sua первого цифрового человечка на Unity

Как оказалось, это теперь лицо Unity Korea. Можно подробнее в блоге почитать.

Episode 1 : Salad Mug DYNAMO DREAM

Можно сказать, что видео создавалось на протяжении 3-х лет. Ян Хьюберт могёт.

Немного про то, как создавались визуальные эффекты в Shadow and Bone

Создания брызг в океане в Houdini

Тянь Цю, работавший над многими сериалами DC в качестве технического директора, рассказал о процессе создания и поделился полезными советами для тех, кто планирует работать с VFX.

Полезная статья о том, как создавать доступные игры для людей с ограниченными возможностями

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

Портрет Носферату

Кестутис Ринкявичюс из Playground Games (Forza, новый Fable) рассказал, как была разработана каждая крошечная деталь портрета Носферату, и поделился своими ссылками, которые помогают с текстурированием.

Шейдер чёрной дыры в Unity 2020.3.1f1 с помощью Universal Render Pipeline (URP)

Саму статью можно почитать в блоге. Исходники можно с Гитхаба скачать.

Создание шейдера карты с 3D-контентом в Unity с помощью URP и Amplify Shader Editor

Можно было бы использовать и Shader Graph, но там сложности с Stencil Buffer и другими вещами. Автор рекомендует использовать Amplify Shader Editor вместо Shader Graph, поскольку он намного быстрее, полнее и с ним приятней работать.

Разное

Как при разработке Senuas Saga: Hellblade II используются карты потоков, чтобы облака формировались и растворялись естественным образом

Это не фото

Можно посмотреть на странице автора на Artstation. Там есть и другие работы.

Подробнее..

Недельный геймдев 21 6 июня, 2021

09.06.2021 00:09:23 | Автор: admin

Из новостей на неделе: вышел Unity 2021.2a19 с обновлением пайплайна работы с ассетами, исходники Периметра на Гитхаб выложили, вышел Blender 2.93 LTS, AMD FidelityFX Super Resolution появится в первых играх уже 22 июня, в Steam появились совместные наборы, прогресс по GDScript в Godot по пути к 4.0, вышли Howler 2022, KeyShot 10.2 и новый пакет Arm Mobile Studio для Unity.

Из интересностей: исследование того, как Nanite работает изнутри, советы по оптимизации работы с Substance, анимированный мост в Unreal Engine чисто в шейдере, интересные примеры VFX из недавних фильмов.

Обновления/релизы/новости

Вышел Unity 2021.2a19

Из крутого, как по мне, появилась поддержка параллельного импорта моделей и текстур (включить в Project Settings -> Editor -> Refresh).

И появилось окно импорта ассетов: позволяет посмотреть дату импорта, предыдущие версии ассетов, список зависимостей, импортеры, используемые для импорта артефакта, длительность импорта и многое другое.

Исходники Периметра на Гитхаб выложили

Можно собрать, как минимум, на XP и Win7.

AMD FidelityFX Super Resolution появится в первых играх уже 22 июня

  • На картах AMD прирост до 60%.

  • 4 пресета качества.

  • А так как оно не привязано к AMD, то будет работать и на картах Nvidia. На GTX 1060 даёт +41%

Вышел Blender 2.93 LTS

Который знаменует собой завершение более чем двадцатилетней истории разработки. А скоро появится и 3.0.

  • Новый воркспейс и редактор спредшитов для геометрических нод.

  • Новые способы использовать маски на моделях во время лепки.

  • Более быстрый и качественный рендеринг в Cycles и Eevee.

  • Крупное обновления набора инструментов Grease Pencil для 2D-анимации.

  • Обновились многие тулсеты.

Подробнее на сайте.

В Steam появилась новая функция под названием совместные наборы

С её помощью разработчики и издатели могут создавать наборы из нескольких игр в Steam совместно с другими разработчиками и издателями.

Подробнее в доках.

Вышел Verge3D 3.7 для Blender

Добавили технику глобального освещения для значительного улучшения реализма сцен, clipping planes, чтобы заглянуть внутрь сложных объектов, интегрировали инструменты для разработки мобильных и настольных приложений, добавили батчинг геометрии в рантайме.

Кроме того, включили в это обновление 3 новых демки.

Прогресс по GDScript в Godot по пути к 4.0

Ещё многое предстоит сделать, но и в этом обновлении достаточно интересных новшеств.

  • Типизированные массивы.

  • Лямбда-выражение.

  • Статические методы для встроенных типов.

  • Оптимизации. В частности, ведётся работа над уменьшением ветвлений. Вводится временный стек, чтобы убрать переинициализацию вариантов. Это поможет с типами, которые требуют аллокаций, например, Transform.

  • Покрытие тестами.

В библиотеку Chaos Cosmos для V-Ray было добавлено 100 новых ассетов

Идеально подходящих для интерьера: столы, стулья, лампы и посуда. В дополнение библиотека теперь включает несколько человеческих персонажей от AXYZ Design, одного из ведущих поставщиков отсканированных фотографий людей в 3D.

Unity хотят переосмыслить Unite

Поэтому в этому году конференции не будет. Ждём 2022.

Вышел Howler 2022

Новая версия инструмента для цифровой живописи, анимации и обработки видео, который можно использовать для создания кинематографических визуальных эффектов в интуитивной среде.

Обновление добавляет функцию аналитического апскейла для повышения разрешения изображений и возможность пометить изображения как нестираемые, чтобы те пережили рестарт приложения.

SpookyGhost вышел в опенсорс по MIT лицензии

Кроссплатформенный инструмент для процедурной анимации, созданный с использованием игрового движка nCine 2D на C++ и ImGui для UI, можно посмотреть на Гитхабе.

Инструмент позволяет легко создавать 2D-анимацию с использованием процедурных контроллеров, а также вашего собственных сценариев на Lua и экспортировать их в виде фреймов спрайтов или готовых к игре спрайтшитов.

Вышел KeyShot 10.2

Добавили инструмент Mesh Simplification для уменьшения числа треугольников моделей, предназначенный для упрощения их повторного использования в AR приложениях, таких как KeyVR KeyShot.

Библиотека материалов KeyShot также была обновлена: теперь пользователи могут выбирать семь различных 3D-моделей для предварительного просмотра материалов в виде миниатюр.

Вышел новый пакет Arm Mobile Studio для Unity

Новый инструмент для анализа производительности мобильных устройств.

Интересные статьи/видео

Nanite: взгляд изнутри

Эмилио Лопес, Senior Graphics Engineer из Playground Games, поделился своим взглядом на Nanite и попытался разобрать то, как система работает. При исследовании во многом полагался на RenderDoc.

Запись стрима на канале Unreal Ungine по работе с Nanite в UE5 и про саму технологию

Оптимизация работы с Substance

Виктор Андреенков поделился советами, как сделать Substance Painter менее требовательным по железу без снижения качества работы.

Анимированный мост в Unreal Engine чисто в шейдере

Т. к. с мостом не нужно взаимодействовать, то и skeletal mesh не нужен, поэтому шейдер в таком случае показался автору разумным решением.

Создание мифических существ и VFX дыма с огнём

Члены команды REALTIME рассказали 80lv о создании мифических существ для тв-сериалов и обсудили процесс создания реалистичных визуальных эффектов дыма и огня.

Разное

Про создание VFX в Армии мертвецов Зака Снайдера

Использовалось сочетание наземных и воздушных снимков LIDAR и дронов.

Примеры VFX в Майор Гром: Чумной Доктор

Подробнее..

Недельный геймдев 22 13 июня, 2021

16.06.2021 00:20:01 | Автор: admin

Из новостей на этой неделе: стала доступна превью версия Unreal Engine 4.27 с включёнными в движок Bink Video и Bink Audio, Unity выпустили новый стартовый пак, вышла новая версия движка Diligent Engine 2.5, Activision выпустили расширение для Windows для просмотра USD-файлов, разработчики Cascadeur получили дополнительные 1.5 миллиона долларов на развитие продукта, PolyHertz выпустил новый скрипт UnChamfer Pro для 3ds Max, Khronos запускают программу сертификации 3D-просмотровщиков.

Из интересностей: подробный доклад от Insomniac про работу со светом в Marvels Spider-Man, занятная механика для VR игры, полезный доклад от Риотов про то, как они балансят и нерфят персонажей.

Обновления/релизы/новости

Стала доступна превью версия Unreal Engine 4.27

Из ключевого:

  • Oodle и Bink теперь встроены в движок.

  • Улучшения по части Open XR.

  • Path Tracer теперь в бетке.

  • Куча улучшений GPU Lightmass: запекания, теней, поддержки mGPU.

  • Обновление Niagara: версионность модулей, новый дебагер, куча улучшений UX/UI.

  • Оптимизировали рендеринг на мобильных платформах. Ключевые направления оптимизаций: Distance Field Shadows, Fast Approximate Anti-Aliasing (FXAA), Temporal Anti-Aliasing (TAA).

  • Изменения по части Datasmith, особенно в Datasmith Exporter Plugin для ArchiCAD.

  • Огромное число изменения инструмента для синематиков и виртуального продакшена.

Bink Video и Bink Audio теперь доступны в Unreal Engine бесплатно

Epic Games выпустили последние изменения по интеграции технологии RAD Game Tools (которые приобрела в начале этого года) в Unreal Engine. Доступно в Unreal Engine 4.27 и Unreal Engine 5.

Bink Video и Bink Audio кроссплатформенные видео и аудио кодеки с упором на производительность.

Разработчики Cascadeur получили дополнительные 1.5 миллиона долларов от Nekki

Спустя 2 месяца после выхода в ранний доступ этот инструмент для анимаций насчитывает уже 80000 пользователей. Команда, тем времени, из 25 разработчиков должна вырасти до 30. Компания планирует достичь двух важных этапов к полноценному релизу в 2022:

  1. Улучшить по максимуму уникальные инструменты Deep Physics с поддержкой AI.

  2. Функциональные возможности Cascadeur должны быть расширены дополнительными стандартными инструментами.

Unity выпустили новый стартовый пак

Набор содержит бесплатные и легковесные базовые контроллеры персонажей от первого и третьего лица для последней версии Unity 2020 LTS и более поздних версий с использованием Cinemachine и Input System.

Старые версии Unity также могут работать с паком, но, вероятно, нужно будет что-то дотюнить.

МФТИ и Gaijin Entertainment запускают магистерскую программу (4 семестра) по программированию игр

Заявку нужно подать до 30 июня. Обучение очное.

Вышла новая версия движка Diligent Engine 2.5

В этой версии:

  • Трассировка лучей теперь включена на Metal.

  • Сигнатуры ресурсов пайплайна позволяют приложениям определять явные слои ресурсов шейдеров, которые позволяют совместно использовать ресурсы между различными состояниями пайплайна без необходимости их повторной привязки.

  • Несколько контекстов позволяют выполнять асинхронные вычисления и параллельный рендеринг.

  • Запросы на RT позволяют бросать лучи из обычных шейдеров (пиксельные, вычислительные и т. д.).

Activision выпустили расширение с открытым исходным кодом для Windows для просмотра USD

Проект доступен на GitHub под лицензией Apache 2.0 и позволяет пользователям Windows взаимодействовать с USD файлами (всё более широко используемым отраслевым форматом, созданным Pixar) прямо в Windows Explorer.

Первую версию браузера ассетов добавят в Blender 3.0

Пользователи давно ждут нечто подобное.

Epic Games поделились кратким руководством для тех, кто хочет познакомиться с Unreal Engine 5

В 16-страничной pdf-ке разобраны основные вопросы и приведены полезные ссылки. Ничего того, что есть в онлайн документации, но зато всё кратко и сжато. Как раз подойдёт для ознакомления.

PolyHertz выпустил новый скрипт UnChamfer Pro для 3ds Max

Предназначенный для быстрого, простого и точного удаления или масштабирования фасок меша.

BuildBox решили поменять прайс на свой движок после негодования клиентов

Они не будут брать комиссию с ревенью, если вы не используете их специальное решение для монетизации.

Ранее компания изменила прайс, помомо подписки захотели процент процент с ревенью: 30%, если у вас план Plus, 10%, если у вас PRO план. Вовремя одумались.

Faceware Technologies запустили Faceware Studio PLE, новую бесплатную версию Faceware Studio, программы для мокапа в реальном времени

Лицензия позволяет художникам бесплатно использовать Faceware Studio в течение шести месяцев для некоммерческой разработки игр, визуальных эффектов.

Вышла бетка Kinetix Advanced

Облачный инструмент позволяет сгенерировать 3d-анимацию из .MP4 видео.

Можно попробовать бесплатно, преобразовав 60-секундное видео. За 15 евро в месяц можно конвертировать до 3 минут видео и иметь доступ ко всем инструментам, которые предлагает Kinetix, а за 120 евро в месяц 30 минут видео.

Khronos запускают программу сертификации 3D-просмотровщиков

Консистентное отображение на различных платформах повышает доверие потребителей. CGTrader и Sketchfab уже подписались.

Новая программа сертификации направлена на устранение различий в способе отображения ассетов в разных онлайн-программах для просмотра, в первую очередь в визуализации материалов и световых эффектов.

Интересные статьи/видео

4 полезных совета по работе с Substance Painter для новичков

  1. Маски ваши лучшие друзья.

  2. Будьте рассказчиком. Недостаточно просто навесить текстурки, важно понимать, как эти объекты будут смотреться рядом с другими.

  3. Нужно понимать/осознавать как свет и игровые движки влияют на ваши текстуры.

  4. Создайте свою собственную библиотеку материалов.

На этой неделе Epic Games обсудили новую систему глобального освещения Lumen из Unreal Engine 5

Специалисты рассказали, что это такое, как включить, и что технология из себя предоставляет.

Полезный доклад от Риотов про то, как они балансят и нерфят персонажей

Не смотрите на оценки под видео, контент норм. Там приведены реальные данные и метрики и, что самое главное, докладчик рассказывает, как они принимают решения на основе этих данных.

Подробный доклад от Insomniac про работу со светом в Marvels Spider-Man

Разное

Разбор VFX из фильма Поколение Вояджер (2021)

Подробнее..

Монстрация-онлайнстрация

01.05.2021 12:14:42 | Автор: admin

Дело было вечером, делать было нечего.

Я вспомнил, что завтра Первое мая и обычно на него мы идём на Монстрацию - творческий митинг, родом из Новосибирска, куда каждый желающий может прийти со своим плакатом любого содержания.

По известным всем обстоятельствам в этом году Монстрацию не разрешили, о чём я немного взгрустнул и подумал: "А что я могу сделать, как разработчик?".

Короче, развернул сервер, сделал небольшое API для обработки запросов на NodeJS+Express+MongoDB, а затем в Unity 3D сварганил небольшое приложение, в котором создал простенькое окружение, логику взаимодействия с API и пару игровых персонажей: участников митинга и полицию, которая их "охраняет". С контентом мне помогли несколько ребят, сделав модели полицейского УАЗика, Новосибирского оперного театра, основу персонажей.

Логика работы простая: пользователь заходит, оставляет в форме свой никнейм и текст плаката, а затем на сцене появляются он сам и все, кто зарегистрировался до него.

Поскольку просто ходить и смотреть плакаты быстро наскучило, то вдобавок добавил полицейское окружение и небольшой хулиганистый сценарий, в котором в полицию можно чем-нибудь швырнуть и нарваться на крупные неприятности.

Что получилось в итоге можете поглядеть прямо сейчаспо этой ссылке. Работает только для ПК и ноутов, мобильные устройства не поддерживаются.

Сейчас у нас уже 40 участников, думаю какие ещё игровые сценарии туда запихнуть. Мне кажется неплохо получилось для одного вечера, а?

Подробнее..

Спасибо хабравчанам за участие в Онлайнстрации

02.05.2021 20:23:47 | Автор: admin

Ребята, спасибо всем, ктопоучаствовал в мероприятии. Зарегилось 424 человека, получился настоящий творческий первомайский митинг в онлайне.

В честь этого создал специального персонажа!

Регистрация больше недоступна, но вы можетепоходить и посмотреть плакаты, которые оставили участники. Откровенный спам, агрессию и бессмыслицу я удалил, уж извините. Игровые сценарии также все доступны.

Подробнее..

Парящие Острова настраиваем стилизованные шейдера с помощью HDRP в Unity

08.05.2021 22:04:46 | Автор: admin

Автор статьи Maciej Hernik и главный редактор портала 80.lv Kirill Tokarev любезно позволили нам сделать этот перевод.

Maciej Hernik обсудил с нами детали его стилизованной сцены Парящие Острова: шейдеры для травы, деревьев и воды, Volume Overrides, текстурирование асcетов и многое другое.

Вступление

Всем привет! Меня зовут Maciej Hernik и я самоучка, художник по окружению (Level Artist) из Польшы. Сколько я себя помню, я всегда обожал игры. Мое знакомство с видеоиграми произошло, когда я увидел несколько на PS1. В то время я был ребенком, у меня была мечта, что однажды я буду делать свои собственные игры, хотя я еще и понятия не имел как их делать. Я стал старше, узнал что такое 3D арт и нашел это очень интересным и увлекательным занятием. Моя страсть к игровому арту дала мне возможность получить свою первую работу и в этот момент я понял, что хочу зарабатывать на жизнь созданием игрового арта и самих игр.

Парящие Острова: идея

В начале, эта работа предполагалась, как бессрочный проект вроде площадки для экспериментов с HDRP в Unity, это бы помогло мне чуть больше ознакомиться с инструментарием HDRP. Однако, когда я сделал первые итерации шейдеров растительности и показал их своим друзьям, они вдохновили меня сделать красочную сцену с использованием этих шейдеров. Тогда то я и решил сделать эту художественную работу в стиле волшебной сказки, вдохновленный игрой The Legend of Zelda. И в этой статье я хочу разобрать некоторые компоненты из этой работы.

Начало проекта

Сначала я использовал заглушки (Placeholders) для ландшафта и несколько ассетов вроде камней, деревьев и травы, для того, чтобы понять масштаб всего острова, а также форму Монолита и деревьев. Так же я понял, что наиболее подходящие мне тени и солнечные лучи, отбрасывали деревья именно треугольной формы.

Затем я решил разместить первые итерации травы и деревьев с правильными шейдерами и материалами, и начать шлифовать эффекты для воды. В тот момент я знал, что внешний вид сцены возможно изменится в процессе работы, но благодаря этой итерации я увидел, что с точки зрения стилизации, работа идет в правильном направлении.

Монолит и вода должны были стать точкой фокуса в сцене. Это было довольно легко сделать, потому что вода получилась очень динамичной за счет анимированного шейдера или эффектов частиц, а Монолит и цепи слегка двигались сверху вниз, что немного добавляло истории.

В какой-то момент я почувствовал, что одинокий остров не так интересен как казалось. После некоторых раздумий, я решил добавить маленькие острова вокруг главного острова для завершения композиции. Поначалу я думал, что добавление этих маленьких островов может забрать на себя слишком много внимания от главного острова, однако, после того как я их чуть-чуть увеличил, добавил немного деревьев и камней на них, я понял, что они только помогают общей композиции. В конце я добавил некоторое количество облаков. На самом деле, я вручную нарисовал их на плоскостях (Planes) с прозрачностью и заставил двигаться по небу используя аниматор в Unity.

Создание травы

Шейдер травы был одним из первых созданных для этого проекта. Однако он прошел через множество итераций, чтобы достичь наиболее подходящего эффекта. Я выбрал более процедурный подход, так как посчитал, что это будет наиболее сложный и интересный путь для создания травы. Для создания шейдера я использовал Shader Graph в Unity версии 2020.1.0f1.

Давайте рассмотрим часть этого шейдера, который отвечает за покраску. Он разделен на 4 слоя, каждый из которых представляет из себя цвет. Эти цвета смешиваются между собой с помощью функции Lerp. Эти слои:

  • Primary color + Shadow color (Главный цвет + Цвет тени)

  • Additional color (Дополнительный цвет)

  • Ground color ( Цвет земли)

  • Highlights (Блики)

Primary color и Shadow color разделены на две части. Одна часть это просто цвет травы, а вторая часть отвечает за нанесение поверх другого цвета. Это достигается за счет проекции текстуры на траву из вида сверху в мировых координатах (World Position). Additional color использовался, чтобы достичь большего разнообразия травы, потому что я чувствовал, что двух цветов будет недостаточно, особенно если смотреть сверху. Ground color нужен, чтобы достичь деликатной, почти незаметной имитации окружающего затенения (Ambient Occlusion) снизу травы, чтобы немного разбить элементы. В конечном итоге, я уменьшил его, потому что он создавал слишком сильный контраст между пучками травы и это не подходило для стилизации, по-моему мнению. Именно поэтому, этот эффект окружающего затенения (Ambient Occlusion) от Ground color, очень слабо использован в этой сцене. Последний слой создает блики (Highlights) в верхней части травы, зависящие от скорости перемещения текстуры маски.

Скомбинировав все эти слои, я смог настроить финальный вид травы, изменяя параметры в материалах, и лично мне очень понравился такой подход.

Деревья

Я создал отдельный шейдер для деревьев и другой растительности, так как он немного отличается от шейдера травы. В этот раз использовался освещаемый шейдер как основу (Lit Master Shader). Благодаря этому я очень легко мог получить информацию от направления света, а также в HDRP освещаемый шейдер (Lit Master Shader) дает возможность управлять полупрозрачностью (Translucency) объекта.

Модель дерева состоит из простых плоскостей (Planes), но с измененными нормалями, чтобы достичь более мягкий вид и получить лучший контроль над распределением света по дереву. Я добился этого в Blender благодаря модификатору передачи данных (Data Transfer Modifier), который позволил мне перенести нормали c другого меша на плоскости (Planes) из которых состоит дерево.

Деревья не имеют диффузную текстуру (Diffuse). Вместо этого, в шейдере я использовал информацию о направлении света. Благодаря этому я смог регулировать полупрозрачность (Translucency) дерева, чтобы решить - как много света будет проходить сквозь него. Я так же использовал информацию о направлении света, чтобы менять цвет в освещенной части и в затененной части дерева. Эти функции были ключем для меня, и помогли достичь стилизованного вида растений.

Я также реализовал возможность регулировать цвет в зависимости от высоты объекта, по локальному значению Y. Это позволило мне создать симпатичный градиент сверху вниз, что также помогло сделать более мягкий переход от земли к дереву. В итоге, я решил добавить деликатное окружающее затенение (Ambient Occlusion) между листьями, чтобы немного разделить их.

Эффекты воды

Как я уже упоминал, вода это важная часть моей сцены, потому что она выделяется на фоне зеленой травы и деревьев и содержит много динамических частей, которые помогают сосредоточить фокус внимания зрителя на центре сцены.

Водопад на самом деле представляет из себя микс двух составляющих:

  • Вода с шейдером воды

  • Частицы пены и пузырьков

Я знал, что я хочу добиться стилизованного вида воды, вот почему я создал простой шейдер, который выполняет операции по вращению текстуры типа Voronoi, изменению ее размера и перемещению во времени. Кроме того, я использовал Waterfall Mask текстуру в красном канале, чтобы замаскировать место, где я хотел спрятать паттерн от текстуры Voronoi, например в начале водопада.

Я добавил немного частиц для симуляции пены и пузырьков, используя систему частиц Unity c кастомным мешем в виде сферы. Чтобы добиться финального вида, я поиграл с такими настройками как эмиссия, форма системы частиц и размер частиц в течение их времени жизни (Size Over Lifetime).

Для большего водопада, я применил тот же метод, только скорректировал настройки в системе частиц для увеличения размера.

Ассеты не растительного происхождения

Аcсеты, такие как камни, мост и Монолит я сделал стандартным образом - смоделировал их в Blender, заскульптил высокополигональные модели (High Poly) в ZBrush, и запек их на низкополигональные меши (Low Poly) в Substance Painter.

В Substance Painter, я просто использовал запеченные карты, чтобы придать моделям немного цвета и деталей. Однако, я хотел бы поделиться с вами одной хитростью, которая была использована во время текстурирования.

Фокус в том, чтобы использовать Blur Slope фильтр на слое Baked Lighting в Substance Painter, который я кладу поверх всех остальных слоев. Это позволило мне достичь стилизованного неоднородного эффекта.

Очень важно знать, что слой Baked Lighting добавляет тени в текстуру, основываясь на освещении именно от окружения в самом Substance Painter. Из-за этого, я выбрал окружение, которое не такое направленное с точки зрения освещения.

Лучший способ уменьшить направленность освещения в запеченных текстурах - это создать пару Baked Lighting слоев с разными настройками угла вращения. А затем просто убрать ненужное с помощью маски, чтобы достичь наилучшего результата.

Настройка Volume

Последняя часть проекта, о которой я хотел поговорить, это компонент Volume в Unity HDRP, он позволил мне отрегулировать освещение и пост-обработку (Post-Processing). В Volume я добавил несколько Overrides, что помогло мне достичь разных эффектов.

В виде Overrides были добавлены Visual Environment, HDRI Sky и Indirect Lighting Controller, это обеспечило мне немного окружающего освещения (Ambient Light) от неба. При использовании Indirect Lighting Controller я мог регулировать интенсивность окружающего освещения (Ambient Light), что было довольно удобно для меня.

Еще одна полезная опция, которую я действительно люблю использовать внутри Unity HDRP, это туман (Fog) и особенно объемный туман (Volumetric Fog). Я обнаружил, что лучший способ использовать его - это настроить пару компонентов Density Volume в сцене. Density Volume - это компонент, который позволяет вам вручную регулировать область, где будет отображаться туман и на сколько сильным он будет в этой области.

Я также добавил некоторые эффекты пост-обработки (Post-Processing) в Volume. Потому что мне казалось, что сцена была слишком темной и нужно было добавить больше свечения (Bloom), чтобы добиться сказочного вида, к которому я стремился. В итоге, сцена получила больше синих оттенков, особенно в тенях и я остался доволен конечным результатом.

Заключение

Я очень счастлив, что начал этот проект, даже не смотря на то, что это заняло какое-то время. Я приобрел некоторый опыт в создании стилизованных сцен и особенно важно, что теперь я знаю больше о шейдерах и HDRP. Я также должен сказать, что без отзывов моих друзей, эта сцена вероятно не получилась бы такой хорошей. Поэтому я благодарен им.

Если у вас есть какие-то вопросы об этой сцене или о творчестве в целом, то не стесняйтесь писать мне наArtStation.

Спасибо вам за прочтение! Пока!
Maciej Hernik, Level Artist

С оригинальной статьей можно ознакомиться на 80.lvздесь.

Перевод подготовлен при поддержке проектаAlmost There.

Подробнее..

Перевод Парящие Острова настраиваем стилизованные шейдера с помощью HDRP в Unity

09.05.2021 10:20:54 | Автор: admin

Автор статьи Maciej Hernik и главный редактор портала 80.lv Kirill Tokarev любезно позволили нам сделать этот перевод.

Maciej Hernik обсудил с нами детали его стилизованной сцены Парящие Острова: шейдеры для травы, деревьев и воды, Volume Overrides, текстурирование асcетов и многое другое.

Вступление

Всем привет! Меня зовут Maciej Hernik и я самоучка, художник по окружению (Level Artist) из Польши. Сколько я себя помню, я всегда обожал игры. Мое знакомство с видеоиграми произошло, когда я увидел несколько на PS1. В то время я был ребенком, у меня была мечта, что однажды я буду делать свои собственные игры, хотя я еще и понятия не имел как их делать. Я стал старше, узнал что такое 3D арт и нашел это очень интересным и увлекательным занятием. Моя страсть к игровому арту дала мне возможность получить свою первую работу и в этот момент я понял, что хочу зарабатывать на жизнь созданием игрового арта и самих игр.

Парящие Острова: идея

В начале, эта работа предполагалась, как бессрочный проект вроде площадки для экспериментов с HDRP в Unity, это бы помогло мне чуть больше ознакомиться с инструментарием HDRP. Однако, когда я сделал первые итерации шейдеров растительности и показал их своим друзьям, они вдохновили меня сделать красочную сцену с использованием этих шейдеров. Тогда то я и решил сделать эту художественную работу в стиле волшебной сказки, вдохновленный игрой The Legend of Zelda. И в этой статье я хочу разобрать некоторые компоненты из этой работы.

Начало проекта

Сначала я использовал заглушки (Placeholders) для ландшафта и несколько ассетов вроде камней, деревьев и травы, для того, чтобы понять масштаб всего острова, а также форму Монолита и деревьев. Так же я понял, что наиболее подходящие мне тени и солнечные лучи, отбрасывали деревья именно треугольной формы.

Затем я решил разместить первые итерации травы и деревьев с правильными шейдерами и материалами, и начать шлифовать эффекты для воды. В тот момент я знал, что внешний вид сцены возможно изменится в процессе работы, но благодаря этой итерации я увидел, что с точки зрения стилизации, работа идет в правильном направлении.

Монолит и вода должны были стать точкой фокуса в сцене. Это было довольно легко сделать, потому что вода получилась очень динамичной за счет анимированного шейдера или эффектов частиц, а Монолит и цепи слегка двигались сверху вниз, что немного добавляло истории.

В какой-то момент я почувствовал, что одинокий остров не так интересен как казалось. После некоторых раздумий, я решил добавить маленькие острова вокруг главного острова для завершения композиции. Поначалу я думал, что добавление этих маленьких островов может забрать на себя слишком много внимания от главного острова, однако, после того как я их чуть-чуть увеличил, добавил немного деревьев и камней на них, я понял, что они только помогают общей композиции. В конце я добавил некоторое количество облаков. На самом деле, я вручную нарисовал их на плоскостях (Planes) с прозрачностью и заставил двигаться по небу используя аниматор в Unity.

Создание травы

Шейдер травы был одним из первых созданных для этого проекта. Однако он прошел через множество итераций, чтобы достичь наиболее подходящего эффекта. Я выбрал более процедурный подход, так как посчитал, что это будет наиболее сложный и интересный путь для создания травы. Для создания шейдера я использовал Shader Graph в Unity версии 2020.1.0f1.

Давайте рассмотрим часть этого шейдера, который отвечает за покраску. Он разделен на 4 слоя, каждый из которых представляет из себя цвет. Эти цвета смешиваются между собой с помощью функции Lerp. Эти слои:

  • Primary color + Shadow color (Главный цвет + Цвет тени)

  • Additional color (Дополнительный цвет)

  • Ground color ( Цвет земли)

  • Highlights (Блики)

Primary color и Shadow color разделены на две части. Одна часть это просто цвет травы, а вторая часть отвечает за нанесение поверх другого цвета. Это достигается за счет проекции текстуры на траву из вида сверху в мировых координатах (World Position). Additional color использовался, чтобы достичь большего разнообразия травы, потому что я чувствовал, что двух цветов будет недостаточно, особенно если смотреть сверху. Ground color нужен, чтобы достичь деликатной, почти незаметной имитации окружающего затенения (Ambient Occlusion) снизу травы, чтобы немного разбить элементы. В конечном итоге, я уменьшил его, потому что он создавал слишком сильный контраст между пучками травы и это не подходило для стилизации, по-моему мнению. Именно поэтому, этот эффект окружающего затенения (Ambient Occlusion) от Ground color, очень слабо использован в этой сцене. Последний слой создает блики (Highlights) в верхней части травы, зависящие от скорости перемещения текстуры маски.

Скомбинировав все эти слои, я смог настроить финальный вид травы, изменяя параметры в материалах, и лично мне очень понравился такой подход.

Деревья

Я создал отдельный шейдер для деревьев и другой растительности, так как он немного отличается от шейдера травы. В этот раз использовался освещаемый шейдер как основу (Lit Master Shader). Благодаря этому я очень легко мог получить информацию от направления света, а также в HDRP освещаемый шейдер (Lit Master Shader) дает возможность управлять полупрозрачностью (Translucency) объекта.

Модель дерева состоит из простых плоскостей (Planes), но с измененными нормалями, чтобы достичь более мягкий вид и получить лучший контроль над распределением света по дереву. Я добился этого в Blender благодаря модификатору передачи данных (Data Transfer Modifier), который позволил мне перенести нормали c другого меша на плоскости (Planes) из которых состоит дерево.

Деревья не имеют диффузную текстуру (Diffuse). Вместо этого, в шейдере я использовал информацию о направлении света. Благодаря этому я смог регулировать полупрозрачность (Translucency) дерева, чтобы решить - как много света будет проходить сквозь него. Я так же использовал информацию о направлении света, чтобы менять цвет в освещенной части и в затененной части дерева. Эти функции были ключем для меня, и помогли достичь стилизованного вида растений.

Я также реализовал возможность регулировать цвет в зависимости от высоты объекта, по локальному значению Y. Это позволило мне создать симпатичный градиент сверху вниз, что также помогло сделать более мягкий переход от земли к дереву. В итоге, я решил добавить деликатное окружающее затенение (Ambient Occlusion) между листьями, чтобы немного разделить их.

Эффекты воды

Как я уже упоминал, вода это важная часть моей сцены, потому что она выделяется на фоне зеленой травы и деревьев и содержит много динамических частей, которые помогают сосредоточить фокус внимания зрителя на центре сцены.

Водопад на самом деле представляет из себя микс двух составляющих:

  • Вода с шейдером воды

  • Частицы пены и пузырьков

Я знал, что я хочу добиться стилизованного вида воды, вот почему я создал простой шейдер, который выполняет операции по вращению текстуры типа Voronoi, изменению ее размера и перемещению во времени. Кроме того, я использовал Waterfall Mask текстуру в красном канале, чтобы замаскировать место, где я хотел спрятать паттерн от текстуры Voronoi, например в начале водопада.

Я добавил немного частиц для симуляции пены и пузырьков, используя систему частиц Unity c кастомным мешем в виде сферы. Чтобы добиться финального вида, я поиграл с такими настройками как эмиссия, форма системы частиц и размер частиц в течение их времени жизни (Size Over Lifetime).

Для большего водопада, я применил тот же метод, только скорректировал настройки в системе частиц для увеличения размера.

Ассеты не растительного происхождения

Аcсеты, такие как камни, мост и Монолит я сделал стандартным образом - смоделировал их в Blender, заскульптил высокополигональные модели (High Poly) в ZBrush, и запек их на низкополигональные меши (Low Poly) в Substance Painter.

В Substance Painter, я просто использовал запеченные карты, чтобы придать моделям немного цвета и деталей. Однако, я хотел бы поделиться с вами одной хитростью, которая была использована во время текстурирования.

Фокус в том, чтобы использовать Blur Slope фильтр на слое Baked Lighting в Substance Painter, который я кладу поверх всех остальных слоев. Это позволило мне достичь стилизованного неоднородного эффекта.

Очень важно знать, что слой Baked Lighting добавляет тени в текстуру, основываясь на освещении именно от окружения в самом Substance Painter. Из-за этого, я выбрал окружение, которое не такое направленное с точки зрения освещения.

Лучший способ уменьшить направленность освещения в запеченных текстурах - это создать пару Baked Lighting слоев с разными настройками угла вращения. А затем просто убрать ненужное с помощью маски, чтобы достичь наилучшего результата.

Настройка Volume

Последняя часть проекта, о которой я хотел поговорить, это компонент Volume в Unity HDRP, он позволил мне отрегулировать освещение и пост-обработку (Post-Processing). В Volume я добавил несколько Overrides, что помогло мне достичь разных эффектов.

В виде Overrides были добавлены Visual Environment, HDRI Sky и Indirect Lighting Controller, это обеспечило мне немного окружающего освещения (Ambient Light) от неба. При использовании Indirect Lighting Controller я мог регулировать интенсивность окружающего освещения (Ambient Light), что было довольно удобно для меня.

Еще одна полезная опция, которую я действительно люблю использовать внутри Unity HDRP, это туман (Fog) и особенно объемный туман (Volumetric Fog). Я обнаружил, что лучший способ использовать его - это настроить пару компонентов Density Volume в сцене. Density Volume - это компонент, который позволяет вам вручную регулировать область, где будет отображаться туман и на сколько сильным он будет в этой области.

Я также добавил некоторые эффекты пост-обработки (Post-Processing) в Volume. Потому что мне казалось, что сцена была слишком темной и нужно было добавить больше свечения (Bloom), чтобы добиться сказочного вида, к которому я стремился. В итоге, сцена получила больше синих оттенков, особенно в тенях и я остался доволен конечным результатом.

Заключение

Я очень счастлив, что начал этот проект, даже не смотря на то, что это заняло какое-то время. Я приобрел некоторый опыт в создании стилизованных сцен и особенно важно, что теперь я знаю больше о шейдерах и HDRP. Я также должен сказать, что без отзывов моих друзей, эта сцена вероятно не получилась бы такой хорошей. Поэтому я благодарен им.

Если у вас есть какие-то вопросы об этой сцене или о творчестве в целом, то не стесняйтесь писать мне наArtStation.

Спасибо вам за прочтение! Пока!
Maciej Hernik, Level Artist

С оригинальной статьей можно ознакомиться на 80.lvздесь.

Перевод подготовлен при поддержке проектаAlmost There.

Подробнее..

Интеграция и серверная валидация инаппов для стора Google Play как защититься от читеров

25.05.2021 20:20:29 | Автор: admin

Онлайн-проекты рано или поздно сталкиваются со взломом внутреннего стора, когда читеры накручивают себе игровые предметы, оружие или валюту. Классика. Наш PvP-шутер не стал исключением брешь мы в итоге закрыли, хотя и пришлось повозиться.

В этой статье расскажу про интеграцию и серверную валидацию инаппов с точки зрения клиента: какой плагин использовать для Google Play и на что обращать внимание независимо от платформы, а моя коллега поделится кодом серверной части.

Как уже говорилось в блоге, наш флагманский проект это мобильный PvP-шутер с DAU около 1 млн пользователей, большинство из которых на Android. В игре сотни видов оружия и предметов. И чтобы защититься от взлома, естественно, нужна валидация покупок. Пойдем по порядку.

В Google Play наш проект использует consumable in-apps, которые после успешной покупки и валидации начисляются игроку и сразу потребляются. По историческим причинам для Google Play мы используем плагин от Prime31.

Отмечу, что если бы мы сегодня добавляли встроенные покупки с нуля на эти платформы, то взяли бы Unity IAP (а, например, на Huawei публиковались бы через Unity Distribution Portal).

В игре много спецпредложений, но мы не стали заводить отдельный id инаппа под каждую акцию, вместо этого используем один набор айдишников инаппов для разных предметов и предложений. В момент нажатия на конкретную покупку мы запоминаем контент, который нужно выдать игроку при успешной покупке. При завершении покупки выдаем его.

Перейдем к коду покупки и валидации инаппов.

На старте приложения подписываемся на события покупки:

// GoogleIABManager  класс из плагина Prime31GoogleIABManager.purchaseSucceededEvent += HandleGooglePurchaseSucceeded;

Когда игрок нажимает на инапп в интерфейсе запускаем покупку:

// GoogleIAB  класс из плагина Prime31GoogleIAB.purchaseProduct(productId);

В обработчике успешного завершения покупки оборачиваем платформо-специфичную покупку в объект, реализующий IMarketPurchase. IMarketPurchase мы используем на всех платформах, чтобы сделать код валидации кроссплатформенным. В этот интерфейс мы оборачиваем классы из плагинов конкретных магазинов.

public interface IMarketPurchase{  string ProductId { get; }   string OrderId { get; }  string PurchaseToken { get; }  object NativePurchase { get; }}class GoogleMarketPurchase : IMarketPurchase{  internal GoogleMarketPurchase(GooglePurchase purchase)  {     _purchase = purchase;  }  public string ProductId => _purchase.productId;  public string OrderId => _purchase.orderId;  public string PurchaseToken => _purchase.purchaseToken;  public object NativePurchase => _purchase;  private GooglePurchase _purchase;}internal static class MarketPurchaseFactory{// GooglePurchase  класс из плагина Prime31  internal static IMarketPurchase CreateMarketPurchase(GooglePurchase purchase)  {     return new GoogleMarketPurchase(purchase);  }}private void IapManagerOnBuyProductSuccess(PurchaseResultInfo purchaseResult){  var purchaseData = new InAppPurchaseData(purchaseResult.InAppPurchaseData);  IMarketPurchase marketPurchase = MarketPurchaseFactory.CreateMarketPurchase(purchaseData);  ValidatePurchase( marketPurchase );}

Отправляем покупку на наш сервер на валидацию:

private void ValidatePurchase(IMarketPurchase purchase){  var request = new InappValidationRequest  {     orderId = purchase.OrderId,     productId = purchase.ProductId,     purchaseToken = purchase.PurchaseToken,     OnSuccess = () => ProvidePurchase(purchase),     OnFail = () => Consume(purchase)  };   WebSocketCallbacks.Subscribe(ServerEventNames.PurchasePrevalidate, PrevalidatePurchaseHandler);   Dictionary<object, object> data = new Dictionary<object, object>();  data.Add("orderId", request.orderId);  data.Add("productId", request.productId);  data.Add("data", request.purchaseToken);  int reqId = WebSocketManager.Instance.Send(ServerEventNames.PurchasePrevalidate, data);   _valdationRequests.Add(reqId, request);}

Если валидация проходит неуспешно потребляем (Consume) продукт без начисления пользователю.

Если все хорошо потребляем продукт с начислением пользователю:

void ProvidePurchase(IMarketPurchase purchase){  GiveInGameCurrencyAndItems(purchase);  Consume(purchase);}

Важный момент: метод Consume перед отправкой в магазин запроса на потребление запоминает, что мы уже начислили покупку игроку. Это нужно, если из-за проблем с сетью (или каких-то других) запрос на консьюм не дойдет до магазина. В таком случае, когда после перезапуска приложения нам придут незаконсьюмленные покупки, мы увидим, за какие из них уже начисляли игроку валюту и предметы.

Обработчик ответа с сервера:

private const int ERROR_CODE_SERVER_ERROR = 30;private const int ERROR_CODE_VALIDATION_ERROR = 31;private void PrevalidatePurchaseHandler(Dictionary<string, object> response){  int reqId = Convert.ToInt32(response["req_id"], CultureInfo.InvariantCulture);  _valdationRequests.TryGetValue(reqId, out InappValidationRequest request);  if (request == null)     return;  _valdationRequests.Remove(reqId);  if (response["status"].Equals("ok"))  {     request.OnSuccess();  }  else  {     int code = Convert.ToInt32(response["err_code"], CultureInfo.InvariantCulture);     switch (code)     {        case ERROR_CODE_VALIDATION_ERROR:           request.OnFail();           break;        case ERROR_CODE_SERVER_ERROR:           CoroutineRunner.DeferredAction(5f, () => TryValidateAgain());           break;        default:           // неизвестная ошибка, начисляем инапп (поступаем в пользу игрока)           request.OnSuccess(null);           break;     }  }}

В случае, если сервер вернул OK в статусе валидации, производим начисление и консьюм покупки. Если сервер вернул неизвестную ошибку, трактуем результат валидации в пользу игрока.

Для следующего раздела передаю слово нашему серверному программисту Ире Поповой.

Серверная валидация

Валидация на сервере состоит из двух этапов:

  • превалидация когда данные по платежу отправляются на сервер соответствующей платформы для проверки валидности;

  • начисление в случае успешно пройденной валидации купленных позиций.

Сначала сервер получает в качестве входных параметров данные, необходимые для проведения валидации. В Android это id позиции и токен. Методы валидации являются платформо-зависимыми. Но, как правило, включают в себя логику отправки данных на сервер валидации соответствующей платформы, обработку полученного результата и возврат соответствующего ответа на клиент. Дополнительно результат валидации записывается в redis для последующей быстрой проверки при начислении.

def validate_receipt(self, uid, data, platform):    InAppSlot = PlayerProgress.first(f"player_id={uid} AND slot_id='35'")    if not InAppSlot:        raise RuntimeError(f"Fail get slot purchases: not found player:{uid} data:{data}")    tid = data.get("tid")    params = []    orders_data = []    valid_orders = []    if not tid or tid in InAppSlot.content:        return False    params = str(tid).split(self.IN_APP_ID_SEPARATOR)    if platform == "ios":        transaction_id = params[0]        product_id = params[1]        orders_data = self._get_receipt_ios(data.get("data"), data.get("test") == 1, transaction_id, product_id)        error("[VALIDATION] {} {} {}".format(transaction_id, product_id, orders_data))    elif platform == "android":        product_id = params[1]        purchase_token = data.get("data")        orders_data = self._get_receipt_android(product_id, purchase_token)    elif platform == "amazon":        receipt_sku = params[0]        user_id = params[1]        orders_data = self._get_receipt_amazon(user_id, receipt_sku)    elif platform == "huawei":        product_id = params[1]        orders_data = self._get_receipt_huawei(product_id, tid, data.get("data", ""), data.get("account_flag", 0))    elif platform == "udp":        product_id = params[1]        orders_data = self._get_receipt_udp(product_id, params[0], data.get("data", ""))    elif platform == "samsung":        product_id = params[1]        transaction_id = params[0]        product_id = params[1]        orders_data = self._get_receipt_samsung(data.get("data", ""), product_id)    else:        error("[InAppValidator] unknown platform")        return False    if not orders_data:        error(f"[InAppValidator] fail get receipt {platform} player:{uid} data:{data}")        return False    key = f"inapp:{uid}:{tid}"    for order in orders_data:        if not  order.is_success():            continue        valid_orders.append(order)        try:            self.inapp_redis.setex(key, order.to_json(), 86400)        except Exception as ex:            exception(f"[InAppValidator] fail save inapp to redis: {ex}")    if not valid_orders:        warning(f"[InAppValidator] not valid receipt {orders_data[0].order_id}")       return False    return True

Пример получения данных с соответствующего сервера валидации для Android. Для обращения к серверу Google были использованы пакеты Google для Python apiclient и oauth2client.

def _get_receipt_android(self, product_id, token):    if not self.android_authorized:        self._android_auth()    debug(f"[InAppValidator] android product_id: {product_id}, token: {token}")    try:        product = self.android_publisher.purchases().products().get(            packageName=config.GOOGLE_SERVICE_ACCOUNT['package_name'], productId=product_id, token=token).execute()            except client.AccessTokenRefreshError:        self.android_authorized = False        return self._get_receipt_android(product_id, token)    except google_errors.HttpError as ex:        if ex.resp.status == 401 or ex.resp.status == 503:            self.android_authorized = False            return self._get_receipt_android(product_id, token)        return False    if not product:        warning("[InAppValidator] android product is NONE")        return None    order_id = product.get('orderId')    if not order_id:        warning(f"order_id is NONE: {product}")        return None    return [Receipt(order_id, product.get('purchaseState', -1), product_id)]class Receipt:    def __init__(self, order_id, status, product_id, user_id=None, expire=0, trial=0, refund=0, latest_receipt=''):        self.order_id = order_id        self.status = status        self.product_id = product_id        self.user_id = user_id        self.expire = expire        if str(trial) == 'true':            self.trial = 1        else:            self.trial = 0        self.refund = refund        self.latest_receipt = latest_receipt    def is_success(self):        return self.status == 0    def is_canceled(self):        return self.status == 3    def is_valid(self):        return self.order_id and self.product_id    def to_dict(self):        return {"id": self.order_id, "s": self.status, "p": self.product_id, "u": self.user_id, "e":self.expire, "t":self.trial,"r":self.refund,"lr":self.latest_receipt}    def to_json(self):        return json.dumps({"id": self.order_id, "s": self.status, "p": self.product_id, "u": self.user_id, "e":self.expire, "t":self.trial,"r":self.refund,"lr":self.latest_receipt})

Отдельной командой/набором команд происходит начисление купленных позиций. Одна позиция может содержать разнотипные итемы (например, деньги и оружие), и для каждого типа итема на сервере существует отдельная команда начисления.

Чтобы логически объединить несколько команд, привязанных к одному действию игрока, на клиенте и на сервере введено понятие снапшота специальной конструкции, представляющей собой объединение команд, в которой ни одна команда не выполнится, если хотя бы какая-то не пройдет проверку. Можно сказать, что это некий аналог транзакций в БД. В данном случае снапшот включает специальную команду валидации и команды начисления купленных позиций.

Команда валидации:

def validate_receipt(self, data):    neededSlotsNames = [self.slotName]    self.slots = self.get_slots_data(*neededSlotsNames)    InAppSlot = self.slots.get(self.slotName, [])    tid = data.get("tid")    platform = data.get("pl")    params = []    orders_data = []    valid_orders = []    if not tid:        self.ThrowFail("not found required parameter")    elif tid in InAppSlot:        self.ThrowFail("already in slot")    if not self.IsFail():        params = str(tid).split(self.IN_APP_ID_SEPARATOR)    if not self.IsFail():        inapp_storage = InappStorage.get_instance()        if inapp_storage.exists_transaction(self.platform, params[0]):            self.ThrowFail("already_purchased {0} d".format(params[0]),                           VALIDATOR_RESULT_CODE.ALREADY_PURCHASED)            self.FinalizeRequest({self.slotName: InAppSlot}, data)            return        # Try get from redis        player_platform = self.platform        if platform is not None and int(platform) == 4:            player_platform = "udp"        _prevalidate_order = self.inapp_redis.check_tid(self._player_id, tid)        if _prevalidate_order:            orders_data = Receipt.from_json(_prevalidate_order)        elif player_platform == "ios":            transaction_id = params[0]            product_id = params[1]            if not transaction_id or not product_id:                self.ThrowFail(f"fail get receipt {self.platform}")            else:                orders_data = self._get_receipt_ios(data.get("data"), data.get("test") == 1, transaction_id, product_id)        elif player_platform == "android":            product_id = params[1]            purchase_token = data.get("data")            orders_data = self._get_receipt_android(product_id, purchase_token)        elif player_platform == "amazon":            receipt_sku = params[0]            user_id = params[1]            orders_data = self._get_receipt_amazon(user_id, receipt_sku)        elif player_platform == "huawei":            product_id = params[1]            orders_data = self._get_receipt_huawei(product_id, tid, data.get("data", ""),                                                   data.get("account_flag", 0), data.get("subscribe"))        elif platform == "udp":            product_id = params[1]            orders_data = self._get_receipt_udp(product_id, params[0], data.get("data", ""))        elif platform == "samsung":            product_id = params[1]            transaction_id = params[0]            product_id = params[1]            orders_data = self._get_receipt_samsung(data.get("data", ""), product_id)        else:            self.ThrowFail("unknown platform")    if not orders_data:        self.ThrowFail(f"fail get receipt {player_platform} {self.platform}")    if not self.IsFail():        for order in orders_data:            if order.is_success():                valid_orders.append(order)        if not valid_orders:            self.ThrowFail("already_purchased {0}".format(orders_data[0].order_id),                           VALIDATOR_RESULT_CODE.ALREADY_PURCHASED)        else:            InAppSlot.append(tid)            self.SetRequestSuccessful()    if self._player_id in LOG_PLAYER_IDS:        HashLog.error(f"[INAPP] id:{self._player_id} receipt:{data}")    self.FinalizeRequest({self.slotName: InAppSlot}, data)

Команда валидации проверяет транзакцию если есть данные превалидации, то используются они. В противном случае, данные отправляются на сервер валидации для соответствующей платформы.

В случае успешного начисления, id транзакции сохраняется в соответствующий слот игрока запись в БД, которая хранит данные по платежным транзакциям данного игрока. Во избежание взлома платежки методом, когда одну валидную транзакцию используют для многократного начисления, в рамках валидации осуществляется проверка на существование данного id транзакции.

Кроме того, в завершение начисления отправляется соответствующая статистика на сервер аналитики.

На что еще обратить внимание

Вне зависимости от платформы, для которой реализуются встроенные покупки, важно проверить и обработать следующие ситуации:

  1. При показе нативных окон магазина в процессе покупки игра может вылететь по памяти. Поэтому следует протестировать такой сценарий, чтобы удостовериться, что покупка после перезапуска корректно завершается и начисляется игроку.

  2. На большинстве платформ в процессе взаимодействия с окнами платформенного магазина приложение уходит в бэкграунд, и при завершении покупки выводится из бэкграунда. За это время игра вполне может дисконнектнуться от серверов. Если для валидации или начисления покупки нужен коннект с сервером, то после возвращения в приложение нужно будет соединиться с ним вновь, и только потом производить валидацию или начисление.

  3. Нужно тестировать сценарий, когда во время покупки и валидации игрок запускает новую покупку. Мы после тестирования этого сценария обнаружили баги и добавляли запрет запуска покупки, пока идет покупка другого инаппа.

Дополнительные ссылки

И последнее: когда мы реализовывали валидацию инаппов для Google Play несколько лет назад, нам оказалось полезной статья на Хабре, вам она тоже может пригодиться.

Также использовали решения, предложенные здесь и здесь. Ссылка на документацию по API серверной валидации Google здесь.

Подробнее..

Тир. Стрельба рейкастами на Unity 3D

06.05.2021 18:22:32 | Автор: admin

Учебные материалы для школы программирования. Часть17

Предыдущие уроки можно найти здесь:

В этом проекте рассмотрим процесс работы:

- с рейкастами и векторами;
- с методами других пользовательских классов;
- с AudioSource и с Rigidbody через код;
- три основных составляющих выстрела, психологически действующих на игрока (звук, свет и свечение, анимация и след от выстрела);
- инстанцирование префабов.

Ключевыми темами проекта выступают именно рейкасты и векторы. Последним, необходимо уделять довольно много времени каждый день, и на простых примерах объяснять, как они устроены. Но если вам удалось быстро выполнить проект с учениками, то в этом уроке будет уместно рассмотреть mecanim-систему.

Порядок выполнения

Создаём новый проект, импортируем приложенный ассет.
Помимо стандартных ресурсов пакет имеет сторонний плагин для рисовки декалей. Его работа в контексте данного урока не рассматривается.

Проект урока разбит на 2 части - тир и гранаты.

Тир

Тир - самый простой способ научить детей пользоваться рейкастами. Для упрощения, был заранее подготовлен ассет, содержащий все необходимые ресурсы, группе же перекидывается ассет, не содержащий скрипта мишени и её префаба.
Во время этой части урока необходимо объяснить, каким образом работает рейкаст и то, как можно взаимодействовать с другими скриптами из него.

Внутри проекта есть скрипт DecalShooter, который создаёт декали, и в котором расположен весь код стрельбы, включая рейкаст. В нём будет вводиться код взаимодействия с мишенью.
Для начала, необходимо подготовить саму мишень. Ею служит цилиндр, который необходимо уменьшить по Y до состоянии платины, удалить CapsuleCollider и поставить MeshCollider с галочкой Convex. Дополнительно, на цилиндр устанавливается текстура мишени, внутри цилиндра создаётся point light, подсвечивающий мишень, и объект с AudioSource для воспроизведения звука, а на сам цилиндр устанавливается Rigidbody с обработкой коллизий типа Continius Dynamic и галочкой isKinematik. У AudioSource не забудьте убрать галочку PlayOnAwake и закинуть звук попадания в мишень.

Следующим шагом становится создание скрипта Target, который сразу же накладываем на нашу мишень.

Мишень после этих действий необходимо добавить в новый префаб, чтобы при размножении мишени и внесении изменений не возникало необходимости править каждую. Теперь можно перейти к коду мишени.

using System.Collections;using System.Collections.Generic;using UnityEngine; public class Target : MonoBehaviour {    public GameObject light;    public Rigidbody rig;    public AudioSource src;     bool enabled = true;     // Use this for initialization     void Start() {        rig = GetComponent<Rigidbody>();        src = GetComponent<AudioSource>();     }     // Update is called once per frame     void Update() {     }     public void shoot() {        if (!enabled) {           return;        }         rig.isKinematic = false;         light.SetActive(false);         src.Play();         enabled = false;    } }

Код мишени содержит публичный методshoot, который необходим для того, чтобы другие скрипты могли обращаться к нашей мишени. Следующий шаг - проверка из файла DecalShooter попали ли мы в мишень и вызов методаshoot. В рейкаст необходимо включить следующий участок кода:

   if (Input.GetKeyDown(KeyCode.Mouse0)) {            time = 0.3f;             ShootSource.Play();             anim.Play("fire");             Muzzleflash.SetActive(true);            // Сама стрельба             RaycastHit hitInfo;            Vector3 fwd = transform.TransformDirection(Vector3.forward);                         if (Physics.Raycast(transform.position, fwd, out hitInfo, 100f)) {                GameObject go = Instantiate(                    DecalPrefab,                    hitInfo.point,                     Quaternion.LookRotation(                        Vector3.Slerp(-hitInfo.normal, fwd, normalization)                     )                ) as GameObject;                go.GetComponent<DecalUpdater>().UpdateDecalTo(                    hitInfo.collider.gameObject,                     true                );                Vector3 explosionPos = hitInfo.point;                Target trg = hitInfo.collider.GetComponent<Target>();                                if (trg) {                    trg.shoot();                }                                Rigidbody rb = hitInfo.collider.GetComponent<Rigidbody>();                                if (rb != null) {                    rb.AddForceAtPosition(fwd * power, hitInfo.point, ForceMode.Impulse);                    Debug.Log("rb!");                }             }            // Сама стрельба         }

Данный код пытается получить компонент из объекта hitInfo и, если это удаётся, вызывает методshoot. Мишень падает, свет от мишени выключается, звук попадания воспроизводится. Далее, желательно дать группе свободное задание по кастомизации своего проекта. Как альтернативу, можно предложить изменить код таким образом, чтобы мишень меняла цвет при попадании. Делается это заменой в Target строк:

light.SetActive(false);

на

light.GetComponent<Light>().color = Color.red;

Таким образом, свет меняется и не удаляется.

Гранаты

Эта часть урока должна ещё больше закрепить понимание векторов и работы рейкастов.

Для начала, создадим другой скрипт - дальномер.

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI; public class Lenght :  MonoBehaviour {    public Text Dalnost;    float rasstoyanie = 0; // переменная для расстояния до цели     // Use this for initialization     void Start() {     }     // Update is called once per frame     void Update() {        RaycastHit hitInfo;                if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hitInfo, 200)) {            rasstoyanie = hitInfo.distance;            Dalnost.text = rasstoyanie.ToString();        }    }}

Скрипт закинем в FPScontroller/FirstPersonCharacter. В Canvas создадим текст, закинем его в скрипт.
В этом скрипте реализован простейший рейкаст, и на его примере мы разбираем, как рейкаст передаёт информацию в структуру и как нам получать из структуры эту информацию.
При срабатывании рейкаста мы выводим дальность на экран.

Следующий шаг - бросок гранаты. Мы не используем никаких анимаций, мы создаём гранату на небольшом расстоянии от игрока и придаём ей скорость по направлению взгляда. Изменим скрипт, добавив в него поля для префаба гранаты и код броска.

using System.Collections;using System.Collections.Generic; using UnityEngine;using UnityEngine.UI; public class Length :  MonoBehaviour {    public Text Dalnost;    float rasstoyanie = 0; // переменная для расстояния до цели     public GameObject sharik;     // Use this for initialization    void Start() {     }     // Update is called once per frame     void Update() {        RaycastHit hitInfo;                 if(Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), outhitInfo, 200)) {            rasstoyanie = hitInfo.distance;             Dalnost.text = rasstoyanie.ToString ();            if(Input.GetKeyDown(KeyCode.Mouse1)) {                GameObject go = Instantiate(                    sharik,                     transform.position + Vector3.Normalize(hitInfo.point - transform.position),                     transform.rotation                );                Rigidbody rig = go.GetComponent<Rigidbody>();                rig.velocity = Vector3.Normalize(hitInfo.point - transform.position) * 10;            }         }    } }

Сразу же возникнет вопрос - зачем такое решение? К тому же, при направлении в небо бросок не произойдёт. Сделано всё это в учебных целях. Т.к. в рамках урока код пишется поэтапно, на данном участке объясняется, как найти вектор направления на объект по конечным и начальным координатам. Теперь дело за самой гранатой. Создадим скрипт Grenade.

using System.Collections;using System.Collections.Generic;using UnityEngine; public class Grenade :  MonoBehaviour {    public Transform explosionPrefab;     void OnCollisionEnter(Collision collision) {        ContactPoint contact = collision.contacts[0];                // Rotate the object so that the y-axis faces along the normal of the surface        Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal);        Vector3 pos = contact.point;         Instantiate(explosionPrefab, pos, rot);        Destroy(gameObject);    }}

Закидываем на сцену гранату, на меш Body ставим коллайдеру Convex, добавляем гранате RIgidbody и наш скрипт. Получившуюся гранату добавляем в префаб и удаляем со сцены.

Скрипт удаляет объект, на котором висит, но до этого инстантиирует объект, который мы выбираем в качестве эффекта взрыва.

Создадим эффект взрыва. В нём должен быть свет от взрыва, AudioSource с галочкой PlayOnAwake и звуком взрыва, Spital Blend на 90 процентов переведённый в 3д и увеличенный радиус распространения звука.
Для правильной отработки всех эффектов и разлёта Rigidbody нужно создать ещё один скрипт. Его мы назовём Explosion:

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Explosion :  MonoBehaviour {    public float radius = 5.0f;    public float power = 10.0f;    public GameObject svet;    void Start() {        Destroy(svet, 0.1f);        Vector3 explosionPos = transform.position;        Collider[] colliders = Physics.OverlapSphere(explosionPos, radius);        foreach(Collider hit in colliders) {            Rigidbodyrb = hit.GetComponent<Rigidbody>();            if (rb != null) {                rb.AddExplosionForce(power, explosionPos, radius, 3.0f);            }        }    }}

Его нужно закинуть на эффект взрыва и создать префаб.
Данный префаб и используется в скрипте гранаты для создания взрыва.

Готово!

Подробнее..

Перевод Предупреждение для разработчиков о грядущих критических изменениях в движке

16.06.2021 14:10:53 | Автор: admin

Поддержка движка отстает, а исправление положения - задача не из легких

Разработчик программного обеспечения Unity Джош Питерсон рассказал нам о будущем поддержки .NET в широко используемом движке для разработки игр.

Согласно опросу, проведенному в конце прошлого года, использование C# в разработке игр является одной из основных причин популярности C#, но его реализация в Unity несколько беспорядочна.

Обработчик сценариев C# использует Mono, но разработчики также могут использовать .NET Framework при работе в Windows. Mono - это старая реализация .NET с открытым исходным кодом, созданная до того, как Microsoft выпустила .NET Core. Microsoft получила контроль над Mono вместе с Xamarin в 2016 году, и Mono теперь имеет много общего кода с .NET Core, но он все равно остается отдельным продуктом, в котором по-прежнему в некоторых сценариях используется рантайм.

Unity поддерживает собственный форк Mono, который, по словам Питерсона, примерно на два года отстает от upstream кода. Сейчас команда обновляет его до последней версии кода из upstream репозитория Mono - изменение, в котором он уверен на 95%, что оно попадет в следующий релиз, Unity 2021.2. Он добавил, что эта работа улучшит производительность и исправит ошибки, но сама по себе не привнесет никаких новых фич .NET - хотя она закладывает фундамент для фич, которые будут добавлены в будущем.

Тем не менее, Питерсон ожидает, что в Unity 2021.2 будет добавлена поддержка .NET Standard 2.1, но на этот раз с 75% уверенностью. Версии .NET Standard определяют набор API, которые должна поддерживать реализация .NET. Сложный аспект .NET Standard 2.1 заключается в том, что .NET Framework навсегда застрял на .NET Standard 2.0. Питерсон говорит: Хотя .NET Framework не поддерживает .NET Standard 2.1, библиотеки классов Mono его поддерживают, поэтому мы должны быть в состоянии выстроить хороший мост к экосистеме на основе .NET Core.

Это обновление не может произойти быстро, поэтому некоторые разработчики разочарованы таким медленным прогрессом. Предпринимаются ли какие-либо подвижки в направлении отказа от Mono в пользу полной интеграции .NET? Особенно сейчас, когда .NET становится настолько кроссплатформенным, - спросил пользователь в августе прошлого года. К числу востребованных фич относятся Span<T>, представленный в C# 7.2, и оператор диапазона, представленный в C# 8.0. Microsoft выпустила C# 8.0 в сентябре 2019 года, и внедрение полного набора фич в Unity заняло много времени. Пользователи также обеспокоены отставанием в производительности .NET в Unity.

Питерсон говорит, что поддержка C# 8.0 в 2021 году по-прежнему будет реализована на основе Mono. Он также выразил надежду, что C# 9.0, выпущенный Microsoft в ноябре 2020 года, также будет поддерживаться, но это зависит от добавления фич в Mono и IL2CPP (который преобразует код .NET в C++ для компиляции), в чем его уверенность снизилась до 50%, сказал он.

Что касается перехода на .NET Core, это вряд ли будет скоро. Питерсон сказал, что Unity, вероятно, откажется от .NET 5 в пользу .NET 6, который является предстоящим релизом с долгосрочной поддержкой. Даже тут он заметил, что похоже, что JIT рантайм здесь будет Mono, но он не уверен в этом и добавил, что нам может потребоваться перейти непосредственно к CoreCLR в целях поддержки .NET 6.

Одна из проблем заключается в том, что функция редактора Unity, называемая перезагрузкой домена (domain reloading), которая сбрасывает состояние сценария, зависит от функции (AppDomains), которой нет в .NET Core. Питерсон говорит, что это может быть реализовано другим способом, но это будет критическое изменение. Для разработчиков игр .NET 6 в любом случае станет критическим изменением, поскольку любые сборки, скомпилированные с использованием mscorlib.dll из экосистемы .NET Framework, не будут работать и должны быть перекомпилированы.

Сложность, связанная с .NET Standard, .NET Framework, .NET Core и Mono, является проблемой для разработчиков Unity и показывает, что унификация .NET, которую затеяла Microsoft, на самом деле является длительным процессом, а не тем, что может произойти в одночасье с выпуском .NET 5.0 в прошлом году.

Единственное, что меня волнует, это поддержка .NET 6. Самая большая проблема, с которой я столкнулся, заключалась в низкой производительности редактора и длительном времени итерации по мере увеличения размера проекта. В настоящее время я отказался от Unity, потому что его было слишком неудобно использовать, и перешел на Unreal, - сказал другой пользователь, добавив, что Mono скоро станет историей, и у него нет будущего.

В отдельном посте Питерсон подчеркнул, что все, что здесь обсуждается, может быть изменено. Пожалуйста, не рассматривайте обсуждения в этой ветке как официальные объявления. Вместо этого он сказал, что он скорее предлагает нам понимание их прогресса.


В преддверии старта курса "Unity Game Developer. Professional" приглашаем всех желающих посетить бесплатный двухдневный интенсив в рамках которого мы разработаем все необходимые инструменты и архитектуру для диалоговой системы (чтобы наш персонаж мог общаться с неигровыми персонажами), реализуем инвентарь, добавим в игру торговцев и создадим систему квестов. Всего два занятия и практически готовая RPG у вас в кармане.

Подробнее..

Еще пять инструментов против читеров на мобильном проекте с DAU 1 млн пользователей

27.04.2021 20:11:49 | Автор: admin

Когда-то нам пришлось полностью переработать защиту популярного PvP-шутера. Результатом стал ряд инструментов, которые мы готовили и зарелизили одновременно, чтобы не дать читерам возможность постепенно отслеживать апдейты.

Про первые пять решений обфускацию, хранение данных, миграцию прогресса и систему бана шла речь в этойстатье. Сегодня расскажу про остальные, а именно:

  • Защита от измененных версий.

  • Photon Plugin.

  • Серверная валидация инаппов.

  • Защита от взлома оперативной памяти.

  • Собственная аналитика.

И немного про то, почему так важен был одновременный релиз всех решений.

Чтобы не возникло путаницы, продолжим нумерации из прошлой статьи, где уже были опубликованы пять пунктов.

Решение 6. Защита от измененных версий

В дополнительные места мы расставили защиту от переподписывания версий, лаунчеров (на Android) и твиков (на iOS), спрятав уже в обфусцированном коде.

Проверка на твики (iOS)

На устройствах с Jailbreak с помощью Cydia пользователи могут устанавливать твики, которые способны внедрять свой код в системные и установленные приложения. Каждый твик имеет информацию (файл *.plist), с какими бандлами они должны работать.

Механизм детекта осуществляется проверкой этих файлов в папке /Library/MobileSubstrate/DynamicLibraries/ (на наличие внутри нашего бандла).

Большинство твиков, скрывающих Jailbreak, способно спрятать эту папку от просмотра, поэтому есть способ просматривать ее через созданный нами симлинк.

string finalPath = string.Empty;string substratePath = "/Library/MobileSubstrate/DynamicLibraries/";bool bySymlink = false;if (!Directory.Exists(substratePath)) //Если папки не существует (скрыт твиком xCon), то пытаемся получить доступ к файлам через созданный нами симлинк{string symlinkPath = CreateSymlimk(substratePath);if (!string.IsNullOrEmpty(symlinkPath)){bySymlink = true;finalPath = symlinkPath;}}else{finalPath = substratePath;}bool detected = false;string detectedFile = string.Empty;try{if (!string.IsNullOrEmpty(finalPath)){string[] plistFiles = Directory.GetFiles(finalPath, "*.plist"));foreach (var plistFile in plistFiles){if (File.Exists(plistFile)){StreamReader file = File.OpenText(plistFile);string con = file.ReadToEnd();string bundle = "app_bundle"; if (con.Contains(bundle)){detectedFile = plistFile;detected = true;break;}}}}}catch (Exception ex){Debug.LogError(ex.ToString());}

Но также есть твики, которые запрещают создание симлинков по проверяемому нами пути (KernBypass, A-Bypass). При их наличии мы не можем осуществить проверку, поэтому считаем это за возможное читерство.

Общего механизма детекта таких твиков нет, тут нужен индивидуальный подход.

Детект KernBypass (который был активен в отношении нашего бандла):

if (File.Exists("/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist") {StreamReader file = File.OpenText("/var/mobile/Library/Preferences/jp.akusio.kernbypass.plist"); string con = file.ReadToEnd();if (con.Contains("app_bundle") {//detected}}

Определение запуска через лаунчер (Android)

Запуск приложения через лаунчер это, по сути, запуск вашего приложения внутри другого приложения (по типу Parallel Space). Некоторые реализации взломов используют такой механизм для внедрения своего кода, и для этого на устройстве не требуется root-доступ. Обычно они имитируют всю среду: выделяют папку под файлы приложения, возвращают фейковый Application Info и так далее.

При таком запуске у нас все равно сохраняется доступ ко всем файлам, к которым имеет доступ сам лаунчер. Самый простой способ детекта это проверить доступ к материнской папке от нашего приложения (dataDir в applicationInfo) через функцию access (в нативном коде). В обычном случае операционная система не предоставит доступ, а в случае лаунчера это будет папка, которая все еще находится внутри Persistent Data приложения.

Код для плагина на C:

JavaVM*java_vm;jint JNI_OnLoad(JavaVM* vm, void* reserved) {        java_vm = vm;    return JNI_VERSION_1_6;}int CheckParentDirectoryAccess(){    JNIEnv* jni_env = 0;    (*java_vm)->AttachCurrentThread(java_vm, &jni_env, NULL);    jclass uClass = (*jni_env)->FindClass(jni_env, "com/unity3d/player/UnityPlayer");    jfieldID activityID = (*jni_env)->GetStaticFieldID(jni_env, uClass, "currentActivity", "Landroid/app/Activity;");    jobject obj_activity = (*jni_env)->GetStaticObjectField(jni_env, uClass, activityID);    jclass classActivity = (*jni_env)->FindClass(jni_env, "android/app/Activity");        jmethodID mID_func = (*jni_env)->GetMethodID(jni_env, classActivity,                                                      "getPackageManager", "()Landroid/content/pm/PackageManager;");        jobject pm = (*jni_env)->CallObjectMethod(jni_env, obj_activity, mID_func);        jmethodID pmmID = (*jni_env)->GetMethodID(jni_env, classActivity,                                                      "getPackageName", "()Ljava/lang/String;");        jstring pName = (*jni_env)->CallObjectMethod(jni_env, obj_activity, pmmID);    jclass pm_class = (*jni_env)->GetObjectClass(jni_env, pm);    jmethodID mID_ai = (*jni_env)->GetMethodID(jni_env, pm_class, "getApplicationInfo","(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;");    jobject ai = (*jni_env)->CallObjectMethod(jni_env, pm, mID_ai, pName, 128);    jclass ai_class = (*jni_env)->GetObjectClass(jni_env, ai);        jfieldID nfieldID = (*jni_env)->GetFieldID(jni_env, ai_class,"dataDir","Ljava/lang/String;");    jstring nDir = (*jni_env)->GetObjectField(jni_env, ai, nfieldID);        const char *nDirStr = (*jni_env)->GetStringUTFChars(jni_env, nDir, 0);    char parentDir[200];    snprintf(parentDir, sizeof(parentDir), "%s/..", nDirStr);    if (access(parentDir, W_OK) != 0)    {         return 1;    }else{ return 0;}}

Защита от переподписи apk (Android)

При любом вмешательстве внутрь apk-файла пакет необходимо переподписать, иначе система не позволит его установить. Поэтому можно определить модифицированную игру, если хеш подписи не совпадает с нашим.

Однако такую системную защиту можно отключить, если на устройстве есть root-доступ, и устанавливать пакеты с невалидной подписью. Поэтому данный пункт не является каким-то особенно важным, но и не будет лишним в общей массе.

Получение хеша подписи в С# через обращение в Java-код:

Lazy<byte[]> defaultResult = new Lazy<byte[]>(() => new byte[20]);            if (Application.platform != RuntimePlatform.Android)                return defaultResult.Value;#if UNITY_ANDROIDvar unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");if (unityPlayer == null)throw new InvalidOperationException("unityPlayer == null");var _currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");if (_currentActivity == null)throw new InvalidOperationException("_currentActivity == null");            var packageManager = _currentActivity.Call<AndroidJavaObject>("getPackageManager");            if (packageManager == null)                throw new InvalidOperationException("getPackageManager() == null");            // http://developer.android.com/reference/android/content/pm/PackageManager.html#GET_SIGNATURES            const int getSignaturesFlag = 64;            var packageInfo = packageManager.Call<AndroidJavaObject>("getPackageInfo", PackageName, getSignaturesFlag);            if (packageInfo == null)                throw new InvalidOperationException("getPackageInfo() == null");            var signatures = packageInfo.Get<AndroidJavaObject[]>("signatures");            if (signatures == null)                throw new InvalidOperationException("signatures() == null");            using (var sha1 = new SHA1Managed())            {                var hashes = signatures.Select(s => s.Call<byte[]>("toByteArray"))                    .Where(s => s != null)                    .Select<byte[], byte[]>(sha1.ComputeHash);                var result = hashes.FirstOrDefault() ?? defaultResult.Value;                return result;            }#else            return defaultResult.Value;#endif

Решение 7. Photon Plugin

Для организации сетевого взаимодействия между пользователями в игровых комнатах мы используем Photon Cloud, который сам по себе предполагает отсутствие серверной логики и отвечает только за пересылку пакетов между пользователями. А для защиты кора нам нужна была именно серверная логика.

Переделывать всё на Photon Server было бы достаточно долгой и сложной задачей, так как в игре уже было много различных режимов, механик и прочего. Поэтому, пообщавшись с ребятами из Photon, мы решили попробовать Photon Plugin.

Photon Plugin доступен на тарифе Enterprise Cloud и пишется на С#. Он запускается на серверах Photon и позволяет мониторить пересылаемый между пользователями игровой трафик, добавлять серверную логику, которая может:

  • блокировать или добавлять сетевые сообщения;

  • контролировать изменения свойств комнат и игроков;

  • кикать из комнаты;

  • взаимодействовать при помощи http-запросов со сторонними серверами.

Мы переписали сетевое взаимодействие игроков так, чтобы можно было отследить тех, у кого действия выходят за рамки допустимого (например, количество здоровья, урон, скорость перемещения, использование запрещенных предметов и так далее), или код изменен так, что нарушается логика режимов.

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

Решение 8. Серверная валидация иннапов

В нашей игре сотни видов оружия и предметов. Несмотря на баланс и большое влияние личного скилла игроков на результат матча, все инаппы нужно защитить от взлома. Для этого мы ввели валидацию покупок.

Валидация на сервере состоит из двух этапов:

  1. Превалидация. Когда данные по платежу отправляются на сервер соответствующей платформы для проверки валидности.

  2. Начисление. В случае успешно пройденной валидации купленных позиций.

Сначала сервер получает в качестве входных параметров данные, необходимые для проведения валидации (например, на Android это id инаппа и токен).

Отдельной командой/набором команд происходит начисление купленных позиций. Одна позиция может содержать разнотипные итемы (например, деньги и оружие) и для каждого типа итема на сервере существует отдельная команда начисления.

Чтобы логически объединить несколько команд, привязанных к одному действию игрока, на клиенте и на сервере мы ввели понятие снапшота. Это специальная конструкция из последовательности команд, которые одновременно выполняются или не выполняются на сервере.

Команда валидации проверяет транзакцию в случае, если есть данные превалидации, то используются они. В противном случае данные отправляются на сервер валидации для соответствующей платформы.

В случае успешного начисления id транзакции сохраняется в соответствующий слот игрока запись в БД, которая хранит данные по платежным транзакциям данного игрока. Во избежание взлома платежки методом, когда одну валидную транзакцию используют для многократного начисления, в рамках валидации осуществляется проверка на существование данного id транзакции.

Кроме того, в завершение начисления отправляется соответствующая статистика на сервер аналитики.

Подробнее про интеграцию инаппов и серверную валидацию вместе с кодом расскажем в отдельной статье.

Решение 9. Защита от взлома оперативной памяти

Локальные данные, которые не защищены валидацией с сервера, можно взломать в памяти (например, с помощью GameGuardian на Android). Механизм взлома заключается в поиске значений в памяти путем отсеивания. Ищется текущее значение, затем оно изменяется в игре, а среди найденных адресов в памяти они отсеиваются по новому значению, пока не будет найден нужный адрес.

Для их защиты они засаливаются при помощи случайно сгенерированной соли:

 internal int Value{get { return _salt ^ _saltedValue; }set { _saltedValue = _salt ^ value; }}

Когда пользователь не в состоянии изменить искомое значение для отсеивания, он может попытаться заменить все найденные значения на свои. Для их детекта используется простая ловушка. Следующий пример показывает, как можно определить вмешательство в память с числами от 0 до 1000 (заранее храним массив чисел, которые никогда не должны измениться, кроме как после редактирования памяти).

private static int[] refNumbers;internal static void Start(){refNumbers = new int[1000];for (int i = 0; i < refNumbers.Length; i++) {refNumbers[i] = i;}}internal static bool Check(){for (int i = 0; i < 1000; i++) {if (!refNumbers [i].Equals(i))return true;}}

Решение 10. Собственная аналитика

Изначально мы пользовались платным решением от devtodev и бесплатным от Flurry. Основная проблема была в отсутствии детализации происходящих в игре событий. Мы собирали только агрегированные данные и поверхностные метрики.

Но с ростом экспертизы внутри команды стала очевидной необходимость писать собственное решение с нужными именно нам фичами. Основная цель была в повышении продуктовых метрик и повороте компании в сторону Data-Driven подхода. Но в итоге аналитика также стала незаменимым инструментом в борьбе с читерами.Например, раньше система не привязывалась к пользователю, а просто считала ивенты. То есть мы знали, что 500 человек совершили какие-то действия, но кто эти 500 человек, и что они делали до этого нет. Сейчас можно посмотреть все действия каждого конкретного игрока и, соответственно, отследить подозрительные операции.

Все пользовательские ивенты отправляются в одну большую SQL-евскую базу. Там есть как элементарные ивенты (игрок залогинился, сколько раз в день он залогинился и так далее), так и другие. Например, прилетает ивент, что игрок покупает оружие за столько-то монет, а вместо суммы написано 0. Очевидно, что он сделал что-то неправомерное.Большинство выгрузки с подозрительными действиями нарабатываются с опытом. Например, у нас есть скрипт, который показывает, что столько-то людей с конкретными id получили определенное большое количество монет. Но это не всегда читеры обязательно нужно проверять.

Также читеров опознаем по несоответствию значений начисления валют. Аналитик знает, что за покупку инаппа начисляется конкретное количество гемов. У читеров часто это количество бывает 9999 значит, что-то взломали в памяти. Еще бывают игроки с аномальными киллрейтами. По ним у нас тоже есть специально обученное поле, и когда появляется пользователь, у которого киллрейт 15 или 30, становится понятно, что, скорее всего, это читер.В основном отслеживанием занимается один скрипт, который пачкой прогоняет по детектам и сгружает все в таблицу. Аналитики получают id и видят игроков, которые залогинились утром с огромным количеством голды, в соседнем листе лежат игроки, открывшие 1000 сундуков, в следующем игроки с тысячей гач и так далее. Затем вариантов несколько.

Предполагаемых читеров можно прогнать по флагам, а так как подобные способы читерства задетекчены и пофиксены, то подобные игроки уже могли ранее получить значок читера и попасть в отдельную базу. Если они там есть, то бан прилетает автоматически с помощью скрипта.

Если нет детекта, данные передаются разработчикам. Они думают, как читер смог обойти защиту и решают, как закрыть эти дыры, как таких игроков детектить и банить.А про причины перехода на собственную аналитическую систему и внутренние фичи тоже расскажем в отдельном материале.

Одновременный релиз всех решений

Чтобы достичь максимального эффекта от всех новых защит, было решено выкатить их одномоментно. Так значительно повышался входной порог для тех взломщиков, которые уже были хорошо знакомы с нашим необфусцированным кодом.

Кроме того, мы не просто применили обфускацию приложения, но и изменили структуру большинства значимых частей приложения. В корне была изменена работа внутри, и все это свело к минимуму имеющиеся к тому моменту у читеров наработки. А разбираться в новом стало куда гораздо сложнее.

Всего на глобальный ввод большинства защит ушло около семи месяцев.

Самым масштабным пунктом была реализация системы хранения на наших серверах именно она определяла запуск в продакшен всех решений из списка. Кроме аналитики, которая развивалась самостоятельно.

Также было важно развивать приложение в целом и релизить апдейты для игроков, чтобы не потерять их интерес, пока глобальные решения против читеров складывались отдельно в ожидании своего момента.

Сейчас игра надежно защищена и не имеет распространенных методов взлома. Иногда встречаются единичные случаи, но они быстро отслеживаются благодаря введенным инструментам.

Подробнее..

Недельный геймдев 15 25 апреля, 2021

28.04.2021 06:05:22 | Автор: admin

Из новостей на этой неделе:


  • Стал доступен Agility SDK для DirectX 12
  • Новый формат файлов для сжатых реалистичных 3D-текстур на любом графическом процессоре
  • Вышел Godot 3.3 с фокусом на оптимизацию и надёжность
  • The Blender Foundation представили Cycles X
  • Дорожная карта Blender 2021
  • Rider для Unreal Engine 2021.1.1 с поддержкой macOS
  • В Visual Studio 2022 завезут x64
  • Состоялся полноценный релиз Steam Playtest


Из интересного:


  • Набор Rural Australia для UE4 теперь бесплатен
  • Мастер-класс по работе со светом в CRYENGINE
  • Пример того, чего можно добиться Roblox
  • Занятный прототип в VR
  • Моделирование прыжков в высоту. Новый подход к воссозданию спортивных движений в 3D
  • Как создавалось оружие в Cyberpunk 2077




Обновления/релизы/новости


Стал доступен Agility SDK для DirectX 12



Этот набор инструментов позволяет разработчикам использовать новые функции DirectX без необходимости игрокам обновлять Windows. Теперь разработчики игр могут исходить из того, что большинство компьютеров с Windows будут работать со всеми функциями DirectX 12.


Игры могут использовать Agility SDK на системах с обновлением Windows 10 за ноябрь 2019 г. или новее. Разработчики уже могут ознакомиться со стартовым руководством, страницей загрузок.


Новый формат файлов для сжатых реалистичных 3D-текстур на любом графическом процессоре



Khronos объявили о ратификации KTXTM 2.0, добавив в этот контейнерный поддержку суперсжатия Basis Universal.


Команда обещает надёжное и повсеместное распространение текстур GPU. Basis Universal это технология сжатия, разработанная Binomial, которая позволяет создавать компактные текстуры, которые можно эффективно транскодировать в различные сжатые форматы на GPU во время выполнения.


Вышел Godot 3.3 с фокусом на оптимизацию и надёжность



После 7 месяцев разработки вышла новая версия. Godot 3.3 совместим с Godot 3.2.x, всем пользователям 3.2.x рекомендуется обновиться.


Как и предыдущая версия 3.0, предстоящая версия Godot 4.0 будет значительным изменением в экосистеме Godot, и ожидается, что пользователи будут продолжать использовать Godot 3.x в течение некоторого времени, пока Godot 4.x не станет достаточно стабильным.


The Blender Foundation представили Cycles X



Крупную грядущую переработку рендерера Cycles.


Новая версия, которая должна быть включена в Blender 3.1, значительно улучшает время рендеринга на современных графических процессорах и интерактивность вьюпорта, а также добавит поддержку возобновляемого и инкрементного рендеринга.


Дорожная карта Blender 2021



Первая альфа-версия Blender 3.0 совсем недавно была выпущена, и в ближайшем будущем ожидается много нового, как указано в дорожной карте разработки Blender 2021.


Blender 2.93 выйдет в конце мая. Как и версия 2.83, это будет LTS выпуск, который будет поддерживаться в течение двух лет.


Во втором квартале начинается проект по работе над пайплайном анимации персонажей.


Импорт USD появится после выпуска Blender 2.93.


Rider для Unreal Engine 2021.1.1 с поддержкой macOS



Rider для Unreal Engine отмечает год с момента релиза первой превью версии и в новом обновлении получает поддержку macOS и uproject.


Поддерживаются почти все фичи, которые есть в Windows версии.


В Visual Studio 2022 завезут x64. Ну и куча других улучшений



Ну и куча других улучшений.


  • Улучшения производительности в основном отладчике.
  • Поддержка .NET 6, можно использовать для создания веб-приложений, стендэлон и мобильных приложений как для Windows, так и для Mac, а также улучшенная поддержка для разработки приложений под Azure.
  • Пользовательский интерфейс обновился.
  • Поддержка инструментов C++ 20.
  • Интеграция текстового чата в функцию совместной работы Live Share.
  • Дополнительная поддержка Git и GitHub.
  • Улучшен поиск по коду.

Состоялся полноценный релиз Steam Playtest



Инструмент позволяет разработчикам бесплатно настраивать и запускать плейтесты в Steam. В последнем обновлении Valve добавили возможность предоставлять доступ к плейтесту для игроков из определённой страны. Это может быть полезно, если у вас есть региональные серверы, для которых необходимо провести нагрузочное тестирование перед полноценным запуском игры во всём мире.


Кроме того, у Playtest приложений собственный центр сообщества, поэтому у них также могут быть свои собственные события и объявления.


Халява/раздачи/бандлы/курсы


Набор Rural Australia для UE4 теперь бесплатен



Команда Unreal Engine в сотрудничестве с Эндрю Сванбергом Гамильтоном сделала пакет Rural Australia Environment Pack бесплатным. Rural Australia это коллекция ассетов, основанных на фотограмметрии, снятых в Австралии, что даёт пользователям возможность создавать прекрасные и точные изображения ландшафтов.


Интересные статьи/видео


Как создавалось оружие в Cyberpunk 2077



Чаба Силаги, старший художник в CD PROJEKT RED, рассказывает о создании оружия в Cyberpunk 2077.


Мастер-класс по работе со светом в CRYENGINE



Пример того, чего можно добиться Roblox



Занятный прототип в VR



И круто, и неприятно.


Моделирование прыжков в высоту



Новый подход к воссозданию спортивных движений в 3D.


В новой работе Discovering Diverse Athletic Jumping Strategies описываются стратегии, которые реализованы как политики управления физическими персонажами. По словам авторов, сочетание симуляции физики и глубокого обучения с подкреплением (DRL) обеспечивает подходящую отправную точку для обучения.


Исследователи заявляют, что новый метод предлагает более реалистичные результаты и большую гибкость. Подробнее можно почитать тут.

Подробнее..

Ремастеринг игрового контента, или как создать 800 единиц контента за семь месяцев

17.05.2021 12:14:39 | Автор: admin

Как переделать весь контент в игре в короткие сроки, какие методы в этом помогут, с какими трудностями мы столкнулись и как их решали этим аспектам разработки ремастеров посвящена данная статья.

Итак, перед нами стояла задача переработки почти всего контента в игре: графического пайплайна, роботов, пушек, визуальных эффектов, карт и т. д. Для этого нужно было внести множество технологических изменений в существующий код и поддержать эти изменения со стороны контента. Этот процесс занимает немало времени, но мы не могли остановить разработку самой игры для внесения всех этих изменений, ведь одно из требований к ремастеру параллельная разработка с основным продуктом. Поэтому в первую очередь нам необходимо было выстроить процесс работы программистов и художников команды War Robots Remastered в рамках основного проекта. И для решения этой проблемы мы пошли по пути итерационной разработки и системы обратной совместимости.

Организация технического процесса работы над ремастером

Разработка War Robots ведется в системе управления версиями git по модели git-flow. В проекте есть основная ветка (develop), в которую по мере готовности вливаются ветки фичей (features). Из основной ветки создаются релизы, которые затем уходят в прод.

В эту схему мы добавили три элемента:

  1. Ветку Remastered-develop, содержащую весь актуальный контент для проекта War Robots Remastered. Мы договорились о том, что эта ветка будет обратно совместимой с актуальной основной веткой игры. Это значит, что в этой ветке будут использоваться те же технологии и все фичи из ветки develop.

  2. Ветки фичей для ремастера: Remastered/feature/branch. В этих ветках ведется работа над фичами, относящимися к контенту и графическому пайплайну ремастера. Они создаются от Remastered-develop и вливаются в нее же.

  3. Ветки фичей для поддержания обратной совместимости feature/branch: в этих ветках ведется работа над технологиями, необходимыми в первую очередь для ремастера, но несовместимыми с основной веткой War Robots. К таким фичам относятся система загрузки ассетов, система управления качеством (Quality Manager) и т. д.

Процесс работы получался следующим:

  1. В Remastered-develop постоянно заливалась ветка develop с актуальным кодом и контентом основного проекта;

  2. Геймдизайнеры, художники и графические программисты, работающие над контентом для ремастера, работали в ветках Remastered/feature/branch;

  3. Все новые технологии, ломающие обратную совместимость, сначала попадали в develop War Robots, а потом уже в develop War Robots Remastered.

Такая схема позволила нам работать над проектом итеративно, не опасаясь того, что War Robots Remastered не будет содержать тех же фичей, что и основная версия игры.

Однако поддержание обратной совместимости имеет свою цену: все фичи, кардинально меняющие технологии на проекте например, система загрузки ассетов, необходимо вносить сначала в основную ветку War Robots, а потом уже в ремастер. Это нетривиальный процесс: раз эти фичи подают в develop, значит, они попадают и в релизы. А чтобы вывести фичу в релиз, ее необходимо согласовать с годовым релиз-планом проекта, выбрать релиз, в который ее сможет забрать и поддержать команда основного продукта, и полноценно протестировать силами QA-отдела. Это увеличивало время разработки ремастера. Однако, как плюс, мы получили то, что на момент релиза War Robots Remastered большинство технологий уже были обкатаны в продакшене, и мы снимали часть технических рисков с запуском проекта.

Как мы переделывали контент для трех качеств ремастера и чем нам помог переход на Unity 2018

Итак, когда фундамент заложен, и у нас появилось понимание того, как будет организован процесс работы над проектом, пора приступать к самому ремастеру. И первая проблема, которую мы будем решать с момента старта проекта и почти до самого его релиза, это его размер.

За свою семилетнюю историю War Robots успела обрасти множеством фичей и еще большим количеством контента. К моменту релиза ремастера в игре существовали:

  • 81 робот;

  • 109 пушек;

  • 83 единиц эквипа: щиты, модули, встроенные абилки;

  • 10 дронов.

Весь контент необходимо было пересобрать в Unity: обновить материалы, анимации, VFX и подготовить все это в трех качествах: Ultra Low Definition (ULD), Low Definition (LD), High Definition (HD). А после пересборки еще и протестировать.

Итого мы имеем: 81 робота, 109 пушек, 83 эквипа, 10 дронов в трех качествах 849 единицы контента.

В Unity используется понятие префаба (шаблона) как единицы контента. Это отдельный объект, включающий в себя модель, текстуры, материалы, анимации, визуальные и звуковые эффекты, сконфигурированные геймдизайнером. И его можно использовать в процессе работы приложения.

Сборка нового префаба робота из готовых ассетов в Unity может занимать от 1 до 3 дней, его полное тестирование, включая логику и визуал, от 2 до 3 дней. Несложно подсчитать, что на сборку и тестирование одних только роботов у нас ушло бы 11 месяцев, и это без учета рисков в виде задержек со стороны подготовки арта, багов и т.д. Это нас не устраивало, ведь мы хотели завершить проект менее, чем за год.

Перед нами стояла еще одна проблема: единожды собранный робот в трех качествах не мог оставаться неизменным в течение целого года. В War Robots постоянно исправляются какие-то баги, меняется баланс, добавляется что-то новое. И если раньше геймдизайнеру нужно было внести изменения в один префаб робота, то теперь необходимо было это сделать в четырех местах: в основном проекте War Robots и в трех префабах качеств на War Robots Remastered. Это существенно увеличивало риск возникновения ошибок в связи с человеческим фактором, а мы хотели бы этого избежать.

Нам необходимо было автоматизировать процесс сборки контента и облегчить процесс тестирования. К счастью, в момент старта проекта War Robots Remastered в релиз-плане ванильной War Robots был запланирован переход на новую на тот момент версию Unity 2018 LTS. Эта версия Unity добавляла в движок новую технологию Prefab Variants, которой мы и решили воспользоваться.

Наша идея заключалась в том чтобы создать единую базу для каждого робота (пушки, дрона и т. д.), содержащую всю игровую логику и общие компоненты для всех качеств: настройки абилок, звуков, точки креплений пушек и т. д., а также создать префаб-варианты для каждого пресета качества (ULD, LD, HD), которые будут содержать только визуальную составляющую: геометрию, материалы, VFX.

Для примера базовый префаб робота Cossack и его HD-префаб вариант:

Красным выделены объекты, присущие только HD-качеству: скелет, геометрия и материал, а также дополнительные звуковые эффекты.

Схема разделения префабов:

Заметим, что на схеме отображено четыре качества, а не три: Legacy, ULD, LD и HD. Качеством Legacy было принято называть контент из основного проекта War Robots. Также сам механизм разделения на базовые (base) префаб-варианты стал одной из фичей основной игры по поддержке обратной совместимости с War Robots Remastered.

Такая схема построения контента решала две проблемы:

  • Внесение изменений в префаб теперь можно было делать только один раз. При изменении общих значений для всех качеств, таких как настройки абилок, геймдизайнер редактирует только base префаб, а при изменении визуальной составляющей только префаб необходимого качества.

  • Сокращается время тестирования единицы контента. В случае с четырьмя независимыми префабами QA необходимо было протестировать и логику, и визуал каждого префаба в отдельности. В новой же схеме тестирование логики проводится только один раз, что сокращает время тестирования единицы контента на 70%.

Что касается самого процесса разделения префабов на базу и префаб-варианты, для этой задачи силами программистов был создан отдельный инструмент, который позволил автоматизировать этот процесс.

Как уже упоминалось ранее, создание нового префаба робота в War Robots может занимать от 1 до 3 дней работы геймдизайнера и это очень много, когда дело касается более 80 префабов роботов. Однако, благодаря общей базе и префаб-вариантам нам уже не нужно было создавать робота целиком необходимо было лишь заменить ему визуальные компоненты.

Префаб-варианты качеств ULD, LD и HD отличаются между собой всего несколькими элементами:

  • материалом (шейдером и набором текстур);

  • набором LOD-ов;

  • структурой VFX (набором систем частиц).

Замена этих компонентов легко поддается автоматизации.

HD, LD и ULD-вариант робота Griffin. Можно заметить различия в детализации и прорисовке тенейHD, LD и ULD-вариант робота Griffin. Можно заметить различия в детализации и прорисовке тенейHD, LD и ULD-вариант робота CossackHD, LD и ULD-вариант робота Cossack

Однако изначально у нас было только одно качество: Legacy, которое мы получили на выходе работы инструмента по разделению на префаб-варианты. Legacy-качество отличалось от ремастерных качеств, помимо прочего, еще и структурой скелета и анимациями. И если замену материалов, лодов и VFX-ов мы легко могли автоматизировать, то замена скелета анимаций требовала ручных усилий от геймдизайнеров для настройки новых точек креплений пушек, VFX и т. д. В результате мы создали два инструмента для геймдизайнеров: утилиту по портации префаба Legacy в качество ремастера и инструмент для генерации качеств HD, LD, ULD из готового качества ремастера.

Процесс создания ремастер-контента со стороны геймдизайнера теперь стал разделяться на следующие этапы:

  1. Геймдизайнер использует инструмент для портации Legacy-качества в ремастер-качество (обычно в LD). Этот инструмент заменяет скелет, анимации, материалы и VFX.

  2. Геймдизайнер вручную донастраивает ремастер-префаб: указывает новые ссылки на точки креплений пушек, VFX и т. д. На выходе этого этапа мы имеем один полноценный префаб-вариант для ремастер-качества.

  3. Геймдизайнер запускает инструмент по генерации остальных качеств, автоматически заменяющий оставшиеся визуальные компоненты.

Такой процесс позволил сократить время сборки одного робота в трех качествах с трех дней до пары часов.

Помимо этого, мы получили инструмент, который позволит нам в дальнейшем интегрировать ассеты любых качеств в пару кликов мыши например, Medium Definition (MD) и Ultra High Definition (UHD).

В текущем виде инструмент по созданию и инспектированию роботов выглядит так:

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

  • настройки освещения рендеров;

  • использование правильных материалов и текстур в префабах;

  • наличие самих префабов в билде;

  • настройки лодирования.

Эти проверки выполняются автоматически при каждой сборке билда. Где они проходят, кто их запускает и в каком виде получает результат вполне себе тянет на тему для отдельной статьи, поэтому пока остановимся на этом.

Суммарно внедрение процесса автоматизации сборки контента позволило нам собрать и протестировать более 800 единиц контента всего за 7 месяцев, а не порядка трех лет, как было оценено изначально.

Подводя итоги: какие практики хороши при переделке контента для крупного игрового проекта

  • При работе над таким большим проектом, как War Robots, невозможно вносить крупные изменения одномоментно. Только итерационный процесс позволяет внедрять технологии, кардинально меняющие структуру проекта в продакшене.

  • Итерационный процесс позволяет тщательнее протестировать новые технологии в продакшене. На момент запуска ремастера мы протестировали и закрыли большинство рисков, связанных с их использованием.

  • Грамотное использование gitflow, декомпозиция задач и контента и работа над поддержкой обратной совместимости позволяет избежать множества конфликтов в ходе внесения изменений в проект несколькими независимыми командами. Но важно, чтобы все члены команды понимали схему работы над проектом, и лиды координировали свои действия при планировании задач на свои отделы.

  • Не стоит забывать о силе автоматизации: чем объемнее проект, тем больше в нем однотипных задач, которые можно автоматизировать. В таких случаях несколько недель работы программистов могут сократить работу других отделов на несколько месяцев.

Подробнее..

Обзор технологий трекинга AR маркеры

20.06.2021 10:05:00 | Автор: admin

Всем привет. Меня зовут Дядиченко Григорий, я СТО Foxsys, и я всё ещё люблю трекинг. Продолжим серию статей после долгого перерыва и поговорим про AR маркеры. Какие технологии есть, чем они отличаются, в чём плюсы и минусы каждой на данной момент. Если интересуетесь AR технологиями - доброе пожаловать под кат!

Что чаще всего подразумевают под AR маркером?

AR маркер это плоская картинка, по которой мы можем определить позицию устройства в пространстве относительно этой картинки. Задачи бывают самые разные, и о них мы поговорим. Просто в общем случае понятие маркера или метки не обязано быть просто плоской картинкой. Но как-то так повелось в AR - что под маркером имеется в виду именно плоская метка, а остальные виды меток уточняются, как цилиндрическая метка и так далее.

По сути, в качестве метки можно использовать любое изображение, которое отвечает требованиям технологии. Чем лучше картинка подходит под технологию, тем точнее будет трекинг, реже будет теряться и в целом с ним будет меньше проблем. Относительно требования к этим картинкам, чтобы трекинг хорошо работал - всё зависит от технологии. Тот же самый инстаграм сильно отличается от того, что считается хорошим маркером в той же вуфории. Базовые правила почти для любой хорошей метки, чтобы определять их на глаз. Больше контрастов, меньше градиентов, отсутствие симметрий (чтобы случайно не получить эффект балерины), так как если скажем картинка центрсимметрична, то невозможно однозначно определить её ориентацию в пространстве.

Применение AR меток и общие рекомендации

Технология меток имеет большое количество применений в самых различных сферах. Основным плюсом данной технологии является широкое покрытие устройств и стабильность работы. Да, в том числе она обладает рядом ограничений, но по сравнению со всеми остальными технологиями при грамотной интеграции - метки в дополненной реальности работают как часы.

Поэтому они отлично подходят для решений, рассчитанных на большую аудиторию со средне бюджетными или дешёвыми устройствами, так как большая часть технологий не требует большой производительности, работает на достаточно широком спектре устройств и работает стабильно если соблюдать несколько правил.

Во-первых, правильно делать сам маркер в соответствии с гайдлайнами конкретной технологии. Во-вторых, печатать сами маркеры на матовой бумаге и стараться не размещать их за стеклом, особенно если стекло не музейное и создаёт большое количество бликов и отражений. И третье, стараться размещать контент, как можно ближе к центру маркера. Так как есть такое понятие, как угловая ошибка.

В случае размещения объекта далеко от центральной оси маркера из-за неточности трекинга и того, что на самом деле с точки зрения ориентации в пространстве всегда есть некоторой шум получается видимое смещение в виде тряски. В разы хуже чистой линейной ошибки смещения за счёт того, что чем дальше объект от места определения ориентации тем выше смещение относительно реального объекта в зависимости от угла ошибки.

Ну и конечно же, последнее - тестировать. Причём в идеале на целевых устройствах, на целевом расстоянии, с целевым размером маркера и в месте, где он будет эксплуатироваться. В последнем, по сути, важную роль играет только освещение в пространстве, так как проблемы трекингом могут создавать два критерия. Освещённость ниже 100 люкс и люминесцентное освещение. Со вторым проблема заключается в частоте мерцания лампы, так как не все технологии корректно обрабатывают данный эффект. С целевым же размером и расстоянием важную роль играет разрешающая способность камеры и воздушная перспектива. Она может влиять на чёткость границ маркера, которые используются для трекинга и из-за это маркер просто не будет распознаваться.

Целевые устройства важны, так как у многих различаются камеры и датчики, что так же влияет на трекинг. Для теста того же ARFoundarion (ARCore) или для сборки B2B проекта на аркоре, чтобы не было никаких вопросов с лицензией, по соотношению цена-качество я выделил для себя Redmi Note серию, нужно только внимательно смотреть, на каком устройстве есть поддержка аркор. Самый недорогой с достаточно качественной работой ARCore это восьмой ноут. Он сравнительно недорогой, но при этом проверять на нём качество трекинга данной технологии самое то. А так всё зависит от множества факторов. Бюджет, требования к системе, ограничения в плане покупке лицензий, какими устройствами обладает целевое устройства или бюджет на устройства для обустройства конкретной локации, и так далее.

Что-то мы глубоко ушли в нюансы работы с технологиями. Поговорим теперь про применение.

В рекламе

Одно из моих любимых это рекламная акция бургеркинга. Она красивая, и при этом отлично показывает, как можно применять технологию в рекламе. Но креативов вокруг маркерной технологии можно придумать достаточно много, а основной плюс её относительно других технологий стабильность. Так как при рекламной акции, рассчитанной на широкую аудиторию негатив никому не нужен, а пользователям будет плевать, почему на их телефоне не работает конкретная технология.

В благотворительности

По сути, очень похоже на рекламу, так как средства примерно такие же, только с целью осветить какую-то проблему.

В музеях и выставках

С помощью технологии меток можно оживить выставку и добавить интересного интерактива. Но, помимо этого, интересно к этому подошли в приложении Artefact. Навигация по точкам интереса обращая внимание на какие-то детали в картинах и рассказ истории об этой точке. Удобно и очень интересно.

В целом применений технологии очень много, и их полное перечисление само по себе растянется на несколько статей. Если даже отойти от телефонов метки используют в навигации дронов в пространстве, как систему рекалибровки и релокализации, в робототехнике, в инженерных задачах. К любой технологии трекинга можно придумать множество применений, так как определение позиции объекта в пространстве позволяет сделать много очень интересных взаимодействий. Когда-то даже был очень интересный концепт оживления настольных игр на Playstaition с помощью Eye Toy. Но перейдём же к самим технологиям. Да в основном я буду разбирать те технологии, с которыми я работал и запускал коммерческие проекты. Итак, приступим!

ARUCo (OpenCV)

Цена: Бесплатное

Поддерживаемые платформы: Android/IOS/WebGL/Win/Mac/Linux/Hololens

Совместимость с Unity: есть

Поддержка устройств: Широкая

https://docs.opencv.org/4.5.2/d5/dae/tutorial_aruco_detection.html

Если вы когда-то видели такие странные квадраты - это Аруко. Это технология для хардкорщиков. Из коробки для AR работает она откровенно плохо. Очень много шума. Но всегда можно реализовать обвязку которая решит эти проблемы. Допустим хорошо тречится так называемый аруко борд. Плюс если реализовать фильтр Маджевика, то тогда и jitter (тряска) будет не так сильно мешать. Основные плюсы: бесплатное (правда на допил уйдёт вероятно больше бюджета), умеет одновременно отслеживать очень большое число маркеров. Минусы: нельзя использовать изображения, кроме квадратов, что подходит далеко не для всех применений.

Vuforia

Цена: зависит от применения

Поддерживаемые платформы: IOS/Android/Win/Hololens

Совместимость с Unity: есть

Поддержка устройств: широкая

https://www.ptc.com/en/products/vuforia

Какой разговор про AR метки без Вуфории. Можно перейти правда сразу к главному недостатку технологии. Правильно купить её из РФ довольно сложно, и стоит это очень дорого. В остальном отличная технология. Имеет определение качества в вебе (правда ему стоит доверять с оговорками, так как он не определяет те же самые симметрии нормально). Работает очень хорошо, имеет интересную технологию вумарок. Недостатком с точки зрения долго развиваемого продукта является только тот факт, что у команды вуфории нет привычки к обратной совместимости. То есть каждая большая (а иногда и не очень) новая версия сдк ломает проект чуть ли не целиком, и требует переписать большую часть модулей. Что означает, что в долгосрочной перспективе решение требует поддержки, и поддержка будет дороже других технологий.

EasyAR

Цена: $1400 на приложение /$39 в месяц на приложение

Поддерживаемые платформы: IOS/Android/Win/Mac

Совместимость с Unity: есть

Поддержка устройств: широкая

https://www.easyar.com/

Отличная замена Vuforia. До третьей версии требовала небольшого допила в плане фильтрации данных и ряда мелочей, но версии постарше уже работают хорошо и без этого. Хотя на всякий случай со всеми технологиями лоупасс простой не помешает. Довольно просто купить ключ, одна из немногих технологий не по подписке. Работает с точки зрения маркерного трекинга отлично. В целом для широко рынка пока одно из лучших решений по соотношению цена/качество на мой взгляд.

XZIMG

Цена: бесплатно/2100 для использования в html5

Поддерживаемые платформы: IOS/Android/Win/Mac/Html5

Совместимость с Unity: есть

Поддержка устройств: широкая

https://www.xzimg.com/Products?nav=product-XAV

Одно из преимуществ технологии, одна из немногих коробочных технологий, работающих с WebGL и вебом в целом https://www.xzimg.com/Demo/AV. Работает неплохо в целом, но меньше спектр подходящих маркеров по сравнению с конкурентами. Бесплатное для коммерческого использования везде, кроме веба.

ARFoundation (aka ARCore/ARkit)

Цена: Бесплатно

Поддерживаемые платформы: IOS/Android

Совместимость с Unity: есть

Поддержка устройств: на Android порядка 20% устройств из общего числа моделей, IOS устройства с процессором A9+

Отличная технология для мобильных устройств. Бесплатная, хорошо работает и за счёт аппаратной поддержки тратит не так много ресурсов телефона. А недостатки, как всегда всё те же. Ограниченная поддержка на устройствах у пользователей. Соотношение постепенно растёт и в пределах нескольких лет возможно устройства будут у большего числа людей. Но пока по оценке данного ресурса (которая по методологии вызывает недоверие в плане точности) https://www.aronplatform.com/mobile-ar-penetration-2020/ AR supported устройства есть у 41.6% держателей смартфонов. Если посчитать точнее, думаю это число будет меньше. Но терять 60% аудитории в рамках акции или некоей платформы из-за технологии AR это должно быть весьма обоснованным решением.

SparkAR

Цена: Бесплатно

Поддерживаемые платформы: Android/IOS

Совместимость с Unity: нет, но можно сделать ссылку в приложении

Поддержка устройств: широкая

https://sparkar.facebook.com/ar-studio/

В спарке также есть возможность пользоваться AR метками. И если целевой аудиторией компании являются пользователи инстаграма и основной таргет шейринг это отличное решение. Основные минусы: политики фейсбука и инстаграма + ограниченность инструментов для отрисовки графики. К плюсам можно отнести бесплатность, быстрый доступ к аудитории. Важный нюанс, который надо иметь в виду разработчикам, то, что маркеры для той же вуфории отличаются от маркеров для спарка. У фейсбука своя технология и свой гайдлайн, который имеет ряд отличий в том, какой маркер будет хорошо работать.

Достойные упоминания

Это были не все технологии маркерного трекинга, так же на рынке присутствует Wikitude, ARmedia, MAXST - но с данными технологиями мне не довелось поработать, поэтому ничего не могу сказать про их качество работы, и какие-то тонкие моменты.

Спасибо за внимание! Если вам была интересна тема, то, когда у меня в следующий раз появится время постараюсь написать обзор на тему Hand Tracking или чего-нибудь её.

Подробнее..

Перевод Пулинг объектов в Unity 2021

03.06.2021 18:21:18 | Автор: admin

Разрабатывая игры, вы могли заметить, что создание порядка 100 экземпляров пуль в секунд.

  • A: уменьшить количество пуль до 20

  • B: реализовать свою собственную пулинговую систему

  • C: заплатить 50 долларов за пулинговую систему в Asset Store

  • D: использовать новый Pooling API Unity, представленный в 2021 году

(СМОТРИТЕ ВИДЕО В ОРИГИНАЛЕ СТАТЬИ)

В этой статье мы рассмотрим последний вариант.

Сегодня вы узнаете, как использовать новый Pooling API, представленный в 2021 году.

Начиная с Unity 2021, у вас есть доступ к широкому набору фич для работы с пулами, которые помогут вам разрабатывать высокопроизводительные проекты на Unity.

Готовы узнать о них побольше?

Когда вам нужен пул?

Начнем с самого главного вопроса: когда вам нужен пул?

Я задаю его, потому что пулы не должны быть вашим дежурным решением.

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

Но мы рассмотрим это позже.

Если вкратце, то вам стоит рассматривать возможность использования пулов, когда:

  • Вы создаете и уничтожаете игровые объекты очень быстро, например пули оружия.

  • Вы часто аллоцируете и высвобождаете объекты, хранящиеся в куче (вместо их повторного использования). Это относится и к коллекциям C#.

Эти операции вызывают много аллокаций, следовательно вы сталкиваетесь с:

  • Избыточным расходом тактов процессора на операций создания и уничтожения (или new/dispose).

  • Преждевременной сборкой мусора, вызывающей фризы, которые ваши игроки не оценят.

  • Фрагментацией памяти, которая затрудняет поиск свободных смежных областей памяти.

Не кажется ли вам, что эти проблемы могут представлять для вас угрозу?

(Если сейчас - нет, то они могут позже)

Но давайте продолжим.

Итак, что же такое это (объектный) пулинг в Unity в конце-то концов?

Теперь, когда вы понимаете, попали ли вы в беду (или все еще в безопасности), позвольте мне быстро объяснить, что такое пулинг.

Пулинг - это метод оптимизации производительности, который заключается в повторном использовании сущностей C# вместо того, чтобы создавать и уничтожать их каждый раз, когда они вам нужны.

Сущность может быть чем угодно: игровым объектом, инстансом префаба, словарем C# и т. д.

Позвольте мне продемонстрировать концепцию пулинга в контекст реального примера.

Допустим, вам нужно завтра утром пойти за продуктами.

Что вы обычно берете с собой, кроме кошелька и ключей?

Ну, можно взять многоразовые сумки. В конце концов, вам нужны какие-то контейнеры, чтобы донести продукты домой.

Итак, вы берете пустые многоразовые сумки, наполняете их продуктами и возвращаетесь домой.

Вернувшись домой, вы опустошаете свои сумки и кладете их обратно в ящик.

Это и есть пул.

Многоразовые сумки лучшая альтернатива, чем покупка (аллокация) пластиковых пакетов и их выбрасывание (высвобождение) каждый раз, когда вы идете за покупками.

Вам нужна сумка?

Хорошо, вы идете к своему пулу сумок (например, ящик на кухне), берете несколько, используете их, вытаскиваете все из них и, наконец, возвращаете их обратно в пул.

Поняли в чем соль?

Вот основные детали юзкейса пулинга:

  • Элементы, для которых вы хотите задействовать пул, например, многоразовые сумки, инстанцированная пуля и т.д. ...

  • Глобальная цель для всех этих элементов, например, перенос продуктов, стрельба пулями и т.д. ...

  • Функции, которые вы выполняете над пулом и его элементами: Take (взять), Return (вернуть), Reset (сбросить).

В случае с шутером вы можете создавать и уничтожать пули каждый раз при выстреле или вы можете заранее создать определенное количество, а затем повторно использовать их следующим образом:

  • Вы создаете тысячу пуль и помещаете их в пул.

  • Каждый раз, когда вы стреляете из своего оружия, вы берете пулю из этого пула.

  • Когда пуля попадает во что-то и исчезает, вы возвращает ее обратно в пул.

Таким образом вы экономите такты процессора, необходимые для создания и уничтожения этих префабов. Кроме того, вы уменьшаете нагрузку на сборщик мусора.

Теперь, прежде чем сразу нырнуть в пулинг, обратите внимание на несколько моментов...

Когда следует отказаться от использования пула?

У техники пулинга есть несколько (потенциальных) проблем:

  1. Ваши элементы могут загрязняться. Поскольку они уже использовались в прошлом, вы могли оставить их в непригодном состоянии, например, пули с небольшими остатками красной краски на них. Это означает, что вам нужно потратить несколько тактов процессора, чтобы очистить свои элементы перед их использованием: операция reset.

  2. Вы резервируете память, которая может вам так и не понадобиться. Если вы создаете пул с тысячами пуль, но все, что ваш игрок хотел сделать, это полюбоваться видами, то вы зря потратили память.

  3. Это усложняет вашу кодовую базу. Вам необходимо управлять жизненным циклом своих пулов. Это не только увеличивает количество тактов процессора, но и увеличивает количество мозговых тактов из-за обработки большей кодовой базы.

Все, что вам нужно сделать, это избегать пулов в тех случаях, когда вы не получите от них никакой выгоды.

Скажем, нет никакой необходимости пулить финального босса. В конце концов, он существует в единственном экземпляре.

Помните: самое главное это частота ваших операций создания и уничтожения.

Если вы делаете их часто, рассматривайте возможность использования пулов. В противном случае даже не думайте об этом.

Позже мы рассмотрим больше проблем с пулами.

Теперь давайте посмотрим на наши доступные варианты для реализации пулов.

Пулы объектов в Unity 2021: ваши варианты

Если вы хотите добавить пул объектов в свой проект Unity, у вас есть три варианта:

  • Создать свою собственную систему

  • Купить стороннюю систему пулинга

  • Импортировать UnityEngine.Pool

Давайте рассмотрим их.

A) Создаем свою собственную систему пулинга

Один из вариантов применить на практике свое мастерство.

Внедрение вашей собственной системы пулинга не выглядит слишком сложным, поскольку вам нужно всего лишь реализовать несколько операций:

  • Создать и удалить пул (Create & dispose)

  • Взять из пула (Take)

  • Вернуться в пул (Return)

  • Операции сброса (Reset)

Но это часто становится гораздо сложнее, когда вы начинаете думать о:

  • Типобезопасности

  • Управление памятью и структурах данных

  • Пользовательской аллокации/высвобождении объектов

  • Потокобезопасности

Это уже больше похоже на головную боль? Чувствую, ваше лицо побледнело...

Предлагаю не изобретать велосипед (если только это не учебное упражнение).

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

Сосредоточьтесь на том, чтобы доставить удовольствие своим игрокам. В любом случае они вам за это заплатят. Проверим второй вариант.

B) Сторонние системы пулинга объектов

Здесь вам всего лишь нужно выбирать одного из таких поставщиков, как:

  • The Unity Asset Store

  • Github

  • Друг или член семьи

Давайте рассмотрим несколько примеров:

Pooling Toolkit

13Pixels Pooling

Pure Pool

Pro Pooling

Но прежде чем вы нажмете кнопку покупки прочитайте немного дальше.

Сторонние инструменты могут творить чудеса и обладают множеством фич.

Но у них есть недостатки:

  • Вы полагаетесь на их поддержку в исправлении проблем и обновлении пакетов для более новых версий редактора.

  • Если у вас нет исходного кода, вы не сможете исправить проблемы самостоятельно.

  • Больше фич = сложнее код. Вам потребуется время, чтобы понять и поддерживать их систему.

  • Они могут быть достаточно дорогими (и по деньгам и по времени).

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

И в настоящее время осталось еще меньше причин для использования сторонних решений, поскольку Unity втихаря зарелизила новый API для пулинга в Unity 2021.

И это основная тема этой статьи.

C) Новый Pooling API от Unity

Начиная с версии 2021 года, Unity зарелизила несколько механизмов пулинга C#, которые помогут вам во множестве юзкейсов.

Эти новые пулы объектов напрямую интегрированы в движок Unity. Не требуется никаких дополнительных загрузок, и они поддерживается в актуальном состоянии при каждом обновлении Unity.

Огромный плюс у вас есть доступ к их исходному коду.

И я должен отметить, что их реализации довольно просты. Это приятное вечернее чтиво.

Давайте посмотрим, как вы можете начать использовать Unity Pooling API прямо сегодня, чтобы снизить затраты на операции, о которых и вы, и я прекрасно знаем.

Как использовать новый Object Pooling API в Unity 2021

Первый шаг убедиться, что вы используете Unity 2021+.

(Я имею в виду, вы можете просто скопировать и вставить код в любой из ваших старых проектов... но эй, я этого не говорил, если что)

После этого, это просто вопрос знания Unity Pooling API:

  • Операции пулинга

  • Различные контейнеры пулов

Я уже рассказывал вам несколько спойлеров о пулах. Но теперь давайте углубимся в них.

(СМОТРИТЕ ВИДЕО В ОРИГИНАЛЕ СТАТЬИ)

1. Построение вашего пула

Первая операция, которую вам нужно сделать, это построить контейнер для пула по вашему выбору. Обычно это делается в одну строчку кода, так что здесь не беспокойтесь.

Параметры конструктора зависят от конкретного контейнера, который вы хотите использовать, но они очень похожи. Вот обычные параметры конструктора пула Unity:

createFunc

Вызывается для создания нового экземпляра вашего объекта, например () => new GameObject(Bullet) or () => new Vector3(0,0,0)

actionOnGet

Вызывается, когда вы берете экземпляр из пула, например, для активации игрового объекта.

actionOnRelease

Вызывается, когда вы возвращаете экземпляр в пул, например, чтобы очистить и деактивировать экземпляр.

actionOnDestroy

Вызывается, когда пул уничтожает этот элемент, то есть когда он не помещается (превышает максимальный размер) или пул уничтожается.

collectionCheck

True, если вы хотите, чтобы Unity проверяла, что этот элемент еще не был в пуле, когда вы пытаетесь его вернуть (только в редакторе).

defaultCapacity

Размер пула по умолчанию: начальный размер стека/списка, который будет содержать ваши элементы.

maxSize

Размер пула: максимальное количество свободных элементов, которые находятся в пуле в любой момент времени. Если вы вернете предмет в заполненный пул, он будет уничтожен.

Вот как вы можете создать пул GameObjects:

_pool = new ObjectPool<GameObject>(createFunc: () => new GameObject("PooledObject"), actionOnGet: (obj) => obj.SetActive(true), actionOnRelease: (obj) => obj.SetActive(false), actionOnDestroy: (obj) => Destroy(obj), collectionChecks: false, defaultCapacity: 10, maxPoolSize: 10);

Я оставил названия параметров для наглядности; не стесняйтесь пропускать их в вашем коде.

И, конечно же, это всего лишь пример с GameObject. Вы можете использовать его с любым типом, с которым захотите.

Хорошо, теперь у вас есть пул _GameObject_ов.

Как нам им пользоваться?

2. Создание элементов пула

Первое, что Unity нужно знать, это как создавать больше ваших _GameObject_ов, когда вы запрашиваете больше, чем доступно.

Мы уже указали это в конструкторе, поскольку передали функцию createFunc в качестве первого параметра конструктору пула.

Каждый раз, когда вы захотите взять GameObject из пустого пула, Unity создаст его для вас и отдаст вам.

И для его создания он будет использовать переданную вами функцию createFunc.

А как нам взять GameObject из пула?

3. Извлечение элемента из пула

Теперь, когда ссылка на пул хранится в _pool, вы можете вызвать его функцию Get:

GameObject myGameObject = _pool.Get();

Вот и все.

Теперь вы можете использовать объект по своему усмотрению (в определенных рамках).

Когда вы закончите с ним, вам нужно вернуть его обратно в свой пул, чтобы вы могли использовать его позже.

4. Возврат элемента в пул

Итак, вы использовали свой элемент несколько минут, и теперь он вам больше не нужен. Что дальше?

Вот чего вы сейчас не делаете: вы не уничтожаете (destroy/dispose) его сами. Вместо этого вы возвращаете его в пул, чтобы пул мог правильно управлять своим жизненным циклом в соответствии с предоставленными вами функциями.

Как это сделать? Легко:

_pool.Return(myObject);

Тогда пул:

  1. Вызовет функцию actionOnRelease, которую вы предоставили с этим элементом качестве аргумента, чтобы деактивировать его, остановить систему частиц и т.д. ...

  2. Проверит, есть ли достаточно места в своем внутреннем списке/стеке на основе MaxSize

  3. Если есть достаточно свободного пространство в контейнере, он поместит туда объект.

  4. Если свободного места нет, то он уничтожит объект, вызвав actionOnDestroy.

Вот и все.

А теперь об уничтожении элементов.

5. Уничтожение элемента из вашего пула

Всякий раз, когда вы утилизируете (dispose) свой пул, или в нем нет внутреннего пространства для хранения возвращаемых вами элементов, пул уничтожает эти элементы.

И делает это он, вызывая функцию actionOnDestroy, которую вы передали в его конструкторе.

Эта функция может быть совершенно пустой или вызывать Destroy(myObject), если мы говорим об объектах, управляемых Unity.

И, наконец, когда вы закончите работу с пулом, вы должны его утилизировать.

6. Очистка и утилизация вашего пула

Утилизация вашего пула это высвобождение ресурсов, принадлежащих пулу. Часто внутри вашего пула есть стек или список, содержащий элементы, которые можно свободно из него брать. Что ж, вы избавляетесь от своего пула, вызывая:

_pool.Dispose();

Вот это собственно и есть вся функциональность пула. Но нам все еще не хватает одного важного момента.

Не все пулы созданы для одних и тех же юзкейсов

Давайте посмотрим, какие типы пулов предлагает Unity, чтобы удовлетворить ваши потребности.

Типы пулов в Unity 2021+

LinkedPool и ObjectPool

Первая группа пулов это те, которые охватывают обычные объекты C# (95%+ элементов, которые вы, возможно, захотите поместить в пул).

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

Разница между LinkedPool и ObjectPool заключается во внутренней структуре данных, которую Unity использует для хранения элементов, которые вы хотите поместить в пул.

ObjectPool просто использует Stack C#, который использует массив C# под капотом:

private T[] _array;

Будучи стеком, он содержит большой кусок непрерывной памяти.

Наихудший случай наличие 0 элементов (длина = 0) в большом пуле (емкость = 100000). Там у вас будет большой кусок зарезервированной памяти, который вы не используете.

Изменение размера стека происходит, когда вы превышаете его емкость. И это дорого, так как вам нужно выделить больший кусок и скопировать элементы.

Подсказка: вы можете избежать изменения размера стека, играя с параметром конструктора maxCapacity.

LinkedPool использует связанный список, который может улучшить управление памятью в зависимости от вашего юзкейса. Вот как выглядит эта структура данных:

internal class LinkedPoolItem { internal LinkedPool<T>.LinkedPoolItem poolNext; internal T value; }

Используя LinkedPool, вы используете память только для элементов, которые фактически хранятся в пуле.

Но это требует дополнительных затрат: вы тратите больше памяти на элемент и больше тактов процессора для управления этой структурой данных. В любом случае вы, вероятно, знаете разницу между массивами и связанными списками.

Итак, давайте поговорим о следующей категории классов пулов в Unity.

ListPool, DictionaryPool, HashSetPool, CollectionPool

Теперь мы поговорим о пулах коллекций C# в Unity.

Видите ли, при разработке игр вам, скорее всего, придется использовать списки, словари, хеш-множества и коллекции.

И достаточно часто вам нужно часто создавать/уничтожать эти коллекции.

Мы часто делаем это в структурах ИИ при выполнении определенных одноразовых действий или алгоритмов. Там нам часто требуются вспомогательные структуры данных для выполнения поиска, оценки и скоринга.

Вот в чем собственно дело.

Каждый раз, когда вы создаете и уничтожаете коллекции, вы оказываете давление на систему управления памятью. Это потому, что вы:

  • Аллоцируете и высвобождаете коллекцию плюс ее внутренние структуры данных.

  • Вы можете динамически изменять размер своих коллекций.

Таким образом, решение, которое помогает с некоторыми из этих рантайм аллокаций в Unity, - это пулинг коллекций.

Когда вам понадобится список, вы можете просто взять его из пула, использовать и вернуть, когда закончите.

Вот пример:

var manuallyReleasedPooledList = ListPool<Vector2>.Get();manuallyReleasedPooledList.Add(Random.insideUnitCircle);// Use your pool// ...ListPool<Vector2>.Release(manuallyReleasedPooledList);

А вот другая конструкция, которая освобождает для вас пул коллекций:

using (var pooledObject = ListPool<Vector2>.Get(out List<Vector2> automaticallyReleasedPooledList)){   automaticallyReleasedPooledList.Add(Random.insideUnitCircle);   // Use your pool   // ...}

Каждый раз, когда вы выходите за пределы этого using блока, Unity будет возвращать список в пул за вас.

CollectionPool - это базовый класс для этих конкретных коллекций; поэтому, если вы создаете свои собственные коллекции, вы можете создать для них пул, унаследовав от него.

ListPool, DictionaryPool и HashSetPool - это особые пулы для соответствующих типов коллекций.

Но вы должны быть осторожны с этими пулами для коллекций. Я говорю это, потому что внутри все эти пулы коллекций в Unity работают на основе статической переменной пула. Это означает вот что.

Использование этих пулов коллекций нарушит функцию, которая сокращает время итерации в редакторе: отключение перезагрузки домена. Если вы необдуманно используете такие статические пулы, объединенные элементы будут сохраняться на протяжении выполнений в редакторе. И это не очень весело.

Наконец, давайте посмотрим на других плохишей: GenericPool и его близнеца UnsafeGenericPool.

Они, как и описывают их названия, являются пулами общих объектов. Но в них есть кое-что особенное.

GenericPool и UnsafeGenericPool

Итак, что такого особенного с этими пулами объектов?

Опять же, GenericPool и UnsafeGenericPool являются пулами статических объектов. Таким образом, их использование не позволит вам отключить перезагрузку домена, чтобы сократить время итерации редактора.

С другой стороны, вам не нужно беспокоиться о создании их для любого из ваших юзкейсов.

Вы просто используете их, когда и где бы (и кем бы) вы ни находились.

var pooledGameObject = GenericPool<GameObject>.Get(); pooledGameObject.transform.position = Vector3.one; GenericPool<GameObject>.Release(pooledGameObject);

Вот так просто.

Вариант UnsafeGenericPool работает лучше за счет пропуска важной проверки: проверки уже возвращенного объекта. Видите ли, когда вы возвращаете объект в пул, возможно, вы уже возвращали его в прошлом (и не вынимали его из пула). Это может быть проще, чем вы думаете, особенно если вы используете статические пулы и одни и те же объекты используются в нескольких местах.

В этом случае элемент может дважды появиться во внутренней структуре данных пула. И угадайте, что происходит, когда вы берете два элемента?

БАБАХ!

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

Подводя итоги различий:

GenericPool использует статический ObjectPool с collectionCheck = true

UnsafeGenericPool использует статический ObjectPool с collectionCheck = false

Хорошо, как вы видели, не все в пулах красиво и аккуратно. Но вотрем еще немного соли в рану.

Проблемы с пулами (почему вы не должны ими злоупотреблять)

Я мог бы написать как минимум 3 статьи, подробно описывающих неприятные проблемы, которые могут возникнуть с пулами.

Но вместо того, чтобы делать это, я просто обозначу их здесь, основываясь на отличном посте Джексона Данстана.

Вот некоторые из проблем, с которыми вы можете столкнуться при использовании пулов:

  1. Вашим объектам требуется явный возврат. Если вы забудете вернуть объект пула, сборщик мусора должен будет выполнить ту работу, которой вы хотели избежать в первую очередь (в лучшем случае).

  2. Вы должны сбросить состояние ваших объектов. Если вы не сбросите состояние своих объектов, старые данные будут попадать в экземпляры, которые вы получаете из пула. Объект больше не будет свежим. Ваши пули могут содержать следы крови.

  3. Если вы используете один и тот же объект из пула в разных местах, вы должны вернуть объект только тогда, когда все его пользователи закончат с ним. Вы не хотите возвращать объект несколько раз, так как это гарантированно вызовет у вас головную боль. Таким образом, вам потребуется какой-то ручной подсчет ссылок, дорогостоящие для процессора проверки или стратегия, чтобы убедиться, что есть только один владелец.

  4. Управление памятью коллекций трудно. Вы можете пулить списки, хэш-множества, словари и тому подобное. Но делать предположения об их размерах сложно. Когда вы получаете список из пула, он может иметь размер 4, в то время как вам действительно нужен список размером 1000+. Вы бы принудительно изменили размер. Бывает и наоборот. Короче говоря, вы можете в конечном итоге потратить много памяти на поддержание жизни огромных коллекций, когда вам нужно всего несколько предметов для них.

  5. По умолчанию пулы не являются потокобезопасными. А если вы добавите механизмы для поддержки потоковой безопасности, тогда вы добавите накладные расходы на процессор, которые могут больше не окупаться.

Хорошая пища для размышлений.

Так, что еще?

Пулы отличные инструменты для снижения:

  • затрат производительности, связанных с распределением ресурсов в игровом процессе;

  • давления, которое вы оказываете на бедный сборщик мусора;

А с Unity 2021+ теперь стало проще, чем когда-либо, принять пул как образ жизни разработчика, поскольку теперь у нас есть встроенное pooling API.

Однако я объяснил темную сторону пулов. Сторона, которая может доставить вам массу боли во время разработки вашего проекта.

Пул это еще один инструмент повышения производительности, который вы должны знать. И чем больше инструментов вы знаете, тем лучше.


Перевод материала подготовлен в рамках курса "Unity Game Developer. Professional". Если вам интересно узнать о курсе подробнее, приглашаем на день открытых дверей: на нем преподаватель расскажет о формате и особенностях обучения, о программе и выпускном проекте.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru