Пишем свою игру: Введение В данной статье я бы хотел показать программирование/разработку игр на эльфах с помощью классов на примере всем известной игры "Snake", эта статья не для изучения использования различных функций в библиотеке. Сообщения в этой теме будут добавляться по мере написания, готовые исходники игры вы можете скачать в аттаче (исходники последней версии вы можете скачать тут), и разобраться самостоятельно если не хотите ждать продолжения. Для прочтения данной статьи вам нужно как минимум: прочитать: http://ru.wikipedia.org/wiki/Парадигма_программирования http://ru.wikipedia.org/wiki/Объектно-ориентированное_программирование другие уроки в этой теме знать азы Си/Си++ просмотреть и что-то там понять: http://perk11.info/svn/SE/classlib/cl.cpp http://perk11.info/svn/SE/classlib/cl.h Для того, чтобы начать программировать, нам нужно выделить обособленные друг от друга элементы игры, подумать как и где их выводить, а также примерно представить: как они будут взаимодействовать между собой и с пользователем.
В нашем примере я выделил 2 объекта: 1) Змейка (я ее представил в виде направленного набора квадратиков (частей,фрагментов), которые будут в процессе движения чередовать свои координаты от 1-ых к последним, также к ней могут добавляться новые части) 2) Еда (квадратик, который будет появляться на некоторое время вне занятых участках поля и который змейка может съесть) Разработка Класс "Змейка" Итак, начнем описание объекта "Змейка", для этого нужно выделить все ее свойства, а также методы для задания/изменения этих свойств. Код:
class snake { };
Подумаем, что из себя представляет часть змейки. Я ее представил в виде набора координат (x;y), то есть чтобы описать фрагмент змейки мы можем воспользоваться типом записи (структурой) XY:
тык:
Код:
typedef struct { int x,y; }XY;
Хранить координаты всех частей, на мой взгляд лучше в динамическом листе->добавляем поле LIST*list в класс
Код:
class snake { LIST*list; };
Змейка может двигаться в разных направлениях->добавляем поле char _way для хранения ее направления Также змейкой можно управлять (т.е. изменять ее свойство _way), для этого добавляем метод void setway(char way),
директива public означает, что полями/методами находящимися под ней можно будет воспользоваться "не изнутри" самого объекта. Продолжим: Змейка движется->добавляем метод void move() Змейке "важно знать до каких пор ей можно ползти"->добавляем свойства int _mx,_my для хранения максимальных координат К змейке могут добавляться новые части->добавляем метод void add(int x,int y) Змейку нужно вывести на экран->добавляем метод void out() (его мы оставим пустым, так как пока мы еще не решили как будем все это выводить) Змейка не может пересечь сама себя->добавляем метод bool check() для проверки общих координат в фрагментах
Во время игры нам понадобятся координаты головы и хвоста, чтобы фиксировать когда еда будет съедена и когда добавиться в конец->добавляем методы XY*getlast(); XY*getfirst(); само собой нам нужно как то задать начальные свойства змейки: координаты головы, ее направление, кол-во частей, максимальные координаты. Для этого добавим конструктор с перечисленными выше параметрами, ну а раз есть конструктор, то должен быть и деструктор у нас должно получиться что-то наподобие этого:
итак, сразу определимся, что в свойстве _way 0 означает вверх 1 - вправо 2 - вниз 3 - влево
начинаем описывать методы (подробно комментировать я их не буду, для чтения этой статьи, вы уже сами должны представлять алгоритмы для описания нужного вами действия)
тык:
Код:
snake::snake(int x,int y,int n,char way,int maxx,int maxy) { list=List_Create(); _way=way; _mx=maxx; _my=maxy; XY*elem=new XY; elem->x=x; elem->y=y; List_InsertFirst(list,elem);//голова змейки XY*nextelem; for(int i=1;i<n;i++)//добавляем части до n { nextelem=new XY; switch(way)//координаты след. фрагментов будут зависеть от координат предыдущего фрагмента а также от направления { case 0: nextelem->x=elem->x; nextelem->y=elem->y+1; break; case 1: nextelem->x=elem->x-1; nextelem->y=elem->y; break; case 2: nextelem->x=elem->x; nextelem->y=elem->y-1; break; case 3: nextelem->x=elem->x+1; nextelem->y=elem->y; break; } List_InsertLast(list,nextelem);//добавили часть elem=nextelem; } };
snake::~snake()//освобождаем что загадили { while(list->FirstFree) delete (XY*)List_RemoveAt(list,0); List_Destroy(list); };
void snake::move() { XY*elem1; XY*elem2; for(int i=list->FirstFree-1;i>0;i--)//меняемся координатами частей кроме первой { elem1=(XY*)List_Get(list,i); elem2=(XY*)List_Get(list,i-1); elem1->x=elem2->x; elem1->y=elem2->y; } elem1=(XY*)List_Get(list,0); switch(_way)//изменяем координату головы в зависимости от направления { case 0: elem1->y--; break; case 1: elem1->x++; break; case 2: elem1->y++; break; case 3: elem1->x--; break; } //если вышли за пределы границ if(elem1->x>_mx-1) elem1->x=0; if(elem1->x<0) elem1->x=_mx-1; if(elem1->y>_my-1) elem1->y=0; if(elem1->y<0) elem1->y=_my-1; };
bool snake::check() { XY*top_elem=(XY*)List_Get(list,0); XY*elem; for(int i=1;i<list->FirstFree;i++)//проверяем нет ли общих координат головы с каким-либо другим фрагментом { elem=(XY*)List_Get(list,i); if(top_elem->x==elem->x && top_elem->y==elem->y) return false; } return true; };
void snake::add(int x,int y)//тут все просто, создали->добавили в хвост { XY*elem=new XY; elem->x=x; elem->y=y; List_InsertLast(list,elem); };
void snake::out() { //оставляем его пустым };
void snake::setway(char way)//меняем направление (кроме противоположных) { if((_way-way)!=2 || (_way-way)!=-2) _way=way; };
XY*snake::getfirst()//координаты головы { return (XY*)List_Get(list,0); };
У "Еды" должны быть координаты->добавляем поля int _x,_y; "Еда" существыет не вечно->добавляем поле int timeleft; "Еда" может быть съедена, а может и нет->добавляем поле bool state; ну и прикрутим цвет для еды->добавляем поле COLOR _col; В процессе игры нам придется воспользоваться свойствами: _x,_y,timeleft,state,_col - поэтому добавляем методы: int getx(); int gety(); bool getstate(); int gettime(); int getcol();
Время, которое "Еда" существует должно уменьшаться->добавляем метод void timedown(int dtime); "Еду" может скушать "Змейка"->добавляем метод void eated(); Также нужен конструктор, который определит свойства объекта food(int x,int y,COLOR col);, добавим также деструктор ~food();. Он будет пустым И метод вывода void out(); Получается вот это:
тык:
Код:
class food { int _x,_y; int timeleft; bool state; COLOR _col; public: food(int x,int y,COLOR col); int getx(); int gety(); bool getstate(); void eated(); void timedown(int dtime); int gettime(); void out(); int getcol(); ~food(); };
Класс "Дисплей" Теперь пришло время подумать, а как выводить наши объекты. Мне хочется максимально приблизить вид игры к тетрису. Для этого я сделаю класс экран тетриса, через который мы будем выводить, что нам потребуется. Итак, дисплей состоит из пикселей, поэтому сначала нам нужно описать его. У пикселя должен быть свой цвет, а также статус вкл/выкл:
тык:
Код:
typedef struct { bool state; COLOR col; }PXL;
Для того, чтобы наш дисплей мог быть любого размера, я размещу пиксели в динамической памяти, сделав что-то наподобие двумерного массива (на самом деле указатель на массив указателей на массив пикселей) Поэтому добавляем поле PXL**_display; Так же нам надо хранить размеры экрана, а также размер пикселя в экранных пикселях телефона->добавляем поля: int _x,_y; int _pixel;
Также я хочу хранить изображения 8 пикселей разного цвета->добавляем поле GC*gc_pixel[8]; Для того, чтобы вкл/выкл пиксель и установить цвет добавляем метод void set(int x, int y, COLOR col,bool state); добавляем метод void clear(); , чтобы отключить все пиксели также нам нужно выводить свой дисплей на нужный нам GC -> void out(GC*gc); добавляем конструктор для установки размеров, а также инициализации изображения 8 пикселей ну и деструктор, чтобы подчистить за собой. Я бы также добавил функцию получения цвета по его номеру int getColor(COLOR col); Должно получиться:
тык:
Код:
class display { PXL**_display; int _x,_y; int _pixel; GC*gc_pixel[8]; int getColor(COLOR col); public: display(int x,int y,int pixel); void clear(); void set(int x,int y,COLOR col,bool state); void out(GC*gc); ~display(); };
Описываем методы:
тык:
Код:
void display::out(GC*gc)//рисуем наши пиксели на переданный нам GC { GC_DrawFRect(gc,0xFFFFFFFF,0,0,_x*_pixel,_y*_pixel);//фон GVI_GC gvi_gc_pixel; GVI_GC gvi_gc; CANVAS_Get_GviGC(gc->pcanvas ,&gvi_gc); for(int y=0;y<_y;y++) for(int x=0;x<_x;x++) if(_display[y][x].state) { CANVAS_Get_GviGC(gc_pixel[_display[y][x].col]->pcanvas ,&gvi_gc_pixel);//берем GVI пикселя нужного нам цвета GVI_BitBlt(gvi_gc,x*_pixel,y*_pixel,_pixel,_pixel,gvi_gc_pixel,0,0,204,0,0,0);//и рисуем его по его номерам в массиве }
int display::getColor(COLOR col) { switch(col) { case BLACK: return 0xFF000000; case BLUE: return 0xFF0000FF; case GREEN: return 0xFF00FF00; case LIGHTBLUE: return 0xFF00FFFF; case RED: return 0xFFFF0000; case PURPLE: return 0xFFFF00FF; case YELLOW: return 0xFFFFFF00; case WHITE: return 0xFFFFFFFF; default: return 0; } };
Теперь опишим наши методы out в классах snake и food
Для этого меняем параметры этого метода, должно получиться:
class food { int _x,_y; int timeleft; bool state; COLOR _col; public: food(int x,int y,COLOR col); int getx(); int gety(); bool getstate(); void eated(); void timedown(int dtime); int gettime(); void out(display*disp); int getcol(); ~food(); };
Класс "Игра" Опишем сам класс игры (CGame) итак в игре учавствуют: "Змейка", динамический лист с "Едой", дисплей. выделим некоторые состояния игры: "Конец игры", "Пауза" - а также методы возвращающие их значения. bool pause; bool isgameover; bool IsGameOver(); bool IsPause(); еще нужно добавить метод устанавливающий паузу void SetPause(); Игре нужно обновляться через нужное ей время для этого добавляем поле и метод возвращающий его значение int time; int GetRefreshTime(); В играх можно набирать очки int score; int GetScore(); На игру влияют нажатия клавиш void OnKey(int key,int mode); Игру нужно обновлять void OnMove(); Игру нужно выводить на экран void OnDraw(GC*gc); Добавляем конструктор для инициализации игры и деструктор для подчищения за собой. Должно получиться:
тык:
Код:
class CGame { bool pause; display*disp; snake*sh; int score; int time; bool isgameover; LIST*foodlist; public: CGame(); ~CGame(); int GetScore(); void OnKey(int key,int mode); void OnDraw(GC*gc); void OnMove(); int GetRefreshTime(); bool IsGameOver(); bool IsPause(); void SetPause(); };
Описываем методы:
тык:
Код:
bool CGame::IsPause() { return pause; };
void CGame::SetPause() { pause=true; };
bool CGame::IsGameOver() { return isgameover; };
int CGame::GetRefreshTime() { return time; };
void CGame::OnMove() { if(!pause)//не пауза ли { food*f; int i=0; XY*xy; bool add=false;//флаг для добавления части к змейке while(i<foodlist->FirstFree) { f=(food*)List_Get(foodlist,i); if(!f->getstate()) f->timedown(GetRefreshTime());//уменьшаем время еды if(!f->getstate() && f->gettime()<0) { delete (food*)List_RemoveAt(foodlist,i);//удаляем еду если она не съедена и время ее истекло continue; } xy=sh->getfirst(); if(f->getx()==xy->x && f->gety()==xy->y) f->eated();//проверяем съела ли змея еду xy=sh->getlast(); if(f->getx()==xy->x && f->gety()==xy->y) //проверяем дошла ли еда до конца змейки { score+=f->getcol();//добавляем очки в зависимости от цвета еды time-=15;//уменьшаем время обновления игры add=true;//устанавливаем флаг, о том что нужно добавить часть к змейке delete (food*)List_RemoveAt(foodlist,i);//удаляем еду из списка continue; } i++; } xy=sh->getlast();//берем координаты хвоста змейки int x=xy->x; int y=xy->y; sh->move();//двигаем змейку if(add) sh->add(x,y);//если нужно добавить часть - добавляем add=false; for(int i=0;i<foodlist->FirstFree;i++) if(!((food*)List_Get(foodlist,i))->getstate()) add=true; //проверяем есть ли не съеденная еда if((rand()%23)==6 || !add)//добавляем еду { x=rand()%20; y=rand()%25; f=new food(x,y,(COLOR)(1+rand()%7)); List_InsertLast(foodlist,f); }; if(!sh->check()) isgameover=true;//проверяем на конец игры } };
void CGame::OnDraw(GC*gc)//рисование { disp->clear();//очищаем дисплей sh->out(disp);//выводим "Змейку" на дисплей food*f; for(int i=0;i<foodlist->FirstFree;i++) { f=(food*)List_Get(foodlist,i); f->out(disp);//выводим все объекты "Еда" }; disp->out(gc);//выводим дисплей на экран };
Делаем свой DispObj И так мы создали объект игры, но как пользователь будет взаимодействовать с ним? Ответ - через DispObj. В cl.h уже есть описанный класс CDispObjT, мы создадим свой объект на основе этого, переопределив нужные нам методы, а именно: onDraw, onKey, onRefresh, onDestroy, onCreate, getName Добавить поля CGame*game и bool isondraw получаем:
int CGameDisp::onCreate() { isondraw=false; game=new CGame();//создаем игру InvalidateRect(NULL);//обновляем экран SetRefreshTimer(game->GetRefreshTime());//устанавливаем время из игры для обновления return 1; };
void CGameDisp::onDestroy() { delete game;//удаляем игру };
void CGameDisp::onKey(int key,int,int repeat,int type) { if(!game->IsGameOver()) game->OnKey(key,type);//передаем нажатия игре если не конец игры else { if(key==KEY_DIGITAL_0+5)//если конец перезапускаем игру { delete game; game=new CGame(); SetRefreshTimer(game->GetRefreshTime()); } } if(!isondraw) InvalidateRect(NULL); };
void CGameDisp::onDraw(int a,int b,int c) { isondraw=true; GC*gc=get_DisplayGC(); GC_DrawFRect(gc,0xFF000000,0,0,Display_GetWidth(0),Display_GetHeight(0));//темный фон game->OnDraw(gc);//выводим игру if(!game->IsGameOver()) GC_DrawFRect(gc,0xAA000000,0,0,Display_GetWidth(0),Display_GetHeight(0)); isondraw=false; };
Создаем нашу книгу Опишем нашу книгу (CMyBook), она будет наследовать все свои свойства от класса CBook в cl.h Добавим страницу книги base_page, и методы для нее static int ShowAuthorInfo(CBookBase**bm_book,CMyBook*mbk); static int TerminateElf(CBookBase**bm_book,CMyBook*mbk); Добавляем CGuiBase*gui; Добавляем конструктор и деструктор
тык:
Код:
CMyBook:public CBook { static int ShowAuthorInfo(CBookBase**bm_book,CMyBook*mbk); static int TerminateElf(CBookBase**bm_book,CMyBook*mbk); CGuiBase*gui; virtual ~CMyBook(); DECLARE_PAGE_DESC_MEMBER(base_page) public: CMyBook(); };
CMyBook::CMyBook()//создание книги :CBook("Snake",&base_page) { gui=new CGuiT<CGameDisp>(this,0);//создание нашего гуя gui->SetStyle(4); gui->SetTitleType(1); gui->SoftKeys_Hide(); gui->Show(); };
ну и опишем функцию int main();
Код:
int main() { new CMyBook(); return 0; };
Первые результаты итак у меня получилось 16 файлов в папке с проектом: CGame.c CGame.h CGameDisp.c CGameDisp.h CMyBook.c CMyBook.h display.c display.h food.c food.h gametypes.h main.c rand.c rand.h snake.c snake.h
в проект также надо включить файл cl.cpp. Все лежит в аттаче этого поста. если проект не компилируется, попробуйте убрать строчку #include "..\deleaker\mem2.h" из cl.cpp
Эльф готов. На разработку эльфа ушло от силы 2 часа, на урок весь день
Ну и на последок хотел бы вам дать что-то наподобие задания: 1). Исправить баг типа: змейка идет вверх, я жму влево а потом вниз и происходит конец игры 2). Добавить ускорение на джойстик/5 3). Добавить вывод очков 4). Изменить появление еды так, чтобы она не могла появляться на змейке и на другой еде
этих недочетов нет в исходниках из первого поста) можете посмотреть как там все это реализовано Вопросы/предложение/выполнение задания можете оставлять в этой теме Удачи в программировании! (с) MoneyMasteR aka mmcorp
Закрытие книги при нажатии кнопки Итак продолжим улучшать игру. В этом сообщении я буду добавлять/исправлять что-то в игре, и описывать. Чтобы добавить выход по долгом нажатию на "С" нужно: изменить base_page в CMyBook.c
void CGameDisp::onKey(int key,int,int repeat,int type) { if(key==KEY_DEL && type==KBD_LONG_PRESS) GetGUI()->GetBook()->UI_Event(TERMINATE_SESSION_EVENT);//выход if(!game->IsGameOver()) game->OnKey(key,type);//передаем нажатия игре если не конец игры else { if(key==KEY_DIGITAL_0+5)//если конец перезапускаем игру { delete game; game=new CGame(); SetRefreshTimer(game->GetRefreshTime()); } } if(!isondraw) InvalidateRect(NULL); };
Появление "Еды" только в пустых местах Чтобы добавить проверку на занятость данного места поля добавляем метод bool in(int x, int y) в объект snake (snake.h) и описываем его (snake.c):
теперь чуть модифицируем метод OnMove объекта CGame
тык:
Код:
void CGame::OnMove() { if(!pause)//не пауза ли { food*f; int i=0; XY*xy; bool add=false;//флаг для добавления части к змейке while(i<foodlist->FirstFree) { f=(food*)List_Get(foodlist,i); if(!f->getstate()) f->timedown(GetRefreshTime());//уменьшаем время еды if(!f->getstate() && f->gettime()<0) { delete (food*)List_RemoveAt(foodlist,i);//удаляем еду если она не съедена и время ее истекло continue; } xy=sh->getfirst(); if(f->getx()==xy->x && f->gety()==xy->y) f->eated();//проверяем съела ли змея еду xy=sh->getlast(); if(f->getx()==xy->x && f->gety()==xy->y) //проверяем дошла ли еда до конца змейки { score+=f->getcol();//добавляем очки в зависимости от цвета еды time-=15;//уменьшаем время обновления игры add=true;//устанавливаем флаг, о том что нужно добавить часть к змейке delete (food*)List_RemoveAt(foodlist,i);//удаляем еду из списка continue; } i++; } xy=sh->getlast();//берем координаты хвоста змейки int x=xy->x; int y=xy->y; sh->move();//двигаем змейку if(add) sh->add(x,y);//если нужно добавить часть - добавляем add=false; for(int i=0;i<foodlist->FirstFree;i++) //проверяем есть ли не съеденная еда { f=(food*)List_Get(foodlist,i); if(!f->getstate()) { add=true; break; } } if(((rand()%23)==6) || !add)//добавляем еду { x=rand()%20; y=rand()%25; add=true; add=sh->in(x,y);//проверяем не занято ли поле змейкой for(int i=0;i<foodlist->FirstFree;i++)//едой { if(add) break; f=(food*)List_Get(foodlist,i); add=add||f->in(x,y); }; if(!add)//если не занято то добовляем { f=new food(x,y,(COLOR)(1+rand()%7)); List_InsertLast(foodlist,f); } }; if(!sh->check()) isgameover=true;//проверяем на конец игры } };
Добавление ускорения добавляем ускорение на джойстик/5 Для этого добавляем поле bool isfast в CGame, модифицируем методы GetRefreshTime,OnKey и конструктор объекта CGame:
тык:
Код:
int CGame::GetRefreshTime() { if(isfast) return time/12; return time; };
CGame::CGame() { DATETIME dt; REQUEST_DATEANDTIME_GET(0,&dt); srand( (dt.time.sec<<16) | (dt.time.min<<8) | (dt.time.hour) );//задаем комбинацию для рандома foodlist=List_Create();//создаем лист для еды score=0;//кол-во очков в начале игры time=1000;//время обновления isfast=false; isgameover=false; pause=false; int pxl; int pxl1=Display_GetHeight(0)/25; int pxl2=Display_GetWidth(0)/20; if(pxl1>pxl2) pxl=pxl2; else pxl=pxl1; if(pxl<5) pxl=5; disp=new display(20,25,pxl);//создаем дисплей sh=new snake(10,12,3,0,20,25);//создаем змейку };
Исправление бага с направлением "Змейки" Исправляем баг: MoneyMasteR писал:
1). Исправить баг типа: змейка идет вверх, я жму влево а потом вниз и происходит конец игры
И так, это происходит из-за того, что за одно движение направление змейки может меняться несколько раз, надо сделать так, чтобы направление менялось только один раз, перед самим движением, для этого нам потребуется в класс snake добавить поле char _nextway куда будем заносить следующее направление, 0xFF будет значить, что нового направления не было. Менять _way на _nextway мы будем в методе move() объекта snake перед перестановкой координат, так же в конструктор нужно добавить начальное значение _nextway, и переделать метод setway():
void snake::move() { XY*elem1; XY*elem2; if((_nextway-_way)!=2 && (_nextway-_way)!=-2 && _nextway!=0xFF) _way=_nextway;//тут и меняем направление _nextway=0xFF; for(int i=list->FirstFree-1;i>0;i--) { elem1=(XY*)List_Get(list,i); elem2=(XY*)List_Get(list,i-1); elem1->x=elem2->x; elem1->y=elem2->y; } elem1=(XY*)List_Get(list,0); switch(_way) { case 0: elem1->y--; break; case 1: elem1->x++; break; case 2: elem1->y++; break; case 3: elem1->x--; break; } if(elem1->x>_mx-1) elem1->x=0; if(elem1->x<0) elem1->x=_mx-1; if(elem1->y>_my-1) elem1->y=0; if(elem1->y<0) elem1->y=_my-1; };
void snake::setway(char way)//следующее направление { _nextway=way; };
Добавляем вывод очков на экран Для вывода очков я хочу использовать стандартные изображения цифорок из прошивки телефона: MIDI_COMP_BL_NBR_0_ICN MIDI_COMP_BL_NBR_1_ICN MIDI_COMP_BL_NBR_2_ICN MIDI_COMP_BL_NBR_3_ICN MIDI_COMP_BL_NBR_4_ICN MIDI_COMP_BL_NBR_5_ICN MIDI_COMP_BL_NBR_6_ICN MIDI_COMP_BL_NBR_7_ICN MIDI_COMP_BL_NBR_8_ICN MIDI_COMP_BL_NBR_9_ICN Добавляем в объект CGameDisp: 1) поле wchar_t numimage[10] здесь будем хранить ид этих изображений 2) методы: void DrawNumeral(GC*gc,int &x,int &y,int numeral);//выводит цифру void DrawNumber(GC*gc,int x,int y,int number);//выводит число 3) изменяем метод onCreate, чтобы инициализировать поле numimage 4) изменяем метод onDraw
Вы не можете начинать темы. Вы не можете редактировать свои сообщения. Вы не можете создавать опросы. Вы не можете вкладывать файлы в сообщения. Вы не можете отвечать на сообщения. Вы не можете удалять свои сообщения. Вы не можете голосовать.