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

в 11:56, , рубрики: c plus plus, c++, game development, sdl, метки: , ,

Пишем игры на C++, Часть 1/3 — Написание мини-фреймворка
Пишем игры на C++, Часть 3/3 — Классика жанра

Здравствуй!

Поздравляю вас, если вы прочитали первый урок! Он достаточно большой. Обещаю, что тут кода будеть меньше, а результатов больше :)

О чем эта часть?

  • Мы попытаемся постичь state-based programming, с помощью которого новые уровни и меню делаются очень легко

В следующем посте будут натуральные игры :)

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

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);
}

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

Более сведущий в плане производительности программист сразу поймет, что бессмысленно каждый цикл рисовать картинки, если они не меняют свое местоположение. Действительно — строчки

		graphics->DrawImage(test,0,0);
		graphics->Flip();

Надо переместить из Update() в конец Start()

2.3. Итоги

Я надеюсь, вы были впечатлены и узнали много нового :)
Тогда переходите к третьему уроку без сомнений

По всем вопросам обращайтесь в ЛС, а если вам не повезло быть зарегистрированным на хабре, пишите на мейл izarizar@mail.ru

Автор: Izaron

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js