Shambler team
Статьи по программированию под движком Gold Source (игры Half-Life 1) и работе с SDK

Программирование дождя и снега

Вступление
1. Клиентская часть
1.1. Заголовочный файл
1.2. Функции обработки дождя
1.3. Функции рисования дождя
1.4. Управление дождём через текстовый файл
1.5. Организация приёма данных от сервера
2. Серверная часть
2.1. Необходимые приготовления
2.2. Отправка данных клиенту
2.3. Добавление объектов-энтить

Вступление

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

Вот мне любопытно, чем вы обычно пользуетесь для имитации атмосферных осадков на своих картах? Ну если вы делаете карты под CS 1.6 и ConditionZero, то тогда вам повезло — поставил одну-единственную энтитю на карту, и успокоился. А что же делать, если у вас собственный мод под старую клячу Халфу? Возиться с лучами, спрайтами, и прочими сомнительными методами, вроде env_rain из Spirit of Half-Life? Нет уж, спасибо. Зачем, когда у нас модостроителей-разработчиков есть штука, которая всяким CS-мапперам и не снилась — комплект средств разработки (Software Development Kit — SDK).

Как можно догадаться, речь в этом руководстве пойдёт об имитации в Халфе таких любимых нами погодных явлений, как дождь и снег. Помимо Халф-Лайфа от вас потребуется как минимум наличие MS Visual C++ 6.0, исходного кода из SDK и умение нажимать Ctrl+c и Ctrl+v (или Ctrl+Insert и Shift+Insert). Хотя если вы раньше не имели дело с программированием под Half-Life или же просто хотите полюбоваться на дождь в действии, то можете скачать уже откомпилированные библиотеки с демонстрационными картами отсюда (1,9 Мб). Там всё упаковано в отдельный мод, который, как обычно, запускается через Custom game. Ну а если вам лень чего-то ещё качать и запускать, то вот вам парочка картинок (щёлкните для увеличения).

Перед тем, как переходить к реализации дождя, давайте немного поговорим о том, что он будет из себя представлять, чего он умеет, а чего — нет, и каким образом мы будем его настраивать на карте.

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


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

Короче говоря, добавление дождя на карту сводится к установке парочки энтить, и настройке их параметров. Больше от маппера ничего не требуется.


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

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


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

Теперь, когда вы имеете некоторое представление об основных принципах дождя, можно браться за кодинг — остальные подробности будут разобраны по ходу дела.



1. Клиентская часть

Я решил поступить традиционно, разбив руководство на работу с клиентской и серверной частью. Начну с клиентской, так как здесь находится всё самое важное.

Пожалуй, сейчас стоит разобраться, где же мы собираемся хранить все эти капли и снежинки, с их координатами, скоростями, и ещё бог знает с чем. Здесь вам не сервер — просто так энтитю капельки не забацаешь. Да и изврат это — тратить на каждую каплю отдельную энтитю. Что же тогда? Может, темпентити? Казалось бы, хороший вариант — просто создал, указал нужный спрайт и скорость, и она сама рисуется, двигается, просчитывает столконовения, удаляется, и так далее. Вот только если бы не максимальный предел в пятьсот штук, и не удурчающая скорость их рисования, было бы вообще замечательно. Вздохнув, приходим к такому решению — создаем собственную систему хранения частиц в памяти, а рисуем их "вручную" средствами TriAPI. И, естественно, заботиться о перемещении частиц в пространстве и высчитывании их столкновения с поверхностью тоже придется нам самим.

Можно было бы, конечно, создать один гигантский массив для хранения всех частиц, но это и неудобно, и не эстетично. Пойдём лучше путём динамического выделения пямяти операторами new и delete. Если вы занимались программированием исключельно под Half-life, то такие операции могут быть для вас в новинку.

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

Ну что ж, поехали.



1.1. Заголовочный файл

Начать стоит с создания и добавления в проект клиентской библиотеки файла rain.h, где мы разместим объявления всех необходимых констант, структур и функций. Откройте этот файл, и добавьте туда такой блок определений [фрагмент кода 1]:

#ifndef __RAIN_H__
#define __RAIN_H__

#define DRIPSPEED	900		// скорость падения капель (пикселей/с)
#define SNOWSPEED	200		// скорость падения снежинок
#define SNOWFADEDIST	80

#define MAXDRIPS	2000	// предельное количество капель (можно увеличить при необходимости)
#define MAXFX		3000	// предельное кол-во допол-х частиц (круги по воде и т.п.)

#define DRIP_SPRITE_HALFHEIGHT	46
#define DRIP_SPRITE_HALFWIDTH	8
#define SNOW_SPRITE_HALFSIZE	3

// "радиус" круга на воде, до которого он разрастается за секунду
#define MAXRINGHALFSIZE		25

(Если табуляция выглядит косо, не обращайте внимания — после вставки в MSVC код будет выглядеть нормально).

Некоторые определения, думаю, требуют пояснения. Для снежинок прикручен специальный эффект, чтобы они появлялись не резко, а плавно — SNOWFADEDIST регулирует расстояние, на протяжении которого снежинки после рождения набирают свой уровень прозрачности. Для капель я не стал это делать, так как они летят очень быстро, и эффекта не заметно. Пределы, установленные ниже, ничем не подкреплены — т.е. их можно увеличить или уменьшить. Они нужны, в основном, чтобы мапперы не борзели :). MAXFX, ограничивающий количество кругов на воде, взят с очень большим запасом — на тот случай, если уж у вас дождь идёт посреди океана (кстати, запомните — вода, на которую попадает дождь, обязательно должна быть энтитей func_water!).

Далее идут половинные размеры, до которых растягиваются спрайты капель и снежинок. Они не обязаны соответствовать размерам соответствующих спрайтов. По текущим настройкам, спрайт капли растянут до 92х16, а снежинки — ужат до 6х6.


Теперь давайте напишем структуру для хранения настроек дождя, и структуру, которая будет представлять частичку (каплю или снежинку) [фрагмент кода 2]:

typedef struct
{
	int			dripsPerSecond;
	float		distFromPlayer;
	float		windX, windY;
	float		randX, randY;
	int			weatherMode;	// 0 - snow, 1 - rain
	float		globalHeight;
} rain_properties;


typedef struct cl_drip
{
	float		birthTime;
	float		minHeight;	// капля будет уничтожена на этой высоте.
	vec3_t		origin;
	float		alpha;

	float		xDelta;		// side speed
	float		yDelta;
	int			landInWater;
	
	cl_drip*		p_Next;		// next drip in chain
	cl_drip*		p_Prev;		// previous drip in chain
} cl_drip_t;

Настройки дождя содержат в себе количество капель, появляющихся в секунду, радиус дождя (distFromPlayer), силу ветра и степень рандомизации полета частиц по сторонам, тип осадков, и высоту, на которой появляются все капли.

В свойства частицы, помимо координат и скорости, входит прозрачность (частицы чуть-чуть отличаются ей друг от друга), время рождения, и высота, при достижении которой частица будет уничтожена. Эта высота определятся с помощью trace при создании частицы. В тот же момент и определяется, куда упадёт частица — на землю или в воду — и записывается в переменную landInWater


Но помимо капель и снежинок, выраженных структурой cl_drip, у нас есть ещё один тип объектов, для которых надо также придумать способ размещения — это специальные эффекты, типа водяных кругов. Точнее, пока что водяные круги — единственный тип этих эффектов, но вы с легкостью можете потом сами добавить всякие всплески от удара капли об асфальт, или стелющуюся по земле дымку, как в CZ. Обрабатывать эти объекты будем по тому же принципу, что и капли — добавляем ещё одну структуру [фрагмент кода 3]:

typedef struct cl_rainfx
{
	float		birthTime;
	float		life;
	vec3_t		origin;
	float		alpha;
	
	cl_rainfx*		p_Next;		// next fx in chain
	cl_rainfx*		p_Prev;		// previous fx in chain
} cl_rainfx_t;

Этот файл почти готов. Осталось только добавить объявления ещё не написанных нами функций, в которых будет сконцентрирована вся обработка дождя [фрагмент кода 4]:

void ProcessRain( void );
void ProcessFXObjects( void );
void ResetRain( void );
void InitRain( void );

#endif

Всё, на этой директиве наш rain.h можно считать завершённым.


1.2. Функции обработки дождя

Под функциями обработки дождя подразумеваются те функции, которые мы объявили выше. Они выполняют всю работу по созданию, уничтожению, и передвижению частиц. Они не занимаются, только разве что, непосредственным их рисованием на экране.

Создайте и добавьте в проект новый файл по имени rain.cpp. Здесь, помимо описания функций обработки дождя, мы также разместим всю нужную им мелочь.

Написание модуля rain.cpp мы начнем со списка подключаемых заголовков и с объявления глобальных переменных [фрагмент кода 5]:

#include <memory.h>
#include "hud.h"
#include "cl_util.h"
#include "const.h"
#include "entity_types.h"
#include "cdll_int.h"
#include "pm_defs.h"
#include "event_api.h"

#include "rain.h"

void WaterLandingEffect(cl_drip *drip);


rain_properties     Rain;

cl_drip     FirstChainDrip;
cl_rainfx   FirstChainFX;

double rain_curtime;    // current time
double rain_oldtime;    // last time we have updated drips
double rain_timedelta;  // difference between old time and current time
double rain_nextspawntime;  // when the next drip should be spawned

