Ru/Authoring a Logical Entity: Difference between revisions

From Valve Developer Community
< Ru
Jump to navigation Jump to search
No edit summary
Line 1: Line 1:
Для начала мы создадим логическую энтити которая будет выполнять простейшую работу. Она будет хранит значение и увеличивать его каждый раз когда получает ввод (input) от другой энтити. Когда счетчик достигает заданного нами значения, энтити сгенерирует вывод (output) позволяющий оповестить другие энтити об этом условии.
Логические объекты (объект = entity) это самые простые для создания игровые объекты т.к. они не имеют позиции в пространстве, не отображаются в игре и нужны только для того чтобы получать входные данные от других объектов. Например объект <code>[[math_counter]]</code> содержит число, к которому можно прибавлять или вычитать другое число; другие объекты на карте могут изменять значение этого числа через входы (inputs) или получать информацию от этого объекта через выход (output).


=Создаем CPP файл для новой энтити=
В этой статье мы создадим логический объект, который будет выполнять простую задачу хранения числа и увеличения его значения каждый раз когда объект будет получать на вход соответствующую команду. По достижении заданного нами значения наш объект будет генерировать вывод (output), чтобы уведомить другие объекты о достижении порогового значения счетчика.
[[Image:Add existing item.gif|Добавляем исходный файл в проект server.dll по правому щелчку.]]


* Создаем файл называемый <code>sdk_logicalentity.cpp</code>. Этот файл должен быть внутри пакпки dlls, которая, в свою очередь,  находится в папке с вашим исходным кодом. Например, если вы установили код в <code>C:\MyMod\src</code>, то вы должны создать файл из <code>C:\MyMod\src\dlls\sdk_logicalentity.cpp</code>.
= Создание исходного файла =
* Далее копируем [[Logical Entity Code |этот код]] и вставляем его в новый файл.
* В последнею очередь добавляем этот файл в ваш проект server.dll. Если вы открыли <code>game_sdk.sln</code> solution, тогда вы можете щелкнуть правой кнопкой на проекте hl в окне Solution Explorer и выбрать Add, затем Add Existing Item.


=Обзор кода=
В этой статье предполагается что вы используете Visual Studio или Visual C++ Express. Смотрите статью про выбор компилятора: [[Compiler Choices]].
==Определение класса==


<pre>
[[Image:AddNewItem.png|center|Добавление нового .cpp-файла в собственную папку.]]
class CMyLogicalEntity : public CLogicalEntity
{
public:
      DECLARE_CLASS( CMyLogicalEntity, CLogicalEntity );
};
</pre>


Мы наследуем структуру нашей новой энтити от класса <code>CLogicalEntity</code>. Этот класс содержит энтити, которыя находятся на стороне сервера и не передают данные на сторону клиента. Также мы будем использовать вспомогательный макрос <code>DECLARE_CLASS</code> который скрывает от нас некоторые рутинные действия. Унаследовав <code>CMyLogicalEntity</code> от <code>CLogicalEntity</code> мы можем использовать тип <code>BaseClass</code> с этим классом. Это будет важно позднее для обращения к функциональности которую несет <code>CLogicalEntity</code> от которого мы унаследовали класс.
Добавьте в проект <code>Server</code> новую папку ("filter") и назовите её как вам угодно. Затем добавьте в эту папку .cpp файл и назовите его '''sdk_logicalentity'''. В этом файле будет весь код, который вы напишите при прочтении этой статьи.


==Связываем класс с именем энтити==
Хранение каждого объекта в своём собственном .cpp-файле снижает задержки, т.к. это позволяет компилятору разделять ваш код на более дискретные сегменты, и ускоряет компиляцию, т.к. только измененные .cpp файлы нуждаются в повторной компиляции.


Затем мы связываем класс <code>CMyLogicalEntity</code> с  фактической энтитей <code>classname</code> на которую сможет ссылаться движок.
= Заголовочные файлы =


<pre>
Чтобы наш код работал везде необходимо сообщить компилятору какие "библиотеки" мы собираемся использовать. Рассматривайте команды, которые мы будем использовать, как книги из этих библиотек.
LINK_ENTITY_TO_CLASS( my_logical_entity, CMyLogicalEntity );
Добавьте эту строчку кода:
</pre>


