Мы знаем, что существует такой метод, который нам вернет случаное число (псевдослучайное, если точнее).

Итак, обычно это выглядит как-нибудь так:

var rnd = new Random(
var value = rnd.NextValue();

То есть есть некий класс рандома, который что-то делает и на выходе выдает "случайное" число. Как это работает?

На самом деле все довольно прозаично. Для того, чтобы выдать "случайное число", нужно знать некое другое число (или seed). Обычно дефолтное значение этого самого seed берется из тиков, времени, да чего угодно положительного.

Т.е. мы фактически в каждый момент времени уже имеем случайное число - это время.

А теперь каким образом работает этот самый rnd.NextValue()?

Random внутри себя хранит seed + в зависимости от реализации рандома может хранить другие параметры. Мне нравится больше всего самая простая реализация только с seed: 

struct Random {
   uint seed;
   uint NextValue() {
      var next = this.seed;
      this.seed += 123;
      return next;
   } 
} 

Т.е. сейчас мы при каждом обращении к NextValue к seed прибавляем некое число, изменяя этот самый seed таким образом, чтобы при следующем обращении нам выдали уже другое число.

На этом месте можно рассмотреть вариант Unity.Mathematics:

uint next = seed;
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed << 5;
return next;

По сути мы сдвигаем значение seed, получая "рандомное" число. Отсюда, кстати, и ограничение, что seed не может быть 0, т.к. чего бы мы там не двигали 0 всегда останется нулем.

Это я все к тому, что такой рандом можно передавать по сети и "откатывать", т.к. оно всегда будет выдавать некую последовательность чисел, основанную на этом самом seed. Т.е. взяв на одном клиенте 5 чисел, начиная с seed = 5:

5, 15, 60, 70, 1

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

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

Read More  

Чтобы понять как они работают, нужно понять как работает Enumerator. Если коротко, то это некий объект, у которого есть метод MoveNext(), если его вызвать, то произойдет переключение на следующий шаг:

// step
yield return null;
// step 2

А теперь каким образом юнити собственно это делает. Раз у нас есть объект, мы можем добавить его в какой-нибудь список.

IEnumerator MyMethod() {
    // step 1
    yield return null;
    // step 2 
}

list.Add(MyMethod()); 

А теперь в update мы просто переключаем шаги:

for (int i = 0; i < list.Count; ++i) {
     if (item.MoveNext() == false) {
         item.Current // тут мы можем проверить возвращаемое значение, например, если там внутренняя корутина, то ее тоже хорошо бы выполнить 🙂
         list.RemoveAt(i);
         --i;
     }
}  

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

Корутины в юнити работают примерно таким образом. Еще я бы добавил, что при вызове StartCoroutine сразу выполняется первый шаг, т.е. вызывается MoveNext(), если он возрващает true, то значит дальше что-то есть и нужно добавлять корутину в список выполнения.

Т.е. нужно понять главное: корутины - это не какая-то особенная штуковина, которая работает только в юнити, это стандартный синтаксис и коллекции C#.

Read More  

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

Пример:

void Method(in Vector3 vector) {
  vector.x = 123f; // будет ошибка доступа
  vector.Method(); // будет сделана копия vector, если метод не readonly
}

А вот тот же пример с ref:

void Method(ref Vector3 vector) {
  vector.x = 123f; // ошибки не будет, значение будет изменено&nbsp;
  vector.Method(); // копии не будет
}

Теперь рассмотрим вариант с возвращаемым значением по ссылке:

Vector3[] arr;
public ref Vector3 Method(int index) {
  return ref arr[index];
}

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

void Method() {
  ref var a = 123;
}

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

ref var data = ref Method(index);
System.Array.Resize(ref arr, ...); // Меняем данные, на которые держим ссылку
data.x = 123f; // Изменит данные в предыдущем массиве, а не в новом, т.е. фактически изменений не будет

В-третьих, можно вернуть данные только для чтения:

public ref readonly Vector3 Method() {...}

только не путать с этим:

public readonly ref Vector3 Method() {...}

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

Особенно прекрасно выглядит это:

public readonly ref readonly Vector3 Method() {...}

Лично я считаю, что это накосячили разрабы языка, т.к. readonly ref readonly - это дичь, почему было не использовать слово const?

Ну и последнее - нужно понимать, что in и ref - это ссылки, а значит void Method(in LargeStruct s) имеет смысл, чтобы избежать копирования, а вот void Method(in int val) не имеет, т.к. такой int будет передаваться по ссылке ref, но запрещать изменения, при этом ссылка будет занимать 8 байт.

Read More  

Иногда в редакторе нужно использовать SerializedProperty у объекта, до которого просто никак не дойти. Допустим, я хочу вывести поля класса, а класс этот находится не в ScriptableObject и не в компоненте.

Для этого можно использовать простой хак:

public class Temp : ScriptableObject {
     [SerializedReference]
     public object data;
}

var temp = Temp.CreateInstance<Temp>(); 
temp.data = yourInstance; 
var so = new SerializedObject(temp); 
var prop = so.FindProperty("data"); 

Еще нужно не забыть убить этот Temp 🙂

Read More  

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

https://pastebin.com/6gdiGwde

Его не существует в Unity, поэтому его нужно объявить самостоятельно, Unity сама найдет его по имени. Аттрибут можно использовать для одного класса/структуры/метода несколько раз с разными значениями.

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

Read More  

У SerializedProperty появился boxedValue. Не во всех случаях он работает, но во всяком случае для большого числа кейсов можно теперь читать и писать значение нормально 🙂

Read More  

Используйте указание на конкретный тип в enum, если вы используете меньше, чем int:

enum MyEnum : byte {
     Value1,
     Value2,
     Value3,
     Value4,
}  

Таким образом:

struct Test {
     public MyEnum e1;
     public MyEnum e2;
     public MyEnum e3;
     public MyEnum e4;
} 

Будет запаковано как 4 байта. Но не забывайте, что любая математика с enum приводит к int 🙂

Read More  

Для дебага полезная штука - написать свой Proxy:

[System.Diagnostics.DebuggerTypeProxyAttribute(typeof(DebugClass))]
public class YourClass { ... }

Где DebugClass - это отдельный класс, который может содержать геттеры и поля. Еще у него должен быть конструктор, который будет принимать инстанс YourClass.

Это сильно помогает при дебаге сложных штук.

Read More  

Мы знаем, что структуры нужно инициализировать в конструкторе полностью:

struct MyStruct {
     public int field1;
     public int field2;
     ...
     public int fieldN;
     public MyStruct(int field1) {
          this.field1 = field1;
          // тут нужно инициализировать все поля
          this.field2 = default;
          ...
          this.fieldN = default;
     }
} 

Иногда полей много и можно написать гораздо короче:

struct MyStruct {
     public int field1;
     public int field2;
     ...
     public int fieldN;
     public MyStruct(int field1) {
          this = default;
          this.field1 = field1;
     }
} 
Read More  

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

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

int value = 0b1011; // ставим дефолтное значение
// проверка, является ли второй бит установленным
if ((value & (1 << 1)) != 0) {
  // бит установлен
}

// установка третьего бита
value |= (1 << 2); // теперь value == 0b1111

// сброс третьего бита
value &= ~(1 << 2); // теперь value == 0b1011

Можно использовать биты в enum, записывать их можно по-разному:

enum MyEnum {
  None = 0,
  Value1 = 1 &lt;&lt; 0,
  Value2 = 1 &lt;&lt; 1,
  Value3 = 1 &lt;&lt; 2,
  Value4 = 1 &lt;&lt; 3,
  Value5 = 1 &lt;&lt; 4,
  Value1OrValue3 = Value1 | Value3,
}

enum MyEnum {
  None = 0,
  Value1 = 0x1,
  Value2 = 0x2,
  Value3 = 0x4,
  Value4 = 0x8,
  Value5 = 0x10,
  Value1OrValue3 = Value1 | Value3,
}

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

Для вывода можно использовать аттрибут System.Flags, но он не является обязательным, хотя влияет на отображение в Unity Inspector и на вывод в лог.

Read More  

Мы хотим выполнить несколько итераций (jobCount) и внутри каждой итерации еще по 128 итерации. Внутри всего этого мы хотим посчитать сумму из NativeArray input и положить в массив NativeArray arrSum: 

for (int j = 0; j < jobCount; ++j) {
     for (int i = 0; i < 128; ++i) {
         var sum = arrSum[i];
         sum += input[(j * 128) + i];
         arrSum[i] = sum;
     }
} 

Как видно из примера, массив input намного больше, чем arrSum. Мы думаем-думаем и решаем оптимизировать наш код. Получается примерно следующее: 

for (int i = 0; i < 128; ++i) {
     var sum = arrSum[i];
     for (int j = 0; j < jobCount; ++j) {
         sum += input[(j * 128) + i];
     }
     arrSum[i] = sum;
} 

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

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

И да, первый вариант векторизуется и будет работать быстрее.

Read More  

Лет 5-6 назад я встретил в одном из плагинов facebook следующую запись:

public some::Item Method() {
...
}

Я особо не задумывался зачем оно надо, но потом я сформулировал идею: использовать :: между using-сокращением и типом, т.е. когда мы пишем using someVar = Some.Type; Тогда в коде будет

someVar::Example

Позже я стал замечать примерно такую же концепцию в разных фреймах:

global::SomeClass.someVar


Read More