Ru/Authoring a Logical Entity: Difference between revisions
TomEdwards (talk | contribs) (header fix) |
TomEdwards (talk | contribs) (ru templates) |
||
Line 63: | Line 63: | ||
Не печатайте многоточие (<code>...</code>) - здесь оно поставлено только для того чтобы показать где будет находиться продолжение описания класса. Пропустим пока это описание, а вместо него завершим команду <code>class</code> напечатав <code>};</code>. Закрывающая скобка - это пара для открывающей скобки, напечатанной нами ранее, а точка с запятой выполняет ту же функцию, что и после команды <code>DECLARE_CLASS</code>. | Не печатайте многоточие (<code>...</code>) - здесь оно поставлено только для того чтобы показать где будет находиться продолжение описания класса. Пропустим пока это описание, а вместо него завершим команду <code>class</code> напечатав <code>};</code>. Закрывающая скобка - это пара для открывающей скобки, напечатанной нами ранее, а точка с запятой выполняет ту же функцию, что и после команды <code>DECLARE_CLASS</code>. | ||
{{note|Обычно закрывающая фигурная скобка (<code>}</code>) и точка с запятой <code>;</code> не используются вместе. Команда <code>class</code> это особый случай.}} | {{note:ru|Обычно закрывающая фигурная скобка (<code>}</code>) и точка с запятой <code>;</code> не используются вместе. Команда <code>class</code> это особый случай.}} | ||
=== Описание DATADESC'а, конструктора и функции === | === Описание DATADESC'а, конструктора и функции === | ||
Line 152: | Line 152: | ||
Когда вы пишите свои таблицы описания данных не забывайте ставить запятую после каждой команды <code>DEFINE_*</code>. | Когда вы пишите свои таблицы описания данных не забывайте ставить запятую после каждой команды <code>DEFINE_*</code>. | ||
{{note|То, что здесь отсутствуют точки с запятой является довольно необычной практикой!}} | {{note:ru|То, что здесь отсутствуют точки с запятой является довольно необычной практикой!}} | ||
== Создание входной функции == | == Создание входной функции == |
Revision as of 06:13, 19 July 2009
Логические объекты (объект = entity) это самые простые для создания игровые объекты т.к. они не имеют позиции в пространстве, не отображаются в игре и нужны только для того чтобы получать входные данные от других объектов. Например объект math_counter
содержит число, к которому можно прибавлять или вычитать другое число; другие объекты на карте могут изменять значение этого числа через входы (inputs) или получать информацию от этого объекта через выход (output).
В этой статье мы создадим логический объект, который будет выполнять простую задачу хранения числа и увеличения его значения каждый раз когда объект будет получать на вход соответствующую команду. По достижении заданного нами значения наш объект будет генерировать вывод (output), чтобы уведомить другие объекты о достижении порогового значения счетчика.
Создание исходного файла
В этой статье предполагается что вы используете Visual Studio или Visual C++ Express. Смотрите статью про выбор компилятора: Compiler Choices:ru.
Добавьте в проект Server
новую папку ("filter") и назовите её как вам угодно. Затем добавьте в эту папку .cpp файл и назовите его sdk_logicalentity. В этом файле будет весь код, который вы напишите при прочтении этой статьи.
Хранение каждого объекта в своём собственном .cpp-файле снижает задержки, т.к. это позволяет компилятору разделять ваш код на более дискретные сегменты, и ускоряет компиляцию, т.к. только измененные .cpp файлы нуждаются в повторной компиляции.
Заголовочные файлы
Чтобы наш код работал везде необходимо сообщить компилятору какие "библиотеки" мы собираемся использовать. Рассматривайте команды, которые мы будем использовать, как книги из этих библиотек. Добавьте эту строчку кода:
#include "cbase.h"
Заголовочный файл cbase.h
содержит базовый набор команд Valve для создания объектов. Так как сейчас мы используем лишь простейшие команды нам нужна только одна базовая библиотека. В более сложных случаях на этом месте может быть очень много разных объявлений заголовочных файлов.
Открывающая "c" означает, что это серверная библиотека, а закрывающее ".h" означает что это заголовочный, а не .cpp файл - но пока можете об этом не думать.
Объявление класса
Этот параграф будет довольно большим, но после его прочтения у вас появится понимание всего С++ кода.
В С++ функции содержатся в "объектах". Воспринимаете их, по нашей аналогии, как библиотекарей.
Объекты создаются по шаблонам, таким как этот:
class CMyLogicalEntity : public CLogicalEntity { public: DECLARE_CLASS( CMyLogicalEntity, CLogicalEntity ); ... };
Весь этот процесс (а не только строка DECLARE_CLASS
, несмотря на название) называется "описание класса". Каждый экземпляр вашего объекта в игре это объект построенный по этому шаблону. Это справедливо для каждого объекта: каждый Combine Soldier, который бегает в игре это независимый экземпляр соответствующего С++ класса.
В первой строке кода командой class говорится о том что мы объявляем класс. Затем мы даём ему имя CMyLogicalEntity
, а затем двоеточие говорит компилятору что мы наследуем наш класс от существующего класса Valve CLogicalEntity
(за это можем поблагодарить включенный нами ранее файл cbase.h
). Создаваемый нами класс называется дочерним, а класс CLogicalEntity
- родительским. Модификатор public говорит компилятору о том, что все открытые (и защищенные) члены родительского класса остаются открытыми (защищенными) и в дочернем классе.
Наследование означает что мы работаем над копией родительского класса, перекрывая и дополняя его, вместо того чтобы начинать писать всё с нуля или изменять родительский класс непосредственно. Это почти тоже самое что скопировать какой-нибудь файл на свой рабочий стол: вы получаете содержимое оригинального файла и можете редактировать его отдельно.
Затем мы открываем фигурные скобки. Все функции, находящиеся в пределах фигурных скобок являются членами нашего класса. Заметьте, что здесь отсутствуют точка с запятой. Технически мы всё ещё пишем всё ту же строчку кода.
Затем следует модификатор "public:
", который означает что следующие за ним члены класса доступны из функций других классов (потом мы создадим и несколько private-членов).
Первая команда
Теперь у нас есть каркас для того, чтобы добавлять в него команды. DECLARE_CLASS
это 'макрос' созданный valve для автоматизации действий, которые необходимо проделать при объявлении нового класса.
В круглых скобках мы передаём макросу "аргументы", или "параметры": в продолжение нашей анологии мы говорим (в данном случае) какую книгу мы хотим добавить и к какому классу книг она относится. Как вы могли заметить мы передаем макросу ту же самую информацию, которая уже содержится в первой строке кода, но разделённую запятой.
Макрос DECLARE_CLASS
нужен для того чтобы когда ваш класс будет инициализироваться при запуске вашего мода, где-нибудь еще, в коде написанном Valve, для вас было выполнено изрядное кол-во невразумительной и скучной работы. Закрывающие точка с запятой говорит компилятору, что это конец команды и дальше последует уже другая команда (конец строки и пустое место в расчет не берутся - компилятор их просто пропускает).
Конец описания
Не печатайте многоточие (...
) - здесь оно поставлено только для того чтобы показать где будет находиться продолжение описания класса. Пропустим пока это описание, а вместо него завершим команду class
напечатав };
. Закрывающая скобка - это пара для открывающей скобки, напечатанной нами ранее, а точка с запятой выполняет ту же функцию, что и после команды DECLARE_CLASS
.
Описание DATADESC'а, конструктора и функции
Теперь вернитесь к многоточию и добавьте вместо него следующие строки:
DECLARE_DATADESC(); // Конструктор CMyLogicalEntity () { m_nCounter = 0; } // Входная функция void InputTick( inputdata_t &inputData );
Первая строка это еще один макрос. На этот раз он автоматизирует описание таблицы описания данных (DATADESC), которое мы добавим позже. Две косые черты //
говорят что вторая строка это комментарий, который игнорируется компилятором, и помогает другим людям (а так же вам самим, после того как вы вернетесь к работе после длительного перерыва) разобраться в вашем коде.
Третья строка это "конструктор" класса. Конструктор - это разновидность функции: одна или несколько команд заключенных в фигурные скобки {
и }
, которые "вызываются" (или "выполняются") одним блоком. Из-за того, что конструктор имеет такое же имя, что и наш класс, компилятор знает, что эту функцию (конструктор) необходимо выполнить при создании нового экземпляра класса. В данном примере мы используем конструктор для того, чтобы "инициализировать" m_nCounter нулём. Переменные не имеют начального значения, и если не присвоить им его перед использованием, то это может привести к странным и непонятным результатам.
Мы только что полностью "определили" конструктор, однако большинство функций (включая конструкторы для более объёмных классов) слишком большие, чтобы полностью определять их внутри классов, поэтому мы лишь описываем их, а определения позже записываем в отдельные .срр файлы.
Соответственно последняя строка этого кода - это описание функции, которая будет вызвана при получении соответствующего входного параметра и, если вы всё ещё помните нашу первоначальную задачу, увеличит хранимое нами значение на единицу. void
означает что функция не "возвращает" никаких данных в вызывающую функцию так как вывод данных будет идти другим путём (конструктор это частный случай функции, которому никогда не требуется возвращать значение, поэтому тип конструктора всегда void
).
Назовем нашу функцию InputTick
и в круглых скобках определим параметры, которые ей нужны - в данном случае это одно значение типа inputdata_t
, которое называется &inputData
; это информация о произошедшем событии, которая автоматически генерируется движком игры во время выполнения, и содержит, среди прочего, имя события, входные и выходные параметры с карты (здесь отсутствуют) и ссылку на объект от которого поступило сообщение.
Знак &
означает что мы, вместо того чтобы передавать функции абсолютно все детали о произошедшем событии, передаём в функцию адрес экземпляра inputdata_t
, хранящегося в памяти. Этот "указатель" позволяет нам обращаться к данным экземпляра не копируя его лишний раз. Это похоже на открытие файла непосредственно с чьего-нибудь компьютера вместо того чтобы сначала скопировать его на свой компьютер - только у нас нет сетевых задержек, т.к. С++ воспринимает всё как единую часть памяти произвольного доступа (Random Access Memory).
Заметьте что в описании функции отсутствуют фигурные скобки {}
: они появятся позже, когда мы будем писать содержимое функции. Точку с запятой необходимо использовать для того чтобы обозначить конец команды.
Описание закрытых (private) членов
Теперь мы опишем закрытые члены объекта. Закрытые члены доступны только внутри объекта, частью которого они являются и больше ниоткуда.
private: int m_nThreshold; // Пороговое значение, по достижении которого будет произведен вывод int m_nCounter; // Внутренний счётчик COutputEvent m_OnThreshold; // Выходное событие когда счётчик достигает порогового значения
Первая строка это модификатор доступа - все следующие за ним описания будут закрытыми (private). Вторая и третья строка - это описания "переменных". Переменные - это некие сосуды в физической памяти компьютера, которые могут содержать в себе различные значения, которые, в свою очередь, могут использоваться в вычислениях, передаваться в функции, возможно записываться на диск или обнуляться. В нашем случае объявляется две переменные целого типа ("integer") то есть типа "int", которые могут использоваться для хранения целых чисел, таких как -1, 0, 1, 2, 3, и т.д., однако они не могут хранить слова или символы, а так же нецелочисленные значения. С++ очень строг в такого рода вопросах.
m_OnThreshhold
это экземпляр COutputEvent
- класса, который в Valve написали для вас в файле cbase.h
. Вы будете использовать его в функции InputTick
чтобы передать значение через систему ввода/вывода когда выполнятся соответствующие условия. Вообще COutputEvent
это тоже тип переменной, как и int
, но COutputEvent
- это пользовательская переменная, которая не поставляется вместе с компилятором. Единственной причиной, по которой этот тип не выделяется синим цветом, является то что Visual Studio не опознаёт его.
Пару слов об именах в этой секции. m_
означает, что объекты являются членами данного класса. n
перед именем переменной означает что переменная хранит целочисленные значения (Для целых чисел так же может использоваться i
). Вы не обязаны придерживаться этих правил, но это строго рекомендуется.
Итог
Итак, описание класса завершено. Просмотрите то, что вы написали и убедитесь что всё соответствует началу этого образца. Если так и есть, то вы готовы продвинуться дальше за };
и начать писать тело нашего объекта.
Связывание класса с именем игрового объекта
Мы закончили описание класса. В Visual Studio вы можете свернуть всё это описание щёлкнув знак "-" слева от команды class.
Теперь после };
добавьте эту строчку:
LINK_ENTITY_TO_CLASS( my_logical_entity, CMyLogicalEntity );
Это еще один макрос (об этом говорят заглавные буквы), который даёт классу "имя игрового объекта". Программный код обращается к игровым объектам не под тем же именем нежели система ввода/вывода или Hammer по причинам, которые вы поймёте позже.
Таблица описания данных
Таблица описания данных - это макросы, которые обеспечивают движок Source метаданными о любом члене класса, который в нём содержится. Комментарии должны пояснить для чего нужен каждый макрос: например DEFINE_FIELD
гарантирует, что значение m_nCounter
будет храниться в сохранённой игре чтобы избежать его сбрасывания в ноль при перезагрузке игры. DEFINE_KEYFIELD
проделывает ту же работу, а так же делает доступным редактирование этого параметра в Hammer'е.
Этот код имеет пример коричневых "строковых" значений. В отличие от остальных значений они являются простыми данными, которые компилятор принимает, хранит и слепо передаёт движку. В ковычках вы можете написать всё что угодно и код будет компилироваться, но когда дело дойдёт до обращения к этой строковой переменной, движок Source будет сбит с толку если значение окажется недопустимым.
// Начало нашего определения данных для класса CMyLogicalEntity. BEGIN_DATADESC( CMyLogicalEntity ) // Для сохранения/загрузки. DEFINE_FIELD( m_nCounter, FIELD_INTEGER ), // Связывает нашу переменную с ключевым значением из Hammer'a. DEFINE_KEYFIELD( m_nThreshold, FIELD_INTEGER, "threshold" ), // Даёт нашей функции ввода имя в Hammer'e DEFINE_INPUTFUNC( FIELD_VOID, "Tick", InputTick ), // Даёт нашей переменной вывода имя в Hammer'е DEFINE_OUTPUT( m_OnThreshold, "OnThreshold" ), END_DATADESC()
Заметьте, что наш ввод (input) осуществляется через функцию, а вывод (output) через переменную, которую мы определили ранее.
Когда вы пишите свои таблицы описания данных не забывайте ставить запятую после каждой команды DEFINE_*
.
Создание входной функции
Теперь мы "определим" нашу входную функцию. Это последний шаг в написании С++ кода. Заметьте, что первая строка определения соответствует строке её описания в описании класса за исключением части CMyLogicalEntity::
. Эта часть гарантирует, что мы описываем функцию нужного нам класса. В нашем случае в файле всего один класс, однако их может быть несколько.
//----------------------------------------------------------------------------- // Назначение: Принимает ввод от других объектов. //----------------------------------------------------------------------------- void CMyLogicalEntity::InputTick( inputdata_t &inputData ) { // Увеличение нашего счетчика. m_nCounter++; // Проверяем достиг ли счетчик порогового значения. if ( m_nCounter >= m_nThreshold ) { // Запуск события вывода. m_OnThreshold.FireOutput( inputData.pActivator, this ); // Сброс нашего счетчика. m_nCounter = 0; } }
Вот что происходит в этой функции:
m_nCounter++
- Это сокращенная запись выражения "увеличить на единицу". В полном варианте это выглядит так:
m_ncounter = m_ncounter + 1
. if ( m_nCounter >= m_nThreshold )
- Оператор
if
проверяет выполняется ли выражение, стоящее в круглых скобках (), и если да, то выполняет те команды, которые стоят в фигурных скобках {}, следующих сразу после круглых.>=
означает "больше или равно". Соответственно если значениеm_nCounter
больше или равно значенияm_nThreshold
, то выполняются команды стоящие в фигурных скобках {}. {
и}
- Это "вложенная" в функцию пара фигурных скобок. Она групирует команды, которые будут выполнены, когда выполнится условие оператора
if
. По мере необходимости может быть множество вложенных друг в друга фигурных скобок. m_OnThreshold.FireOutput
- Если вы скопировали код из этой статьи и вставили его в Visual Studio, то лучше перепишите эту строку заново и отметьте что произойдёт когда вы напечатаете точку. Вам надо вызвать открытую (public) функцию экземпляра
m_OnThreshold
классаCOutputEvent
и Visual Studio помогает вам, показывая список доступных вариантов. Продолжайте печатать, пока не будет выделен нужный вам член класса, или выберите его с помощью стрелок клавиатуры и клавиши enter. - Если бы мы вызывали набираемую нами функцию из другого класса, то нам необходимо было бы напечатать
MyLogicalEntity1.InputTick(<аргументы>)
, гдеMyLogicalEntity1
это имя нужного нам экземпляра класса. (Что означает, что нам понадобилось бы сначала создать этот экземпляр - нельзя обращаться к обычным функциям через название класса (шаблона)!). ( inputData.pActivator, this )
- Это аргументы, которые мы передаём функции
FireOutput
. Они автоматически генерируются движком Source и передаются в нашу функцию через параметры, которые находятся в круглых скобках (). - В фукнцию
FireOutput
мы передаём:- Объект, который положил начало этой цепочке событий (на жаргоне Source это "активатор" (activator)).
- Объект, который вызывает функцию
FireOutput
- "caller". this - это указатель на объект, которому принадлежит функция, которую мы сейчас пишем.
- Эти параметры нужны для "ключевых слов имён цели" (targetname keywords).
m_nCounter = 0
- Если мы не сбросим счётчик он будет продолжать увеличиваться выше порогового значения.
Примите поздравления - объект готов к компиляции. Для её начала нажмите F7. Если вы получили уведомление об ошибке, то убедитесь, что ваш код соответствует этому!
Мы не сможем использовать наш объект непосредственно из движка - если вы когда-нибудь создавали свою карту, то вы знаете что существует еще много информации ввода-вывода, которой не нашлось места в нашем коде. Однако наша таблица описания данных позволяет движку Source автоматически подключить наш объект к более широкой системе ввода/вывода; поэтому всё что нам осталось это дать Hammer'у знать о нашем объекте.
Создание записи в FGD-файле
Файл FGD - это текстовый файл, который описывает какие объекты содержит ваш мод и что с ними можно делать. Мир не идеален и Hammer не умеет считывать эту информацию напрямую из кода мода, поэтому важно поддерживать FGD-файл в актуальном состоянии.
Если у вас пока нет FGD-файла сохраните пустой текстовый файл в папке вашего мода и назовите его <имя_мода>.fgd
. Затем загрузите Hammer и зайдите в Tools > Options
. Убедитесь что ваш мод стоит в "активной конфигурации" Hammer'a, нажмите кнопку "Add" и укажите путь к вашему файлу 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." ]
Что же, вы это сделали! Ваш первый игровой объект готов к использованию.