Здесь для класса <code>CMyLogicalEntity</code> объявляется его имя <i>classname</i> как <code>"my_logical_entity"</code>. Это имя которое Хаммер и движок будут использовать для ссылки на тип энтити. Имя класса (<code>classname</code>) отличается от имени назначения (<code>targetname</code>) энтити, которое является строкой которая идентифицирует одну энтитю или группу энтитей. <code>classname</code> указывает на все энтити данного типа, тогда как <code>targetname</code> может связывать несколько разных типов энтитей (например у вас может быть энтитя с именем класса <code>env_splash</code> которая принадлежит группе энтитей, у каждой из которых <code>targetname</code> равен <code>splash_group</code>).
<span style="color:blue;">#include</span> <span style="color:brown;">"cbase.h"</span>


Заголовочный файл <code>cbase.h</code> содержит базовый набор команд Valve для создания объектов. Так как сейчас мы используем лишь простейшие команды нам нужна только одна базовая библиотека. В более сложных случаях на этом месте может быть очень много разных объявлений заголовочных файлов.


Открывающая "c" означает, что это серверная библиотека, а закрывающее ".h" означает что это заголовочный, а не .cpp файл - но пока можете об этом не думать.


==Добавляем переменные в класс==
= Объявление класса =


<pre>
Этот параграф будет довольно большим, но после его прочтения у вас появится понимание всего С++ кода.
int    m_nThreshold;  // Пороговое значение срабатывания output
int    m_nCounter;    // Внутренний счетчик
</pre>


Здесь мы описали две переменные целого типа, которые будут в дальшейшем использоваться в счетчике.
В С++ функции содержатся в "объектах". Воспринимаете их, по нашей аналогии, как библиотекарей.


==Используем описание данных энтити==
Объекты создаются по шаблонам, таким как этот:
<pre>
. . .


public:
<span style="color:blue;">class</span> CMyLogicalEntity : <span style="color:blue;">public</span> CLogicalEntity
      DECLARE_CLASS( CMyLogicalEntity, CLogicalEntity);
{
      DECLARE_DATADESC();
<span style="color:blue;">public</span>:
DECLARE_CLASS( CMyLogicalEntity, CLogicalEntity );
...
};


. . .
Весь этот процесс (а не только строка <code>DECLARE_CLASS</code>, несмотря на название) называется "описание класса". Каждый экземпляр вашего объекта в игре это объект построенный по этому шаблону. Это справедливо для каждого объекта: каждый [[Combine Soldier]], который бегает в игре это независимый экземпляр соответствующего С++ класса.


LINK_ENTITY_TO_CLASS( my_logical_entity, CMyLogicalEntity );
В первой строке кода командой class говорится о том что мы объявляем класс. Затем мы даём ему имя <code>CMyLogicalEntity</code>, а затем двоеточие говорит компилятору что мы наследуем наш класс от существующего класса Valve <code>CLogicalEntity</code> (за это можем поблагодарить включенный нами ранее файл <code>cbase.h</code>). Создаваемый нами класс называется дочерним, а класс <code>CLogicalEntity</code> - родительским. Модификатор <span style="color:blue;">public</span> говорит компилятору о том, что все открытые (и защищенные) члены родительского класса остаются открытыми (защищенными) и в дочернем классе.
BEGIN_DATADESC( CMyLogicalEntity )


DEFINE_FIELD( m_nCounter, FIELD_INTEGER ),
Наследование означает что мы работаем над копией родительского класса, перекрывая и дополняя его, вместо того чтобы начинать писать всё с нуля или изменять родительский класс непосредственно. Это почти тоже самое что скопировать какой-нибудь файл на свой рабочий стол: вы получаете содержимое оригинального файла и можете редактировать его отдельно.
DEFINE_KEYFIELD( m_nThreshold, FIELD_INTEGER, "threshold" ),


END_DATADESC()
Затем мы открываем фигурные скобки. Все функции, находящиеся в пределах фигурных скобок являются членами нашего класса. Заметьте, что здесь отсутствуют точка с запятой. Технически мы всё ещё пишем всё ту же строчку кода.
</pre>