int dripcounter = 0;
int fxcounter = 0;

Переменные FirstChainDrip и FirstChainFX являются началами соответствующих цепочек. Сами они не рисуются, и в них используется только указатель p_Next.

dripcounter и fxcounter содержат в себе количество существующих в данный момент частиц и дополнительных эффектов. Нужны чтобы проверять на превышение допустимых пределов, и чтобы при необходимости выводить их значения в консоль для отладочных целей.


Сейчас мы начнём описание самой главной функции, выполняющей почти всю работу по обработке дождя. Начнётся она, как всегда, с объявления нужных впоследствии переменных [фрагмент кода 6]:

/*
=================================
ProcessRain
Перемещает существующие объекты, удаляет их
при надобности, и, если дождь включён, создает новые.

Должна вызываться каждый кадр.
=================================
*/
void ProcessRain( void )
{
	rain_oldtime = rain_curtime; // save old time
	rain_curtime = gEngfuncs.GetClientTime();
	rain_timedelta = rain_curtime - rain_oldtime;

	// first frame
	if (rain_oldtime == 0)
	{
		// fix first frame bug with nextspawntime
		rain_nextspawntime = gEngfuncs.GetClientTime();
		return;
	}

	if (Rain.dripsPerSecond == 0 && FirstChainDrip.p_Next == NULL)
	{
		// keep nextspawntime correct
		rain_nextspawntime = rain_curtime;
		return;
	}

	if (rain_timedelta == 0)
		return; // not in pause

	double timeBetweenDrips = 1 / (double)Rain.dripsPerSecond;

	cl_drip* curDrip = FirstChainDrip.p_Next;
	cl_drip* nextDrip = NULL;

	cl_entity_t *player = gEngfuncs.GetLocalPlayer();

	// хранение отладочной информации
	float debug_lifetime = 0;
	int debug_howmany = 0;
	int debug_attempted = 0;
	int debug_dropped = 0;

Следущим шагом будет написание цикла, в котором, собственно, и происходит последовательная проверка всех существующих капель. Чтобы передвигаться по связанной цепочке капель, мы будем использовать два указателя, объявленных выше — curDrip и nextDrip. Как следует из их названия, curDrip указывает на обрабатываемую в данный момент каплю, а nextDrip запоминает указатель на следующую за ней (вдруг текущую нам придётся удалить) [фрагмент кода 7]:

	while (curDrip != NULL) // go through list
	{
		nextDrip = curDrip->p_Next; // save pointer to next drip

		if (Rain.weatherMode == 0)
			curDrip->origin.z -= rain_timedelta * DRIPSPEED; // rain
		else
			curDrip->origin.z -= rain_timedelta * SNOWSPEED; // snow

		curDrip->origin.x += rain_timedelta * curDrip->xDelta;
		curDrip->origin.y += rain_timedelta * curDrip->yDelta;
		
		// remove drip if its origin lower than minHeight
		if (curDrip->origin.z < curDrip->minHeight) 
		{
			if (curDrip->landInWater/* && Rain.weatherMode == 0*/)
				WaterLandingEffect(curDrip); // create water rings

			if (gHUD.RainInfo->value)
			{
				debug_lifetime += (rain_curtime - curDrip->birthTime);
				debug_howmany++;
			}
			
			curDrip->p_Prev->p_Next = curDrip->p_Next; // link chain
			if (nextDrip != NULL)
				nextDrip->p_Prev = curDrip->p_Prev; 
			delete curDrip;
					
			dripcounter--;
		}

		curDrip = nextDrip; // restore pointer, so we can continue moving through chain
	}

В первой половине цикла происходит перемещение капли в соответствии с её скоростью и типом осадков, а во втором проверяется — не вылетела ли она за заранее просчитанный при её рождении предел. Если вылетела, то капля исключается из цепочки и удаляется оператором delete. Перед этим, если капля упала в воду, вызывается функция WaterLandingEffect, создающая круги на воде. (Если вы потом захотите добавить эффекты, возникающие при ударе об землю, то здесь же добавьте вызовы своих функций. Можно даже сделать эффекты, зависящие от типа текстуры.)


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

Здесь стоит пояснить одну важную деталь — то, что вы установили некоторое количество капель в секунду, ещё не значит, что в секунду будет появляться именно столько капель. Механизм работает следующим образом — для капли случайно выбираются координаты внутри "радиуса дождя", и если вдруг они оказываются не в области карты, то эту каплю мы пропускаем. То есть, если заданно появление двух тысяч капель в секунду, то именно две тысячи капель за секунду появлятся, только если игрок стоит в чистом поле. Если игрок находится в переулке, рядом с высоким зданием, и так далее, то часть капель будет отброшена. Таким образом достигается равномерность дождя.

Как я уже писал, при рождении капли производится trace, чтобы узнать, на какой высоте капля столкнется с землей. Естественно, trace производится с учетом скорости полета капли в сторону под воздействием ветра. (помимо ветра, каждая капля может также иметь свою, чуть отличную от других капель скорость). Если trace выполнился успешно, то остается только проверить, не находится ли точка приземления в воде. Если да, то получить координату поверхности воды (для этого вода обязательно должна быть энтитей func_water), и сделать пометку в свойствах капли, что при её удалении должен быть создан водяной круг.

Вот он — это довольно большой блок кода [фрагмент кода 8]:

       int maxDelta; // maximum height randomize distance
        float falltime;
        if (Rain.weatherMode == 0)
        {
                maxDelta = DRIPSPEED * rain_timedelta; // for rain
                falltime = (Rain.globalHeight + 4096) / DRIPSPEED;
        }
        else
        {
                maxDelta = SNOWSPEED * rain_timedelta; // for snow
                falltime = (Rain.globalHeight + 4096) / SNOWSPEED;
        }

        while (rain_nextspawntime < rain_curtime)
        {
                rain_nextspawntime += timeBetweenDrips;
                if (gHUD.RainInfo->value)
                        debug_attempted++;

                if (dripcounter < MAXDRIPS) // check for overflow
                {
                        float deathHeight;
                        vec3_t vecStart, vecEnd;

                        vecStart[0] = gEngfuncs.pfnRandomFloat(player->origin.x - Rain.distFromPlayer, player->origin.x + Rain.distFromPlayer);
                        vecStart[1] = gEngfuncs.pfnRandomFloat(player->origin.y - Rain.distFromPlayer, player->origin.y + Rain.distFromPlayer);
                        vecStart[2] = Rain.globalHeight;

                        float xDelta = Rain.windX + gEngfuncs.pfnRandomFloat(Rain.randX * -1, Rain.randX);
                        float yDelta = Rain.windY + gEngfuncs.pfnRandomFloat(Rain.randY * -1, Rain.randY);

                        // find a point at bottom of map
                        vecEnd[0] = falltime * xDelta;
                        vecEnd[1] = falltime * yDelta;
                        vecEnd[2] = -4096;

                        pmtrace_t pmtrace;
                        gEngfuncs.pEventAPI->EV_SetTraceHull( 2 );
                        gEngfuncs.pEventAPI->EV_PlayerTrace( vecStart, vecEnd, PM_STUDIO_IGNORE, -1, &pmtrace );

                        if (pmtrace.startsolid)
                        {
                                if (gHUD.RainInfo->value)
                                        debug_dropped++;

                                continue; // drip cannot be placed
                        }

                        // falling to water?
                        int contents = gEngfuncs.PM_PointContents( pmtrace.endpos, NULL );
                        if (contents == CONTENTS_WATER)
                        {
                                int waterEntity = gEngfuncs.PM_WaterEntity( pmtrace.endpos );
                                if ( waterEntity > 0 )
                                {
                                        cl_entity_t *pwater = gEngfuncs.GetEntityByIndex( waterEntity );
                                        if ( pwater && ( pwater->model != NULL ) )
                                        {
                                                deathHeight = pwater->curstate.maxs[2];
                                        }
                                        else
                                        {
                                                gEngfuncs.Con_Printf("Rain error: can't get water entity\n");
                                                continue;
                                        }
                                }
                                else
                                {
                                        gEngfuncs.Con_Printf("Rain error: water is not func_water entity\n");
                                        continue;
                                }
                        }
                        else
                        {
                                deathHeight = pmtrace.endpos[2];
                        }

                        // just in case..
                        if (deathHeight > vecStart[2])
                        {
                                gEngfuncs.Con_Printf("Rain error: can't create drip in water\n");
                                continue;
                        }


                        cl_drip *newClDrip = new cl_drip;
                        if (!newClDrip)
                        {
                                Rain.dripsPerSecond = 0; // disable rain
                                gEngfuncs.Con_Printf( "Rain error: failed to allocate object!\n");
                                return;
                        }

                        vecStart[2] -= gEngfuncs.pfnRandomFloat(0, maxDelta); // randomize a bit

                        newClDrip->alpha = gEngfuncs.pfnRandomFloat(0.12, 0.2);
                        VectorCopy(vecStart, newClDrip->origin);

                        newClDrip->xDelta = xDelta;
                        newClDrip->yDelta = yDelta;

                        newClDrip->birthTime = rain_curtime; // store time when it was spawned
                        newClDrip->minHeight = deathHeight;

                        if (contents == CONTENTS_WATER)
                                newClDrip->landInWater = 1;
                        else
                                newClDrip->landInWater = 0;

                        // add to first place in chain
                        newClDrip->p_Next = FirstChainDrip.p_Next;
                        newClDrip->p_Prev = &FirstChainDrip;
                        if (newClDrip->p_Next != NULL)
                                newClDrip->p_Next->p_Prev = newClDrip;
                        FirstChainDrip.p_Next = newClDrip;

                        dripcounter++;
                }
                else
                {
                        gEngfuncs.Con_Printf( "Rain error: Drip limit overflow!\n" );
                        return;
                }
        }

Возможно, вы обратили внимание на иногда проскакивающее обращение к gHUD.RainInfo->value. Это cvar, который мы добавим чуть позже. Он служит для вывода в консоли служебной информации, касающейся дождя.

Функция почти закончена, осталось только добавить код, выводящий эту самую информацию [фрагмент кода 9]:

	if (gHUD.RainInfo->value) // print debug info
	{
		gEngfuncs.Con_Printf( "Rain info: Drips exist: %i\n", dripcounter );
		gEngfuncs.Con_Printf( "Rain info: FX's exist: %i\n", fxcounter );
		gEngfuncs.Con_Printf( "Rain info: Attempted/Dropped: %i, %i\n", debug_attempted, debug_dropped);
		if (debug_howmany)
		{
			float ave = debug_lifetime / (float)debug_howmany;
			gEngfuncs.Con_Printf( "Rain info: Average drip life time: %f\n", ave);
		}
		else
			gEngfuncs.Con_Printf( "Rain info: Average drip life time: --\n");
	}
	return;
}

