Пишем игры на C++, Часть 1/3 — Написание мини-фреймворка
Пишем игры на C++, Часть 3/3 — Классика жанра
Здравствуй!
Поздравляю вас, если вы прочитали первый урок! Он достаточно большой. Обещаю, что тут кода будеть меньше, а результатов больше :)
О чем эта часть?
- Мы попытаемся постичь state-based programming, с помощью которого новые уровни и меню делаются очень легко
В следующем посте будут натуральные игры :)
2.1. Состояния
Теперь неплохо бы понять, из чего, собственно, состоит игра.
Допустим, у нас есть игра, где много менюшек, уровней и прочих «состояний». Как можно с ними взаимодействовать? Понятно, что код типа:
void Update()
{
switch(state)
{
case State::MENU:
// 100 строк
case State::SETTINGS:
// 200 строк
case State::LEVEL1:
// Страшно считать
}
}
Вызывает лютый незачет в плане удобства.
Как насчет того, чтобы каждому состоянию сделать своего наследника от какого-нибудь класса с названием, допустим, Screen, и использовать его в Game?
Создайте Screen.h
#ifndef SCREEN_H
#define SCREEN_H
#include "Incl.h"
#include "Game.h"
class Game;
class Screen
{
protected:
Game* game;
public:
void SetController(Game* game);
virtual void Start();
virtual void Update();
virtual void Destroy();
};
#endif
Этот класс имеет экземпляр Game, откуда наследники берут указатели на Graphics и Input
Его виртуальные функции для наследников:
- Start — вызов каждый раз при старте (назначение состоянием)
- Update — вызов каждый цикл
- Destroy — вызов по уничтожению (завершение работы программы либо назначение другого состояния)
Screen.cpp
#include "Screen.h"
void Screen::SetController(Game* game)
{
this->game = game;
}
void Screen::Start()
{
}
void Screen::Update()
{
}
void Screen::Destroy()
{
}
Обновляем Game.h и Game.cpp
#ifndef _GAME_H_
#define _GAME_H_
#include "Project.h"
#include "Graphics.h"
class Graphics;
#include "Input.h"
class Input;
#include "Screen.h"
class Screen;
class Game
{
private:
bool run;
Graphics* graphics;
Input* input;
Screen* screen;
public:
Game();
int Execute(Screen* startscreen, int width, int height);
Graphics* GetGraphics();
Input* GetInput();
Screen* GetScreen();
void SetScreen(Screen* screen);
void Exit();
};
#endif
В класс Game включается объект Screen и изменяется функция Execute, куда из main.cpp передаем объект своего наследника Screen
Game.cpp
#include "Game.h"
Game::Game()
{
run = true;
}
int Game::Execute(Screen* startscreen, int width, int height)
{
graphics = new Graphics(width,height);
input = new Input();
screen = startscreen;
screen->SetController(this);
this->screen->Start();
while(run)
{
input->Update();
screen->Update();
}
screen->Destroy();
delete graphics;
delete input;
delete screen;
SDL_Quit();
return 0;
}
Graphics* Game::GetGraphics()
{
return graphics;
}
Input* Game::GetInput()
{
return input;
}
Screen* Game::GetScreen()
{
return screen;
}
void Game::SetScreen(Screen* screen)
{
this->screen->Destroy();
delete this->screen;
this->screen = screen;
this->screen->SetController(this);
this->screen->Start();
}
void Game::Exit()
{
run = false;
}
Важным изменениям подвергается метод Execute — он обрабатывает текущее состояние
SetScreen устанавливает новое состояние, сбрасывая старое.
GetScreen, на мой взгляд, почти бесполезен — разве что для перезагрузки уровня таким макаром
SetScreen(GetScreen());
Но глупость заново загружать все ресурсы. В общем, решайте сами :)
2.2. Компилировать! Компилировать!
Поиграемся?
Откройте файл main.cpp и измените его до такого состояния:
#include "Project.h"
class MyScreen : public Screen
{
public:
void Start()
{
MessageBox(0,"Hello, HabraHabr!","Message",MB_OK);
}
};
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
Game game;
return game.Execute(new MyScreen(),500,350);
}
Все, что он делает — выводит стандартное Windows-сообщение.
Внимательный пользователь обратит внимание на то, что окно не закрывается по клику на красный крестик, и заглавие окна тоже не помешало бы убрать. Нет проблем — принимайте работу:
#include "Project.h"
class MyScreen : public Screen
{
private:
Input* input;
public:
void Start()
{
input = game->GetInput();
SDL_WM_SetCaption("Hello, HabraHabr!",0);
MessageBox(0,"Hello, HabraHabr!","Message",MB_OK);
}
void Update()
{
if(input->IsKeyDown('w') || input->IsExit())
game->Exit();
}
};
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
Game game;
return game.Execute(new MyScreen(),500,350);
}
Мы не хотим работать с черным окном, давайте что-нибудь нарисуем?
#include "Project.h"
class MyScreen : public Screen
{
private:
Input* input;
Graphics* graphics;
Image* test;
public:
void Start()
{
input = game->GetInput();
graphics = game->GetGraphics();
SDL_WM_SetCaption("Hello, HabraHabr!",0);
test = graphics->NewImage("habr.bmp");
}
void Update()
{
if(input->IsExit())
game->Exit();
graphics->DrawImage(test,0,0);
graphics->Flip();
}
};
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
Game game;
return game.Execute(new MyScreen(),300,225);
}
Более сведущий в плане производительности программист сразу поймет, что бессмысленно каждый цикл рисовать картинки, если они не меняют свое местоположение. Действительно — строчки
graphics->DrawImage(test,0,0);
graphics->Flip();
Надо переместить из Update() в конец Start()
2.3. Итоги
Я надеюсь, вы были впечатлены и узнали много нового :)
Тогда переходите к третьему уроку без сомнений
По всем вопросам обращайтесь в ЛС, а если вам не повезло быть зарегистрированным на хабре, пишите на мейл izarizar@mail.ru
Автор: Izaron