GameDev
Yesterday

Создаём простой объект на Shot

В этой статье поговорим о том, как добавить в игру простейший объект, используя встроенный язык программирования Shot.

Использовать будем уже существующую кодобазу от игры Comrades, которая, в свою очередь, основана на игровом коде от Quake II, но портирована на Shot и вместо множества разрозненных функций теперь представляет собой симпатичные классы-объекты.

Для начала создадим самый простой объект, который ничего не делает, а просто показывает в игре модель без коллизии. Пусть это будет модель подбираемого предмета 'Power Cube' (но вы конечно можете взять и любую другую).

1. Подготовка к работе

Первым делом мы должны настроить наши исходники для того, чтобы их можно было скомпилировать. Они находятся в XashNT_SDK\src_user

Если у вас уже имеется бета-версия XashNT_SDK, то внутри проектов пути настроены правильным образом и вам остается только ассоциировать в Windows расширения файлов .hsproj с компилятором-линковщиком языка Shot, который расположен по адресу XashNT_SDK\bin\hshot32.exe.

После ассоциирования, файлы .hsproj будут компилироваться по двойному клику на них. Результат автоматически помещается в comrades/client.program для клиента и ../../comrades/server.program для сервера, соответственно.

Проще всего будет вставить новый объект в конец уже имеющегося файла проекта, который служит для такого рода объектов - XashNT_SDK\src_user\ComradesServer\misc.shot

2. Немного теории

Организация кодовой базы Comrades похожа на таковую в бизнес-логике Half-Life. Что необходимо знать:

  1. Все игровые объекты наследуются от CBaseEntity и их производных, в зависимости от того, какой функционал вам нужен.

    Так, если нужна анимированная модель, то наследоваться нужно от CBaseAnimating. Если NPC, то наследование идёт от CBaseMonster.
    Для платформы-давилки, разумнее всего выбрать CBaseToggle.
    В нашем простейшем случае будет достаточно CBaseEntity.

    Важно!
    Наследование от того или иного класса не налагает жестких ограничений, поскольку основной функционал предоставляется всё же самим игровым движком. Эти специализированные классы в основном предоставляют удобные обертки для работы с движковыми функциями и различные вспомогательные функции.
    При желании вы можете скопировать их в собственный класс, например чтобы получить уникальное сочетание свойств объекта, вроде "монстр-дверь" или "спрайт-поезд".
  2. Для видимых объектов мы должны предварительно закэшировать нашу модель с помощью функции PRECACHE_MODEL, а затем сделать её доступной в игре с помощью функции SetModel. В этом случае, модель будет корректно отображаться сразу при старте игры.
  3. Объекты с видимой моделью должны содержать как минимум две функции-члена класса для правильной работы. Это
    void Precache(void);
    и
    void Spawn(void);
  4. Чтобы мы могли установить наш новый объект на уровень в редакторе нам недостаточно просто создать класс. Как минимум потому, что неиспользованный класс будет удалён компилятором в рамках оптимизации. Мы должны выполнить инстанцирование и связать наш класс с именем объекта (entity, описывается в FGD-файле), которое и будет добавлено на карту.

    На один и тот же класс может ссылаться неограниченное кол-во имён объектов, а вот использование одних и тех же имён инстанцирования с разными классами приведёт к ошибке компиляции.

    Типовое связывание объекта с инстансом выглядит так:

    LINK_ENTITY_TO_CLASS(misc_banner, CBanner);

    где misc_banner это имя объекта, который будет установлен в редакторе на игровой уровень, а CBanner - имя класса, который связывается с этим объектом.

4. Создаём свой первый объект

Итак, теперь мы можем приступить к созданию нового объекта.
Выше мы уже условились, что будем использовать модель от 'Power Cube', а писать код в конце файла XashNT_SDK\src_user\ComradesServer\misc.shot

Добавляем в конец файла:

struct CRotationCube : public CBaseEntity
{
	string model_path = "models/items/keys/power.model";

	void Precache() { PRECACHE_MODEL(model_path);}

	void Spawn() {
		Precache();
		SetModel(model_path);
	}
};

LINK_ENTITY_TO_CLASS(misc_rotation_cube, CRotationCube);

Здесь вы видите необходимые функции Spawn и Precache.

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

Аргумент обоих функций - это относительный (от XashNT_SDK/Comrades/) путь к нашей модели. Вы можете заменить его на встроенную переменную m_model, а путь указывать в редакторе уровней, в настройках нашего объекта в поле "model".

Макроподстановка LINK_ENTITY_TO_CLASS связывает наш класс объекта с именем инстанса misc_rotation_cube.

Компилируем игровой код двойным щелчком по XashNT_SDK\src_user\ComradesServer\server.hsproj

Готово.

Открываем FGD-файл Джека (XashNT_SDK\J.A.C.K\comrades\quake2_comrades.fgd) и добавляем в конец его объект:

@PointClass base(Keys) studio("models/items/keys/power.model") = 
misc_rotation_cube : "Test Entity\n\nRotating cube model." []