Макрос <code>DECLARE_DATADESC</code> должен включаться в код для того чтобы компилятор мог знать что мы добавляем описание таблицы данных (data description table) ниже в реализации класса. Описание данных содержит различные определения для переменных и специальных функций для этого класса. В нашем случае, <code>m_nCounter</code> описывается для сохранения/загрузки значения счетчика, а <code>m_nThreshold</code> - для того чтобы сообщить игре об использовании значения с именем <code>"threshold"</code> (конечного значения) для связи этой переменной с значением ключа энтити в Хаммере.
Затем следует модификатор "<code>public:</code>", который означает что следующие за ним члены класса доступны из функций других классов (потом мы создадим и несколько private-членов).


Смотрите документ [[Data Descriptions:ru|Таблицы описания данных]] для дополнительной информации.
== Первая команда ==


==Создаем событие вывода (output event)==
Теперь у нас есть каркас для того, чтобы добавлять в него команды. <code>DECLARE_CLASS</code> это 'макрос' созданный valve для автоматизации действий, которые необходимо проделать при объявлении нового класса.
<pre>
COutputEvent  m_OnThreshold;
DEFINE_OUTPUT( m_OnThreshold, "OnThreshold" ),
</pre>


Это событие будет включаться при встрече описанного конечного значения. Для дополнительной информации смотрите [[Entity Input and Outputs|вводы и выводы энтитей]].
В круглых скобках мы передаём макросу "аргументы", или "параметры": в продолжение нашей анологии мы говорим (в данном случае) какую книгу мы хотим добавить и к какому классу книг она относится. Как вы могли заметить мы передаем макросу ту же самую информацию, которая уже содержится в первой строке кода, но разделённую запятой.


==Создаем функцию ввода (input function)==
Макрос <code>DECLARE_CLASS</code> нужен для того чтобы когда ваш класс будет инициализироваться при запуске вашего мода, где-нибудь еще, в коде написанном Valve, для вас было выполнено изрядное кол-во невразумительной и скучной работы. Закрывающие точка с запятой говорит компилятору, что это конец команды и дальше последует уже другая команда (конец строки и пустое место в расчет не берутся - компилятор их просто пропускает).


<pre>
== Конец описания ==
void InputTick( inputdata_t &inputData );


void CMyLogicalEntity::InputTick( inputdata_t &inputData )
Не печатайте многоточие (<code>...</code>) - здесь оно поставлено только для того чтобы показать где будет находиться продолжение описания класса. Пропустим пока это описание, а вместо него завершим команду <code>class</code> напечатав <code>};</code>. Закрывающая скобка - это пара для открывающей скобки, напечатанной нами ранее, а точка с запятой выполняет ту же функцию, что и после команды <code>DECLARE_CLASS</code>.
{
      // Увеличиваем наш счетчик.
      m_nCounter++;


      // Проверяем, не превысили ли мы пороговое значение.
{{note|Обычно закрывающая фигурная скобка (<code>}</code>) и точка с запятой <code>;</code> не используются вместе. Команда <code>class</code> это особый случай.}}
      if ( m_nCounter >= m_nThreshold )
      {
              // Если превысили - вызываем событие output.
              m_OnThreshold.FireOutput( inputData.pActivator, this );
           
              // Сбрасываем счетчик на ноль.
              m_nCounter = 0;
      }
}
</pre>


Эта функция просто увеличивает счетчик и генерирует событие output когда значение счетчика достигает конечного значения, как это указано в свойстве энтити в Хамере. Данная функция не извлекает никаких значений из Хаммера.
== Описание DATADESC'а, конструктора и функции ==


=Создаем запись в FGD файле=
Теперь вернитесь к многоточию и добавьте вместо него следующие строки:


Для использования энтити в Хаммере, мы должны создать запись в FGD файле. Смотрите [[FGD |документ FGD формата]] для дополнительной информации о FGD файлах.
DECLARE_DATADESC();
<span style="color:green;">// Конструктор</span>
CMyLogicalEntity ()
{
m_nCounter = 0;
}
<span style="color:green;">// Входная функция</span>
<span style="color:blue;">void</span> InputTick( inputdata_t &inputData );


Если вы ещё не создали .FGD файл для вашего мода, вы наверное это сейчас захотите сделать. Чтобы сделать это создайте пустой файл с расширением .FGD гденибудь на вашем жестком диске (лучше положить его в папку с вашим модом). Вставьте код внизу в этот файл. Перейдите в Хаммер и выберите Tools->Options и добавьте .FGD файл в секцию <code>Game Data files</code>. Диалог Game Configurations описан [[Hammer Game Configurations |в этом]] документе.
Первая строка это еще один макрос. На этот раз он автоматизирует описание таблицы описания данных ([[DATADESC]]), которое мы добавим позже. Две косые черты <code>//</code> говорят что вторая строка это комментарий, который игнорируется компилятором, и помогает другим людям (а так же вам самим, после того как вы вернетесь к работе после длительного перерыва) разобраться в вашем коде.