Что означает каждая из этих четырех строк? Ну во-первых, здесь есть информация о количестве капель и частиц эффектов, существующих в данный момент — Drips exist и FX exist. Количество капель, прямо как r_speeds, непостоянно, и зависит от многих факторов. Например, в открытом поле оно будет выше, чем в каком-нибудь переулке, где часть капель просто не появится. Также, чем большую высоту приходится пролетать каплям, тем выше будет это значение. Включив эту информацию, маппер может пройтись по карте, и принять какие-либо решения об изменении радиуса дождя или его интенсивности.

Числа attempted/dropped показывают, соответственно, сколько капель попыталось появиться за этот кадр, и сколько из них были пропущены. Например, если вы стоите рядом со зданием, и показано, что пропущена где-то половина капель, то можно понять, что если вы выйдете в открытую область, количество Drips exist увеличится примерно в два раза. Или, к примеру, если ваша карта состоит из сплошных переулков, и процент отброшенных капель весьма велик, то, может быть, имеет смысл уменьшить радиус дождя.

Average drip life time показывает среднее время жизни капель. Оно зависит от высоты, которую они пролетают. Например, если вы поставили интенсивность дождя в две тысячи капель в секунду, то при среднем времени жизни одной капли в пол-секунды, в кадре, при условии низкого количества отброшенных капель, будет присутствовать около тысячи штук. Стоит увеличить высоту, на которой появляются капли — время жизни капель увеличится, и, соответсвенно, увеличится количество капель в кадре. (Заметка: снежинки из-за более медленной скорости живут дольше, поэтому для них интенсивность надо ставить гораздо ниже, чем для дождя)



И так, с функцией ProcessRain мы покончили. Давайте ненадолго отвлечемся от rain.cpp, и добавим новый cvar, отвечающий за вывод служебной информации. Для этого надо вставить всего лишь две строчки. Зайдите в описание класса CHud (это в hud.h), и где-нибудь в секции public, к остальным cvar'ам (чтобы не было путаницы), добавьте новую переменную:

(серым цветом для ориентирования показан уже существующий код. Добавляемый нами код, по-прежнему, выделяется синим) [фрагмент кода 10]:

hud.h
	cvar_t  *m_pCvarStealMouse;
	cvar_t	*m_pCvarDraw;
	cvar_t	*RainInfo; // rain tutorial

И в функции CHud :: Init, которая расположена в hud.cpp, где-нибудь в начале вставьте строчку [фрагмент кода 11]:

hud.cpp
	RainInfo = gEngfuncs.pfnRegisterVariable( "cl_raininfo", "0", 0 );	// rain tutorial

На этом cvar можно считать благополучно созданным. Вернемся к нашему любимому rain.cpp.

Следующей функцией, которую мы добавим в rain.cpp, будет объявленная нами ранее WaterLandingEffect, создающая водный круг. Делает она это по тому же принципу, что и использовался при создании капель — проверяет на лимит, создает новый объект, и добавляет его в начало цепочки. В функцию передается указатель на каплю, которая в скором времени будет удалена, чтобы можно было узнать координаты круга. Вертикальная координата эффекта берется из minHeight капли. Время жизни водных кругов колеблется от 0.7 до 1 cекунды. Если его увеличить, то это повлечет также увеличение радиуса кругов, так как скорость их разрастания (MAXRINGHALFSIZE) одинакова для всех. Также, чем больше время жизни кругов, тем выше будет значение счетчика "FX exist" [фрагмент кода 12]:

/*
=================================
WaterLandingEffect
создает круг на водной поверхности
=================================
*/
void WaterLandingEffect(cl_drip *drip)
{
	if (fxcounter >= MAXFX)
	{
		gEngfuncs.Con_Printf( "Rain error: FX limit overflow!\n" );
		return;
	}	
	
	cl_rainfx *newFX = new cl_rainfx;
	if (!newFX)
	{
		gEngfuncs.Con_Printf( "Rain error: failed to allocate FX object!\n");
		return;
	}
			
	newFX->alpha = gEngfuncs.pfnRandomFloat(0.6, 0.9);
	VectorCopy(drip->origin, newFX->origin);
	newFX->origin[2] = drip->minHeight; // correct position
			
	newFX->birthTime = gEngfuncs.GetClientTime();
	newFX->life = gEngfuncs.pfnRandomFloat(0.7, 1);

	// add to first place in chain
	newFX->p_Next = FirstChainFX.p_Next;
	newFX->p_Prev = &FirstChainFX;
	if (newFX->p_Next != NULL)
		newFX->p_Next->p_Prev = newFX;
	FirstChainFX.p_Next = newFX;
			
	fxcounter++;
}

В принципе, если вы разобрались в том, как создавались капли в ProcessRain, то здесь ничего нового нет. Однако, нам ещё надо написать код, который будет удалять эти самые круги. Отличие от капель будет в том, что капли удаляются при пересечении некой границы, а круги — по истечению их времени жизни. Удаление кругов будет происходить в отдельной функции, которая, как и ProcessRain, должна вызываться каждый кадр [фрагмент кода 13]:

/*
=================================
ProcessFXObjects
удаляет FX объекты, у которых вышел срок жизни

Каждый кадр вызывается перед ProcessRain
=================================
*/
void ProcessFXObjects( void )
{
	float curtime = gEngfuncs.GetClientTime();
	
	cl_rainfx* curFX = FirstChainFX.p_Next;
	cl_rainfx* nextFX = NULL;	

	while (curFX != NULL) // go through FX objects list
	{
		nextFX = curFX->p_Next; // save pointer to next
		
		// delete current?
		if ((curFX->birthTime + curFX->life) < curtime)
		{
			curFX->p_Prev->p_Next = curFX->p_Next; // link chain
			if (nextFX != NULL)
				nextFX->p_Prev = curFX->p_Prev; 
			delete curFX;					
			fxcounter--;
		}
		curFX = nextFX; // restore pointer
	}
}

Почти все основные функции обработки дождя мы создали, но перед тем как переходить к функциям рисования его на экране, есть ещё одна вещь, о которой надо позаботиться — это инициализация переменных и освобождение памяти. Инициализация, в данном случае — это присвоение всем глобальным переменным нулевых значений. А освобождение памяти — это удаление всех капель и прочих эффектов, которое надо произвести, если загружается другая карта, или просто халфа прекращает свою работу. Есть такое правило, что для каждого объекта, созданным операцией new, должен быть соответсвующий вызов delete, который удалит этот объект. Если этого не сделать, то появится "утечка", когда память забита блоком данных, которые нельзя ни удалить и не прочесть, так как указатель на них потерян.

Добавьте код этих двух функций [фрагмент кода 14]:

/*
=================================
ResetRain
очищает память, удаляя все объекты.
=================================
*/
void ResetRain( void )
{
// delete all drips
	cl_drip* delDrip = FirstChainDrip.p_Next;
	FirstChainDrip.p_Next = NULL;
	
	while (delDrip != NULL)
	{
		cl_drip* nextDrip = delDrip->p_Next; // save pointer to next drip in chain
		delete delDrip;
		delDrip = nextDrip; // restore pointer
		dripcounter--;
	}

// delete all FX objects
	cl_rainfx* delFX = FirstChainFX.p_Next;
	FirstChainFX.p_Next = NULL;
	
	while (delFX != NULL)
	{
		cl_rainfx* nextFX = delFX->p_Next;
		delete delFX;
		delFX = nextFX;
		fxcounter--;
	}

	InitRain();
	return;
}

