Постепенное прибавление здоровья by KiQ


Спасибо за помощь XaeroX-у и Дяде Мише



Эта мысль пришла мне сумбурно, но пришлась по душе. Результат можно будет посмотреть в моей игре.
Собственно, идея заключается в том, что после поднятия аптечки в оригинальном Half-Life здоровье игроку начисляется мгновенно.
Но ведь это совсем не реалистично, хотя такая мелочь теряется в атмосфере самой игры.
Но вот я решил что подобная мелочь будет смотрется не лучшим образом в моей модификации.
Кроме того, то, что пришло мне в голову позволяет добавить реалистичности и в то же время подчеркнуть атмосферу игры.
Собственно поэтому я и решил опубликовать результат моей двухдневной работы - готовый код,
который помимо всего прочего можно изменять, подбирать наиболее подходящие для вас параметры.

Часть первая: подготовка, декларация функций и переменных

Для человека, который знаком с программированием на C++, не секрет, что для того, чтобы дополнить код новыми функциями, надо сначала задекларировать их в файле заголовков.
На данном этапе мы выполним именно эту часть работы.
Нам понадобятся следующие переменные (естественно, что вы можете выбрать для них другие имена):
int m_flIdealHealth - эта переменная нужна нам для того, чтобы хранить значение здоровья, полученного через gSkillData.healthkitCapacity. Иными словами, это количество процентов здоровья, которые дает поднятая аптечка.
int m_idealHealthBuff - эта переменная нужна для того, чтобы сохранить значение количества здоровья игрока до поднятия аптечки. Это необходимо, чтобы здоровье не прибавлялось бесконечно.
BOOL isHealthCharge - логическая переменная. Служит индикатором, который включается если игрок подобрал аптечку, и выключается после получения нужного количества здоровья. Служит одним из условий, при которых происходит прибавление здоровья.

Итак, нам нужно задекларировать их. Так как в основном они будут фигурировать в коде игрока, а именно в файле player.cpp, я решил связать их именно с этим файлом.
Открываем player.h и переходим в район 90 строки. Там уже есть множество задекларированных переменных, туда мы и запишем наши. Я сделал это сразу после объявления класса, в поле public. Вот небольшой отрывок кода:

C++ code:



				class CBasePlayer : public CBaseMonster
				{
				public:


        				//KiQ: this is three variables for function getGradualyHealth
        				int m_flIdealHealth;
        				int m_idealHealthBuff;
        				BOOL isHealthCharge;
			


Теперь у нас есть все переменные для манипуляции. Но нужно также задекларировать те функции, которые будут их использовать. А точнее, в данном случае это одна функция.
Я назвал ее getGradualyHealth, что в переводе значит "Поступенное получение здоровья". Вы можете назвать как угодно, возможно вы придумаете более информативный вариант :)
Итак, задекларируем ее. Для этого перейдем в район 250 строки. Это место в коде легко найти по соответствующему комментарию Valve - // custom player functions
После него мы и задекларируем нашу функцию таким образом:

C++ code:



				// custom player functions
        			void getGradualHealth( void ); // KiQ: method for gradualy health charging
			


Собственно на этом наша подготовительная часть завершена, и мы можем переходить непосредственно к изменению кода :)

Часть вторая: работа в коде игрока

Данная часть посвящена основной работе. Именно здесь будут происходить метаморфозы кода, необходимые для получения желаемой цели. В последней части мы лишь дополним их реализацию.
Итак, откроем файл player.cpp. В начале нам необходимо инициализировать те переменные, которые мы добавили в player.h и задать им дефолтное значение.
Для этого перейдем в район строки 190 (вы можете видеть там множество других переменных), и перед функцией void LinkUserMessages( void ) добавим наши переменные следующим образом:

C++ code:



				int m_flIdealHealth = 0;
				int m_idealHealthBuff = 0;
				BOOL isHealthCharge = false;
			


Как вы могли заметить, мы задали дефолтное значение у переменной BOOL isHealthCharge равным false. Это сделано для того, чтобы при старте игры здоровье игрока не начало прибавлятся само собой. Это логически правильно.
Теперь, когда все переменные на своих местах, мы можем принятся за реализацию функции getGradualyHealth. Привожу здесь ее полный код, а затем обьясню его как можно доступней:

C++ code:



			//KiQ: this is function of gradualy health charging
			void CBasePlayer :: getGradualHealth(){
    				if(pev->health < 100 && pev-> health < (m_idealHealthBuff + m_flIdealHealth) && m_idealHealthBuff > 80) {
        				pev->health += 0.01;
    				} else {
        				if(pev->health < 100 && pev-> health < (m_idealHealthBuff + m_flIdealHealth) && m_idealHealthBuff > 50) {
            					pev->health += 0.05;
        				} else {
            					if(pev->health < 100 && pev-> health < (m_idealHealthBuff + m_flIdealHealth) && m_idealHealthBuff > 30) {
                					pev->health += 0.08;
            					} else {
            						if(pev->health < 100 && pev-> health < (m_idealHealthBuff + m_flIdealHealth) && m_idealHealthBuff > 15) {
                						pev->health += 0.15;
            						} else {
            							isHealthCharge = false;
          						}
        					}
      					}
    				}
			}
			


Я разместил этот код перед функцией void CBasePlayer::PreThink(void) и настоятельно рекомендую сделать также. Хотя, можете поэксперементировать.
Итак, что же делает этот код. По сути он, после поднятия аптечки начинает постепенно восстанавливать здоровье, пока оно не достигнет нужной суммы.
Этой суммой является изначальное здоровье игрока к которому прибавлено значение аптечки(я уже писал об этом в блоке с переменными).
Давайте подробнее остановимся на операторах if (громоздкая конструкция, я знаю). В принципе все if, что идут после первого абсолютно идентичны. Разница лишь в значении одной переменной. Итак, if(pev->health < 100 && pev-> health < (m_idealHealthBuff + m_flIdealHealth) && m_idealHealthBuff > 80), что же это значит.
Первым условием является то, что у игрока присутсвуют повреждения, ибо бессмысленно давать ему здоровье если оно и так полное(если это конечно не артефакт вроде Megahealth, но это из другой оперы). Следовательно, мы должны узнать, поврежден ли игрок, и это одно из главнейших условий.
Следующее условие служит для того, чтобы не надавать лишнего =) Пока здоровье меньше чем изначальное+скилл аптечки, функция будет выполнятся. Если же оно равно, то условие выполнено, здоровье получено и дальнейшее выполнение бессмысленно.
И, наконец, последнее условие, то самое что различается во вложенных if. Оно проверяет, чему было равно здоровье игрока в момент поднятия аптечки, и в зависимости от этого подбирает значение скорости получения здоровья. В принципе эту проверку можно и опустить, но я думаю это интересная геймплейная фишка. Кроме того, стоит подумать о игроке - допустим угораздило его "убить" здоровье процентов до десяти, впереди еще толпа врагов, а он должен ждать, пока здоровье дойдет хотя бы до приемлегого уровня. Поэтому я решил сделать таким образом, что если у игрока мало здоровья, оно прибавляется быстрее. И наоборот, если здоровья много, то можно и подождать пока оно прибавится, даже в процессе боя. Именно этот параметр pev->health += 0.01; я имел в виду, говоря о гибкости этого кода. Вы можете подобрать нужные значения под свою игру. Например (и это будет более реалистично), если игрок получил сильные повреждения, то наоборот, ему нужно больше времени, чтобы восстановиться (ну, остановить кровотечение там, перебинтоваться =)). Собственно неограничен и уровень детелизации этих условий - вы можете добавить свою скорость для каждого процента здоровья.
Ну, и еще одна "фишка" этой функции, после чего мы перейдем к дальнейшему коду. Вы также можете настроить и второй параметр - максимальное значение получаемого здоровья. Таким образом мы получаем несколько скиллов для одной энтити (которые можно еще и динамически изменять).


Ну чтож, продолжим нашу работу. Теперь у нас уже есть функция, которую можно вызвать, но пока что этого вызова нигде не происходит. Давайте добавим его!
Перейдем в функцию void CBasePlayer::PreThink(void) (это около 1790 строки). Там мы можем увидеть различные функции, относящиеся к игроку, а также условия выполнения. Именно это нас и интересует. Давайте добавим свое условие, которое будет служить для вызова getGradualyHealth. Я сделал это после следующего кода:

C++ code:



			if (pev->deadflag >= DEAD_DYING)
			{
				PlayerDeathThink();
				return;
			}
			


А вот текст моего условия:

C++ code:



        		//KiQ: next if is procedure of player's health gradualy addition

        		if( isHealthCharge ) {
            			getGradualHealth();
			}
			


Как видите, все очень просто =) Да не совсем. Ведь мы должны каким - то образом привести isHealthCharge в состояние true. И сделать это в момент поднятия аптечки. И вот тут начинается последняя часть нашего рассказа.

Часть третья: усовершенствование аптечки

Данная часть посвящена полностью коду аптечки. Это был один из самых проблемных участков. Потому - что умные люди из Valve заключили нужный нам код в очень интересное условие, идущее вразрез с нашими планами. Но в конце концов разум победил, и решение было найдено. Подробнее об этом расскажу ниже.
А для начала откроем файл healthkit.cpp Он содержит код сразу двух энтить - точечной item_healthkit и брашевой func_healthcharger. Нас интересует первый объект - в коде он называется CHealthKit соответственно. Перейдем к функции BOOL CHealthKit::MyTouch( CBasePlayer *pPlayer ). Именно она отвечает за поднятие себя игроком, а качестве параметра принимает ссылку на класс игрока, что очень даже нам удобно. При помощи этого указателя мы сможем обратиться к переменным, необходимым для работы нашей функции.
А вот теперь начинаются серьезные перестановки. Сравните два варианта - первый, это первоначальная функция, второй - мой вариант:

C++ code:



			BOOL CHealthKit::MyTouch( CBasePlayer *pPlayer )
			{
				if ( pPlayer->pev->deadflag != DEAD_NO )
				{
					return FALSE;
				}

				if ( pPlayer->TakeHealth( gSkillData.healthkitCapacity, DMG_GENERIC ) )
				{
					MESSAGE_BEGIN( MSG_ONE, gmsgItemPickup, NULL, pPlayer->pev );
					WRITE_STRING( STRING(pev->classname) );
					MESSAGE_END();

					EMIT_SOUND(ENT(pPlayer->pev), CHAN_ITEM, "items/smallmedkit1.wav", 1, ATTN_NORM);

				if ( g_pGameRules->ItemShouldRespawn( this ) )
				{
					Respawn();
				}
				else
				{
					UTIL_Remove(this);	
				}

		return TRUE;
	}

	return FALSE;
}
			

C++ code:



			BOOL CHealthKit::MyTouch( CBasePlayer *pPlayer )
			{
				if ( pPlayer->pev->deadflag != DEAD_NO )
			{
				return FALSE;
			}

        		//KiQ: this is original function with my changes. This if for correctly work
        		if( pPlayer->pev->health < 100) {

				pPlayer->m_flIdealHealth = gSkillData.healthkitCapacity;
                		pPlayer->m_idealHealthBuff = pPlayer->pev->health;
				pPlayer->isHealthCharge = true;

				MESSAGE_BEGIN( MSG_ONE, gmsgItemPickup, NULL, pPlayer->pev );
					WRITE_STRING( STRING(pev->classname) );
				MESSAGE_END();

				EMIT_SOUND(ENT(pPlayer->pev), CHAN_ITEM, "items/smallmedkit1.wav", 1, ATTN_NORM);

				if (g_pGameRules->ItemShouldRespawn( this ) )
				{
					Respawn();
				}
			else
			{
                      		UTIL_Remove(this);
			}
                return TRUE;
        }
	return FALSE;
}
			


Итак, в первоначальном варианте от Valve делалась проверка на то, получил ли игрок здоровье, но делала это единовременно. Таким образо было невозможно осуществить постепенное прибавление - функция все равно вначале прибавляла единовременно скилл аптечки, а затем начинала заново прибавлять его но уже при помощи нашей функции. После долгих абстрактных размышлений было решено сделать именно такую проверку - если здоровье игрока меньше ста - аптечка берется, а затем совершает все что ей нужно - респавнится или же пропадает с карты. В противном случае она остается лежать как ни вчем не бывало, что собственно и требуется. Кстати, интересный побочный эффект получился. Теперь, если вы в консоли напишете give item_healthkit (ай-я-яй), то если ваше здоровье полное, аптечка респавнится прямо из вас >_<. Я подумаю, как это можно использовать, может быть в кооперативе можно будет таким способом лечить товарищей :)
Теперь подробнее о новых строках. Собственно это задание значений жизненно важных для нашей функции переменных. Сначала мы получаем "емкость" поднятой аптечки (pPlayer->m_flIdealHealth = gSkillData.healthkitCapacity;). Она должна быть прописана в skill.cfg и берется именно оттуда. Например у меня это значение равно 15. Далее мы сохраняем текущее значение здоровья игрока в своеобразный буфер - переменную m_idealHealthBuff. Для чего, я уже описывал ранее. Ну, а последняя строчка - самая важная! Именно она приводит переменную isHealthCharge в состояние true и таким образом инициирует выполнение нашей функции получения здоровья =)

Congratulations!


Можете компилировать и радоваться улучшенному механизму получения здоровья =)

Оригинальная идея - я (KiQ)
Помощь - XaeroX и Дядя Миша
02.02.2011