Затем мы объявляем знвчение <i>"threshold"</i> связанное с переменной <code>m_nThreshold</code>, функцией ввода <code>Tick</code> и функцией вывода <code>OnThreshold</code>.
Третья строка это "конструктор" класса. Конструктор - это разновидность функции: одна или несколько команд заключенных в фигурные скобки <code>{</code> и <code>}</code>, которые "вызываются" (или "выполняются") одним блоком. Из-за того, что конструктор имеет такое же имя, что и наш класс, компилятор знает, что эту функцию (конструктор) необходимо выполнить при создании нового экземпляра класса. В данном примере мы используем конструктор для того, чтобы "инициализировать" m_nCounter нулём. Переменные не имеют начального значения, и если не присвоить им его перед использованием, то это может привести к странным и непонятным результатам.


<pre>
Мы только что полностью "определили" конструктор, однако большинство функций (включая конструкторы для более объёмных классов) слишком большие, чтобы полностью определять их внутри классов, поэтому мы лишь описываем их, а определения позже записываем в отдельные .срр файлы.
@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."
]
</pre>


Если ваш .FGD не пустой, убедитесь что добавили строку <code>@include "base.fgd"</code>, которая даст вам некоторые нужные функции Хаммера.
Соответственно последняя строка этого кода - это описание функции, которая будет вызвана при получении соответствующего входного параметра и, если вы всё ещё помните нашу первоначальную задачу, увеличит хранимое нами значение на единицу. <code>void</code> означает что функция не "возвращает" никаких данных в вызывающую функцию так как вывод данных будет идти другим путём (конструктор это частный случай функции, которому никогда не требуется возвращать значение, поэтому тип конструктора всегда <code>void</code>).
 
Назовем нашу функцию <code>InputTick</code> и в круглых скобках определим параметры, которые ей нужны - в данном случае это одно значение типа <code>inputdata_t</code>, которое называется <code>&inputData</code>; это информация о произошедшем событии, которая автоматически генерируется движком игры во время выполнения, и содержит, среди прочего, имя события, входные и выходные параметры с карты (здесь отсутствуют) и ссылку на объект от которого поступило сообщение.
 
Знак <code>&</code> означает что мы, вместо того чтобы передавать функции абсолютно все детали о произошедшем событии, ''передаём в функцию адрес экземпляра <code>inputdata_t</code>, хранящегося в памяти''. Этот "указатель" позволяет нам обращаться к данным экземпляра не копируя его лишний раз. Это похоже на открытие файла непосредственно с чьего-нибудь компьютера вместо того чтобы сначала скопировать его на свой компьютер - только у нас нет сетевых задержек, т.к. С++ воспринимает всё как единую часть памяти произвольного доступа ([[Wikipedia:Random access memory|Random Access Memory]]).
 
Заметьте что в описании функции отсутствуют фигурные скобки <code>{}</code>: они появятся позже, когда мы будем писать содержимое функции. Точку с запятой необходимо использовать для того чтобы обозначить конец команды.
 
== Описание закрытых (private) членов ==
 
Теперь мы опишем закрытые члены объекта. Закрытые члены доступны только внутри объекта, частью которого они являются и больше ниоткуда.
 
<span style="color:blue;">private</span>:
<span style="color:blue;">int</span> m_nThreshold; <span style="color:green;">// Пороговое значение, по достижении которого будет произведен вывод</span>
<span style="color:blue;">int</span> m_nCounter; <span style="color:green;">// Внутренний счётчик</span>
COutputEvent m_OnThreshold; <span style="color:green;">// Выходное событие когда счётчик достигает порогового значения</span>
 
