Создание логической сущности
Логические сущности - это самые простые сущности, потому что у них нет расположения в мире, нет визуального компонента, и они используются для работы с другими сущностями через input . Для примера, math_counter
хранит данные, и может их добавлять или вычетать; другие сущности на карте могут изменить данные через inputs или получать информацию через output.
В этом уроке мы создадим логическую сущность, которая выполняет простую задачу хранения и увеличения значения всякий раз, когда приходит команда через input. Как только счетчик достигает определенного значения, энтити сообщит об этом через output.
Создание исходного файла
Это руководство предполагает, что вы используете Visual Studio или Visual C++ Express. См. Выбор компилятора .
Добавьте New Filter
в проект Server
и назовите фильтр по своему усмотрению. Затем добавьте New Item
- .cpp файл и назовите его sdk_logicalentity. Этот файл будет содержать весь код, упомянутый в этом руководстве.
Присвоение каждой сущности собственного .cpp сокращает нагрузку, поскольку это позволяет компилятору разделять ваш код на более дискретные сегменты, и увеличивает скорость компиляции, поскольку требуется компилировать только измененные .cpp файлы.
Заголовочные Файлы
Каждый .cpp файл запускается на своей собственной, изолированной от остальной части проекта. Чтобы обойти это, мы можем включать "заголовочные файлы", которые объявляют компилятору, какие части проекта будут использованы. В нашем случае, очень простая сущность требует только одного файла:
#include "cbase.h"
cbase.h
предоставляет доступ к основным наборам команд Valve для создания сущностей, и каждый написанный .cpp для Source должен его включать. Буква "c" является частью модели названий, используемых во всех Исходниках, и это означает, что код выполняется на стороне сервера (на стороне клиента будет "c_").
Объявление классов
Этот раздел довольно большой, но по окончанию, вы будите понимать, как работает C++.
Почти вся обработка выполняется "объектами" (Objects). Объекты создаются, когда программа выполняется по шаблонам, называемыми "классами" (Classes). Создание энтити означает создание нового класса.
class CMyLogicalEntity : public CLogicalEntity { public: DECLARE_CLASS( CMyLogicalEntity, CLogicalEntity ); ... // Не печатайте эту строку! };
Это "объявление класса" (class declaration), в котором мы рассказываем C++ компилятору, какие объекты будут хранится в данном классе, и какие функции будут выполнятся.
В первой строке кода, мы начали с того, что объявили класс командой class
. Затем дали ему имя CMyLogicalEntity
, а затем, двоеточие говорит компилятору, что он "унаследовал" это от класса Valve CLogicalEntity
(который доступен благодаря нашему #include cbase.h
). Модификатор доступа public
говорит, что открытые (и защищенные) члены родительского класса остаются открытыми (защищенными) и в дочернем классе.
Наследование означает использование существующего класса, а не создание своего собственного с нуля. CLogicalEntity
сама по себе довольно проста, но это огромная часть кода в классах, наследуемая в свою очередь от CBaseEntity
— не имея возможности наследовать все это, любой, пытающийся создать новую сущность, просто сойдет с ума.
После написание команд класса, мы открываем фигурные скобки. Написание кода внутри скобок означает, что вы его группируете; в данном случае, группа это текущий класс. (Заметьте, что здесь нет точки с запятой, которая означает конец строки. Технически, мы продолжаем пишем ту же строку кода.)
Далее идет "public:
", который означает что следующие за ним "члены" класса доступны из других классов (мы вернемся к private объявлению позже). Вы должны делать члены публичными, только когда это строго необходимо: Этот принцип называется Инкапсуляцией и он не является уникальным для C++.
Первая команда
Теперь, когда у нас есть каркас, мы может начать вводить команды. DECLARE_CLASS
это 'макрос ', созданный Valve для автоматизации большей части требуемого кода, необходимого для объявления новой энтити: при компиляции кода, макрос заменяется на различные функции и переменные.
В круглых скобках мы передаем макросу "аргументы" (arguments), или "параметры" (parameters). Они передают информацию, необходимую для правильной работы. Вы, возможно, заметили, что мы посылаем ту же ключевую информацию, как в первой строке нашего объявления, но разделённую запятой.
Линия, закрываемая точкой с запятой, говорит компилятору, что это конец команды, и вы собираетесь начать еще одну. Это необходимо, т.к. строки и пробелы игнорируются - компилятор видит все как непрерывный поток символов.
Закрытие объявления (досрочно)
Не вводите ...
это использовались, чтобы показать вам, куда пишется остальная часть кода объявления. Мы забежим немного вперед, и закроем команду class
с помощью };
. Это достаточно просто: закрывающая скобка противоположна открывающей скобки, которую мы использовали ранее, а точка с запятой выполняет ту же функцию, что и после DECLARE_CLASS
.
}
и ;
, как это показано ранее. Команда class
это особый случай.Объявление DATADESC, конструктор и функции
Теперь вернемся обратно к многоточию, и добавим новые строки:
DECLARE_DATADESC(); // Конструктор CMyLogicalEntity () { m_nCounter = 0; } // Input функция void InputTick( inputdata_t &inputData );
Первая строка, это другой макрос, который автоматизирует процесс объявления DATADESC , таблицу мы добавим позже. Вторая строка, это комментарий (//
), который игнорируется компилятором, но помогает людям читать ваш код (включая вас, когда вы вернетесь к нему через длительный промежуток времени).
Третья строка, это "конструктор". Это тип "функции": команда или несколько команд между {
и }
, которые "вызываются", или "выполняются" в одном потоке. Из-за того, что название такое же, как и у нашего класса, C++ знает, что она вызывается при каждом создании экземпляра класса. В этом примере мы используем его для "инициализации" m_nCounter нулем. По умолчанию у переменных (см. следующий подзаголовок) незадано значение, пропуск его установления может привести к странным и непредсказуемым результатам!
Мы только что полностью "определили" конструктор, но большинство функций включая конструкторы для более объёмных классов) слишком большие, чтобы полностью определять их внутри классов. Поэтому мы объявляем и описываем их позже, в отдельном .cpp файле.
Таким образом, последняя строка в этом коде является объявлением функции, которая будет вызываться при правильном сообщении на input, и, если вы помните нашу первоначальную цель - увеличит нашу переменную на 1. void
означает, что функция не "возвращает" никаких данных в вызывающую функцию, так как вывод данных будет идти другим путём (конструктор это частный случай, которому никогда не требуется возвращать значение, поэтому тип конструктора никогда не указывается).
Мы называем эту функцию InputTick
и в круглых скобках определяем требуемые "параметры". В нашем случае, это переменная inputdata_t
, называемая &inputData
. Это информация об input событиях, которые автоматически генерируются движком во время выполнения, включая I/O аргументы из карты (здесь отсутствуют), и энтити, от которого было получено сообщение.
&
означает, что вместо передачи всей информации о событии, мы передаем запись местонахождения inputdata_t
например, в памяти системы. Этот "указатель" позволяет нам получать доступ к необходимой информации без ее копирования в нашу текущую функцию. Это похоже на открытие файла через ярлык на рабочем столе, вместо прямого перемещения или копирования.
{}
для InputTick()
отсутствуют: они появятся позже, когда мы напишем содержимое функции. Вместо них, необходимо использовать точку с запятой, чтобы обозначить конец команды.
Private объявления
Сейчас мы объявим private члены. Они не будут доступны за пределами этого класса, как если бы мы делали public функцию (чего мы делать не будем).
private: int m_nThreshold; // Пороговое значение, по достижению которого активируется output int m_nCounter; // Внутренний счётчик COutputEvent m_OnThreshold; // Output событие, когда счётчик достигает порогового значения
Первые две из них являются "переменными". Это "сосуды" в физической памяти, которые могут содержать значения, использоваться в вычислениях, записываться на диск, и обнуляться. Это "целое число (integer)" или просто "int" переменные, которые могут использоваться для хранения целых чисел, таких как -1, 0, 1, 2, 3, и т.д. Они не могут хранить слова или символы, ни числа с десятичной точкой. В C++ очень строго с такими вещами!
m_OnThreshold
является экземпляром COutputEvent
класса, который Valve уже написал для вас в cbase.h
. Вы будете использовать его в InputTick
для передачи значения через I/O систему, когда будут выполнены все условия. На самом деле, это почти такая же переменная, как и int
, но пользовательский контент не поставляется с компилятором. Единственной причиной, по которой этот тип не выделяется синим цветом, является то что Visual Studio не опознаёт его.
Пару слов об именах в этом разделе. m_
означает, что объекты, это члены текущего класса, когда n
означает, что это числовое значение (Так же можно использовать i
для 'integer'). Вы не должны следовать этим наименованиям, но они часто встречаются.
Проверка
Это объявление завершено. Сверьтесь с эталонным кодом . Если он такой же, вы готовый уйти за };
и добавить код в тело сущности.
Связывание класса с названием сущности
Мы закончили с объявлением класса. В Visual Studio, вы можете свернуть весь блок, нажатием на второй знак минуса на полях. Теперь, после };
, введите это:
LINK_ENTITY_TO_CLASS( my_logical_entity, CMyLogicalEntity );
Это другой макрос (об этом говорят заглавные буквы), связывающий C++ класс CMyLogicalEntity
с движком Source classname my_logical_entity. Названия классов используются I/O системой в Hammer , и иногда программистами .
Таблица описания данных
Таблица описания данных это серия макросов, которые дают Source метаданные о членах класса, которые вы упомянули в нем. Комментарии в коде служат объяснением, что делает каждая из функций: DEFINE_FIELD
гарантирует, что значение m_nCounter
в сохраненной игре не обнулится до нуля при следующей загрузке. DEFINE_KEYFIELD
делает такую же работу, а так же позволяет редактирование значения через Hammer.
Этот код имеет примеры строковых значений, упомянутых в предыдущем разделе. В отличии от других команд, это простые данные, которые компилятор принимает, хранит, и слепо выполняет в движке. Между кавычками вы можете написать все, что угодно, код будет компилироваться, но когда дело дойдет до его выполнения, движок Source будет сбит с толку если значение окажется недопустимым.
// Начало описания наших данных для класса BEGIN_DATADESC( CMyLogicalEntity ) // Для сохранения/загрузки DEFINE_FIELD( m_nCounter, FIELD_INTEGER ), // Связывает наши переменные с ключевыми значениями из Hammer DEFINE_KEYFIELD( m_nThreshold, FIELD_INTEGER, "threshold" ), // Связывает наше input название из Hammer с нашей input функцией DEFINE_INPUTFUNC( FIELD_VOID, "Tick", InputTick ), // Связывает наш ouput с output названием, используемым Hammer DEFINE_OUTPUT( m_OnThreshold, "OnThreshold" ), END_DATADESC()
Заметьте, что наш input осуществляется через функцию, а наш output является лишь переменной COutputEvent
, которую мы объявили ранее. Хотя input требует обработки, и, следовательно, функции, output будет уже обработан к моменту использования
При написании собственных таблиц описания данных, не забудьте использовать запятую после каждой DEFINE_*
команды.
Создание input функции
Теперь мы намерены "определить" (define) нашу input функцию. Это последний шаг в C++. Обратите внимание, что первая строка идентична той, что мы ввели в объявлении класса, за исключением добавления CMyLogicalEntity:
. Это гарантирует связь с нужным классом. В этом случае имеется только один класс в .cpp, но их может быть больше.
//----------------------------------------------------------------------------- // Назначение: Обработка входящих значений (tick input) от других сущностей //----------------------------------------------------------------------------- void CMyLogicalEntity::InputTick( inputdata_t &inputData ) { // Увеличение нашего счетчика m_nCounter++; // Проверяем, достиг ли счетчик порогового значения if ( m_nCounter >= m_nThreshold ) { // Запуск output события m_OnThreshold.FireOutput( inputData.pActivator, this ); // Сбрасывание нашего счетчика m_nCounter = 0; } }
Вот что происходит внутри функции:
m_nCounter++
- C++ сокращенная запись "увеличить на единицу". Полностью, это будет
m_nCounter = m_nCounter + 1;
. Другой способ написания -m_nCounter += 1;
. if ( m_nCounter >= m_nThreshold )
if
определяет выполнены ли условия.>
означает больше, так что в сочетании с=
мы имеем "оператор" "больше или равно". Если значение m_nCounter больше, или равно значению m_nThreshold, выполняется все, что внутри команды.{ }
- Это "вложенная" в функцию пара фигурных скобок. Она группирует все команды, выполняемые оператором
if
при выполнении всех условий. Там может быть много уровней вложенности, но будьте внимательны, чтобы отслеживать их все! m_OnThreshold.FireOutput
- Возможно, вы просто скопировали код. Удалите его и напишите его полностью, и отметьте, что произойдет, когда вы достигните точки. Вы вызовите общие функции
m_OnThreshold
классаCOutputEvent
, и Visual Studio поможет вам предоставлением списка возможных вариантов. Продолжайте писать, чтобы отфильтровать нужное, или используйте стрелки вверх/вниз. - Если бы мы вызывали набираемую нами функцию из другого класса, нам пришлось бы написать
MyLogicalEntity1.InputTick(<аргументы>)
, гдеMyLogicalEntity1
, это название нужного нам экземпляра класса. (Это означает, что мы должны иметь указатель на экземпляр класса - мы не можем просто получить доступ к шаблону!) ( inputData.pActivator, this )
- Эти аргументы мы передаем
COutputEvent
. Они будут автоматически сгенерированны через Source, и переданы в нашу функцию через параметры, которые указаны между()
. - Мы послали:
- № сущности, которая начинает всю эту цепь (в Source жаргоне "активатор").
- № сущности, которая вызывает функцию "caller", this - это указатель на объект, которому принадлежит функция, которую мы сейчас пишем.
- Это требуется для targetname keywords (ключевых слов имен цели).
m_nCounter = 0
- Если на сбрасывать счетчик, то он будет продолжать расти.
Поздравляем, сущность готова для компиляции - нажмите F7, чтобы начать процесс. Если вы получаете сообщения об ошибках от Visual Studio, сверьтесь с эталоном кода !
Мы не можем использовать эту сущность напрямую из движка, однако - если вы когда-либо делали карту, вы знаете, что существует много I/O информации, которой не нашлось места в нашем коде. К счастью, наша таблица DATADESC позволяет Source подключить эту сущность в более широкую систему I/O автоматически; так что, единственное, что нам предстоит сделать, это рассказать об сущности Hammer у.
Добавление FGD записи
FGD представляет собой текстовый файл, описывающий все сущности, используемые игрой, и делает их доступными. Мир не идеал и Hammer не будет читать код мода из него же, поэтому важно держать FGD файл в актуальном состоянии.
Если у вас еще нет FGD, сохраните пустой текстовый файл в папке вашего мода с названием <modname>.fgd
. Затем запустите Hammer и перейдите к Tools > Options
. Убедитесь, что ваш мод стоит в активной конфигурации, нажмите на кнопку "Add" и укажите путь к вашему FGD.
FGD это другая тема, поэтому он не будет подробно расписываться именно здесь. Просто вставьте следующий код в ваш .FGD файл:
@include "base.fgd" @PointClass base(Targetname) = my_logical_entity : "Tutorial logical entity." [ threshold(integer) : "Threshold" : 1 : "Threshold value." input Tick(void) : "Adds one tick to the entity's count." output OnThreshold(void) : "Threshold was hit." ]
И вот оно! Ваша сущность готова для использования. Используйте Hammer Entity Tool для добавления сущности на карту (вы можете использовать одну из карт-примеров Valve, если вам сложно создать свою), и настройте Inputs и Outputs .