/*
=================================
InitRain
Инициализирует все переменные нулевыми значениями.
=================================
*/
void InitRain( void )
{
	Rain.dripsPerSecond = 0;
	Rain.distFromPlayer = 0;
	Rain.windX = 0;
	Rain.windY = 0;
	Rain.randX = 0;
	Rain.randY = 0;
	Rain.weatherMode = 0;
	Rain.globalHeight = 0;

	FirstChainDrip.birthTime = 0;
	FirstChainDrip.minHeight = 0;
	FirstChainDrip.origin[0]=0;
	FirstChainDrip.origin[1]=0;
	FirstChainDrip.origin[2]=0;
	FirstChainDrip.alpha = 0;
	FirstChainDrip.xDelta = 0;
	FirstChainDrip.yDelta = 0;
	FirstChainDrip.landInWater = 0;
	FirstChainDrip.p_Next = NULL;
	FirstChainDrip.p_Prev = NULL;

	FirstChainFX.alpha = 0;
	FirstChainFX.birthTime = 0;
	FirstChainFX.life = 0;
	FirstChainFX.origin[0] = 0;
	FirstChainFX.origin[1] = 0;
	FirstChainFX.origin[2] = 0;
	FirstChainFX.p_Next = NULL;
	FirstChainFX.p_Prev = NULL;
	
	rain_oldtime = 0;
	rain_curtime = 0;
	rain_nextspawntime = 0;

	return;
}

Естественно, все эти функции должны откуда-то вызываться. О ProcessRain и ProcessFXObjects мы позаботимся позже, а вызовы ResetRain и InitRain вставим в нужное место прямо сейчас.

Откройте файл hud.cpp, и в начале, рядом с остальными заголовочными модулями, добавьте подключение rain.h [фрагмент кода 15]:

#include "demo.h"
#include "demo_api.h"
#include "vgui_scorepanel.h"

#include "rain.h" // rain tutorial

Хорошо, теперь переместитесь в функцию CHud :: Init, и в начале добавьте [фрагмент кода 16]:

	InitRain();	// rain tutorial

Эта функция вызывается один-единственный раз при загрузке библиотеки, и дальнейший игропроцесс её не беспокоит.

Теперь перейдите в функцию CHud :: VidInit, которая отличается от Init тем, что вызывается при каждой загрузке карты, и тоже в начале добавьте [фрагмент кода 17]:

	ResetRain();	// rain tutorial

Но нам также нужно очистить память при выходе из игры. Поэтому зайдите в функцию CHud :: ~CHud(), и в начале добавьте эту же строчку [фрагмент кода 18]:

	ResetRain();	// rain tutorial

Всё, с управлением дождём и вцелом с rain.cpp покончено. Переходим к следующему разделу — выводу полигонов на экран.



1.3. Функции рисования дождя

В принципе, в рисовании дождя особых сложностей нет — просто пробегаем по всему списку капель, и рисуем каждую отдельным полигоном. На производительность влияния дождь оказывает не много — капли и снежинки являются очень мелкими объектами, и затраты на их рисование мизерны. Скажу даже больше — один-единственный Барни, стоящий в метре перед игроком, будет оказывать более сильное влияние на fps, чем тысяча капель или снежинок.

В основном, все проблемы, с которыми мы сейчас столкнемся, будут связаны с геометрией — с вычислением координат вершин полигонов. Но сначала, давайте подумаем, чем отличается снежинка от капельки? Скоростью, спрайтом, и всё? Да нет, рисование снежинки и рисование капельки — это две больше разницы. Если снежинка представляет собой простой спрайт, то капелька должна быть ориентирована по вертикали. То есть, ориентация капли в пространстве не зависит от того, насколько мы задрали голову — ведь, по сути, капли — это лучи, летящие сверху.

Кстати, с целью чуток повысить производительность, можно вместо прямоугольных спрайтов капель рисовать треугольные (ведь прямоугольник состоит из двух треугольников). Если расположить каплю на спрайте так, чтобы она не выходила за отсечённые границы, то видимой разницы не будет никакой. Вот, посмотрите на иллюстрацию — границы прямоугольного спрайта капли, а справа треугольного:

границы прямоугольного и треугольного спрайта капли дождя

Также мы должны учесть ещё одну интересную особенность капель — при ветре они наклоняются, в зависимости от скорости передвижения в сторону. Смотрите иллюстрацию — слева капля с небольшой скоростью движения в сторону, а справа ветер уже побольше, и наклон, соответственно, больше.

Всё дело в пропорции — отношение скорости полета капли в сторону к скорости полета вниз такое же, как и отношение вертикальной длины спрайта к горизонтальному смещению, которое и требуется найти.


Ладно, ближе к делу. Так как рисуем капли мы средствами TriAPI, то функции рисования удобнее будет расположить в модуле tri.cpp. Откройте этот файл, и добавьте в начало подключаемые заголовки [фрагмент кода 19]:

tri.cpp
#include "cl_entity.h"
#include "triangleapi.h"

// rain tutorial
#include "rain.h" 
#include "com_model.h"
#include "studio_util.h"

Зачем нам заголовки из набора рендеринга моделей, я объясню позднее, когда будем рисовать снежинки.