Первая строка это модификатор доступа - все следующие за ним описания будут закрытыми (private). Вторая и третья строка - это описания "переменных". Переменные - это некие сосуды в физической памяти компьютера, которые могут содержать в себе различные значения, которые, в свою очередь, могут использоваться в вычислениях, передаваться в функции, возможно записываться на диск или обнуляться. В нашем случае объявляется две переменные целого типа ("[[integer]]") то есть типа "int", которые могут использоваться для хранения целых чисел, таких как -1, 0, 1, 2, 3, и т.д., однако они не могут хранить слова или символы, а так же нецелочисленные значения. С++ очень строг в такого рода вопросах.
 
<code>m_OnThreshhold</code> это экземпляр <code>COutputEvent</code> - класса, который в Valve написали для вас в файле <code>cbase.h</code>. Вы будете использовать его в функции <code>InputTick</code> чтобы передать значение через систему ввода/вывода когда выполнятся соответствующие условия. Вообще <code>COutputEvent</code> это тоже тип переменной, как и <code>int</code>, но <code>COutputEvent</code> - это пользовательская переменная, которая не поставляется вместе с компилятором. Единственной причиной, по которой этот тип не выделяется синим цветом, является то что Visual Studio не опознаёт его.
 
Пару слов об именах в этой секции. <code>m_</code> означает, что объекты являются членами данного класса. <code>n</code> перед именем переменной означает что переменная хранит целочисленные значения (Для целых чисел так же может использоваться <code>i</code>). Вы не ''обязаны'' придерживаться этих правил, но это строго рекомендуется.
 
== Итог ==
 
Итак, описание класса завершено. Просмотрите то, что вы написали и убедитесь что всё соответствует началу этого [[Logical Entity Code|образца]]. Если так и есть, то вы готовы продвинуться дальше за <code>};</code> и начать писать тело нашего объекта.
 
= Связывание класса с именем игрового объекта =
 
Мы закончили описание класса. В Visual Studio вы можете свернуть всё это описание щёлкнув знак "-" слева от команды class.
Теперь после <code>};</code> добавьте эту строчку:
 
LINK_ENTITY_TO_CLASS( my_logical_entity, CMyLogicalEntity );
 
Это еще один макрос (об этом говорят заглавные буквы), который даёт классу "имя игрового объекта". Программный код обращается к игровым объектам не под тем же именем нежели система [[Inputs_and_Outputs:ru|ввода/вывода]] или [[Hammer]] по причинам, которые вы поймёте позже.
 
= Таблица описания данных =
 
[[Data_Descriptions:ru|Таблица описания данных]] - это макросы, которые обеспечивают движок Source [[Wikipedia:Metadata|метаданными]] о любом члене класса, который в нём содержится. Комментарии должны пояснить для чего нужен каждый макрос: например <code>DEFINE_FIELD</code> гарантирует, что значение <code>m_nCounter</code> будет храниться в сохранённой игре чтобы избежать его сбрасывания в ноль при перезагрузке игры. <code>DEFINE_KEYFIELD</code> проделывает ту же работу, а так же делает доступным редактирование этого параметра в Hammer'е.
 
Этот код имеет пример коричневых "[[string|строковых]]" значений. В отличие от остальных значений они являются простыми данными, которые компилятор принимает, хранит и слепо передаёт движку. В ковычках вы можете написать всё что угодно и код будет компилироваться, но когда дело дойдёт до обращения к этой строковой переменной, движок Source будет сбит с толку если значение окажется недопустимым.
 
<span style="color:green;">// Начало нашего определения данных для класса CMyLogicalEntity.</span>
BEGIN_DATADESC( CMyLogicalEntity )
<span style="color:green;">// Для сохранения/загрузки.</span>
DEFINE_FIELD( m_nCounter, FIELD_INTEGER ),
<span style="color:green;">// Связывает нашу переменную с ключевым значением из Hammer'a.</span>
DEFINE_KEYFIELD( m_nThreshold, FIELD_INTEGER, <span style="color:brown;">"threshold"</span> ),
<span style="color:green;">// Даёт нашей функции ввода имя в Hammer'e</span>
DEFINE_INPUTFUNC( FIELD_VOID, <span style="color:brown;">"Tick"</span>, InputTick ),
<span style="color:green;">// Даёт нашей переменной вывода имя в Hammer'е</span>
DEFINE_OUTPUT( m_OnThreshold, <span style="color:brown;">"OnThreshold"</span> ),
END_DATADESC()
 
Заметьте, что наш ввод (input) осуществляется через функцию, а вывод (output) через переменную, которую мы определили ранее.
<!--Не могу понять смысла этой фразы; если кто-нибудь шарит, то переведите её пожалуйста - This is because, once generated, an output leaves the class it originates from immediately; any processing is done beforehand.-->
 
