вернуться на beanet.ru вернуться к списку проектов вернуться на главную страницу сборника

Тема: Проверка уровня сложности через энтити


Ключевые слова: уровень сложности, добавить энтити логическую, энтити в Hammer, skill level difficulty, FGD

См. также: -
Список используемых понятий, сокращений и обозначений

перейти к общему списку


Это пример для начинающего программировать в Source - создание логической энтити. Здесь будет приведён лёгкий для разбора и понимания код, который вполне можно взять за основу более сложной конструкции.
От Вас требуется лишь некоторое знакомство с языком C++ и программой Visual C++.
Увидите - ничего сложного в реализации логической энтити нет: нам не нужно ни рисовать модель (энтити невидима в игре), ни вводить элементы ИИ (только по запросу извне энтити делает несложную проверку и выдаёт результат).
Тем не менее, настройтесь на восприятие приличного объёма информации. Уж так получилось, что первый обучающий пример изобилует пояснениями. Чтобы не перепутать, конструкции программного кода выделены белым цветом фона. Таким же образом выделен "скриптовый код", добавляемый в *.fgd-файл. То есть, на белом фоне даны фрагменты, которые и составляют практическую часть. Вы можете прямо выделять и переносить их в соответствующие файлы.

За основу статьи взят урок Создание логической сущности. Тем не менее, пример описанной там энтити чисто показательный - вряд ли кто захочет использовать простейший счётчик (см. указанную статью), тем более что есть готовый math_counter.
Я же, в свою очередь, предлагаю более жизнеспособный вариант: посредством новой энтити информировать маппера об уровне сложности игры. Всегда приятно, если на разных уровнях сложности игра происходит с оригинальными добавками. Поскольку маппингом такие вещи делать намного удобнее и интереснее, вот мы с Вами и поможем дизайнеру в работе.


По аналогии с оригинальными объектами рабочее имя энтити будет logic_skill_level (уровень сложности Valve обозначает как skill level - уровень мастерства, а не difficulty level - уровень сложности). Чтобы можно было определить, что энтити добавлена модом, неплохо было бы дополнить её имя отличительным буквосочетанием (например, MOD_). То есть энтити будет называться MOD_logic_skill_level. Учтите, что такое имя как раз использует данный пример.

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



Дебри теории

Чтобы в дальнейшем не отвлекаться на детали, прямо сейчас обсудим несколько моментов.

Во-первых, чтобы не захламлять и без того страшный каталог SteamApps/SourceMods/src (в котором и находится исходный код движка), можно создать отдельную папку src в каталоге Вашего мода. И в эту папку помещать все новые файлы с исходным кодом. Поверьте мне, даже если они там будут "свалены в кучу" - это лучше, чем рыться в упомянутом каталоге. Проще будет и создавать архивные копии (хоть раз уже делали?). В проект Visual C++ новые файлы включаются как обычно.

Во-вторых, поговорим о разделении движка по файлам.
Исходный код движка находится не в одном единственном файле (какого же размера он мог быть?), а во множестве *.cpp-файлов; для связки используются *.h-файлы. Причём большинство сейчас думает: это замечательно, так как значительно сокращает время компиляции или поиска нужной части кода, упорядочивает вид проекта и вообще считается хорошим тоном.
Ну, что я могу на это сказать? Конечно, хорошая идея от создателей языков программирования, но не более того. При таком разделении код внутри отдельного файла автономен - своеобразный "чёрный ящик". И чтобы отдельные участки кода могли "общаться между собой" (вызывать функции, использовать общие переменные, структуры и т.п.), возникла необходимость в добавлении специальных заголовочных *.h-файлов.
Посмотрите, вроде хорошо получилось: вынеси объявления общедоступных функций, классов в *.h-файл, а их реализацию опиши в *.cpp-файле. Чтобы компилятор знал, в каком файле описание, а в каком - реализация кода, поставь в *.cpp-файле макрос #include <соответствующий *.h-файл>. И всё! Если где-то понадобится "общение" с данным *.cpp-файлом - повтори тот же самый #include - станут известны параметры функций, структур данных и прочие вещи, расписанные в данном *.h-файле.
Тем не менее, за это решение пришлось расплачиваться: есть конструкции, которые допустимо определять только один раз, а в случае повторного включения (#include) код попросту не компилируется. "Глобальные" данные реально видны только в пределах файла, в котором они объявлены, а если надо расширить видимость?
Решения, конечно, существуют. Для первого случая это макросы условного ветвления типа #ifndef <маркер> #define <маркер> …<разовые конструкции кода>… #endif. То есть, на этапе компиляции, при первом включении *.h-файла <маркер> ещё не задан, происходит определение кода и данного маркера; при повторных включениях того же *.h-файла <маркер> уже задан, и переопределений кода не происходит. Во втором случае используются директивы типа extern для информирования компилятора, что конкретные данные уже где-то объявлены, а в данном файле рассматриваются как внешние.
Вот видите, сколько забот прибавилось. А Вы говорите - удобно… И всё бы ничего, но каждая такая условность добавляет потенциальные возможности ошибок. Отсюда и вывод: это хорошее решение, но не идеальное.
Вообще, я заговорил о разделении кода по той причине, что программирование в Source опирается на данные принципы, и неплохо было бы их помнить. Почти каждая энтити представлена отдельным *.cpp-файлом (*.h-файлом), то же самое касается группировки классов, интерфейсов. Для модов, как правило, движок не переписывается заново, а лишь дополняется кодом. Чтобы эффективно добавить в систему новые элементы, выгоднее будет сделать их по правилам этой системы (а не по-своему). Всё это экономит время и силы.

В-третьих, каждый уважающий себя автор книги считает за правило возвеличить пользу от комментариев - подробных объяснений, что делает конкретный участок программы. Но здесь я спорить даже не помышляю - чем больше подробностей в комментариях, тем лучше. Но есть три условия: писать по делу; не объяснять то, что и так очевидно; не отвлекаться на посторонние вещи (типа "копирайтов").
Комментарий можно ставить после специального сочетания символов - двойная косая черта - // (или "двойной слеш"). Спецсимвол может быть в самом начале строки или после некоторого кода. В любом случае, данный вид комментария может быть в пределах одной строки и уже до конца этой строки.
Есть ещё одна форма комментария, когда весь текст его заключается между символами /* и */ (играют роль скобок). Допустимо использовать этот комментарий в произвольном месте кода. Обычно таким образом временно (или навсегда) "скрывают" от компилятора участки кода. Либо экономят на использовании комментария //.
Рассмотрим наугад выбранный комментарий в начале файла <путь установки Steam>\SteamApps\SourceMods\src\game\server\util.cpp:
//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
//
// Purpose: Utility code.
//
// $NoKeywords: $
//=============================================================================//
Ну, допустим, аккуратность и выделение комментария из общего кода - это хорошо. А вот "копирайт" в первой строке - это уже излишество (с точки зрения пользы, а не информации о правах). Далее описано назначение кода в нескольких строчках (не слишком ли кратко?) и некая заготовка, которая очевидно использовалась для поиска по ключевым словам. Весь блок комментариев ограничен специально для наглядности, ведь не всякий раз Вы читаете код с подсветкой синтаксиса.
Чуть позже присутствует комментарий к строке кода:
extern short g_sModelIndexSmoke; // (in combatweapon.cpp) holds the index for the smoke cloud
Вот уже встретилась и внешняя переменная, а также комментарий - где она объявлена и что она хранит.
Далее - ещё один вид комментария:
/**
* Returns true if the command was issued by the listenserver host, or by the dedicated server, via rcon or the server console.
* This is valid during ConCommand execution.
*/
И, вот она, экономия. Здесь каждая строка, тем не менее, начинается с символа * (звёздочка). Это служит всё той же наглядности.
Наконец, последний вариант - внутри рабочего кода:
for ( i = 0; i < 1 /*NUM_COLLISION_TESTS*/; i++ )
Здесь, вероятно, старое значение было временно заменено на другое. Однако, в данном виде изменения попали в конечный продукт.
И ещё: несмотря на то, что приведённые примеры могут отображаться с переносами строк, это происходит лишь из-за ограниченной ширины окна справки. В коде, разумеется, строки не переносятся (можете проверить).
Когда Вам понадобится изучать чужой код, обратите внимание, как Вы начнёте "цепляться" за каждый встреченный комментарий. Вот и не забывайте, что полезно насыщать код пояснениями. При небольшой аккуратности и внимании комментарии не будут мешать компиляции, а пользы от них очень и очень много.

В-четвёртых, я подразумеваю, что Вы уже знаете синтаксис языка Visual C++ в общих чертах. То есть, понимаете стандартные конструкции (объявления классов, макросы и пр.), пунктуацию (круглые и фигурные скобки, а также другие знаки), типы данных.
Поэтому если для Вас это "тёмный лес", лучше отложите чтение статьи и ознакомьтесь хотя бы с простым C++, так как он является основой для Visual C++. Думаю, этого будет достаточно, чтобы разобрать пример и вникнуть в смысл кода.



Основная часть

Итак, запускаем Visual C++ (в нашем случае - 2005 Express Edition), выбираем File -> Open -> Project/Solution… и открываем проект <путь установки Steam>\SteamApps\SourceMods\src\Game_Episodic-2005.sln. Указанная здесь папка src (с содержимым) уже должна быть в каталоге с модами, если Вы хоть раз создавали новый мод. Из этой папки мы выбираем указанный проект, поскольку он содержит исходный код движка OrangeBox. Там же находятся все файлы исходного кода этого движка.
Теперь необходимо (если Вы не сделали этого раньше) настроить компилятор в соответствии со статьёй Компиляция под VS2005. Обратите внимание, что эти действия обязательны для нормальной компиляции! Здесь я всего лишь ссылаюсь на них, поскольку нет нужды повторять уже существующее и загромождать учебную статью.
Итак, после настройки компилятора, в левом окне выбираем закладку Solution Explorer и открываем ветку Server (подавляющее большинство энтити описывается на стороне сервера, т.к. это самостоятельные, не "привязанные" к игроку объекты).

Для примера нам понадобится всего один файл - MOD_logic_skill_level.cpp. Нажимаем п.к.м. на ветке Server и создаём новую папку (Add -> New Filter) с именем ModCode (для примера). Отдельная папка бывает полезна, чтобы не искать по всему проекту собственные файлы среди тысяч уже существующих (которые и составляют код оригинального движка).
Таким образом, на уровне проекта Ваши файлы отделены папкой ModCode, а на уровне их фактического местонахождения - папкой src мода (это уже наша папка, а не та, в которой содержатся файлы движка). Конечно, если Вы следуете моим рекомендациям (также см. теорию выше).
Теперь нажимаем п.к.м. на добавленной папке (в Visual C++) и в контекстном меню выбираем Add -> New Item…, в поле Name задаём имя файла MOD_logic_skill_level. В поле Location нужно задать каталог внутри папки src Вашего мода - вручную или нажав кнопку Browse…, затем в поле Templates выбираем C++ File (.cpp), т.к. это файл рабочего кода, и нажимаем Add.
В итоге файл уже сохранён и в дальнейшем будет храниться в заданной папке, но независимо от этого будет присутствовать в оригинальном проекте Valve.

Теперь можно приступать к основной части. Договоримся, что каждая из секций программного кода, которые приводятся ниже, будет отделена от других с помощью пустой строки. Вы увидите, что код станет более аккуратным и "прозрачным".


Начнём с комментария - скопируйте в созданный файл следующие строчки и внимательно прочитайте, о чём в них говорится:
//-------------------------------------------------------
// Логическая энтити MOD_logic_skill_level,
//	которая принимает запрос уровня сложности (Input):
//
//		CheckSkillLevel
//
//	и выдаёт одно из трёх событий (Outputs):
//
//		OnEasy	- лёгкий уровень сложности
//		OnMedium	- средний уровень сложности
//		OnHard	- высокий уровень сложности
//
//	Результат - выбранный на момент запроса
//	уровень сложности в настройках игры.
//	События (Outputs) генерируются только при получении
//	управляющей команды (Input).
//-------------------------------------------------------
Я не призываю для каждой новой энтити подробно расписывать, что она делает. Это более уместно сделать в плане работ, проектной документации (или что Вы там составили). Однако никогда не мешает чуть-чуть просветить стороннего программиста или самого себя через пару месяцев. А то даже обидно, если возникает необходимость рыться в груде документов или "перелопачивать" весь код, только чтобы понять, что он делает, в общих чертах.
Кстати, можете покритиковать комментарий, всё равно ведь не идеальный вариант…


Далее идёт директива включения заголовочного файла (не забываем отделить её от комментария пустой строкой):
#include "cbase.h"
Это предоставит доступ к основным наборам команд, которые используются при создании энтити. Формально, любой *.cpp-файл в Source должен включать cbase.h. Расшифровка названия: c - код выполняется на стороне сервера (на стороне клиента было бы c_ ), base - предоставляет базовую функциональность (уже готовые конструкции кода).
Если посмотреть начало файлов cbase.h и cbase.cpp, мы увидим множество включений других файлов, которые, в свою очередь, могут включать ещё файлы и т.д. То есть, включение одного файла cbase.h влечёт за собой автоматическое открытие доступа к открытым (public) данным во многих других файлах.


Теперь добавим описание класса для нашей энтити:
class CMODLogicSkillLevel : public CLogicalEntity
{
public:
	DECLARE_CLASS( CMODLogicSkillLevel , CLogicalEntity );
	DECLARE_DATADESC();

	// Конструктор класса
	// 	CMODLogicSkillLevel ( void ) {}
	// ничего не делает, его можно опустить.
	// Сработает конструктор базового класса (по умолчанию).
	// Это нормально.

	// функция реализации управляющей команды CheckSkillLevel
	void InputCheckSkillLevel( inputdata_t &inputData );

private:
	COutputEvent	m_OnEasy;	// Событие для случая, когда выставлен
					// низкий уровень сложности
	COutputEvent	m_OnMedium;	// Событие для случая, когда выставлен
					// средний уровень сложности
	COutputEvent	m_OnHard;	// Событие для случая, когда выставлен
					// высокий уровень сложности
};
Самая первая строка объявляет имя класса и говорит компилятору о том, что данный класс базируется (наследуется) на CLogicalEntity - базовом классе для любой логической (logic_*) энтити. Обратите внимание, что благодаря этому нам не нужно будет углубляться в тонкости функционирования кода logic-энтити и добавлять кучу стандартного кода. Надо всего лишь определить небольшую секцию (см. далее) и задать собственные (добавляемые) функции. Остальное "подцепляется" нашим классом автоматически, по причине наследования от базового класса.
Затем, уже внутри объявления класса, ключевое слово public - говорит о том, что следующие элементы будут доступны любому внешнему (по отношению к нашему классу) коду, который оперирует с энтити нашего класса (т.е. с переменной типа CMODLogicSkillLevel). Это также очень важно, поскольку нас в данном случае не волнует, при каких обстоятельствах вызываются функции нашего класса (при инициализации карты, проверке условий, обработке команд и т.д.). Главное - обеспечен доступ ко всему, что может в дальнейшем понадобиться движку.
Следующая строчка - макрос, который опять-таки упрощает последующую инициализацию класса. В нём сообщается то же самое, что и в объявлении имени класса. Но данный макрос, конечно же, выполняет иные задачи: он подставляет некоторые функции и переменные, которые нужны для нормального взаимодействия движка с новым классом.
Затем идёт ещё один макрос, который сигнализирует компилятору, что класс будет включать также таблицу данных, подробно расписанную далее уже за пределами объявления класса (см. ниже DATADESC).

Конструктор класса пропускаем, этот момент хорошо прокомментирован. Следующая команда также снабжена комментариями, поэтому лишь уточню: она объявляет нашу главную функцию, в которой будет выполняться код поведения энтити. Она-то и будет вызываться каждый раз, когда последует запрос уровня сложности игры. Подробнее о параметре inputData будет сказано далее.
Как видите - наша энтити столь проста, что поведение её задаёт одна единственная функция. А всё остальное сделано для правильного включения кода в движок.

Следующее ключевое слово - private. Соответственно, оставшиеся элементы (это три переменных) будут доступны функциям только нашего класса. В принципе, здесь также даны понятные комментарии - эти переменные нужны для обработки движком событий, происходящих с нашей энтити. Перед именем каждой переменной стоит m_ и означает, что это член (member) класса.


Вас, наверное, мучает один вопрос: а каким образом движок узнает, что данный класс будет обрабатывать именно данную энтити? Так вот, непосредственно сейчас, до того, как пойдут наши собственные функции (а точнее, одна функция), необходимо дополнить код следующей информацией:
LINK_ENTITY_TO_CLASS( MOD_logic_skill_level, CMODLogicSkillLevel );
Данный макрос как раз и говорит движку, что энтити MOD_logic_skill_level будет обрабатываться классом CMODLogicSkillLevel. То есть, при встрече в загруженной карте указанной выше энтити, движок отведёт для неё место и в дальнейшем будет искать функции обработки именно на основе указанного конкретного класса.

Но это ещё не всё! Новый класс для движка является в какой-то мере "чёрным ящиком", и необходимо определить "каналы стыковки", через которые движок будет взаимодействовать с нашим классом. Для этого используется упомянутая выше таблица данных, которая в общем случае хранит соответствия между атрибутами, флагами, управляющими командами, событиями энтити, с одной стороны, и переменными, функциями в коде, с другой стороны. В этой таблице можно объявлять сохраняемые данные, которые автоматически записываются в файл сохранения игры.
В нашем случае требуется определить всего лишь одну управляющую команду и три события, а именно:
// Блок описания хранимых данных класса (энтити),
// а также управляющих команд и событий
BEGIN_DATADESC( CMODLogicSkillLevel )

	// Ставим в соответствие управляющей команде CheckSkillLevel
	// функцию реализации
	DEFINE_INPUTFUNC( FIELD_VOID, "CheckSkillLevel", InputCheckSkillLevel ),

	// Ставим в соответствие событиям (генериуемым энтити) члены класса
	DEFINE_OUTPUT( m_OnEasy, "OnEasy" ),
	DEFINE_OUTPUT( m_OnMedium, "OnMedium" ),
	DEFINE_OUTPUT( m_OnHard, "OnHard" ),

END_DATADESC()
Если Вы внимательно читали предыдущие абзацы, то здесь всё должно быть понятно: макрос задаёт начало таблицы данных, для команды CheckSkillLevel ставится в соответствие функция обработки InputCheckSkillLevel (объявленная ранее в нашем классе), для событий OnEasy/OnMedium/OnHard ставятся в соответствие три переменные-члены класса, макрос завершает таблицу данных.


Наконец-то! Теперь все формальности соблюдены, и можно разгуляться вволю. В нашем случае это будет небольшая проверка на сложность игры:
//-----------------------------------------------------------------------
// Основная (и единственная) рабочая функция,
// задающая поведение энтити
//-----------------------------------------------------------------------
void CMODLogicSkillLevel::InputCheckSkillLevel( inputdata_t &inputData )
{
	// Проверяем текущий уровень сложности, сравнивая
	// с предустановленными значениями (по возрастанию)
	if ( g_pGameRules->IsSkillLevel(SKILL_EASY) )
	{	
		// Если сложность игры низкая, выдаём событие OnEasy
		m_OnEasy.FireOutput( inputData.pActivator, this );
	}
	else if ( g_pGameRules->IsSkillLevel(SKILL_MEDIUM) )
	{
		// Иначе если сложность игры средняя, выдаём событие OnMedium
		m_OnMedium.FireOutput( inputData.pActivator, this );
	}
	else if ( g_pGameRules->IsSkillLevel(SKILL_HARD) )
	{
		// Иначе если сложность игры высокая, выдаём событие OnHard
		m_OnHard.FireOutput( inputData.pActivator, this );
	}
	else
	{
		// Иначе - когда ни одно из предыдущих условий не выполнено,
		// это будет ошибка
		// В данном случае реализована "заглушка" - ничего не происходит
	}
}
Как и полагается, сначала идёт определение имени функции с параметром-указателем на inputData. Скоро Вы узнаете, зачем нам этот параметр.
Теперь я буду объяснять лишь отдельные кусочки кода, поскольку логика в целом описана в комментариях.

g_pGameRules - глобальная переменная-указатель (global pointer), через которую мы и выйдем на нужную информацию;
IsSkillLevel(<параметр>) - функция, которая сравнивает текущий уровень сложности со значением своего параметра. В качестве значений <параметра> по-очереди выступают заданные в файле <путь установки Steam>\SteamApps\SourceMods\src\game\shared\shareddefs.h константы: SKILL_EASY, SKILL_MEDIUM и SKILL_HARD;

Примечание: этот способ проверки заимствован мною из оригинального кода Source. Возможно, есть и лучшие варианты, но мы остановимся на данном.

FireOutput - функция, член класса COutputEvent, генерирует событие энтити. Поскольку мы уже определили ранее три переменные этого класса, можно спокойно вызывать данную функцию для каждой из этих переменных;
inputData.pActivator - вот и понадобилась переменная inputData - чтобы получить один из её элементов (как член структуры inputdata_t).
В качестве первого параметра мы передаём указатель на энтити-активатор, из-за которой случился вызов нашей функции InputChekSkillLevel. Обратите внимание, что в скриптах Source и редактора Hammer активатор определяется как !activator - и это по смыслу тот же самый объект;
this - это всегда указатель на конкретный экземпляр (переменную) класса, в описании или методе (функции) которого упоминается данное ключевое слово. В качестве второго параметра мы передаём указатель на энтити, которая и вызвала функцию FireOutput.


Ну, с программированием мы покончили! Теперь достаточно скомпилировать полученный код (учитывая замечания из всё той же статьи - Компиляция под VS2005), и движок станет учитывать новую энтити.


А вот если Вы думаете, что добавление энтити в мод закончено, то Вы ошибаетесь. Здесь кроется ещё один сюрприз: допустим, откомпилирован код, обновлённые *.dll-файлы находятся в папке /bin мода и… и что? Если сейчас запустить Hammer и попытаться добавить новую энтити на карту, Вы даже не найдёте её в списке!
Оказывается, редактору Hammer надо ещё дополнительно сообщить о новой энтити. Причём, нужно подробно расписать атрибуты, флаги, команды, события, а также справочную информацию (рекомендуется).

Информация обо всех объектах, с которыми работает Hammer, записана в специальных *.fgd-файлах. Для конфигурации OrangeBox это файл <путь установки Steam>\SteamApps\<имя аккаунта Steam>\sourcesdk\bin\orangebox\bin\halflife2.fgd. Чтобы редактор Hammer учитывал все описанные в данном файле энтити, путь к нему уже записан по умолчанию в свойствах редактора (поле-список Game Data files на закладке Game Configurations в меню Tools -> Options…). Достаточно будет добавить собственный файл с описанием в указанный список - и Hammer "узнает" о новых объектах.

Но сначала надо ещё создать нужный файл. В каталоге Вашего мода уже должна быть папка bin - туда помещаются скомпилированные client.dll и server.dll. Для примера там же будет находиться *.fgd-файл с описанием новой энтити.
Создаём файл mod.fgd (здесь тоже подойдёт редактор Блокнот) и поместим в него следующий текст:
@include "base.fgd"

//-------------------------------------------------------------------------
//
// Entities
//
//-------------------------------------------------------------------------

@PointClass base(Targetname) = MOD_logic_skill_level :
 "Used for check skill (difficulty) level
 and generate appropriated output commands."
[
       input CheckSkillLevel(void) : "Checks current skill (difficulty) level."
       output OnEasy(void) : "Fired when skill (difficulty) level is EASY"
       output OnMedium(void) : "Fired when skill (difficulty) level is MEDIUM"
       output OnHard(void) : "Fired when skill (difficulty) level is HARD"
]
Первая строчка - это включение основного файла со всеми описаниями, нечто похожее на #include в C++. Таким образом можно пропустить некоторые общие элементы описания.
Далее идёт несколько строк комментария (заимствовано из стиля Valve), говорящих о том, что начинается секция описания энтити.

@PointClass - указывает на описание точечной энтити;
base(Targetname) - добавление базового "скелета" (вот и пригодился base.fgd);
Далее идёт название энтити (MOD_logic_skill_level) и её описание на английском языке (на русском оно не появится в Hammer-е). Это описание будет отображено вначале справочной информации, если нажать кнопку Help у данной энтити в редакторе.

В квадратных скобках идёт описание необходимых нам взаимодействий, где:
input / output - ключевые слова для обозначения команд/событий, соответственно;
После ключевого слова идёт связка:
<название>(<тип параметра>) : "<описание взаимодействия на английском языке>"
И так для каждого из взаимодействий. В нашем случае параметры не нужны, поэтому указано ключевое слово void.

Вот, собственно, и всё. Не забудьте сохранить файл там, где ему положено (в каталоге bin мода).


Теперь уже осталось немного: переходим в редактор Hammer, открываем Tools -> Options…, на закладке по умолчанию Game Configurations справа от поля-списка Game Data files нажимаем кнопку Add. Выбираем наш файл mod.fgd - готово!
Если энтити не появилась в списке, перезапустите Hammer.
Исследуя свойства новой энтити, Вы увидите, что названия команды CheckSkillLevel и событий OnEasy, OnMedium, OnHard взяты непосредственно из mod.fgd.

Примечание: попробуйте открыть справочный файл энтити MOD_logic_skill_level - Вы увидите, что наряду с добавленными элементами и описаниями присутствуют также некоторые стандартные атрибуты и команды (с готовыми описаниями).
Они также доступны для манипуляций, как и у остальных энтити. Именно здесь, на практике, видна польза от включения файла base.fgd в наш mod.fgd.


Теперь уже легко проверить, как работает новая энтити. Схема такая: посылаете ей запрос извне, а в её же событиях задаёте реакцию на конкретный уровень сложности.



Заключение

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


Для новичков:

1) попробуйте сначала поменять имя энтити на logic_skill_level - сменить имя файла, отредактировать код, описание *.fgd-файла. Тем самым Вы немного освоитесь в Source-программировании, привыкнете быть внимательными при создании кода;
2) попробуйте также изменить названия управляющей команды и событий.


Для освоившихся (или даже хорошо подготовленных):

1) попробуйте расширить возможности энтити; иногда может возникнуть необходимость следующих проверок:

- уровень сложности выше самого лёгкого или ниже самого тяжёлого
- уровень сложности не равен лёгкому, среднему или тяжёлому

При этом заметьте, что благодаря всего трём возможным вариантам, оба вида проверок используют общий принцип, а также просты в реализации;

2) обратите внимание, что (в принципе) возможна ситуация, когда ни одна из трёх проверок не дала положительного результата. Попробуйте реализовать обработку такой ошибки; место под неё указано в примере. Можно сделать вывод отладочного сообщения, для оригинальности - обработать этот случай как ещё одно событие (хотя это пожалуй лишнее).


Для опытных:

1) теперь, когда поведение игры зависит от выбранного уровня сложности, хорошо будет запоминать и отслеживать зафиксированную в начале игры настройку. Для того чтобы игрок не менял её по ходу игры, можно постоянно восстанавливать значение. Каким образом это делать - каждую секунду либо после некоторых действий игрока либо перед очередной управляющей командой - решайте сами;
2) также, обратите внимание: логично будет запрашивать уровень сложности непосредственно в начале очередной игры. Из меню настроек можно вообще убрать соответствующую закладку, а при сохранении игры добавлять текущую сложность на "скриншот" или к имени файла.



Статьи (рус): Создание логической энтити, Компиляция под VS2005
Статьи (eng):

перейти к общему списку

Номер статьи: 42

Сборник полезной информации по созданию модификаций на движке Valve Source Engine (игры Half-Life 2, Episode One, Episode Two)