Теперь давайте начнем создание функции, которая будет рисовать капли и снежинки. Её тело будет располагаться между концом тестовой функцией Draw_Triangles (после директивы #endif), и началом HUD_DrawNormalTriangles. Начнем с первой половины функции, которая будет обрабатывать капли [фрагмент кода 20]:

/* rain tutorial
=================================
DrawRain

Рисование капель и снежинок.
=================================
*/
extern cl_drip FirstChainDrip;
extern rain_properties Rain;

void DrawRain( void )
{
        if (FirstChainDrip.p_Next == NULL)
                return; // no drips to draw

        HSPRITE hsprTexture;
        const model_s *pTexture;
        float visibleHeight = Rain.globalHeight - SNOWFADEDIST;

        if (Rain.weatherMode == 0)
                hsprTexture = LoadSprite( "sprites/hi_rain.spr" ); // load rain sprite
        else
                hsprTexture = LoadSprite( "sprites/snowflake.spr" ); // load snow sprite

        // usual triapi stuff
        pTexture = gEngfuncs.GetSpritePointer( hsprTexture );
        gEngfuncs.pTriAPI->SpriteTexture( (struct model_s *)pTexture, 0 );
        gEngfuncs.pTriAPI->RenderMode( kRenderTransAdd );
        gEngfuncs.pTriAPI->CullFace( TRI_NONE );

        // go through drips list
        cl_drip* Drip = FirstChainDrip.p_Next;
        cl_entity_t *player = gEngfuncs.GetLocalPlayer();

        if ( Rain.weatherMode == 0 )
        { // draw rain
                while (Drip != NULL)
                {
                        cl_drip* nextdDrip = Drip->p_Next;

                        Vector2D toPlayer;
                        toPlayer.x = player->origin[0] - Drip->origin[0];
                        toPlayer.y = player->origin[1] - Drip->origin[1];
                        toPlayer = toPlayer.Normalize();

                        toPlayer.x *= DRIP_SPRITE_HALFWIDTH;
                        toPlayer.y *= DRIP_SPRITE_HALFWIDTH;

                        float shiftX = (Drip->xDelta / DRIPSPEED) * DRIP_SPRITE_HALFHEIGHT;
                        float shiftY = (Drip->yDelta / DRIPSPEED) * DRIP_SPRITE_HALFHEIGHT;

                // --- draw triangle --------------------------
                        gEngfuncs.pTriAPI->Color4f( 1.0, 1.0, 1.0, Drip->alpha );
                        gEngfuncs.pTriAPI->Begin( TRI_TRIANGLES );

                                gEngfuncs.pTriAPI->TexCoord2f( 0, 0 );
                                gEngfuncs.pTriAPI->Vertex3f( Drip->origin[0]-toPlayer.y - shiftX, Drip->origin[1]+toPlayer.x - shiftY,
                                        Drip->origin[2] + DRIP_SPRITE_HALFHEIGHT );

                                gEngfuncs.pTriAPI->TexCoord2f( 0.5, 1 );
                                gEngfuncs.pTriAPI->Vertex3f( Drip->origin[0] + shiftX, Drip->origin[1] + shiftY,
                                        Drip->origin[2]-DRIP_SPRITE_HALFHEIGHT );

                                gEngfuncs.pTriAPI->TexCoord2f( 1, 0 );
                                gEngfuncs.pTriAPI->Vertex3f( Drip->origin[0]+toPlayer.y - shiftX, Drip->origin[1]-toPlayer.x - shiftY,
                                        Drip->origin[2]+DRIP_SPRITE_HALFHEIGHT);

                        gEngfuncs.pTriAPI->End();
                // --- draw triangle end ----------------------

                        Drip = nextdDrip;
               }
       }

Обратите внимание на названия спрайтов капли и снежинки — если таких спрайтов не будет в папке sprites, то игра вылетит! Для капли — hi_rain.spr, а для снежинки — snowflake.spr. (Можете использовать прилагающиеся к руководству)


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

(прим: Матрицей преобразований объекта называется таблица размерностью 4 на 3, содержащая три направляющих вектора для основных осей, и координаты, по которым объект будет расположен)

Для перевода углов в матрицу используется функция AngleMatrix. Чтобы потом по матрице перевести координату в мировую систему координат, надо использовать функцию VectorTransform. Для построения матрицы, естественно, будут использоваться углы поворота головы игрока.

Не знаю, как вы, но я не люблю, когда код выглядит неаккуратно. Поэтому всё, что связано с переводом координат, я засунул в одну функцию — SetPoint. Прокрутите в начало файла tri.cpp, и после заголовочных файлов добавьте определение этой функции [фрагмент кода 21]:

// rain tutorial
void SetPoint( float x, float y, float z, float (*matrix)[4])
{
	vec3_t point, result;
	point[0] = x;
	point[1] = y;
	point[2] = z;

	VectorTransform(point, matrix, result);

	gEngfuncs.pTriAPI->Vertex3f(result[0], result[1], result[2]);
}

Теперь вернитесь к том месту, где мы на середине бросили функцию DrawRain. Сейчас мы её закончим [фрагмент кода 22]:

	else
	{ // draw snow
		vec3_t normal;
		gEngfuncs.GetViewAngles((float*)normal);
	
		float matrix[3][4];
		AngleMatrix (normal, matrix);	// calc view matrix

		while (Drip != NULL)
		{
			cl_drip* nextdDrip = Drip->p_Next;

			matrix[0][3] = Drip->origin[0]; // write origin to matrix
			matrix[1][3] = Drip->origin[1];
			matrix[2][3] = Drip->origin[2];

			// apply start fading effect
			float alpha = (Drip->origin[2] <= visibleHeight) ? Drip->alpha :
				((Rain.globalHeight - Drip->origin[2]) / (float)SNOWFADEDIST) * Drip->alpha;
					
		// --- draw quad --------------------------
			gEngfuncs.pTriAPI->Color4f( 1.0, 1.0, 1.0, alpha );
			gEngfuncs.pTriAPI->Begin( TRI_QUADS );

				gEngfuncs.pTriAPI->TexCoord2f( 0, 0 );
				SetPoint(0, SNOW_SPRITE_HALFSIZE ,SNOW_SPRITE_HALFSIZE, matrix);

				gEngfuncs.pTriAPI->TexCoord2f( 0, 1 );
				SetPoint(0, SNOW_SPRITE_HALFSIZE ,-SNOW_SPRITE_HALFSIZE, matrix);

				gEngfuncs.pTriAPI->TexCoord2f( 1, 1 );
				SetPoint(0, -SNOW_SPRITE_HALFSIZE ,-SNOW_SPRITE_HALFSIZE, matrix);

				gEngfuncs.pTriAPI->TexCoord2f( 1, 0 );
				SetPoint(0, -SNOW_SPRITE_HALFSIZE ,SNOW_SPRITE_HALFSIZE, matrix);
				
			gEngfuncs.pTriAPI->End();
		// --- draw quad end ----------------------

			Drip = nextdDrip;
		}
	}
}

Комбинацию AngleMatrix и SetPoint вы можете потом использовать для разворота на нужный угол любых объектов, рисуемых с помощью TriAPI.


Итак, снежинки и капли нарисованы, но у меня такое ощущение, что мы кое-что забыли. Ах да, водные круги! В принципе, их рисование можно было бы засунуть во всё ту же DrawRain, но мне как-то больше хочется, чтобы всё было более систематизировано. Единственная проблема, которую надо решить при рисовании водных кругов — это вычисление их текущего радиуса и уровня прозрачности. Хотя в принципе, вещи это довольно простые и понятные из кода.

Сделайте небольшой отступ от предыдущей функции, и создайте следующую [фрагмент кода 23]:

/* rain tutorial
=================================
DrawFXObjects

Рисование водяных кругов
=================================
*/

extern cl_rainfx FirstChainFX;

void DrawFXObjects( void )
{
        if (FirstChainFX.p_Next == NULL)
                return;
// no objects to draw

        float curtime = gEngfuncs.GetClientTime();

        
// usual triapi stuff
        HSPRITE hsprTexture;
        const model_s *pTexture;
        hsprTexture = LoadSprite( "sprites/waterring.spr" );
// load water ring sprite
        pTexture = gEngfuncs.GetSpritePointer( hsprTexture );
        gEngfuncs.pTriAPI->SpriteTexture( (struct model_s *)pTexture, 0 );
        gEngfuncs.pTriAPI->RenderMode( kRenderTransAdd );
        gEngfuncs.pTriAPI->CullFace( TRI_NONE );

        
// go through objects list
        cl_rainfx* curFX = FirstChainFX.p_Next;
        while (curFX != NULL)
        {
                cl_rainfx* nextFX = curFX->p_Next;

                
// fadeout
                float alpha = ((curFX->birthTime + curFX->life - curtime) / curFX->life) * curFX->alpha;
                float size = (curtime - curFX->birthTime) * MAXRINGHALFSIZE;

                
// --- draw quad --------------------------
                gEngfuncs.pTriAPI->Color4f( 1.0, 1.0, 1.0, alpha );
                gEngfuncs.pTriAPI->Begin( TRI_QUADS );

                        gEngfuncs.pTriAPI->TexCoord2f( 0, 0 );
                        gEngfuncs.pTriAPI->Vertex3f(curFX->origin[0] - size, curFX->origin[1] - size, curFX->origin[2]);

                        gEngfuncs.pTriAPI->TexCoord2f( 0, 1 );
                        gEngfuncs.pTriAPI->Vertex3f(curFX->origin[0] - size, curFX->origin[1] + size, curFX->origin[2]);

                        gEngfuncs.pTriAPI->TexCoord2f( 1, 1 );
                        gEngfuncs.pTriAPI->Vertex3f(curFX->origin[0] + size, curFX->origin[1] + size, curFX->origin[2]);

                        gEngfuncs.pTriAPI->TexCoord2f( 1, 0 );
                        gEngfuncs.pTriAPI->Vertex3f(curFX->origin[0] + size, curFX->origin[1] - size, curFX->origin[2]);

                gEngfuncs.pTriAPI->End();
                
// --- draw quad end ----------------------

                curFX = nextFX;
        }
}

Ну вот, мы уже почти покончили с рисованием дождя! Единственное, что осталось сделать — это вызвать все эти написанные нами функции. А вызываться они все будут, как ни странно, из одного и того же места. Перемотайте ниже к функции HUD_DrawTransparentTriangles, и в её конце вставьте [фрагмент кода 24]:

	// rain tutorial
	ProcessFXObjects();
	ProcessRain();
	DrawRain();
	DrawFXObjects();

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

Если вам не терпится, то вы уже сейчас можете запустить дождь — зайдите в ранее написанную нами функцию RainInit, и там вместо нулевых значений для переменной Rain выставьте нужные настройки (например, dripsPerSecond = 1500, distFromPlayer = 1000, и globalHeight в соответствии с тестовой картой).



1.4. Управление дождём через текстовый файл

В дополнение к традиционному способу управления дождём через энтити и месседжи, который будет разобран в дальнейшем, я также придумал метод, на который стоит обратить внимание авторам модов с поддержкой многопользовательского режима. Этот метод заключается в создании файла "mapname.pcs" (pcs — precipiations — осадки), и прописывании в нём всех настроек дождя. В принципе, вы вольны сами выбрать, какие методы использовать в своем моде — можете пропустить этот мини-раздел, и перейти к созданию месседжей и энтить, либо можете добавить только этот метод, а на энтити забить, а можете предусмотреть оба метода сразу. Возможно этот метод окажется подходящим для многопользовательского мода. Во-первых, исключаются все контакты с сервером, и во-вторых, каждый игрок по желанию может отключить у себя осадки (хотя плюс это или минус — сказать трудно). Сюда же — настройки легко отредактировать, ведь для этого не надо перекомпилировать карту или копаться в ней рипентом.

Файл с настройками дождя будет представлять собой обычный текстовый файл, где перечислен набор параметров и их значений. Например [фрагмент настроек в текстовом файле 25]:

// Rain settings file for map raindemo.bsp

drips	1500
distance	1000
windx	200
windy	0
randx	50
randy	50
mode	0
height	500

Думаю, какой параметр за что отвечает, объяснять не нужно. Сейчас мы создадим одну функцию, которая будет заниматься чтением всего этого дела. Зайдите в rain.cpp и в конце файла добавьте [фрагмент кода 26]:

/*
===========================
ParseRainFile

пытается загрузить настройки дождя из файла.
имеет низкий приоритет перед настройками с карты

Список настроек:
	drips
	distance
	windx
	windy
	randx
	randy
	mode
	height
===========================
*/
void ParseRainFile( void )
{
	if (Rain.distFromPlayer != 0 || Rain.dripsPerSecond != 0 || Rain.globalHeight != 0)
		return;

	char mapname[64];
	char token[64];
	char *pfile;

	strcpy( mapname, gEngfuncs.pfnGetLevelName() );
	if (strlen(mapname) == 0)
	{
		gEngfuncs.Con_Printf( "Rain error: unable to read map name\n");
		return;
	}

	mapname[strlen(mapname)-4] = 0;
	sprintf(mapname, "%s.pcs", mapname); // pcs - precipiations - осадки

	pfile = (char *)gEngfuncs.COM_LoadFile( mapname, 5, NULL);
	if (!pfile)
	{
		if (gHUD.RainInfo->value)
			gEngfuncs.Con_Printf("Rain: couldn't open rain settings file %s\n", mapname);
		return;
	}

	while (true)
	{
		pfile = gEngfuncs.COM_ParseFile(pfile, token);
		if (!pfile)
			break;

		if (!stricmp(token, "drips")) // dripsPerSecond
		{
			pfile = gEngfuncs.COM_ParseFile(pfile, token);
			Rain.dripsPerSecond = atoi(token);
		}
		else if (!stricmp(token, "distance")) // distFromPlayer
		{
			pfile = gEngfuncs.COM_ParseFile(pfile, token);
			Rain.distFromPlayer = atof(token);
		}
		else if (!stricmp(token, "windx")) // windX
		{
			pfile = gEngfuncs.COM_ParseFile(pfile, token);
			Rain.windX = atof(token);
		}
		else if (!stricmp(token, "windy")) // windY
		{
			pfile = gEngfuncs.COM_ParseFile(pfile, token);
			Rain.windY = atof(token);		
		}
		else if (!stricmp(token, "randx")) // randX
		{
			pfile = gEngfuncs.COM_ParseFile(pfile, token);
			Rain.randX = atof(token);
		}
		else if (!stricmp(token, "randy")) // randY
		{
			pfile = gEngfuncs.COM_ParseFile(pfile, token);
			Rain.randY = atof(token);
		}
		else if (!stricmp(token, "mode")) // weatherMode
		{
			pfile = gEngfuncs.COM_ParseFile(pfile, token);
			Rain.weatherMode = atoi(token);
		}
		else if (!stricmp(token, "height")) // globalHeight
		{
			pfile = gEngfuncs.COM_ParseFile(pfile, token);
			Rain.globalHeight = atof(token);
		}
		else
			gEngfuncs.Con_Printf("Rain error: unknown token %s in file %s\n", token, mapname);
	}
	
	gEngfuncs.COM_FreeFile( pfile );
}

Теперь только нужно поместить вызов этой функции в правильное место. Мне показалось, что ParseRainFile лучше всего вызывать при первом вызове ProcessRain. (Из CHud::VidInit слишком рано, а Msg_InitHUD не срабатывает при ченжлевелах).

Отмотайте в начало функции ProcessRain, и добавьте вызов ParseRainFile в этот кусок кода [фрагмент кода 27]:

	// first frame
	if (rain_oldtime == 0)
	{
		// fix first frame bug with nextspawntime
		rain_nextspawntime = gEngfuncs.GetClientTime();
		ParseRainFile();
		return;
	}

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

void ParseRainFile( void );

Если вы считаете, что такого метода управления дождём вам будет достаточно, и если вам не нужны возможности плавного включения/выключения дождя во время однопользовательской игры, то на этом можете считать работу над дождём завершённой.



1.5. Организация приёма данных от сервера

Под такой мутной формулировкой я подразумеваю всего лишь приём месседжа, в котором сервер будет посылать все настройки дождя. В принципе, ничего особенного от этой части кода не требуется, только разве что засунуть принятые числа в нашу глобальную переменную Rain. Сообщение будет называться RainData. Процедура создания месседжа на клиенте стандартная, и, думаю, многим знакомая. Начнём с того, что зайдём в файл hud.cpp, и ближе к началу найдём большой блок функций, начинающихся с __MsgFunc_. Добавьте нашу [фрагмент кода 29]:

// rain tutorial
int __MsgFunc_RainData(const char *pszName, int iSize, void *pbuf)
{
	return gHUD.MsgFunc_RainData( pszName, iSize, pbuf );
}

Теперь зайдите (уже в третий раз за время изучения статьи) в функцию CHud :: Init, которая расположена ниже в этом же файле, и там где-нибудь рядом с остальными HOOK'ами добавьте этот [фрагмент кода 30]:

HOOK_MESSAGE( RainData ); // rain tutorial

Перейдите в описание класса CHud (hud.h), и там, во второй секции public найдите список функций месседжей. Добавьте объявление и нашей [фрагмент кода 31]:

	int _cdecl MsgFunc_SetFOV(const char *pszName,  int iSize, void *pbuf);
	int  _cdecl MsgFunc_Concuss( const char *pszName, int iSize, void *pbuf );
	int  _cdecl MsgFunc_RainData( const char *pszName, int iSize, void *pbuf ); // rain tutorial

Ну и теперь самое главное — добавим, собственно, саму функцию в файл hud_msg.cpp. Промотайте в конец файла, и вставьте [фрагмент кода 32]:

// rain tutorial
extern rain_properties Rain;

int CHud :: MsgFunc_RainData( const char *pszName, int iSize, void *pbuf )
{
	BEGIN_READ( pbuf, iSize );
		Rain.dripsPerSecond =	READ_SHORT();
		Rain.distFromPlayer =	READ_COORD();
		Rain.windX =			READ_COORD();
		Rain.windY =			READ_COORD();
		Rain.randX =			READ_COORD();
		Rain.randY =			READ_COORD();
		Rain.weatherMode =		READ_SHORT();
		Rain.globalHeight =		READ_COORD();
		
	return 1;
}

Чтобы можно было получить доступ к параметрам дождя, не забудьте добавить в список подключаемых заголовков rain.h.



2. Серверная часть

Надеюсь, предыдущую часть руководства вы закончили упешно, и у вас всё компилируется, работает и не глючит. Также надеюсь, что ваши кнопки "c" и "v" ("скопировать" и "vставить") ещё не пришли в негодность :)

