Большая часть взята из опыта использования, куда попала из родной документации движка. Вы легко можете сами найти необходимую информацию, поворошив документацию Unity 3D. Просто, по собственному опыту скажу, что у многих программистов либо нет времени, либо нет желания копаться в мантрах. Поэтому и выкладываю максимально краткое руководство по основным редакторским возможностям, которые я использовал на работе и в своих проектах.
Скриншотики
Всего может содержать три параметра.
string path //полный путь в меню
bool valudate //является ли данный метода валидатором функции (делает пункт меню неактивным)
int order //порядок расположения элемента в рамках одной иерархии
public static bool ValidateInitialization()
{
//просто проверка на то, что выделен любой объект
return Selection.gameObjects.Length > 0;
}
public static void Initialization()
{
//do something...
}
Также, если использовать элементы главного меню, то дополнительная кнопка будет появляться не только там, но и в контекстном меню на правую кнопку мыши. Например, в своем проекте, я добавил копирование пути к ассету.
Кроме того, на методы можно назначить горячие клавиши. Для этого, прямо в пути к меню нужно написать необходимую комбинацию. Для этого нужно использовать один из служебных символов+буква.
%
- ctrl в Windows или cmd в OSX
#
- shift
&
- alt
В моем проекте, с копированием пути к ассету это выглядит так
private static void CopyAssetPath()
{
}
Элемент контекстного меню
Unity - Scripting API: ContextMenu
public class UnitController: MonoBehavior
{
private new Transform transform = null; //храним собственный трансформ в отдельной переменной, во имя оптимизации
//инициализируем переменную при помощи вызова через контекстное меню
public void Initialization()
{
tranform = GetComponent
Пример подписи, подсказки и клампера
Подпись
Unity - Scripting API: HeaderAttribute
Задает подпись над сериализуемым полем, которая отображается в инспекторе.
Отступ
Unity - Scripting API: SpaceAttribute
Задает отступ в инспекторе.
Всплывающая подсказка
Unity - Scripting API: TooltipAttribute
Задает подсказку в инспекторе при наведении на сериализуемую переменную.
Сериализация переменных
Unity - Scripting API: SerializeField
Позволяет сериализовать переменные вне зависимости от их области видимости. Очень полезный атрибут, который позволяет сделать все переменные класса приватными, но настраиваемыми в инспекторе.
Запрет сериализации
Unity - Scripting API: NonSerializable
Позволяет убирать сериализацию у паблик переменных. Очень не рекомендую данных подход. Уж лучше определить свойство get;set; и получать данные по нему. Кроме того, свойство можно сделать виртуальным и перегрузить, при необходимости, в классах наследниках. А тот факт, что оно публичное, позволяет использовать его в интерфейсах.
Скрытие переменной в инспекторе
Unity - Scripting API: HideInInspector
Позволяет скрыть сериализуемое поле в инспекторе. Неважно, будет оно публичным или приватным/протектным с атрибутом SerializeField.
Исполнение в редакторе
Unity - Scripting API: ExecuteInEditMode
Позволяет работать скрипту в редакторе. В основном, полезно для постэффектов, поскольку позволяет сразу оценить результат в камере без запуска проекта. Но иногда можно использовать и для других целей.
Например, в качестве инициализатора сериализуемых полей встроенных типов, типа transform, renderer, rectTranform и т.п. Не рекомендовал бы повсеместно, лучше требовать ручной инициализации, либо написать редакторский скрипт, но иногда удобно.
Необходимость существования другого компонента
Unity - Scripting API: RequireComponent
Заставляет редактор требовать наличие определенного компонента на том же объекте, на котором висит скрипт с данным атрибутом. При добавлении сразу создает на том же объекте компонент указанного типа. Также запрещает удалять уже добавленный компонент.
Новый элемент в меню добавления компонента
Unity - Scripting API: AddComponentMenu
Добавляет подменю в выпадающий список в меню Components →… и AddComponent. Удобно, если у вас большая библиотека кода и нужно организовать её в редакторе.
На этом, простая часть заканчивается и добавлю совсем немного в меру сложной.
Во-первых, нужно определить класс-наследник от стандартного класса PropertyAttribute. Я сразу создам его с конструктором, в котором входящим параметром будет путь к списку того, что нам нужно использовать в атрибуте.
Public class IntAttribute: PropertyAttribute
{
private string path = “”;
public IntAttribute(string path)
{
this.path = path;
}
}
Во-вторых, после этого создаем скрипт редактора, в котором будем рисовать этот самый новый класс. Его нужно унаследовать от PropertyDrawer, а также написать к нему атрибут CustomPropertyDrawer.
Public class IntAttributeDrawer: PropertyDrawer
{
}
Я называю классы наиболее общими наименованиями, дабы просто показать принцип использования настраиваемых.
База готова, теперь нам нужно нарисовать данный атрибут в том виде, в котором он нам нужен. В основном, атрибуты я использую в тех случаях, когда возможностей перечисления (enum) недостаточно, но нужно отрисовать выпадающий список с выбором.
Например, у вас есть база эффектов, у которой есть соответствие id → эффект. Вы храните где-то эту базу, неважно в ScriptableObject’e или на каком-то префабе. Вот простейшая реализация “хранилища”
Примечание - всегда создавайте в классах первое сериализуемое поле строковым. Из-за этого в списках элементы будут именоваться не как element 1, element 2.., а таким образом, каким вы назначите переменную в инспекторе.
Интерфейс и класс эффекта
Public interface IResource
{
int ID
{
get;
}
string Name
{
get;
}
}
public class Effect: IResource
{
private string name = “”;
private int id = 0;
public int ID
{
get
{
return id;
}
}
public string Name
{
get
{
return name;
}
}
}
Интерфейс и класс контейнера
Public interface IContainer
{
IResource Resources
{
get;
}
}
public abstract class ResourcesContainer: MonoBehaviour, IContainer
{
public virtual IResource Resources
{
get
{
return null;
}
}
}
public class EffectsContainer: ResourcesContainer
{
private Effect effects = null;
public override IResource Resources
{
get
{
return effects;
}
}
}
Обычно, объекты с такими данными я располагаю в ресурсах, потом беру оттуда. Можно расположить и просто в проекте и где необходимо определить ссылки. Но я иду по более простому и уже проверенному на не одной платформе пути.
Редактор
Осталось дописать редактор:
Public class IntAttributeDrawer: PropertyDrawer
{
protected string values = null;
protected List
Скриншот с атрибутом
P.S.: Позже, напишу еще пару статей по другим типам апгрейдов редактора, в которые включу:
CustomEditor;
CustomPropertyDrawer;
EditorWindow;
Класс Debug и как его едят;
Класс Gizmos.
А также дополню примеры окном и пользовательским редактором. Пишите в комментариях, нужны ли подобные статьи или можно обойтись тем, что уже есть на Хабре.
Игровые объекты, которые вы создаете, будут иметь определенные свойства, создающие из игровых объектов то, чем они являются. Когда вы добавляете компонент к игровому объекту, то вы изменяете или дополняете его поведение. Это означает, конечно же, что определенная информация, добавляемая конкретным компонентом, добавляется к игровому объекту, должна быть сохранена где-то внутри компонента. Такая информация или данные сохраняются в переменных .
Это настройка скрипта Soldier Controller (SoldierController.js)
Как видите, первая буква имени каждой переменной начинается со строчной буквы.
Что-то было сделано не правильно? Нет, все правильно. Значения, которые вы назначаете переменным в скриптах, считаются значениями по умолчанию. Вы можете поправить значения в окне инспектора для получения более приемлемого поведения игрового объекта. Кроме того, такие поправки, устанавливаемые вами, сохраняются, означая, что вам не нужно будет постоянно вносить изменения в свой скрипт.
Примечание: Если вы вносите изменения в инспекторе во время работающей игры, то эти изменения теряются. Поэтому, во время внесения поправок в значения свойств, когда работает игра, записывайте все настройки, которые хотите выполнить сразу же, как только вышли из игры.
Просто имейте в виду, что ваши поправки не изменят скрипт вообще, они остаются наверняка, как только вы записали их в скрипте.
Чтобы обнулить значения в окне инспектора на первичные значения в скрипте:
Такие действия изменят значения свойств в инспекторе на первоначальные, определенные в скрипте.
Как и в примере с почтовым ящиком, где я давал раннее представление о том, какие различные типы вещей могут находиться в почтовом ящике, переменные также могут содержать различные типы (прим. ред. — types) данных. Они могут содержать все что угодно, от простых чисел, таких как число 3, до целых игровых объектов и префабов.
Чтобы добавить переменные в скрипт, существует некоторая простая грамматика, которую вы должны усвоить.
Каждая переменная , которую вы хотите использовать в скрипте, должна быть объявлена в разделе деклараций. Что это значит? Укажите Unity то, что вы хотите создать так, чтобы система Unity знала, что это такое, когда вы будете использовать это в своем скрипте. Взгляните на строчку 4.
easyName . Справа перед ней находится ключевое слово var , которое, конечно же, является аббревиатурой слова "variable ". Помещая var , мы указываем Unity то, что создаем новую переменную .
Следовательно, перед тем как Unity сможет использовать любую переменную , которую мы хотим, мы должны указать Unity сначала об этом. Указание Unity переменной называется объявлением переменной , которое вы можете сделать только один раз. Теперь переменная easyName может быть использована где угодно в скрипте.
Обратите внимание, что объявление заканчивается точкой с запятой. Все объявления должны заканчиваться точкой с запятой, иначе, Unity не сможет понять, где заканчивается выражение, а где начинается новое. Так же, как использование точки в конце предложения. Вы когда-нибудь пробовали прочитать целую группу предложений без каких-либо точек? Может быть не просто. Вы бы поняли это, поскольку умны. Но Unity не может, потому что является всего лишь скромной компьютерной программой.
Еще одна очень важная вещь, которую вы должны запомнить , и которая, возможно, приведет к программным ошибкам. Во всем этом коде, который вы видите, довольно много знаков равенства (=). В программировании и в скриптах, это не знак равенства, а оператор присваивания . Поэтому, когда вы видите:
То это не означает, что переменная easyName равна 10. Это означает то, что переменной easyName присваивается значение 10 .
Я расскажу о равенстве в следующем уроке.
Вышеперечисленные типы являются самыми распространенными типами переменных практически в любом языке программирования.
Взгляните на строку 6. У этого выражения объявления что-то добавлено в конце. Что это за слово int ?
Хорошо, взгляните на таблицу выше и посмотрите, что это слово означает integer .
Я создал переменную с именем easyNameTyped , за которой следует двоеточие, а затем int .
В системе скриптов Unity, вот как вы указываете Unity о том, что хотите, чтобы переменная содержала конкретный тип данных. Используйте двоеточие, за которым следует тип . Я специально указал Unity, что эта переменная должна будет содержать целочисленное значение, целое число.
Хорошо, и так вы, вероятно, хотите знать, почему другая переменная easyName не определила конкретный тип. Unity позволяет вам быть ленивым и не писать столько много кода, в то время как этого требуют другие языки программирования.
При написании скриптов, Unity обычно выясняет, какого типа будет переменная по присваиваемому ей значению. Читайте дальше.
Вернемся к строке 4, а также к строке 5.
Когда я объявил переменную easyName , то не указал ее тип , и Unity на самом деле не заботится об этом, потому что easyName не используется в скрипте вообще, а только объявляется, что является переменной . В строке 5 easyName используется в первый раз и ей назначается целочисленное значение, число 10. Теперь Unity знает, какой тип значений для переменной easyName будет позволено сохранять.
Вот как работает приведение типов . Тип определяется посредством того, как он используется первый раз в скрипте. С этого момента, когда игра работает, любое число, которое скрипт помещает в переменную easyName , даже десятичные числа, такие как 2.3, Unity сотрет часть со значением.3 и допустит только целочисленную часть значения, в этом случае 2.
Изучая скрипты в Unity, я предлагаю вам обязательно указывать тип переменных , чтобы легче найти потенциальные ошибки в коде. После того, как вы станете более информированным, то сможете полениться.
Способ заставить вас указывать типы — добавить #pragma strict в скрипт в качестве самой первой строки.
Примечание: Каждый раз, когда переменная появляется в скрипте после того, как она была объявлена при помощи слова var , вот такое использование данный переменной , как в строке 5.
Строка 15 является переменной с типом float . Это означает, что переменная будет сохранять десятичное число, которое содержит десятичную точку.
В данном примере я указал переменную withFloatTyped как десятичную в разделе объявлений, поэтому Unity знает наверняка, что может содержать переменная. Я также назначил переменной значение в разделе объявлений.
Если бы я просто назначил переменной значение 7, то для Unity оно было бы фактически равно 7.0 .
Что это еще за тип boolean ?
В окне инспектора Unity данный тип представлен в виде переключателя. Поэтому переменная типа boolean может иметь значение либо true либо false, отмечено или не отмечено.
Мы рассмотрим применение значений true и false позднее, когда доберемся до принятия решений в скриптах.
Обратите внимание, что определяя переменную simpleStringTyped как переменную типа String , слово String начинается с большой буквы, а не с маленькой. Все потому что тип String является классом. Другие типы, int , float и boolean просто встроены как часть системы скриптового языка Unity.
Имена всех классов начинаются с большой буквы. Мы ознакомимся еще с объявлением переменных в качестве типов-классов позже, с такими как класс Transform , например.
Заключайте символы в двойные кавычки, чтобы сформировать строку из них.
Переменные , которые я создал, являются общедоступными переменными , это означает то, что значения, которые содержатся в них, — доступны из других скриптов. Они также могут отображаться в окне инспектора Unity. Только одно замечание было к строке 4. Я не объявил тип для переменной easyName , поэтому система Unity не смогла добавить ее в инспектор. Unity не имеет понятия, какой тип переменной добавить, поэтому и не добавляет вообще.
Примечание: Я расскажу об этом позже, когда речь пойдет о функциях, но пока что, переменные , которые объявляются внутри функций, никогда не будут являться общедоступными.
Как видите, указание переменной withIntTyped скрытой, не позволяет ей отображаться в инспекторе.
Кажется, что много информации, которую нужно знать о переменных . Однако, это всего лишь несколько простых шагов:
Я опубликовал первую статью «50 советов по работе с Unity» 4 года назад. Несмотря на то, что бóльшая её часть всё ещё актуальна, многое изменилось по следующим причинам:
8. Документируйте свои настройки. Бóльшая часть документации должна находиться в коде, но кое-что необходимо задокументировать за его пределами. Заставлять разработчиков рыться в коде в поисках настроек значит тратить их время. Документированные настройки повышают эффективность (если поддерживается актуальность документов). Документируйте следующее:
10. Используйте утверждения (assertions). Утверждения полезны для тестирования инвариантов в коде и помогают избавиться от логических багов. Утверждения доступны через класс Unity.Assertions.Assert . Они проверяют условие и записывают в консоль сообщение, если оно неверно. Если вы не знаете, для чего могут быть полезны утверждения см. The Benefits of programming with assertions (a.k.a. assert statements) .
11. Не используйте строки ни для чего, кроме отображения текста. В частности, не используйте строки для идентификации объектов или префабов. Существуют исключения (в Unity всё ещё есть некоторые элементы, к которым можно получить доступ только через имя). В таких случаях определяйте такие строки как константы в файлах, таких как AnimationNames или AudioModuleNames. Если такие классы становятся неуправляемыми, применяйте вложенные классы, чтобы ввести что-то вроде AnimationNames.Player.Run.
12. Не используйте Invoke и SendMessage. Эти методы MonoBehaviour вызывают другие методы по имени. Методы, вызываемые по имени, тяжело отследить в коде (вы не сможете найти «Usages», а SendMessage имеет широкую область видимости, которую отследить ещё сложнее).
Можно легко написать собственную версию Invoke c помощью Coroutine и actions C#:
Public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time)
{
return monoBehaviour.StartCoroutine(InvokeImpl(action, time));
}
private static IEnumerator InvokeImpl(Action action, float time)
{
yield return new WaitForSeconds(time);
action();
}
Затем вы можете использовать её в MonoBehaviour таким образом:
This.Invoke(ShootEnemy); //где ShootEnemy - это невозвращающий значения (void) метод без параметров.
(Дополнение:
кто-то предложил использовать в качестве альтернативы класс ExecuteEvent , часть системы событий Unity . Пока я знаю о нём не так много, но похоже, что его стоит изучить подробнее.)
13. Не позволяйте спауненным (spawned) объектам запутывать иерархию при выполнении игры. Установите в качестве родителя для них объект в сцене, чтобы при выполнении игры было проще находить объекты. Можно использовать пустой (empty) игровой объект или даже синглтон (см. ниже в этой статье) без поведения (behaviour), чтобы проще было получать к нему доступ в коде. Назовите этот объект DynamicObjects.
14. Будьте точны при использовании null в качестве допустимых значений, и избегайте их там, где это возможно.
Значения null полезны при поиске некорректного кода. Однако если вы приобретёте привычку игнорировать null, некорректный код будет успешно выполняться и вы ещё долго не заметите ошибок. Более того, она может объявиться глубоко внутри кода, поскольку каждый слой игнорирует переменные null. Я стараюсь вообще не использовать null как допустимое значение.
Я предпочитаю следующую идиому: не выполнять проверку на null и позволить коду вывалиться при возникновении проблемы. Иногда в повторно используемых методах я проверяю переменную на null и выдаю исключение вместо того, чтобы передавать её другим методам, в которых она может привести к ошибке.
В некоторых случаях значение null может быть допустимым, и поэтому обрабатываться другим способом. В подобных случаях нужно добавить комментарий с указанием причин того, что значение может равно null.
Обычный сценарий часто используется для значений, настраиваемых в инспекторе. Пользователь может указать значение, но если он этого не сделает, будет использоваться значение по умолчанию. Лучший способ сделать это — использовать класс Optional‹T›, который оборачивает значения T. (Это немного похоже на Nullable‹T›.) Можно использовать специальный рендерер свойств для рендеринга поля с флажком и показывать поле значения только когда флажок установлен. (К сожалению, невозможно использовать непосредственно generic-класс, необходимо расширить классы для определённых значений T.)
Public class Optional
{
public bool useCustomValue;
public T value;
}
В своём коде вы можете использовать его таким образом:
Health = healthMax.useCustomValue ? healthMax.Value: DefaultHealthMax;
Дополнение:
многие люди подсказывают мне, что лучше использовать struct (не создаёт мусора и не может быть null). Однако это означает, что вы не сможете использовать его в качестве базового класса для non-generic-классов так, чтобы применять его для полей, которые можно использовать в инспекторе.
15. Если вы используете корутины (Coroutines), научитесь использовать их эффективно. Корутины могут быть удобным способом решения многих проблем. Однако они сложны в отладке, и с их помощью вы можете легко превратить код в хаос, в котором никто, даже вы, не разберётся.
Вы должны понимать:
В реализации ниже используется typeof вместо generic-версий этих функций. Generic-версии не работают с интерфейсами, а typeof работает. Представленный ниже метод оборачивает его в generic-методы.
Public static TInterface GetInterfaceComponent(this Component thisComponent)
where TInterface: class
{
return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}
17. Используйте методы расширения (extension methods), чтобы сделать синтаксис более удобным.
Например:
Public static class TransformExtensions
{
public static void SetX(this Transform transform, float x)
{
Vector3 newPosition =
new Vector3(x, transform.position.y, transform.position.z);
transform.position = newPosition;
}
...
}
18. Используйте более «мягкую» альтернативу GetComponent.
Иногда принудительное добавление зависимостей через RequireComponent может быть неприятным, оно не всегда возможно или приемлемо, в особенности когда вы вызываете GetComponent для чужого класса. В качестве альтернативы может использоваться следующее расширение GameObject, когда объект должен выдавать сообщение об ошибке, если он не найден.
Public static T GetRequiredComponent(this GameObject obj) where T: MonoBehaviour
{
T component = obj.GetComponent();
if(component == null)
{
Debug.LogError("Ожидается компонент типа "
+ typeof(T) + ", но он отсутствует", obj);
}
return component;
}
19. Избегайте использования разных идиом для выполнения одинаковых действий.
Во многих случаях существуют различные идиоматические способы выполнения действий. В таких случаях выберите одну идиому и используйте её для всего проекта. И вот почему:
Дополнение: Unity поддерживает unscaledTime и unscaledDeltaTime, которые делают собственный класс времени избыточным во многих ситуациях. Но он всё равно может полезен, если масштабирование глобального времени влияет на компоненты, которые вы не писали нежелательными способами.
21. Пользовательские классы, требующие обновления, не должны иметь доступ к глобальному статическому времени. Вместо этого они должны получать дельту времени в качестве параметра метода Update. Это позволяет использовать эти классы при реализации системы паузы, описанной выше, или когда вы хотите ускорить или замедлить поведение пользовательского класса.
22. Используйте общую структуру для выполнения вызовов WWW. В играх с большим объёмом коммуникаций с сервером обычно существуют десятки вызовов WWW. Вне зависимости от того, используете ли вы сырой класс WWW Unity или плагин, удобно будет написать тонкий слой поверх, который будет работать как boilerplate.
Обычно я определяю метод Call (отдельно для Get и Post), корутину CallImpl и MakeHandler. В сущности, метод Call создаёт с помощью метода MakeHandler «суперобработчик» (super hander) из парсера, обработчик on-success и on-failure. Также он вызывает корутину CallImpl, которая формирует URL, выполняет вызов, ожидает его завершения, а потом вызывает «суперобработчик».
Вот как это приблизительно выглядит:
Public void Call
У такого подхода есть несколько преимуществ.
24. Если вы планируете локализацию, отделите все строки в одно место. Существует несколько способов сделать это. Один из них — это определить класс Text с строчным полем типа public для каждой строки, по умолчанию, например, будет установлен английский. Другие языки будут дочерними классам и повторно инициализируют поля с языковыми аналогами.
Более сложный способ (он подходит при большом объёме текста или высоком числе языков) — считывание электронной таблицы и создание логики для выбора нужной строки на основнии выбранного языка.
27. Используйте property drawers, чтобы сделать поля более удобными для пользователей. Property drawers можно использовать для настройки контролов (controls) в инспекторе. Это позволит вас создавать контролы, наиболее подходящие под вид данных и вставлять защиту (например ограничение значений переменных). Используйте атрибут Header для упорядочивания полей, а атрибут Tooltip — для предоставления дизайнерам дополнительной документации.
28. Отдавайте предпочтение property drawers, а не пользовательским редакторам (custom editors) . Property drawers реализуются по типам полей, а значит, требуют гораздо меньше времени на реализацию. Их также удобнее использовать повторно – после реализации для типа их можно использовать для того же типа в любом классе. Пользовательские редакторы реализуются в MonoBehaviour, поэтому их сложнее использовать повторно и они требуют больше работы.
29. По умолчанию «запечатывайте» MonoBehaviours (применяйте модификатор sealed). В общем случае MonoBehaviours Unity не очень удобны для наследования:
Public class MyBaseClass
{
public sealed void Update()
{
CustomUpdate();
... // update этого класса
}
//Вызывается до того, как этот класс выполняет свой update
//Переопределение для выполнения вашего кода update.
virtual public void CustomUpdate(){};
}
public class Child: MyBaseClass
{
override public void CustomUpdate()
{
//Выполняем какие-то действия
}
}
Это предотвратит случайное переопределение вашего кода классом, но всё равно позволяет задействовать сообщения Unity. Я не люблю такой порядок, потому что он становится проблематичным. В примере выше дочернему классу может потребоваться выполнение операций сразу после того, как класс выполнил собственный update.
30. Отделяйте интерфейс от игровой логики. Компоненты интерфейса в целом не должны ничего знать об игре, в которой они используются. Передавайте им данные, которые нужно отображать, и подпишите на события, проверяемые при взаимодействии пользователя с компонентами UI. Компоненты интерфейса не должны выполнять игровую логику. Они могут фильтровать вводимые данные, проверяя их правильность, но основные правила должны выполняться не в них. Во многих играх-головоломках элементы поля являются расширением интерфейса, и не должны содержать правил. (Например, шахматная фигура не должна рассчитывать разрешённые для неё ходы.)
Вводимая информация также должна быть отделена от логики, действующей на основании этой информации. Используйте контроллер ввода, сообщяющий актору о необходимости движения, актор сам принимает решение о том, когда нужно двигаться.
Вот урезанный пример компонента UI, позволяющего пользователю выбрать оружие из заданного списка. Единственное, что знают эти классы об игре, это класс Weapon (и только потому, что класс Weapon — полезный источник данных, которые этот контейнер должен отображать). Игра тоже ничего не знает о контейнере; ей нужно только зарегистрировать событие OnWeaponSelect.
Public WeaponSelector: MonoBehaviour
{
public event Action OnWeaponSelect {add; remove; }
//GameManager может регистрировать это событие
public void OnInit(List weapons)
{
foreach(var weapon in weapons)
{
var button = ... //Создаёт дочернюю кнопку и добавляет её в иерархию
buttonOnInit(weapon, () => OnSelect(weapon));
// дочерняя кнопка отображает опцию,
// и отправляет сообщение о нажатии этому компоненту
}
}
public void OnSelect(Weapon weapon)
{
if(OnWepaonSelect != null) OnWeponSelect(weapon);
}
}
public class WeaponButton: MonoBehaviour
{
private Action<> onClick;
public void OnInit(Weapon weapon, Action onClick)
{
... //установка спрайта и текста оружия
this.onClick = onClick;
}
public void OnClick() //Привязываем этот метод как OnClick компонента UI Button
{
Assert.IsTrue(onClick != null); //Не должно происходить
onClick();
}
}
31. Разделите конфигурацию, состояние и вспомогательную информацию.
Public class Player
{
public class PlayerConfigurationData
{
public float maxHealth;
}
public class PlayerStateData
{
public float health;
}
public PlayerConfigurationData configuration;
private PlayerState stateData;
//вспомогательная информация
private float previousHealth;
public float Health
{
public get { return stateData.health; }
private set { stateData.health = value; }
}
}
32. Не используйте связанные индексами массивы типа public.
Например, не определяйте массив оружия, массив пуль и массив частиц таким образом:
Public void SelectWeapon(int index)
{
currentWeaponIndex = index;
Player.SwitchWeapon(weapons);
}
public void Shoot()
{
Fire(bullets);
FireParticles(particles);
}
Проблема здесь скорее не в коде, а в сложности безошибочной настройки в инспекторе.
Лучше определите класс, инкапсулирующий все три переменные, и создайте из него массив:
Public class Weapon
{
public GameObject prefab;
public ParticleSystem particles;
public Bullet bullet;
}
Такой код выглядит приятнее, но, что важнее, так сложнее сделать ошибки при настройке данных в инспекторе.
33. Избегайте использования массивов для структур, не являющихся последовательностями. Например, у игрока есть три типа атак. Каждая использует текущее оружие, но генерирует разные пули и разное поведение.
Вы можете попытаться засунуть три пули в массив, а затем использовать логику такого типа:
Public void FireAttack()
{
/// поведение
Fire(bullets);
}
public void IceAttack()
{
/// поведение
Fire(bullets);
}
public void WindAttack()
{
/// поведение
Fire(bullets);
}
Enums могут выглядеть красивее в коде…
Public void WindAttack()
{
/// behaviour
Fire(bullets);
}
…но не в инспекторе.
Лучше использовать отдельные переменные, чтобы имена помогали понять, какое содержимое туда записывать. Создайте класс, чтобы всё было удобным.
Public class Bullets
{
public Bullet fireBullet;
public Bullet iceBullet;
public Bullet windBullet;
}
Это подразумевает, что других данных Fire, Ice и Wind нет.
34. Группируйте данные в сериализируемые классы, чтобы всё выглядело удобнее в инспекторе. Некоторые элементы могут иметь десятки настроек. Поиск нужной переменной может стать кошмаром. Чтобы упростить себе жизнь, следуйте этим инструкциям:
Public class MovementProperties //Не MonoBehaviour!
{
public float movementSpeed;
public float turnSpeed = 1; //указываем значение по умолчанию
}
public class HealthProperties //Не MonoBehaviour!
{
public float maxHealth;
public float regenerationRate;
}
public class Player: MonoBehaviour
{
public MovementProperties movementProeprties;
public HealthPorperties healthProeprties;
}
35. Сделайте не являющиеся MonoBehaviour классы Serializable, даже если они не используются для полей public.
Это позволит просматривать поля класса в инспекторе, когда он находится в режиме Debug mode. Это работает и для вложенных классов (private или public).
36. Старайтесь не изменять в коде настраиваемые в инспекторе данные. Настраиваемая в инспекторе переменная — это переменная конфигурации, и с ней нужно обращаться как с константой при выполнении приложения, а не как с переменной состояния. Если вы будете соблюдать это правило, вам будет проще писать методы, сбрасывающие состояние компонента на первоначальное, при этом вы будете чётче понимать, что делает переменная.
Public class Actor: MonoBehaviour { public float initialHealth = 100; private float currentHealth; public void Start() { ResetState(); } private void Respawn() { ResetState(); } private void ResetState() { currentHealth = initialHealth; } }
Эти советы относятся к использованию паттернов конкретно в Unity.
37. Используйте для удобства синглтоны (паттерн «одиночка»). Следующий класс автоматически сделает синглтоном любой наследующий его класс:
Public class Singleton
Синглтоны полезны для менеджеров, например для ParticleManager
, AudioManager
или GUIManager
.
(Многие программисты настроены против классов, расплывчато называемых XManager, потому что это указывает на то, что для класса выбрано плохое имя или у него слишком много несвязанных друг с другом задач. В целом, я с ними согласен. Однако в играх есть всего несколько менеджеров, и они выолняют в играх одни и те же задачи, так что эти классы фактически являются идиомами.)
38. Используйте конечные автоматы (state machines) для создания различного поведения в разных состояниях или для выполнения кода при смене состояний. Лёгкий конечный автомат имеет множество состояний и для каждого состояния вы можете указать действия, выполняемые при входе или нахождении в состоянии, а также действие обновления. Это позволить сделать код более чистым и менее подверженным ошибкам. Хороший признак того, что вам пригодится конечный автомат: код метода Update содержит конструкции if или switch, изменяющие его поведение, или такие переменные как hasShownGameOverMessage.
Public void Update()
{
if(health <= 0)
{
if(!hasShownGameOverMessage)
{
ShowGameOverMessage();
hasShownGameOverMessage = true; //При респауне значение становится false
}
}
else
{
HandleInput();
}
}
С бóльшим количеством состояний такой тип кода может стать запутанным, конечный автомат сделает его намного яснее.
39. Используйте поля типа UnityEvent для создания паттерна «наблюдатель» (observer) в инспекторе. Класс UnityEvent позволяет связывать методы, которые получают до четырёх параметров в испекторе, с помощью того же интерфейса UI, что и события в Buttons. Это особенно полезно при работе с вводом.
40. Используйте паттерн «наблюдатель», чтобы определять, когда изменяется значение поля. Проблема исполнения кода только при изменении переменной часто возникает в играх. Мы создали стандартное решение этой проблемы с помощью generic-класса, позволяющего регистрировать события изменения переменных. Ниже представлен пример со здоровьем. Вот как он создаётся:
/*Наблюдаемое значение*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };
Теперь вы можете менять его где угодно, не проверяя в каждом месте его значение, например, вот так:
If(hit) health.Value -= 10;
Когда здоровье становится ниже 0, вызывается метод Die. Подробные обсуждения и реализацию см. в этом посте .
41. Используйте для префабов паттерн Actor. (Это «нестандартный» паттерн. Основная идея взята из презентации Кирана Лорда (Kieran Lord).)
Actor (актор) — это основной компонент префаба. Обычно это компонент, обеспечивающий «индивидуальность» префаба, и тот, с которым наиболее часто будет взаимодействовать код более высокого уровня. Actor часто использует другие компоненты – помощники (helpers) – для того же объекта (и иногда для дочерних объектов), чтобы выполнить свою работу.
При создании объекта «кнопка» через меню Unity создаётся игровой объект с компонентами Sprite и Button (и дочерний с компонентом Text). В этом случае актором будет являться Button. Главная камера также обычно имеет несколько компонентов (GUI Layer, Flare Layer, Audio Listener), прикреплённые к компоненту Camera. Camera здесь является актором.
Для правильной работы актора могут потребоваться другие компоненты. Можно сделать префаб более надёжным и полезным с помощью следующих атрибутов компонента-актора:
53. Используйте визуальный отладчик, рисующий графики изменения значений со временем. Он чрезвычайно удобен для отладки физики, анимаций и других динамических процессов, и в особенности нерегулярно возникающих ошибок. Вы сможете увидеть эту ошибку на графике и отследить другие переменные, изменяемые в момент ошибки. Также визуальный контроль делает очевидными определённые типы странного поведения, например, слишком часто изменяемые значения или отклонения без явных причин. Мы используем Monitor Components , но существуют и другие инструменты визуальной отладки.
54. Пользуйтесь удобной записью в консоль. Используйте расширение редактора, позволяющее кодировать цветом вывод по категориям и фильтровать вывод согласно этим категориям. Мы применяем Editor Console Pro , но есть и другие расширения.
55. Используйте инструменты тестирования Unity, особенно для тестирования алгоритмов и математического кода. См., например, туториал Unity Test Tools или пост Unit testing at the speed of light with Unity Test Tools .
56. Применяйте инструменты тестирования Unity для выполнения «черновых» тестов. Инструменты тестирования Unity пригодны не только для формальных тестов. Их также можно использовать для удобных «черновых» тестов, которые выполняются в редакторе без запуска сцены.
57. Используйте горячие клавиши, чтобы делать скриншоты. Многие баги связаны с визуальным отображением, и гораздо проще сообщать о них, если можно сделать снимок экрана. Идеальная система должна иметь счётчики PlayerPrefs, чтобы скриншоты не перезаписывались. Скриншоты не нужно сохранять в папке проекта, чтобы сотрудники случайно не фиксировали (commit) их в репозитории.
58. Используйте горячие клавиши для печати снепшотов важных переменных. Они позволят регистрировать информацию, когда во время игры происходит неожиданные события, которые можно исследовать. Набор переменных конечно же зависит от игры. Подсказками для вас могут стать типичные ошибки, возникающие в игре. Например, положения игрока и врагов или «состояние думания» AI-актора (скажем, путь, по которому он пытается следовать).
59. Реализуйте опции отладки, чтобы упростить тестирование. Примеры:
60. Определите константы для горячих клавиш отладки, и храните их в одном месте. Клавиши отладки, в отличие от игрового ввода, обычно не обрабатываются в одном месте. Чтобы избежать конфликтов горячих клавиш, в первую очередь определяйте константы. Альтернативой является обработка всех клавиш в одном месте, вне зависимости от того, имеют ли они функции отладки или нет. (Недостаток такого подхода в том, что этому классу могут понадобиться только для этого дополнительные ссылки на объекты).
61. При процедурной генерации сеток отрисовывайте или спауньте небольшие сферы в вершинах. Это позволит вам убедиться, что вершины находятся в нужных местах и они нужного размера, прежде чем начинать работу с треугольниками и UV для отображения сеток.
64. Научитесь эффективному использованию профайлера для отслеживания причин проблем с производительностью.
66. Замеряйте эффект улучшений производительности. При внесении изменений для повышения производительности замеряйте её, чтобы убедиться, что изменение действительно улучшает производительность. Если изменение неизмеряемо или незначительно, откажитесь от него.
67. Не пишите менее читаемый код для повышения производительности. Исключения:
Скорее всего, вы захотите создать своё собственное соглашение о присвоении имён и структуру папок. Вот одно для примера.
MyGame
Helper
Design
Scratchpad
Materials
Meshes
Actors
DarkVampire
LightVampire
…
Structures
Buildings
…
Props
Plants
…
…
Resources
Actors
Items
…
Prefabs
Actors
Items
…
Scenes
Menus
Levels
Scripts
Tests
Textures
UI
Effects
…
UI
MyLibray
…
Plugins
SomeOtherAsset1
SomeOtherAsset2
...
Main
Debug
Managers
Cameras
Lights
UI
Canvas
HUD
PauseMenu
…
World
Ground
Props
Structures
…
Gameplay
Actors
Items
…
Dynamic Objects
Debug
Gameplay
Actors
Items
…
Framework
Graphics
UI
...