C++
Last updated
Last updated
- функция, возвращающая нормализованный вектор направления от одной точки к другой.
- класс, позволяющий присоединять физические объекты к компонентам и физически корректно их двигать.
- функция, которая телепортирует Actor, и если он не помещается точно в указанное место, пытается немного выдвинуть его за пределы препятствия.
- функция, которая вызывается, когда любая анимация завершается. Очень удобно прятать виджет после FadeOut-анимаций.
- возвращает InstigatorController для этого Actor.
- возвращает APlayerStart, который будет использоваться для следующего появления игрока.
- класс, который обеспечит вращение необходимого компонента.
- функция, позволяющая понять, кто управляет этим Actor: сервер или клиент.
- это Actor, который не имеет визуального представления и физики, но при этом имеет доступ к миру игры.
- возвращает true, если SpringArmComponent с чем-то столкнулся.
- класс с полезными функциями для читов.
- прячет кость (и всю её иерархию). Очень удобная функция для создания вида от первого лица, когда нужно спрятать голову, но оставить всё остальное.
- рисует текст в заданной точке.
- получает имя объекта без необходимости вызывать функцию через указатель.
- рассчитывает LaunchSpeed (например, для ), позволяющий достичь конкретной точки. Очень полезная функция для реализации прыжков AI.
Это более удобный вариант логирования:
Для FName макрос TEXT() использовать не нужно.
Разработка архитектуры для уровня предприятия, когда в действительности нужен маленький и удобный инструмент для настольного компьютера, - это верный рецепт провала.
Модуль (класс, функция) должен иметь одну и только одну причину для изменения. Или модуль должен отвечать за одного и только за одного актора.
Лучше сделать функцию абстрактной и создать несколько её реализаций.
Если есть большой класс, а Вам нужна лишь часть его функций, то лучше сделать просто интерфейс, реализованный классом, и использовать только этот интерфейс.
Неустойчивость компонента: I = FanOut / (FanIn + FanOut)
, где FanOut
- количество исходящих зависимостей, а FanIn
- количество входящих зависимостей. I = 0
соответствует максимальной устойчивости компонента, а I = 1
- максимальной неустойчивости.
Зависимости должны быть направлены в сторону устойчивости или минимизации I
. То есть метрики I
должны уменьшаться в направлении зависимости. Другими словами, метрика I
компонента должна быть больше метрик I
компонентов, от которых он зависит.
Абстрактность: A = Na / Nc
, где Na - число абстрактных классов и интерфейсов в компоненте, а Nc - общее число классов. A = 0 - означает полное отсутствие абстрактных классов, а A = 1 означает, что компонент не содержит ничего, кроме абстрактных классов.
Расстояние до главной последовательности: D = Abs(A + I - 1)
. D = 0
означает, что компонент находится на главной последовательности, что очень хорошо. D = 1
означает, что компонент находится на максимальном удалении от главной последовательности, что требует его пересмотра и реструктуризации.
Метрики управления зависимостями помогают получить количественную оценку соответствия структуры зависимостей и абстракции дизайна тому, что называется "хорошим" дизайном.
Чем дальше политика от ввода и вывода, тем выше её уровень и тем реже она изменяется и по более важным причинам.
Обычно через границы стоит передавать данные в виде простых структур. Данные можно передавать в вызовы функций через аргументы. Или упаковывать их в ассоциативные массивы или простые объекты.
Это важно делать, поскольку снижает степень зависимости между компонентами.
Однако, не всегда требуется делать архитектурную границу полностью. Какие-то можно сделать частично, а какие-то вообще игнорировать.
Структурная зависимость - одна из самых сильных и наиболее коварных форм зависимости тестов. Набор тестов, в котором имеются тестовые классы для всех прикладных классов и тестовые методы для всех прикладных методов - плохая идея, поскольку затрудняет внесение изменений в приложение.
Например, виджет не должен напрямую обращаться к базе данных, а должен обратиться к соответствующему объекту на соседнем снизу уровне и получить от него все данные.
Метод f
класса C
должен ограничиваться вызовом методов следующих объектов:
C
;
объекты, созданные f
;
объекты, переданные f
в качестве аргумента;
объекты, хранящиеся в переменной экземпляра C
.
ctxt.GetAbsolutePathOfScratchDirectoryOption();
или ctxt.getScratchDirectoryOption().GetAbsolutePath();
- оба варианта плохие.
Первый приведёт к разрастанию набора методов объекта ctxt.
Второй вариант предполагает, что getScratchDirectoryOption()
возвращает структуру, а не объект.
Если ctxt
является объектом, то мы должны приказать ему выполнить некую операцию, а не запрашивать у него информацию о его внутреннем устройстве. Зачем нам понадобился абсолютный путь к временному каталогу? Что мы собираемся с ним делать? Почему бы не приказать объекту ctxt
выполнить нужную операцию:
Например, вместо:
Лучше написать так:
Этот оператор позволяет быстро добавлять каталоги и файлы к переменной с директорией.
Этот файл содержит большое количество примеров использования Slate-виджетов.
В области реализации (в отличие от предметной области) композиция означает "реализовано посредством".
Это и логичнее, и безопаснее.
Это повышает степень инкапсуляции, помогает константным функциям-членам быть константными и минимизирует вероятность появления "висячих дескрипторов".
Особенно dynamic_cast. Когда приведение всё же необходимо, то стоит сделать такую функцию, где это приведение будет в ней инкапсулировано.
Это делает программы яснее и повышает их эффективность.
Protected-члены не более инкапсулированы, чем открытые. Private-члены дают клиентам синтаксически однородный доступ к данным, обеспечивают возможность тонкого управления доступом, позволяют гарантировать инвариантность и предоставляют авторам реализации классов гибкость.
Нужно стремиться обеспечивать эти характеристики своим интерфейсам.
Во-первых, это некорректно, во-вторых, это работает не так, как обычно предполагает программист.
Всегда необходимо вручную инициализировать переменные встроенных типов, поскольку C++ делает это далеко не всегда.
В конструкторе стоит отдавать предпочтение применению списка инициализации членов перед прямым присваиванием значений в теле конструктора. Перечислять данные-члены стоит в том порядке, в каком они объявлены в классе.
Модификатор mutable освобождает нестатические данные-члены от ограничений побитовой константности. Отличный вариант для параметров, которые "лениво" инициализируются через const-функции.
Оператор static_cast встречается чаще всего:
Оператор dynamic_cast служит для безопасного приведения типа между уровнями иерархии наследования:
Оператор const_cast чаще всего используется для изменения атрибута const:
Добавление таких функций может привести к их нежелательному (и даже неожиданному) вызову, из-за чего будет труднее понять работу программы, а также увеличится сложность её отладки.
По сути, достаточно лишь просто перечислить все типы параметров, без указания имён, которые не используются в функции:
Для пользовательских типов разница в скорости работы может быть существенной, поэтому если явно не требуется постфиксная форма, то всегда стоит использовать префиксный вариант. Для примитивных типов разницы нет, однако, для консистентности префиксная форма предпочтительна и для них:
Это сильно усложняет чтение логических выражений, например:
Здесь, если expression1 - false, то expression2 даже не вычисляется. Однако, если для этих двух выражений будет переопределён оператор &&, то легко может вычисляться сначала expression2, потом обязательно expression1, даже если expressoin2 - false, и потом ещё вызывается сама функция.
Оператор запятой же просто внесёт огромную путаницу в код хотя бы по той причине, что его стандартное поведение невозможно повторить при его переопределении (поскольку нет гарантии, что выражение до запятой будет вычислено до выражения после запятой).
Например:
Это очевидно, поскольку inline означает "в процессе компиляции подставить вместо вызова функции её тело", но "виртуальная" предполагает "определить вызываемую функцию во время выполнения программы". Таким образом, компиляторы обычно игнорируют директиву inline для виртуальных функций, однако, для корректности кода всё равно не стоит её указывать.
Другими словами, не стоит наследоваться от конкретных классов. Если такая потребность возникла, значит, надо создать ещё один абстрактный класс, перенеся туда всю общую логику, и уже от него унаследовать оба конкретных класса (старый и новый).
Это даёт следующие преимущества:
На уровне компилятора "продвигает" разделение проекта на уровни абстракции
Более удобная организация работы в команде
Более быстрая компиляция
Например, могут быть такие модули (снизу-вверх): PRJUtility, PRJWeapons, PRJEnemies (на том же уровне, что и PRJWeapons), PRJCore (содержащий, например, GameMode, PlayerController, Pawn), PRJUI, плюс, primary-модуль, который будет совпадать с названием проекта.
При этом зависимости возможны только в направлении сверху - вниз. Blueprint можно считать отдельным модулем, который находится на самом верху, а потому может зависеть от любых модулей на C++.
Кстати, обычно в primary-модуле не нужны папки Public/Private, поскольку от primary-модуля ни один другой зависеть не будет.
Если зависимость используется только внутри cpp-файлов модуля, то добавлять её надо в PrivateDependencyModuleNames, а если используется в h-файле, то в PublicDependencyModuleNames. Это будет важно для тех модулей, которые будут зависеть от текущего.
Обычно можно сильно сократить количество добавлений в PublicDependencyModuleNames, если в h-файле использовать Forward Declaration.
Для primary-модуля можно всё добавлять в PublicDependencyModuleNames.
Например, класс AWeapon лучше назвать APRJWeapon (PRJ - префикс, специфичный для проекта), что позволит устранить конфликты имён, а также сразу будет видно, где пользовательский класс, а где нет.
Данное правило так же актуально для перечислений, структур и модулей.
Например, если StaticMeshComponent нужен только для внешнего вида и у нет никакой логики внутри C++, то лучше его добавить в дочернем Blueprint-классе.
Если имеет место отношение «Is a», то применяется наследование. Например, собака – это животное. А если имеется отношение «Has a», то применяется композиция. Например, собака имеет характеристику здоровья (Health Stat).
Или:
Если используется динамическая структура, то лучше использовать интеллектуальные указатели.
Это значение позволяет динамически создать объект класса через указание его в свойстве (например, какой-нибудь тип атаки или хранилище параметров оружия).
Переменные, объявленные как auto, должны быть инициализированы; в общем случае они невосприимчивы к несоответствиям типов, которые могут привести к проблемам переносимости или эффективности; могут облегчить процесс рефакторинга; и обычно требуют куда меньшего количества ударов по клавишам, чем переменные с явно указанными типами.
Фигурная инициализация предотвращает сужающие преобразования и нечувствительна к особенностям синтаксического анализа C++. Однако, с фигурной инициализацией надо быть осторожным, если есть перегрузка конструктора с параметром std::initializer_list.
Также следует избегать перегрузок с указателями и целочисленными параметрами.
То есть лучше писать так:
чем так:
Например:
Он меньше весит, быстрее работает, а также легко преобразуется в std::shared_ptr. Это позволяет использовать его в качестве возвращаемого значения фабричных функций.
Например, не надо делать так:
Лучше писать так:
А лучше всего так:
Их следует применять для кеширования и списков наблюдателей.
По сравнению с непосредственным использованием new, make-функции устраняют дублирование кода, повышают безопасность кода по отношению к исключениям и в случае функций std::make_shared и std::allocate_shared генерируют меньший по размеру и более быстрый код.
Если параметр шаблона функции имеет тип T&& для выводимого типа T или если объект объявлен с использованием auto&&, то параметр или объект является универсальной ссылкой.
Если вид объявления типа не является в точности type&& или если вывод типа не имеет места, то type&& означает rvalue-ссылку.
Универсальные ссылки соответствуют rvalue-ссылкам, если они инициализируются значениями rvalue. Они соответствуют lvalue-ссылкам, если они инициализируются значениями lvalue.
Например:
Здесь компилятор использует RVO (Return Value Optimization) для того, чтобы избежать копирования w. Это означает, что «копирующая» версия makeWidget на самом деле копирования не выполняет. Это работает всегда, если выполняются оба условия:
Тип локального объекта совпадает с возвращаемым функцией
Локальный объект представляет собой возвращаемое значение
Применять std::move к rvalue-ссылкам, а std::forward – к универсальным ссылкам, когда вы используете их в последний раз.
Делать то же для rvalue и универсальных ссылок, возвращаемых из функций по значению.
Никогда не применять std::move и std::forward к локальным объектам, которые могут быть объектом RVO.
То есть, всегда нужно захватывать интересующие переменные, например, так:
Например:
Это ещё один повод избегать режима захвата по умолчанию.
Например, так:
- эта функция в AGameModeBase, по сути, вызывает BeginPlay у объектов на уровне.
- установка точки, на которую должен смотреть контроллер.
- получение Actor, на которого был установлен фокус с помощью .
- установка фокуса (Use Controller Desired Rotation должен быть в true). Удобно использовать вместо связки и .
- получение прошедшего времени с момента, указанного в параметре.
- возвращает местоположение "глаз" у Pawn.
- направление взгляда Pawn (обычно совпадает с ).
- возвращает пустую строку типа FText.
- помимо приведения типа, данная функция добавляет fatal error в лог, если оно было неудачным. После CastChecked() дополнительный check для указателя уже не нужен, тогда как после обычного Cast необходим либо check, либо if.
– возвращает true, если две точки находятся рядом.
Удобный плагин для создания виджетов:
Документация: