ООП в написании игр. Часть 2

ООП в написании игр. Часть 2

автор: Dimouse

Итак, нашлось немного свободного времени и я продолжаю свою статью по кодингу. Сразу без лишних предисловий перейдем к делу. А точнее к тем, кто будет населять нашу игру. Не важно кто это - орки, эльфы и хоббиты или мутанты, гхолы и скорпионы, всегда у нас будет три глобальных вида персонажей - главный герой (или их может быть несколько, если в рпг нет одного главного, либо если к главному герою могут присоединяться npc - по этому поводу я еще предполагаю написать потом, довольно интересно это будет реализовать именно в нижепреведенной модели классов), npc и враги. Все они имеют принципиальные различия, в том числе обрабатываются отдельными циклами (т.е. как у меня это сделано - сначала ходит главный герой, потом враги, потом npc). Но все они имеют какие-то общие, самые глобальный параметры, такие как координаты, хитпойнты, акшн пойнты и т.п. Более того они имеют общие функции, например для отображения их изображений на экран или изображения стрельбы. Все это у меня объединено в один класс hero:


class hero{
public:
char name[10];
int hp,ap;
int maxhp,maxap,maxmn;
int x,y,i,j;
int init_x,init_y; // the coords of initial position
int num,ground;
int mood;
int money;
int mana;

void picture_center(BITMAP*, BITMAP*);
void move(BITMAP*, BITMAP*, int,int);
void shooting(BITMAP*,BITMAP*,int,int,int,int,int);
void draw_hit (BITMAP*,int,int);
};



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

picture_center - отрисовывает экран игрового поля с данным персонажем посредине, в том числе обрабатываются случаи "заезда" экрана за игровое поле, там где ничего нет. Функцию приводить не буду, она довольно очевидная.

move - перемещает героя из его текущего положения в заданную точку.

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

draw_hit - при попадании рисует анимацию попадания (у меня довольно примитивную)

Конечно эти функции это не полный список того, что понадобится для игры, но их можно добавлять по мере надобности. Теперь перейдем к "наследникам" этого класса - npc, enemy и класс главного героя, который называется zubik (в моей игре так зовут главного героя, Dimouse там служит чем-то вроде Лорда Бритиша из Ультимы (заметьте, что я играл в Ультиму значительно позже того как начал создавать игру)). Итак, класс npc:


class npc: public hero{
public:
int inv[10];
};



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


class enemy: public hero{
public:
int inv[5];
int hit(hero*, int*,int);
int find_enemies(int*, int*);
void turn(BITMAP*, BITMAP*, int*,int*);
};



Инвентарь у врага по-меньше, зато уже реализованы функции hit (см. дальше, здесь она немного видоизменена, поскольку у врагов нет скиллов стрельбы и ближнего боя), find_enemies и turn я пока не сделал, только начал пару месяцев назад, но из-за нехватки времени забросил пока. Насколько я помню в turn будет весь алгоритм AI, т.е. цикл хода не будет загромождаться (и так уже сложно найти где что в main'е). И наконец главный класс (т.е. в том смысле что наиболее проработанный пока):


class zubik: public hero{
public:
int inv[10];
int current;
int exp,ztime;
int skill[10][2];

int choose(BITMAP*,BITMAP*,int*,int*,int);
int hit(hero*, int*);
void draw_stats(BITMAP*, BITMAP*);
void draw_inv(BITMAP*, BITMAP*);
};



Что мы имеем - инвентарь на 10 предметов (поскольку у Зубика будет домик, то все свои вещи он будет складывать в свой сундук (или сундуки) и с собой носить только самое необходимое), current - это число, которое говорит, что за предмет у нас в руках, exp - опыт, ztime - это был раньше прикол такой, что если использовать часы, то игра скажет сколько сейчас времени, в новой версии пока этого нет, скиллы - 10 скиллов, по два числа на каждый - первое это идентификатор скилла, второе - его "прокаченность". Скиллы у меня делятся на обязательные и приобретаемые. Обязательные - владение оружием дальнего боя и владение оружием ближнего боя, необязательные - все остальное - сила магии, харизма, открывание дверей и многое-многое другое. Заклинания я решил кстати делать тоже как предметы, так более унифицированно. Теперь функции:

choose - функция для выбора нужной нам клеточки - для стрельбы и всего остального, вообще очень важная функция, поэтому я приведу ее код (NB: все управление в моей игре идет с клавиатуры (как в ascii-rpg), по многим причинам, которые я не хочу сейчас обсуждать, так что эта функция вряд ли имеет какую-нибудь пользу с точки зрения содрать ее к себе, но с точки зрения понимания будет ясно зачем она и как ее можно изменить для выбора мышкой.

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

draw_stats - рисует параметры главного героя - сколько акшн-пойнтов осталось, сколько хитов, сколько денег и т.д.

draw_inv - рисует инвентарь.

Теперь рассмотрим функию choose, она довольно длинная, но интересная:


int zubik::choose(BITMAP* screen,BITMAP* screen2,int *x,int *y,int type){
//type==1 - choose enemy
//type==2 - choose npc
//type==3 - choose everything
//type==4 - dead enemy or npc // not implemented yet

int end=0;
int i=hero::i;
int j=hero::j;
int xtmp=zubik::x, ytmp=zubik::y;
int itmp=zubik::i, jtmp=zubik::j;
int range=0;
BITMAP *tmp=create_bitmap(20,20);
BITMAP *save=create_bitmap(800,600);

blit(screen,tmp,20*itmp,20*jtmp,0,0,20,20);
blit(screen,save,0,0,0,0,800,600);

circle(screen,itmp*20+10,jtmp*20+10,9,1);
line(screen,itmp*20+10,jtmp*20+1,itmp*20+10,jtmp*20+19,1);
line(screen,itmp*20+1,jtmp*20+10,itmp*20+19,jtmp*20+10,1);

int should_update=false;
int cur=zubik::inv[current];

while(!end){

        if (should_update)
        {
                should_update = false;
		clear(screen);
		//zubik::picture_center(sprites,back);
	}
        if (keypressed())
        {
            int k;
            k = readkey();
            
  	    k=k>>8;
            if(k==KEY_SPACE){
//Return to the game if this is right choice
		if(range==1){
		*x=xtmp;
		*y=ytmp;
		end=1;
		}
	    }
            if(k==KEY_UP&&ytmp>zubik::y-zubik::j){
		blit(tmp,screen,0,0,20*itmp,20*jtmp,20,20);
		ytmp--;
		jtmp--;
		blit(screen,tmp,20*itmp,20*jtmp,0,0,20,20);
		range=test(xtmp,ytmp,zubik::x,zubik::y,itm[cur]);
		if(range==1)range=cursorr(screen,itmp,jtmp,xtmp,ytmp,type); //can be shooted at
		else range=cursorr(screen,itmp,jtmp,xtmp,ytmp,0); //cannot be shooted at
		//if range=0 - can't shoot at this place, choose further
	    }
            if(k==KEY_DOWN&&ytmpzubik::x-zubik::i){
		blit(tmp,screen,0,0,20*itmp,20*jtmp,20,20);
		xtmp--;
		itmp--;
		blit(screen,tmp,20*itmp,20*jtmp,0,0,20,20);
		range=test(xtmp,ytmp,zubik::x,zubik::y,itm[cur]);
		if(range==1)range=cursorr(screen,itmp,jtmp,xtmp,ytmp,type);
		else range=cursorr(screen,itmp,jtmp,xtmp,ytmp,0);
	    }
            if(k==KEY_RIGHT&&xtmp


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

Рассмотрим еще как действует функция hit, в ней используется довольно много параметров, и будет ясно их назначение.


int zubik::hit(hero *enemy1, int *dam){

double rnd=(double)randomm()/RRAND_MAX;
int prob;
int inum=zubik::inv[current];

// for Zubik probability to hit is chosen from combat skills:

if(itm[inum].range==1) prob=zubik::skill[0][1];
//skill in close combat
else prob=zubik::skill[1][1];
//skill in ranged combat

rnd=100*(double)randomm()/RRAND_MAX;
zubik::ap=zubik::ap-itm[inum].ap;

if(rndhp=enemy1->hp-hit;
	if(enemy1->hp<=0){
		enemy1->mood=-1; //dead
		return 2;
	}
	return 1;
}
else
	return 0; //missed
}



Хотя первый параметр функции и называется enemy1, он принадлежит классу hero, т.е. теоретически можно бить кого угодно, что может пригодится при различных игровых ситуациях (например, "помутнение рассудка" у главного героя, когда он начинает бить своих же), если бы мы не использовали наследие от общего класса, то пришлось бы писать для каждого варианта - атаки врага, атаки npc и атаки себе подобного по функции. Сама же функия довольно проста - она вычисляет вероятность промаха исходя из скиллов по ближнему бою (если удар происходит вполтную) и дальнего боя (иначе) и высчитывает damage, который будет нанесен врагу, в случае попадания исходя из параметров оружия. После чего у врага отнимается от хитпойнтов это число и в случае, если у него хитпойнтов не остается, он записывается как мертвый (для этого я использовал параметр mood).

Конечно все это очень простая с первого взгляда система и ее можно видоизменить и сделать даже свой Fallout, что и предлагаю всем желающим проделать на досуге, это одновременно и занимательно, и полезно. То, чего здесь не хватает конструктивно - это взаимодействие (помимо функции hit), например обмен предметами и конечно AI, про который надо писать отдельную статью. В качестве же базовых классов для реализации AI и интеракции между собой и игроком я считаю что эти классы довольно хороши и удобны.

Еще один очень важный вопрос, который необходимо будет рассмотреть дальше - это взаимодействие игрока с игрой. Я имею в виду, что можно было убить врага, потом выйти с карты, зайти обратно и его там больше не было, аналогично с ловушками, квестами, с npc, чтобы они не говорили одно и тоже, даже после того, как игрок это уже сделал. Все это обычно реализуется довольно поверхностно, либо слишком грубо - когда мир слишком маленький, чтобы это было чем-то универсальным, либо когда мир огромный, очень поверхностно. Создание сложной системы интеракции с игрой - чтобы каждый npc имел множество фраз в зависимости от тех или иных игровых действий игрока, мог просто так заниматься своими делами - ходить по своим делам из одного места в другое. Также интеракция с игроком возможна с помощью сообщений, возникающих в определенном месте и/или при использовании какого-нибудь предмета на что-нибудь. Раньше подобные сообщения составляли очень большую часть РПГ. Пример - при подходе к стене появляется сообщение - "Вы подошли к стене. Трещины на ней подозрительно напоминают дверь. Возможно за этой стеной есть проход." При использовании какого-нибудь необходимого предмета дверь открывается, появляется сообщение: "За проходом темно, видны очертания сводов большой пещеры. Не похоже, чтобы пещера была обитаема." Когда игрок наступает на следующую клетку, срабатывает триггер на появление врагов и появляется соответствующее сообщение - "Из-за камней и углублений в стенах внезапно появились бандиты, которые тут же на вас набросились". После убийства последнего из них срабатывает еще один триггер на сообщение и появляется какой-нибудь предмет на земле. И так далее, фантазия тут уже безгранична. Все это я собираюсь реализовать с помощью единой унифицированной системы так мной называемых "событий", которые будут содержаться в карте (помимо самой карты и информации о врагах и npc на ней) и которые можно будет легко и просто добавлять и изменять (а что самое главное, информация о "событиях" будет записываться в память и в сейв игры при выходе из нее). Об этом в следующий раз!

Последнее изменение Sun, 24 Jun 2018 автором Dimouse


Назад в раздел Old-games Diskmag 5