Задача, которая стоит перед нами в этой части, заключается в разработке метода управления дождём и добавлении нужных нам для этого энтить. Какие возможности помимо простого указания постоянных настроек дождя для конкретной карты мы бы ещё хотели иметь? Ну, в основном, они касаются однопользовательского режима игры. Может быть кому-то это покажется излишеством, но неплохо было бы сделать эффект начинающегося и затухающего дождя. Для игрока это может быть приятным удивлением :). Например, представьте — идёт он по каким-нибудь мрачным индустриальным кварталам, и тут бац — первая капля, вторая, третья.. А через минуту уже идёт приличный дождик. Так же постепенно он может потом закончиться.

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

Моё решение состоит в том, чтобы сделать настройки дождя собственностью игрока, т.е. частью его класса. Пускай ходит по картам — настройки будут "ходить" вместе с ним. Мы сразу убиваем двух зайцев — и проблему смены карт, и проблему сохранения игры. А энтити, которые мы приспособим для управления дождём, будут записывать в игрока новые настройки.

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

В категории "динамических" настроек у нас остались — количество капель в секунду, сила ветра и степень случайности полета капель. Эти параметры могут меняться в пределах одной карты. Пожалуй, подходящим вариантом в этом случае будет сделать две разные энтити для установки тех и тех параметров. Первую группу настроек, принадлежащих конкретной карте, мы вынесем в энтитю rain_settings, которая на карте должна находиться в единственном экземпляре. Настройки, которые можно менять активацией, пойдут в энтитю rain_modify — в однопользовательской карте их может быть столько, сколько нужно.

Здесь есть небольшой нюанс — динамический дождь, который то начинается, то кончается, не подойдёт для многопользовательского режима. Ведь когда rain_modify активируется, он должен записать в свойства игроков новые настройки. А если какой-то игрок зайдёт в игру позже, то у него дождя не будет до тех пор, пока опять на карте не сработает какой-то rain_modify. Так что для мультиплеера ограничимся неизменным вариантом — rain_modify, у которого не задано имя, он будет означать настройки, постоянные для всей карты (наподобие настроек, устанавливаемых через текстовый файл). Если игрок по ходу однопользовательской игры наткнётся на такую карту, то она перезапишет его настройки дождя. Если же игрок придёт на карту, где нет rain_settings, то тогда его настройки дождя обнулятся.

Для поддержки дождя, плавно изменяющего свою интенсивность, в rain_modify нам надо предусмотреть ещё одно поле — время, в течении которого новые настройки полностью вступят в силу (в общем, Fade time, если кому так более привычно).



2.1. Необходимые приготовления

С чего бы начать? Ну давайте начнем с добавления в класс игрока новых переменных. Откройте описание класса CBasePlayer (файл player.h), и в конце класса (после строчки "float m_flNextChatTime") добавьте в него такую пачку переменных [фрагмент кода 33]:

	// rain tutorial
	int		Rain_dripsPerSecond;
	float	Rain_windX, Rain_windY;
	float	Rain_randX, Rain_randY;

	int		Rain_ideal_dripsPerSecond;
	float	Rain_ideal_windX, Rain_ideal_windY;
	float	Rain_ideal_randX, Rain_ideal_randY;

	float	Rain_endFade; // 0 means off
	float	Rain_nextFadeUpdate;

	int		Rain_needsUpdate;

Угу, много. Первые пять — это текущие настройки дождя и ветра. Вторые пять (которые "ideal") — это настройки, к которым они стремятся во время использования плавного перехода (да, ветер тоже может плавно изменяться). Переменная endFade — это время, когда плавный переход будет завершён, а nextFadeUpdate — время отсылки следующего месседжа о состоянии дождя во время плавного перехода (отсылка сообщений происходит раз в секунду).

Последняя переменная — needsUpdate — указывает на необходимость отправки месседжа о статусе дождя. месседж отправляется из функции CBasePlayer::UpdateClientData. Чтобы обеспечить переотсылку месседжа при загрузке сохранялки, в Precache игрока эта переменная устанавливается в единицу (а Precache, как известно, вызывается при каждой загрузке карты).

