Unity3D.ru • Пускаем волны многопоточности[Job System] Часть 3.

Пускаем волны многопоточности[Job System] Часть 3.

Научился сам? Помоги начинающему.

Пускаем волны многопоточности[Job System] Часть 3.

Сообщение lawsonilka 31 май 2018, 23:38

Пускаем волны многопоточности. Часть 3.

...предыдущая часть

В предыдущих частях я рассказал как создавать задачи, наполнять их данными и использовать эти данные после вычисления. И поэтому в этой части я на примере покажу и расскажу как же пользоваться всеми этими инструментами.

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

В конечном итоге должно получиться что то вроде этого
Скрытый текст:
Изображение

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

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

Для плоскости я использовал в примере простой SpriteRenderer(можно увидеть на картинке), создал простой спрайт на сцене и не много увеличил его размеры. Таким образом я могу получить размеры плоскости, а также изменять эти размеры чтобы динамически перестраивать граф.
Сетка представляет собой точки с линиями обрисованные через Gizmo.

Начнем со скрипта плоскости. Создадим простой mono скрипт Grid.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
}

и наполним его некоторыми параметрами.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 [Range(0.1f, 1f)]
 public float radius = 0.5f;
 public Vector2 offset = new Vector2(0.5f, 0.5f);

}

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

Дальше определим компонент SpriteRenderer чтобы измерять размеры плоскости.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 [Range(0.1f, 1f)]
 public float radius = 0.5f;
 public Vector2 offset = new Vector2(0.5f, 0.5f);

 private new SpriteRenderer renderer;

}

И в методе Awake возьмем этот компонент у объекта.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 [Range(0.1f, 1f)]
 public float radius = 0.5f;
 public Vector2 offset = new Vector2(0.5f, 0.5f);

 private new SpriteRenderer renderer;

 private void Awake() {
  this.renderer = GetComponent<SpriteRenderer>();
 }

}

На этом пока все со скриптом Grid, теперь можно кинуть его на плоскость.

Переходим к точкам сетки, они будут выполнены в простом классе с несколькими свойствами. Создадим класс Point и наполним его несколькими параметрами.
Синтаксис:
Используется csharp
public sealed class Point {

 public readonly int id;
 public readonly Vector2 position;
 public readonly Vector2Int coords;

}

Этот класс будет содержать:
-id для номера ячейки.
-position позиция для того чтобы обрисовать точку в пространстве.
-coords координаты точки на плоскости.
Координаты отличаются от позиции тем, что координаты НЕ привязаны к конкретному месту или расстоянию, они указывают порядковый номер точки на сетке, позиция же наоборот показывает реальное местоположение точки в пространстве.
Ниже для примера на картинке изображена простая схема сетки.
Скрытый текст:
Изображение


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

Теперь продолжим работу с классом Point и создадим для него конструктор который будет принимать необходимые параметры.
Синтаксис:
Используется csharp
public sealed class Point {

 public readonly int id;
 public readonly Vector2 position;
 public readonly Vector2Int coords;

 public Point(int id, Vector2 position, Vector2Int coords) {
  this.id = id;
  this.position = position;
  this.coords = coords;
 }

}


Для удобства работы с экземплярами класса Point, я перегрузил пару базовых методов и унаследовал его от интерфейса System.IEquatable чтобы можно было легко сравнивать эти самые экземпляры.
Синтаксис:
Используется csharp
public sealed class Point : System.IEquatable {

 public readonly int id;
 public readonly Vector2 position;
 public readonly Vector2Int coords;

 public Point(int id, Vector2 position, Vector2Int coords) {
  this.id = id;
  this.position = position;
  this.coords = coords;
 }

 public bool Equals(Point other) {
  return this.id == other.id;
 }

 public override bool Equals(object obj) {
  if (obj is Point) return Equals(obj as Point);
  else return base.Equals(obj);
 }

 public override int GetHashCode() {
  return this.id;
 }

}

Пока нам этого достаточно с классом Point.

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

Создадим простую структурку Node почти также как и класс Point
Синтаксис:
Используется csharp
public struct Node {

 public int id;
 public Vector2 position;
 public Vector2Int coords;

 public Node(int id, Vector2 position, Vector2Int coords) {
  this.id = id;
  this.position = position;
  this.coords = coords;
 }

}

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

Теперь можно создать простую структуру задачи, но разбирать будем ее позже.
Эта задача будет унаследована от интерфейса IJobParallelFor чтобы она выполнялась некоторое число раз.
Синтаксис:
Используется csharp
public struct BuildingJob : IJobParallelFor {

 public void Execute(int index) {

 }

}


Возвращаемся к скрипту плоскости Grid.

Чтобы перестроить граф каждый раз когда плоскость меняет свой размер или радиус ячеек необходимо отследить все изменения.
Для этого сначала создадим несколько переменных для отслеживания изменений.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальные переменные...*/

 private Vector2 tempSize, tempOffset;
 private float tempRadius;

 private void Awake() {
  this.renderer = GetComponent<SpriteRenderer>();
  this.tempSize = this.renderer.bounds.size;
  this.tempOffset = this.offset;
  this.tempRadius = this.radius;
 }

}