Собираем тестовый уровень, размещаем на нем info_player_start, light_environment и misc_rotation_cube. Компилируем и запускаем из Джека.

Видим в игре:

5. Добавляем вращение объекта

Теперь немного усложним наш код - сделаем так, чтобы объект мог вращаться по произвольным осям. Следует отметить, что это будет чисто визуальное вращение без коллизии.

В кодовой базе Comrades есть несколько типов движения объектов:

  • MOVETYPE_NONE - объект неподвижен
  • MOVETYPE_NOCLIP - объект игнорирует коллизии
  • MOVETYPE_PUSH - используется для дверей, поездов и т.п.
  • MOVETYPE_STOP - используется для дверей, поездов если объект может быть заблокирован игроком
  • MOVETYPE_FOLLOW - используется для жесткого скрепления двух объектов между собой, например для оружия от первого лица и игрока.
  • MOVETYPE_WALK - специальный тип для ходьбы игрока
  • MOVETYPE_STEP - наземные монстры
  • MOVETYPE_FLY - летающие монстры
  • MOVETYPE_TOSS - предметы, которые можно выбросить, аптечки или оружие
  • MOVETYPE_FLYMISSILE - используется для ракет
  • MOVETYPE_BOUNCE - используется для гранат

Как видно, лучше всего для наших целей подойдет MOVETYPE_NOCLIP.

Включить его можно добавив в Spawn() строчку:

m_movetype =  MOVETYPE_NOCLIP;

Однако само по себе переключение на данный тип физической симуляции ещё не сделает объект подвижным, ведь мы же не задали ускорение вращения. Давайте это сделаем:

m_avelocity = vec3(30.0f, 30.0f, 30.0f);

Переменная m_avelocity имеет векторный тип и позволяет задавать вращение раздельно по трём осям: PITCH, YAW, ROLL. В данном случае, мы задаём вращение по всем трём осям одновременно со скоростью 30 градусов в секунду. Иными словами за одну секунду объект повернётся на 1/12 часть полного оборота, одновременно по всем осям.

Для того чтобы выбрать какую-то одну ось, достаточно обнулить необходимые аргументы конструктора vec3, к примеру:

m_avelocity = vec3(30.0f, 0.0f, 0.0f);

Это даст нам вращение только по оси PITCH.
Добавим две вышеприведённые строчки прямо в функцию Spawn:

void CRotationCube :: Spawn( void )
{
	Precache();
	SetModel( "models/items/keys/power.model" );
	m_movetype = MOVETYPE_NOCLIP;
	m_avelocity = vec3( 30.0f, 30.0f, 30.0f );
}

Компилируем игровой код двойным щелчком по XashNT_SDK\src_user\ComradesServer\server.hsproj

Карту пересобирать не нужно, поэтому запускаем XashNT_SDK\comrades.exe, жмем Escape для выхода в консоль, выполняем:

map имя-карты

и видим что наш объект теперь вращается по всем осям:

6. Настраиваемое вращение

Добавим возможность настраивать скорость вращения по всем трём осям из редактора уровней:

В наш класс CRotationCube добавим 3 строки в самое начало:

DECLARE_PROP_KEYFIELD_READONLY(float, m_speed_x);    
DECLARE_PROP_KEYFIELD_READONLY(float, m_speed_y);    
DECLARE_PROP_KEYFIELD_READONLY(float, m_speed_z);

Этот макрос создаст члены класса:

float m_speed_x;
float m_speed_y;
float m_speed_z;

Передадим их в конструктора вектора ускорения:

m_avelocity = vec3(m_speed_x, m_speed_y, m_speed_z);

Финальный вид класса:

struct CRotationCube : public CBaseEntity
{
	DECLARE_PROP_KEYFIELD_READONLY(float, speed_x);
	DECLARE_PROP_KEYFIELD_READONLY(float, speed_y);
	DECLARE_PROP_KEYFIELD_READONLY(float, speed_z);

	string model_path = "models/items/keys/power.model";

	void Precache() { PRECACHE_MODEL(model_path);}

	void Spawn() {
		Precache();
		SetModel(model_path);
		m_movetype =  MOVETYPE_NOCLIP;
		m_avelocity = vec3(speed_x, speed_y, speed_z);
	}
};

Теперь добавим эти поля в quake2_comrades.fgd:

@PointClass base() studio("models/items/keys/power.model") = 
misc_rotation_cube : "Test Entity\n\nRotating cube model." 
[
	m_speed_x(float) : "Rotating speed X" : 0
	m_speed_y(float) : "Rotating speed Y" : 0
	m_speed_z(float) : "Rotating speed Z" : 0
]

Формат описания поля:

имя-поля(тип) : "Имя в редакторе" : значение-по-умолчанию

Перезапустим Джек и откроем свойства объекта:

Теперь можно настраивать скорость вращения модели куба по всем осям из редактора.

На этом пока всё, продолжение следует.