Чтобы потом не забыть, давайте пропишем эти переменные в save data. В файле player.cpp, недалеко от начала, найдите большой блок DEFINE_FIELD'ов, начинающийся с комментария "Global Savedata for player". В конец этого блока добавьте [фрагмент кода 34]:

	// rain tutorial
	DEFINE_FIELD( CBasePlayer, Rain_dripsPerSecond, FIELD_INTEGER ),
	DEFINE_FIELD( CBasePlayer, Rain_windX, FIELD_FLOAT ),
	DEFINE_FIELD( CBasePlayer, Rain_windY, FIELD_FLOAT ),
	DEFINE_FIELD( CBasePlayer, Rain_randX, FIELD_FLOAT ),
	DEFINE_FIELD( CBasePlayer, Rain_randY, FIELD_FLOAT ),

	DEFINE_FIELD( CBasePlayer, Rain_ideal_dripsPerSecond, FIELD_INTEGER ),
	DEFINE_FIELD( CBasePlayer, Rain_ideal_windX, FIELD_FLOAT ),
	DEFINE_FIELD( CBasePlayer, Rain_ideal_windY, FIELD_FLOAT ),
	DEFINE_FIELD( CBasePlayer, Rain_ideal_randX, FIELD_FLOAT ),
	DEFINE_FIELD( CBasePlayer, Rain_ideal_randY, FIELD_FLOAT ),

	DEFINE_FIELD( CBasePlayer, Rain_endFade, FIELD_TIME ),
	DEFINE_FIELD( CBasePlayer, Rain_nextFadeUpdate, FIELD_TIME ),

Сейчас сделаем небольшое "дежурное" дело — зайдите в CBasePlayer::Spawn, и в начале функции добавьте установку всех наших переменных в ноль [фрагмент кода 35]:

	// rain tutorial
	Rain_dripsPerSecond = 0;
	Rain_windX = 0;
	Rain_windY = 0;
	Rain_randX = 0;
	Rain_randY = 0;
	Rain_ideal_dripsPerSecond = 0;
	Rain_ideal_windX = 0;
	Rain_ideal_windY = 0;
	Rain_ideal_randX = 0;
	Rain_ideal_randY = 0;
	Rain_endFade = 0;
	Rain_nextFadeUpdate = 0;

Обратили внимание — здесь нет Rain_needsUpdate. Конечно, потому что её значение мы установим в CBasePlayer :: Precache. В конец функции (как раз после какого-то "m_fInitHUD = TRUE") допишите [фрагмент кода 36]:

	// rain tutorial
	Rain_needsUpdate = 1;

Скучные дела ещё не закончены — надо создать новый месседж, по имени "RainData". Объявим переменную — во всё том же player.cpp, чуть ниже того места, где мы добавляли поля в save data, будет список "gmsg..."-переменных. Добавьте туда и нашу [фрагмент кода 37]:

int gmsgStatusText = 0;
int gmsgStatusValue = 0;

int gmsgRainData = 0; // rain tutorial

Теперь промотайте чуть ниже — там сразу же находится функция LinkUserMessages. Добавьте в её конец [фрагмент кода 38]:

	gmsgStatusText = REG_USER_MSG("StatusText", -1);
	gmsgStatusValue = REG_USER_MSG("StatusValue", 3);

	gmsgRainData = REG_USER_MSG("RainData", 16); // rain tutorial



2.2. Отправка данных клиенту

Как ни странно, месседжи мы будем отправлять вовсе не из каких-либо энтить, а из CBasePlayer :: UpdateClientData. Там мы будем проверять переменную Rain_needsUpdate, собирать данные для отправки, посылать их и возвращать переменную в нулевое значение. Это позволяет нам отправить сообщение сразу после загрузки карты, как только клиент будет способен его принять без всяких "nextthink + 1".

Правда перед этим надо сделать одно дельце — обработать плавный переход (aka fading). Во время плавного изменения характеристик дождя, они пересчитываются и перепосылаются раз в секунду (поэтому тоже это лучше не использовать в мультиплеере). Изменяются они по простой арифметической прогрессией — не так уж и плохо, но и не так эффектно, как было бы при какой-нибудь более сложной системе — ведь увеличение числа капель с двадцати до тридцати более заметно, чем, скажем, с четырёхста пятидесяти до четырёхста шестидесяти. Но, впрочем, ладно.

Откройте функцию CBasePlayer :: UpdateClientData (player.cpp), и там найдите такой комментарий: "Update Flashlight". Перед ним мы и разместим наш код (хотя в принципе там разницы особой нет, куда пихать) [фрагмент кода 39]:

        // ====== rain tutorial =======

        // calculate and update rain fading
        if (Rain_endFade > 0)
        {
                if (gpGlobals->time < Rain_endFade)
                { // we're in fading process
                        if (Rain_nextFadeUpdate <= gpGlobals->time)
                        {
                                int secondsLeft = Rain_endFade - gpGlobals->time + 1;

                                Rain_dripsPerSecond += (Rain_ideal_dripsPerSecond - Rain_dripsPerSecond) / secondsLeft;
                                Rain_windX += (Rain_ideal_windX - Rain_windX) / (float)secondsLeft;
                                Rain_windY += (Rain_ideal_windY - Rain_windY) / (float)secondsLeft;
                                Rain_randX += (Rain_ideal_randX - Rain_randX) / (float)secondsLeft;
                                Rain_randY += (Rain_ideal_randY - Rain_randY) / (float)secondsLeft;

                                Rain_nextFadeUpdate = gpGlobals->time + 1; // update once per second
                                Rain_needsUpdate = 1;

                                ALERT(at_aiconsole, "Rain fading: curdrips: %i, idealdrips %i\n", Rain_dripsPerSecond, Rain_ideal_dripsPerSecond);
                        }
                }
                else
                { // finish fading process
                        Rain_nextFadeUpdate = 0;
                        Rain_endFade = 0;

                        Rain_dripsPerSecond = Rain_ideal_dripsPerSecond;
                        Rain_windX = Rain_ideal_windX;
                        Rain_windY = Rain_ideal_windY;
                        Rain_randX = Rain_ideal_randX;
                        Rain_randY = Rain_ideal_randY;
                        Rain_needsUpdate = 1;

                        ALERT(at_aiconsole, "Rain fading finished at %i drips\n", Rain_dripsPerSecond);
                }
        }


        // send rain message
        if (Rain_needsUpdate)
        {
        // search for rain_settings entity
                edict_t *pFind;
                pFind = FIND_ENTITY_BY_CLASSNAME( NULL, "rain_settings" );
                if (!FNullEnt( pFind ))
                {
                // rain allowed on this map
                        CBaseEntity *pEnt = CBaseEntity::Instance( pFind );
                        CRainSettings *pRainSettings = (CRainSettings *)pEnt;

                        float raindistance = pRainSettings->Rain_Distance;
                        float rainheight = pRainSettings->pev->origin[2];
                        int rainmode = pRainSettings->Rain_Mode;

                        // search for constant rain_modifies
                        pFind = FIND_ENTITY_BY_CLASSNAME( NULL, "rain_modify" );
                        while ( !FNullEnt( pFind ) )
                        {
                                if (pFind->v.spawnflags & 1)
                                {
                                        // copy settings to player's data and clear fading
                                        CBaseEntity *pEnt = CBaseEntity::Instance( pFind );
                                        CRainModify *pRainModify = (CRainModify *)pEnt;

                                        Rain_dripsPerSecond = pRainModify->Rain_Drips;
                                        Rain_windX = pRainModify->Rain_windX;
                                        Rain_windY = pRainModify->Rain_windY;
                                        Rain_randX = pRainModify->Rain_randX;
                                        Rain_randY = pRainModify->Rain_randY;

                                        Rain_endFade = 0;
                                        break;
                                }
                                pFind = FIND_ENTITY_BY_CLASSNAME( pFind, "rain_modify" );
                        }

                        MESSAGE_BEGIN(MSG_ONE, gmsgRainData, NULL, pev);
                                WRITE_SHORT(Rain_dripsPerSecond);
                                WRITE_COORD(raindistance);
                                WRITE_COORD(Rain_windX);
                                WRITE_COORD(Rain_windY);
                                WRITE_COORD(Rain_randX);
                                WRITE_COORD(Rain_randY);
                                WRITE_SHORT(rainmode);
                                WRITE_COORD(rainheight);
                        MESSAGE_END();

                        if (Rain_dripsPerSecond)
                                ALERT(at_aiconsole, "Sending enabling rain message\n");
                        else
                                ALERT(at_aiconsole, "Sending disabling rain message\n");
                }
                else
                { // no rain on this map
                        Rain_dripsPerSecond = 0;
                        Rain_windX = 0;
                        Rain_windY = 0;
                        Rain_randX = 0;
                        Rain_randY = 0;
                        Rain_ideal_dripsPerSecond = 0;
                        Rain_ideal_windX = 0;
                        Rain_ideal_windY = 0;
                        Rain_ideal_randX = 0;
                        Rain_ideal_randY = 0;
                        Rain_endFade = 0;
                        Rain_nextFadeUpdate = 0;

                        ALERT(at_aiconsole, "Clearing rain data\n");
                }

                Rain_needsUpdate = 0;
        }
        // ====== end rain =======

Как видите, код можно разбить на две независимые части — это обработка плавного перехода (комментарий "calculate and update rain fading"), и код, отправляющий данные клиенту ("send rain message"). Обе части содержат отладочные сообщения, выводящиеся в консоль при режиме "developer 2" — иногда с помощью них гораздо проще определить, правильно ли тебя поняла программа.

