Пишем игры на C++, Часть 2/3 — State-based программирование

  • Tutorial
Пишем игры на 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 "Project.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

  • +33
  • 66,7k
  • 7
Поделиться публикацией

Комментарии 7

    +2
    По всем вопросам обращайтесь в ЛС

    А комменты к посту, не?
      –3
      А комменты, как ни странно, в основном для комментов, а не вопросов, почему что-то не получилось :)
        +5
        Вопросы лучше размещать именно в комментариях, если это конечно не вопросы личного характера. Так как один и тот же вопрос может возникнуть у многих читателей, а писать вопрос по теме, да еще и в ЛС, будет не каждый.
        Да и статья с вопросами и ответам в комментариях выглядит более расширенной.
      0
      switch(state)
      {
      case State::MENU:
      // 100 строк
      case State::SETTINGS:
      // 200 строк
      case State::LEVEL1:
      // Страшно считать
      }

      После этого отрывка вы рассказываете об элементарном ООП. Для толкового программиста, описываемая схема(разбиение задачи на подзадачи, классы) должна быть как заповедь, а не городить индусский код по 200 строк в свитч-кейсе.
        0
        Да ладно, небольшая выдержка из главы Фаулера «Замена условного оператора полиморфизмом» с еще одним примером — не такая уж большая беда.
        0
        в Game надо иметь std::vector<Screen*> и соотвественно функции добавления и удаления стейтов оттуда
        это даст возможность реализовать в игре мультиоконность в том числе с модальными окнами
          0
          Более сведущий в плане производительности программист сразу поймет, что бессмысленно каждый цикл рисовать картинки, если они не меняют свое местоположение....

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

          главный игровой цикл примерно так:
          for(;;)
          	{
          		
          		if ( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE ) )
          		{ 
          			if (msg.message == WM_QUIT)
          				break;
          
          			TranslateMessage( &msg );
          
          			DispatchMessage(&msg);
          
          			continue;
          		}
          
          			render();
          
          			dt = timeGetTime() - t0;
          
          			do 
          			{ 
          				Sleep(1);
          				dt = timeGetTime() - t0;
          			} 
          			while( dt < 10 );
          
          			fDeltaTime	= dt/1000.0f;
          
          			t0 = timeGetTime();
          
          			update( static_cast<float>( fDeltaTime ) );
          
          			ai( static_cast<float>( fDeltaTime ) );		
          	}
          
          

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

          Самое читаемое