Когда вы пишите свои таблицы описания данных не забывайте ставить запятую после каждой команды <code>DEFINE_*</code>.
 
{{note|То, что здесь отсутствуют точки с запятой является довольно необычной практикой!}}
 
= Создание входной функции =
 
Теперь мы "определим" нашу входную функцию. Это последний шаг в написании С++ кода. Заметьте, что первая строка определения соответствует строке её описания в описании класса за исключением части <code>CMyLogicalEntity::</code>. Эта часть гарантирует, что мы описываем функцию нужного нам класса. В нашем случае в файле всего один класс, однако их может быть несколько.
 
<span style="color:green;">//-----------------------------------------------------------------------------
// Назначение: Принимает ввод от других объектов.
//-----------------------------------------------------------------------------</span>
<span style="color:blue;">void</span> CMyLogicalEntity::InputTick( inputdata_t &inputData )
{
<span style="color:green;">// Увеличение нашего счетчика.</span>
m_nCounter++;
<span style="color:green;">// Проверяем достиг ли счетчик порогового значения.</span>
<span style="color:blue;">if</span> ( m_nCounter >= m_nThreshold )
{
<span style="color:green;">// Запуск события вывода.</span>
m_OnThreshold.FireOutput( inputData.pActivator, <span style="color:blue;">this</span> );
<span style="color:green;">// Сброс нашего счетчика.</span>
m_nCounter = 0;
}
}
 
Вот что происходит в этой функции:
 
;<code>m_nCounter++</code>
:Это сокращенная запись выражения "увеличить на единицу". В полном варианте это выглядит так: <code>m_ncounter = m_ncounter + 1</code>.
;<code>if ( m_nCounter >= m_nThreshold )</code>
:Оператор <code>if</code> проверяет выполняется ли выражение, стоящее в круглых скобках (), и если да, то выполняет те команды, которые стоят в фигурных скобках {}, следующих сразу после круглых. <code>>=</code> означает "больше или равно". Соответственно если значение <code>m_nCounter</code> больше или равно значения <code>m_nThreshold</code>, то выполняются команды стоящие в фигурных скобках {}.
;<code>{</code> и <code>}</code>
:Это "вложенная" в функцию пара фигурных скобок. Она групирует команды, которые будут выполнены, когда выполнится условие оператора <code>if</code>. По мере необходимости может быть множество вложенных друг в друга фигурных скобок.
;<code>m_OnThreshold.FireOutput</code>
:Если вы скопировали код из этой статьи и вставили его в Visual Studio, то лучше перепишите эту строку заново и отметьте что произойдёт когда вы напечатаете точку. Вам надо вызвать открытую (public) функцию экземпляра <code>m_OnThreshold</code> класса <code>COutputEvent</code> и Visual Studio помогает вам, показывая список доступных вариантов. Продолжайте печатать, пока не будет выделен нужный вам член класса, или выберите его с помощью стрелок клавиатуры и клавиши enter.
:Если бы мы вызывали набираемую нами функцию из другого класса, то нам необходимо было бы напечатать <code>MyLogicalEntity1.InputTick(<аргументы>)</code>, где <code>MyLogicalEntity1</code> это имя нужного нам экземпляра класса. (Что означает, что нам понадобилось бы сначала создать этот экземпляр - нельзя обращаться к обычным функциям через название класса (''шаблона'')!).
;<code>( inputData.pActivator, this )</code>
:Это аргументы, которые мы передаём функции <code>FireOutput</code>. Они автоматически генерируются движком Source и передаются в нашу функцию через параметры, которые находятся в круглых скобках ().
:В фукнцию <code>FireOutput</code> мы передаём:
:#Объект, который положил начало этой цепочке событий (на жаргоне Source это "активатор" (activator)).
:#Объект, который вызывает функцию <code>FireOutput</code> - "caller". <span style="color:blue;">this</span> - это указатель на объект, которому принадлежит функция, которую мы сейчас пишем.
:Эти параметры нужны для "[[Targetname#Keywords|ключевых слов имён цели]]" (targetname keywords).
;<code>m_nCounter = 0</code>
:Если мы не сбросим счётчик он будет продолжать увеличиваться выше порогового значения.
 
Примите поздравления - объект готов к компиляции. Для её начала нажмите F7. Если вы получили уведомление об ошибке, то убедитесь, что ваш код соответствует [[Authoring a Logical Entity/Code|этому]]!
 
Мы не сможем использовать наш объект непосредственно из движка - если вы когда-нибудь создавали свою карту, то вы знаете что существует еще много информации ввода-вывода, которой не нашлось места в нашем коде. Однако наша таблица описания данных позволяет движку Source автоматически подключить наш объект к более широкой системе ввода/вывода; поэтому всё что нам осталось это дать [[Hammer]]'у знать о нашем объекте.
 
= Создание записи в FGD-файле =
 
[[Image:Options Game Config.png|right|150px|Диалог настроек Hammer'а]]
 
Файл [[FGD]] - это текстовый файл, который описывает какие объекты содержит ваш мод и что с ними можно делать. Мир не идеален и Hammer не умеет считывать эту информацию напрямую из кода мода, поэтому важно поддерживать FGD-файл в актуальном состоянии.
 
Если у вас пока нет FGD-файла сохраните пустой текстовый файл в папке вашего мода и назовите его <code><имя_мода>.fgd</code>. Затем загрузите Hammer и зайдите в <code>Tools > Options</code>. Убедитесь что ваш мод стоит в "активной конфигурации" 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."
]
 
Что же, вы это сделали! Ваш первый игровой объект готов к использованию.
 
= Смотрите также =
 
*[[Authoring a Logical Entity/Code|Цельный код из этой статьи]]
*[[Your First Entity|Ваш первый игровой объект]]
*[[FGD|Файлы FGD]]


{{otherlang:ru}}
{{otherlang:ru}}

Revision as of 09:39, 15 October 2008

Логические объекты (объект = entity) это самые простые для создания игровые объекты т.к. они не имеют позиции в пространстве, не отображаются в игре и нужны только для того чтобы получать входные данные от других объектов. Например объект math_counter содержит число, к которому можно прибавлять или вычитать другое число; другие объекты на карте могут изменять значение этого числа через входы (inputs) или получать информацию от этого объекта через выход (output).

В этой статье мы создадим логический объект, который будет выполнять простую задачу хранения числа и увеличения его значения каждый раз когда объект будет получать на вход соответствующую команду. По достижении заданного нами значения наш объект будет генерировать вывод (output), чтобы уведомить другие объекты о достижении порогового значения счетчика.

Создание исходного файла

В этой статье предполагается что вы используете Visual Studio или Visual C++ Express. Смотрите статью про выбор компилятора: Compiler Choices.

Добавьте в проект 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.

Note.pngПримечание:Обычно закрывающая фигурная скобка (}) и точка с запятой ; не используются вместе. Команда 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_*.

Note.pngПримечание:То, что здесь отсутствуют точки с запятой является довольно необычной практикой!

Создание входной функции

Теперь мы "определим" нашу входную функцию. Это последний шаг в написании С++ кода. Заметьте, что первая строка определения соответствует строке её описания в описании класса за исключением части 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 мы передаём:
  1. Объект, который положил начало этой цепочке событий (на жаргоне Source это "активатор" (activator)).
  2. Объект, который вызывает функцию FireOutput - "caller". this - это указатель на объект, которому принадлежит функция, которую мы сейчас пишем.
Эти параметры нужны для "ключевых слов имён цели" (targetname keywords).
m_nCounter = 0
Если мы не сбросим счётчик он будет продолжать увеличиваться выше порогового значения.

Примите поздравления - объект готов к компиляции. Для её начала нажмите F7. Если вы получили уведомление об ошибке, то убедитесь, что ваш код соответствует этому!

Мы не сможем использовать наш объект непосредственно из движка - если вы когда-нибудь создавали свою карту, то вы знаете что существует еще много информации ввода-вывода, которой не нашлось места в нашем коде. Однако наша таблица описания данных позволяет движку Source автоматически подключить наш объект к более широкой системе ввода/вывода; поэтому всё что нам осталось это дать Hammer'у знать о нашем объекте.

Создание записи в FGD-файле

Диалог настроек Hammer'а

Файл 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."
]

Что же, вы это сделали! Ваш первый игровой объект готов к использованию.

Смотрите также

Template:Otherlang:ru Template:Otherlang:ru:en