Алгоритм отправки данных, в принципе, нам уже известен — ищем энтитю rain_settings, и если её нет на карте, то все настроки дождя обнуляются (при этом в аиконсоль выводится сообщение "Clearing rain data"). Далее пытаемся найти объект rain_modify без имени, который означал бы константные настройки карты, и если такой есть, то его данные копируются в структуру игрока (rain_modify без имени сам себе выставляет первый спаунфлаг) (кстати, небольшая мысль на заметку — можете прикрутить сюда также проверку глобальной переменной, чтобы константный rain_modify можно было бы включать и выключать из другой карты при помощи глобальных переменных). А затем, собственно, посылается сам месседж, и в аиконсоль выдается "Sending enabling rain message" (или disabling, если этот месседж приказывает выключить дождь).



2.3. Добавление объектов-энтить

Осталось теперь только создать классы CRainSettings, CRainModify, и соответствующие им энтити. Начнём с описания в заголовочном файле. Откройте файл effects.h, и в конец (до завершающего #endif, конечно) добавьте [фрагмент кода 40]:

// ====== rain tutorial ========

class CRainSettings : public CBaseEntity
{
public:
	void	Spawn( void );
	void	KeyValue( KeyValueData *pkvd );

	int	ObjectCaps( void ) { return (CBaseEntity :: ObjectCaps() & ~FCAP_ACROSS_TRANSITION); }

	virtual int		Save( CSave &save );
	virtual int		Restore( CRestore &restore );
	static	TYPEDESCRIPTION m_SaveData[];

	float Rain_Distance;
	int Rain_Mode;
};

class CRainModify : public CBaseEntity
{
public:
	void	Spawn( void );
	void	KeyValue( KeyValueData *pkvd );
	void	Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value );

	int	ObjectCaps( void ) { return (CBaseEntity :: ObjectCaps() & ~FCAP_ACROSS_TRANSITION); }

	virtual int		Save( CSave &save );
	virtual int		Restore( CRestore &restore );
	static	TYPEDESCRIPTION m_SaveData[];

	int Rain_Drips;
	float Rain_windX, Rain_windY;
	float Rain_randX, Rain_randY;
	float fadeTime;
};

rain_settings, фактически, является контейнером — его дело только считать переменные через кейволью (KeyValue(...)).

Теперь перейдём в файл effects.cpp, и в начале добавим новый заголовочный файл "player.h" [фрагмент кода 41]:

#include "func_break.h"
#include "shake.h"

#include "player.h" // rain tutorial

А функции наших энтить мы расположим в самом конце этого файла [фрагмент кода 42]:

// ======= rain tutorial =========

void CRainSettings::Spawn()
{
	pev->solid = SOLID_NOT;
	pev->movetype	= MOVETYPE_NONE;
	pev->effects |= EF_NODRAW;
}

void CRainSettings::KeyValue( KeyValueData *pkvd )
{
	if (FStrEq(pkvd->szKeyName, "m_flDistance"))
	{
		Rain_Distance = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "m_iMode"))
	{
		Rain_Mode = atoi(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else	
	{
		CBaseEntity::KeyValue( pkvd );
	}
}

LINK_ENTITY_TO_CLASS( rain_settings, CRainSettings );

TYPEDESCRIPTION	CRainSettings::m_SaveData[] = 
{
	DEFINE_FIELD( CRainSettings, Rain_Distance, FIELD_FLOAT ),
	DEFINE_FIELD( CRainSettings, Rain_Mode, FIELD_INTEGER ),
};
IMPLEMENT_SAVERESTORE( CRainSettings, CBaseEntity );



void CRainModify::Spawn()
{
	pev->solid = SOLID_NOT;
	pev->movetype	= MOVETYPE_NONE;
	pev->effects |= EF_NODRAW;

	if (FStringNull(pev->targetname))
		pev->spawnflags |= 1;
}

void CRainModify::KeyValue( KeyValueData *pkvd )
{
	if (FStrEq(pkvd->szKeyName, "m_iDripsPerSecond"))
	{
		Rain_Drips = atoi(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "m_flWindX"))
	{
		Rain_windX = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "m_flWindY"))
	{
		Rain_windY = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "m_flRandX"))
	{
		Rain_randX = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "m_flRandY"))
	{
		Rain_randY = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else if (FStrEq(pkvd->szKeyName, "m_flTime"))
	{
		fadeTime = atof(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}
	else	
	{
		CBaseEntity::KeyValue( pkvd );
	}
}

void CRainModify::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
{
	if (pev->spawnflags & 1)
		return; // constant

	if (gpGlobals->deathmatch)
	{
		ALERT(at_console, "Rain error: only static rain in multiplayer\n");
		return; // not in multiplayer
	}

	CBasePlayer *pPlayer;
	pPlayer = (CBasePlayer *)CBaseEntity::Instance(g_engfuncs.pfnPEntityOfEntIndex(1));

	if (fadeTime)
	{ // write to 'ideal' settings
		pPlayer->Rain_ideal_dripsPerSecond = Rain_Drips;
		pPlayer->Rain_ideal_randX = Rain_randX;
		pPlayer->Rain_ideal_randY = Rain_randY;
		pPlayer->Rain_ideal_windX = Rain_windX;
		pPlayer->Rain_ideal_windY = Rain_windY;

		pPlayer->Rain_endFade = gpGlobals->time + fadeTime;
		pPlayer->Rain_nextFadeUpdate = gpGlobals->time + 1;
	}
	else
	{
		pPlayer->Rain_dripsPerSecond = Rain_Drips;
		pPlayer->Rain_randX = Rain_randX;
		pPlayer->Rain_randY = Rain_randY;
		pPlayer->Rain_windX = Rain_windX;
		pPlayer->Rain_windY = Rain_windY;

		pPlayer->Rain_needsUpdate = 1;
	}
}

LINK_ENTITY_TO_CLASS( rain_modify, CRainModify );

TYPEDESCRIPTION	CRainModify::m_SaveData[] = 
{
	DEFINE_FIELD( CRainModify, fadeTime, FIELD_FLOAT ),
	DEFINE_FIELD( CRainModify, Rain_Drips, FIELD_INTEGER ),
	DEFINE_FIELD( CRainModify, Rain_randX, FIELD_FLOAT ),
	DEFINE_FIELD( CRainModify, Rain_randY, FIELD_FLOAT ),
	DEFINE_FIELD( CRainModify, Rain_windX, FIELD_FLOAT ),
	DEFINE_FIELD( CRainModify, Rain_windY, FIELD_FLOAT ),
};
IMPLEMENT_SAVERESTORE( CRainModify, CBaseEntity );

Вряд ли тут что-то требует пояснений. Ну, разве что стоит обратить внимание на то, что rain_modify с именем откажется работать в мультиплеере. Тем более, что он сохраняет настройки только в одного игрока.


Собственно, энтити есть, месседж тоже есть, всё есть.. Можно компилировать. Осталось только добавить наши энтитьки в fgd-файл (ох как я не люблю эти фгд-шки — до сих пор не представляю, как можно было умудриться сделать такой одновременно примитивный, и тем не менее, до ужаса привередливый формат. Сколько раз ни приходилось добавлять энтити, вечно где-нибудь что-то пропускал, и потом по десять минут сидел, вперившись в этот "абзац", пытаясь понять, что не так). В общем, вот [фрагмент кода fgd]:

@PointClass size(-8 -8 -8, 8 8 8) color(160 60 250) = rain_settings : "Constant map settings" 
[
	m_flDistance(integer) : "Rain distance" : 1000
	m_iMode(choices) : "Weather type" : 0 =
	[
		0: "Rain"
		1: "Snow"
	]
]

@PointClass base(Targetname) size(-8 -8 -8, 8 8 8) color(160 20 170) = rain_modify : "Modify rain settings" 
[
	m_flTime(integer) : "Fading time" : 0
	m_iDripsPerSecond(integer) : "Drips per second" : 800
	m_flWindX(integer) : "Wind X" : 0
	m_flWindY(integer) : "Wind Y" : 0
	m_flRandX(integer) : "Rand X" : 0
	m_flRandY(integer) : "Rand Y" : 0
]

Или вместо этого в fgd можно сделать пояснения на русском:

@PointClass size(-8 -8 -8, 8 8 8) color(160 60 250) = rain_settings : "Общие настройки осадков"
[
	m_flDistance(integer) : "Дистанция дождя" : 1000
	m_iMode(choices) : "Тип погоды" : 0 =
	[
		0: "Дождь"
		1: "Снег"
	]
]

@PointClass base(Targetname) size(-8 -8 -8, 8 8 8) color(160 20 170) = rain_modify : "Изменяемые настройки осадков"
[
	m_flTime(integer) : "Время исчезания" : 0
	m_iDripsPerSecond(integer) : "Капель/с" : 800
	m_flWindX(integer) : "Ветер по X" : 0
	m_flWindY(integer) : "Ветер по Y" : 0
	m_flRandX(integer) : "Флуктуации по X" : 0
	m_flRandY(integer) : "Флуктуации по Y" : 0
]

На этом, пожалуй, всё. Надеюсь, статья оказалась вам полезной, и вы нашли эти эффекты простыми и удобными в использовании. Буду рад разумным отзывам и комментариям.

Скачать спрайты для дождя и снега можно тут [в оригиналы добавил пару спрайтов !ra_rain.spr и !ra_snowflake.spr из мода HL Red Alert Xpantion - прим. ОбаГлаза].


BUzer
2004 Shambler Team
  Counter.CO.KZ -     !
Hosted by uCoz