Переменная tempSize будет отслеживать изменения размера плоскости через Bounds, tempOffset будет отслеживать изменения отступа, а tempRadius для отслеживания изменения размера ячеек сетки.
Теперь будем каждый кадр отслеживать эти изменения через метод Update.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private void Update() {
  if (CheckChanges()) {
   //Что то изменилось, необходимо перестроить сетку.
  }
 }

 private bool CheckChanges() {
  float diff = Mathf.Abs(this.tempRadius - this.radius);
  if (diff > 0.01f) {
   this.tempRadius = this.radius;
   return true;
  }
  Vector2 newSize = this.renderer.bounds.size;
  diff = (this.tempSize - newSize).sqrMagnitude;
  if (diff > this.radius) {
   this.tempSize = newSize;
   return true;
  } else {
   diff = (this.tempOffset - this.offset).sqrMagnitude;
   if (diff > 0.01f) {
    this.tempOffset = this.offset;
    return true;
   }
  }
  return false;
 }

}


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

Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private void BuildMap() {

 }

 private void OnBuildingStart() {

 }

 private void OnBuildingFinish() {

 }

}

Метод BuildMap будет начинать собирать необходимые данные и задачи для постройки сетки, OnBuildingStart будет простым методом для запуска задач, а OnBuildingFinish таким же простым методом для сбора результата задач.

Выполнения всех этих действий может задержаться больше чем на один кадр, необходимо использовать переключатель который показывал бы когда идут вычисления, а когда они уже закончились. Для этого создадим простую boolean переменную building.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 public bool building { get; private set;}

 private void BuildMap() {

 }

 private void OnBuildingStart() {
  this.building = true;
 }

 private void OnBuildingFinish() {
  this.building = false;
 }

}


Теперь можно в методе Update установить условие, что пока идут вычисления предыдущей сетки не начинать строить новую.

Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private void Update() {
  if (this.building) {
   //Идет процесс постройки предыдущей сетки.
   return;
  }
  if (CheckChanges()) {
   //Что то изменилось, необходимо перестроить сетку.
  }
 }

}



Теперь у нас есть основа которую можно наполнять действиями.

Для начала нужно создать глобальное хранилище точек сетки, можно использовать список или просто массив, но в примере я использую словарь Dictionary<Vector2Int, Point>, который будет хранить координаты(не позиции) точки как ключ и саму точку как ее значение.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private Dictionary<Vector2Int, Point> points = new Dictionary<Vector2Int, Point>();

}


В предыдущей части мы выполняли задачи в одном методе поэтому и все переменные относящиеся к этой задаче также находились в одном месте. Здесь же построение сетки будет происходить в разных методах в разное время, для этого будем использовать глобальные переменные задач и базы данных внутри скрипта Grid.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private JobHandle handle;
 private Dictionary<BuildingJob, NativeArray<Node>> jobs = new Dictionary<BuildingJob, NativeArray<Node>>();

}

Переменная jobs представляет из себя словарь с задачей BuildingJob в виде ключа, и массивом NativeArray<Node> точек в виде значения. Мы будем наполнять этот словарь точками в процесс вычисления сетки.

Переменная handle будет выполнять задачу, а также даст нам возможность отслеживать процесс ее выполнения через свойство IsCompleted. Когда задачи будут выполнены то просто вывозим метод OnBuildingFinish который будет выполнять сбор вычисленных данных. Опишем это в методе Update.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private void Update() {
  if (this.building) {
   if (this.handle.IsCompleted) OnBuildingFinish();
   return;
  }
  if (CheckChanges()) {
   //Что то изменилось, необходимо перестроить сетку.
   BuildMap();
  }
 }

}


Готово, метод BuildMap создает задачи для постройки сетки, OnBuildingStart запускает выполнение задач, OnBuildingFinish собирает данные из задач.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private void BuildMap() {
  /*...создание задач...*/
  OnBuildingStart();
 }

 private void OnBuildingStart() {
  this.building = true;
  this.handle.Complete();
 }

 private void OnBuildingFinish() {
  this.building = false;
  /*...сбор данных...*/
 }

}


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

Проделаем всю эту работу в методе BuildMap.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private void BuildMap() {
  Vector2 mapSize = this.tempSize - this.offset * 2f;
  int xCount = Mathf.RoundToInt(mapSize.x / this.radius);
  int yCount = Mathf.RoundToInt(mapSize.y / this.radius);
  int totalCount = xCount * yCount;
  if (totalCount <= 0) {
   OnBuildingFinish();
   return;
  }

  OnBuildingStart();
 }

}

Переменная mapSize даст точный размер сетки учитывая отступ offset, xCount и yCount кол-во ячеек по горизонтали и вертикали в сетке, totalCount полное кол-во ячеек в сетке, эта переменная нужна чтобы узнать есть ли вообще смысл строить сетку если допустим плоскость будет очень маленького размера.

Если кол-во ячеек больше 0 то значит можно строить сетку, для этого сначала нужно вычислить место откуда мы будем строить, а точнее начальную позицию, в примере позиция - это левая нижняя точка плоскости, ее можно вычислить взяв центральную позицию плоскости и отняв половину полученного размера сетки.
Синтаксис:
Используется csharp
public sealed class Grid : MonoBehaviour {
 
 /*...остальной код...*/

 private void BuildMap() {
  Vector2 mapSize = this.tempSize - this.offset * 2f;
  int xCount = Mathf.RoundToInt(mapSize.x / this.radius);
  int yCount = Mathf.RoundToInt(mapSize.y / this.radius);
  int totalCount = xCount * yCount;
  if (totalCount <= 0) {
   OnBuildingFinish();
   return;
  }

  Vector2 center = this.transform.position;
  Vector2 origin = center - (mapSize / 2f);

  OnBuildingStart();
 }

}


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

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

Следующая часть...
lawsonilka
UNIверсал
 
Сообщения: 380
Зарегистрирован: 21 окт 2014, 14:48

Вернуться в Уроки

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 2