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

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

Здравствуй, Хабрахабр!

На хабре не очень много уроков по созданию игр, почему бы не поддержать отечественных девелоперов?
Представляю вам свои уроки, которые учат создавать игры на C++ с использованием SDL!

Что нужно знать


  • Хотя бы начальные знания C++ (использовать будем Visual Studio)
  • Терпение


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


  • Мы создадим каркас для всех игр, в качестве отрисовщика будем использовать SDL. Это библиотека для графики.


В следующих постах будет больше экшена, это лишь подготовка :)



Почему SDL?


Я выбрал эту библиотеку как наиболее легкую и быструю в освоении. Действительно, от первой прочитанной статьи по OpenGL или DirectX до стотысячного переиздания змейки пройдет немало времени.

Теперь можно стартовать.

1.1. Начало начал


Скачиваем SDL с официального сайта.
Создаем проект Win32 в Visual Studio, подключаем lib'ы и includ'ы SDL (если вы не умеете этого делать, то гугл вам в помощь!)

Также необходимо использовать многобайтную кодировку символов. Для этого идем в Проект->Свойства->Свойства конфигурации->Набор символов->Использовать многобайтную кодировку.

Создаем файл main.cpp
#include <Windows.h>

int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
	return 0;
}

Пока что он ничего не делает.

Царь и бог каркаса — класс Game
Game.h
#ifndef _GAME_H_
#define _GAME_H_

class Game
{
private:
	bool run;

public:
	Game();
	int Execute();

	void Exit();
};

#endif

Game.cpp
#include "Game.h"

Game::Game()
{
	run = true;
}

int Game::Execute()
{
	while(run);
	return 0;
}

void Game::Exit()
{
	run = false;
}


Создаем файл Project.h, он нам очень пригодится в будущем
#ifndef _PROJECT_H_
#define _PROJECT_H_

#include <Windows.h>

#include "Game.h"

#endif


Изменяем main.cpp
#include "Project.h"

int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
	Game game;
	return game.Execute();
}


Уже чуточку получше, но все равно как-то не густо.

1.2. Графика


Создаем аж 2 класса — Graphics для отрисовки графики и Image для отрисовки картинок

Graphics.h
#ifndef _GRAPHICS_H_
#define _GRAPHICS_H_

#include "Project.h"

#include "Image.h"
class Image;

class Graphics
{
private:
	SDL_Surface* Screen;

public:
	Graphics(int width, int height);

	Image* NewImage(char* file);
	Image* NewImage(char* file, int r, int g, int b);
	bool DrawImage(Image* img, int x, int y);
	bool DrawImage(Image* img, int x, int y, int startX, int startY, int endX, int endY);

	void Flip();
};

#endif


Image.h
#ifndef _IMAGE_H
#define _IMAGE_H

#include "Project.h"

class Image
{
private:
	SDL_Surface* surf;
public:
	friend class Graphics;

	int GetWidth();
	int GetHeight();
};

#endif


Изменяем Project.h
#ifndef _PROJECT_H_
#define _PROJECT_H_

#pragma comment(lib,"SDL.lib")

#include <Windows.h>
#include <SDL.h>

#include "Game.h"
#include "Graphics.h"
#include "Image.h"

#endif


SDL_Surface — класс из SDL для хранения информации об картинке
Рассмотрим Graphics
NewImage — есть 2 варианта загрузки картинки. Первый вариант просто грузит картинку, а второй после этого еще и дает прозрачность картинке. Если у нас красный фон в картинке, то вводим r=255,g=0,b=0
DrawImage — тоже 2 варианта отрисовки картинки. Первый рисует всю картинку целиком, второй только часть картинки. startX, startY — координаты начала части картинки. endX, endY — конечные координаты части картинки. Этот метод рисования применяется, если используются атласы картинок. Вот пример атласа:

image
(изображение взято из веб-ресурса interesnoe.info)

Рассмотрим Image
Он просто держит свой сурфейс и дает право доступа к своим закрытым членам классу Graphics, а он изменяет сурфейс.
По сути, это обертка над SDL_Surface. Также он дает размер картинки

Graphics.cpp
#include "Graphics.h"

Graphics::Graphics(int width, int height)
{
	SDL_Init(SDL_INIT_EVERYTHING);
	Screen = SDL_SetVideoMode(width,height,32,SDL_HWSURFACE|SDL_DOUBLEBUF);
}

Image* Graphics::NewImage(char* file)
{
	Image* image = new Image();
	image->surf = SDL_DisplayFormat(SDL_LoadBMP(file));

	return image;
}

Image* Graphics::NewImage(char* file, int r, int g, int b)
{
	Image* image = new Image();
	image->surf = SDL_DisplayFormat(SDL_LoadBMP(file));

	SDL_SetColorKey(image->surf, SDL_SRCCOLORKEY | SDL_RLEACCEL,
		SDL_MapRGB(image->surf->format, r, g, b));

	return image;
}

bool Graphics::DrawImage(Image* img, int x, int y)
{
	if(Screen == NULL || img->surf == NULL)
        return false;
 
    SDL_Rect Area;
    Area.x = x;
    Area.y = y;
 
    SDL_BlitSurface(img->surf, NULL, Screen, &Area);
 
	return true;
}

bool Graphics::DrawImage(Image* img, int x, int y, int startX, int startY, int endX, int endY)
{
	if(Screen == NULL || img->surf == NULL)
        return false;
 
    SDL_Rect Area;
    Area.x = x;
    Area.y = y;

    SDL_Rect SrcArea;
	SrcArea.x = startX;
	SrcArea.y = startY;
	SrcArea.w = endX;
	SrcArea.h = endY;

	SDL_BlitSurface(img->surf, &SrcArea, Screen, &Area);

	return true;
}

void Graphics::Flip()
{
	SDL_Flip(Screen);
	SDL_FillRect(Screen,NULL, 0x000000);
}

В конструкторе инициализируется SDL и создается экран.
Функция Flip должна вызываться каждый раз после отрисовки картинок, она представляет получившееся на экран и чистит экран в черный цвет для дальнешней отрисовки.
Остальные функции малоинтересны, рекомендую разобраться в них самому

Image.cpp
#include "Image.h"

int Image::GetWidth()
{
	return surf->w;
}

int Image::GetHeight()
{
	return surf->h;
}

Нет, вы все правильно делаете, этот файл и должен быть таким :)

Надо изменить Game.h, Game.cpp и main.cpp
Game.h
#ifndef _GAME_H_
#define _GAME_H_

#include "Project.h"
class Graphics;

class Game
{
private:
	bool run;

	Graphics* graphics;

public:
	Game();
	int Execute(int width, int height);

	void Exit();
};

#endif

Тут мы добавляем указатель на Graphics и в Execute добавляем размер экрана

Game.cpp
#include "Game.h"

Game::Game()
{
	run = true;
}

int Game::Execute(int width, int height)
{
	graphics = new Graphics(width,height);

	while(run);
	
	SDL_Quit();
	return 0;
}

void Game::Exit()
{
	run = false;
}


Ничего особенного, разве что не пропустите функцию SDL_Quit для очистки SDL

main.cpp
#include "Project.h"

int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
{
	Game game;
	return game.Execute(500,350);
}

Тут мы создаем экран размером 500 на 350.

1.3. Ввод


Надо поработать со вводом с клавиатуры

Создаем Input.h
#ifndef _INPUT_H_
#define _INPUT_H_

#include "Project.h"

class Input
{
private:
	SDL_Event evt;

public:
    void Update();

	bool IsMouseButtonDown(byte key);
	bool IsMouseButtonUp(byte key);
	POINT GetButtonDownCoords();

	bool IsKeyDown(byte key);
	bool IsKeyUp(byte key);
	byte GetPressedKey();

	bool IsExit();
};

#endif

SDL_Event — класс какого-нибудь события, его мы держим в Input'е для того, чтобы не создавать объект этого класса каждый цикл
Ниже расположены методы, не представляющие особого интереса. Примечание: методы с окончанием Down вызываются, когда клавиша была нажата, а с окончанием Up — когда опущена.

Input.cpp
#include "Input.h"

void Input::Update()
{
	while(SDL_PollEvent(&evt));
}

bool Input::IsMouseButtonDown(byte key)
{
	if(evt.type == SDL_MOUSEBUTTONDOWN)
		if(evt.button.button == key)
			return true;
	return false;
}

bool Input::IsMouseButtonUp(byte key)
{
	if(evt.type == SDL_MOUSEBUTTONUP)
		if(evt.button.button == key)
			return true;
	return false;
}

POINT Input::GetButtonDownCoords()
{
	POINT point;
	point.x = evt.button.x;
	point.y = evt.button.y;

	return point;
}

bool Input::IsKeyDown(byte key)
{
	return (evt.type == SDL_KEYDOWN && evt.key.keysym.sym == key);
}

bool Input::IsKeyUp(byte key)
{
	return (evt.type == SDL_KEYUP && evt.key.keysym.sym == key);
}

byte Input::GetPressedKey()
{
	return evt.key.keysym.sym;
}

bool Input::IsExit()
{
	return (evt.type == SDL_QUIT);
}

Здесь мы обрабатываем наш объект событий в функции Update, а остальные функции просто проверяют тип события и его значения.

Изменяем теперь Game.h и Game.cpp
#ifndef _GAME_H_
#define _GAME_H_

#include "Project.h"

#include "Graphics.h"
class Graphics;
#include "Input.h"
class Input;

class Game
{
private:
	bool run;

	Graphics* graphics;
	Input* input;

public:
	Game();
	int Execute(int width, int height);

	Graphics* GetGraphics();
	Input* GetInput();

	void Exit();
};

#endif

Как видно, мы добавили указатель на Input и создали методы-возвращатели Graphics и Input

Game.cpp
#include "Game.h"

Game::Game()
{
	run = true;
}

int Game::Execute(int width, int height)
{
	graphics = new Graphics(width,height);
	input = new Input();

	while(run)
	{
		input->Update();
	}
	
	delete graphics;
	delete input;

	SDL_Quit();
	return 0;
}

Graphics* Game::GetGraphics()
{
	return graphics;
}

Input* Game::GetInput()
{
	return input;
}

void Game::Exit()
{
	run = false;
}


1.4. Итоги


Это был первый урок. Если вы дошли до этого места, я вас поздравляю! У вас есть воля, присущая программисту :) Смотрите ссылки в начале статьи на последующие уроки для того, чтобы узнать еще много нового!

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

Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 17

    +1
    Вопрос по пункту «Почему SDL?»: а почему не HGE? Это уже готовый движок, а так же он еще проще и быстрее в освоении, после чего можно опускать гору кода и приступать сразу ко второй части. И просьба автору убрать код под кат. Спасибо.
      +3
      SDL портирован под Android/iOS, и он не умер 5 лет назад :)
        –12
        А можно вас попросить поподробнее об этом написать? А еще бы круче статейку image
          +1
          HGE часто рекомендуют как С++ 2D Engine для начинающих на форумах по геймдеву. Для статьи смысла особого нет, так как он хорошо документирован, есть демки и собственный форум. Основан на DirectX 9, поэтому только под винду. Но нет шейдеров. Встроенный звук на bass.dll не подходит для коммерческих проектов из-за лицензии. А так прост и красиво сделан. Больше подробностей смотрите на оффициальном сайте.
        0
        Еще есть SFML, чуть приятнее чем SDL.
          0
          а чем приятнее? В чем отличия идут?
            0
            Говорят, что SDL более стабилен на других платформах. Но у SFML приятный и простой ООП код. Каких-то радикальных отличий, кажется, нет. Т.е. если для себя поиграться, то я бы выбрал SFML, если на production, то SDL.
              0
              Раньше пробовал написать простое графическое приложение VC++ с очень плохим примером, который порекоммендовал один знакомый «специалист». Пример работал, но сложность этого примера не позволяла совершенствоваться дальше и интерс сразу был похоронен в горе рутинного обязателъного кода.

              Эту статью открыл чисто из любопытства и поразила именно простота кода. Куда уж проще.
                0
                Такое же есть в примерах DirectX11 от Microsoft, увидел интересную фичу — долго разбираешь кучу классов и полуиндусский код. Насколько помню — никогда не хватало силы воли разбирать всю ненужную фигню, которую потом все равно никак не запихаешь в проект тупо из-за отсутствия расширяемости

                P.S. Нифига себе, земляк из одного города 0_о
          +1
          18 March 2008 — последний релиз HGE 1.81
          11 August 2013 — SDL 2.0.0
          SDL поддерживается Valve, и на нем сделано множество прекрасных современных игр, например любимая FTL или Mark of Ninja. И да, это скорее библиотека а не фреймворк или движок, который абсолютно ничего не навязывает а только дает средства для кросс платформенного импорта текстур, обработки управления, создания графических контекстов и т.п. Лично мне больше нравится SFML, но SDL очень достойный выбор
          +7
          необходимо использовать многобайтную кодировку символов

          шел 2013 год…
            +10
            Статья должна называться «Смотрите, я про SDL узнал». На туториал не тянет в первую очередь по качеству.
            Вы видимо SDL1.2 используете? Очень рекомендую посмотреть в сторону SDL2. Аппаратное ускорение графики и переработанный интерфейс больше подходят для игр.
            #include <Windows.h>
            
            int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int)
            {
                return 0;
            }
            

            Прям вредный совет какой-то. SDL кроссплатформенна, поэтому и код не должен быть зависим от винды. Если бы вы использовали исключительно виндовые вещи, тогда всё понятно, но у вас один main (MessageBox не в счёт). Создайте консольное приложение и main будет стандартным, а лучше использовать SDL_Main для SDL1.2. И консолька не время разработки будет полезна. В SDL2 main надо писать обычный сишный. Макросы всё правильно разберут на каждой платформе.
              +1
              Полностью поддерживаю, ужасная статья про одну из лучших библиотек! Мне вообще интересно, что сам автор думает про его цикл обработки событий? ))) А вообще думаю что на текущий момент SDL2 лучшая многоплатформенная библиотека, и нужно начать было с того так ее собрать, под разные платформы, разными компиляторами, как потом ее подключать, статически, динамически и т.д. И потом можно было бы сделать целый цикл отличных статей. А так это ужас.
                –1
                *некропост*
                Спасибо, покажите ваш цикл обработки событий. На мой взгляд, у меня все логично в классе Input
                  +1
                  Читаем документацию про SDL_PollEvent — и там видим — что данная функция забирает событие из очереди, и в зависимости от того, есть ли в очереди еще события возвращает 1 или 0. Поэтому цикл:

                  while(SDL_PollEvent(&evt));


                  «Проглотит» все события а самое последнее останется скопировано в «evt», что показывает полное непонимание как работать с событиями вообще.
                  А должно быть на псевдокоде как-то так:
                  while(SDL_PollEvent(&evt))
                  {
                      switch(InputEventType(evt))
                      {
                          case INPUT:
                                 ProcessInputHandler(evt);
                                 break;
                          case ...
                          default:
                                 DefaultEventHandler(evt);
                      }
                  }
              +1
              Я бы порекомендовал вам изменить содержимое метода Execute:
              — внутри метода инициализировать вспомогательные объекты — я бы инициализацию вынес либо в отдельный метод (e.x. Init);
              — зачем вам возвращаемый тип int? Если для функции main, то у вас внутри никаких проверок нет на NULL (или исключения). И потому вы всегда возвращаете 0;
              — тоже самое касательно delete. Либо в деструктор, либо куда-нибудь ещё.

              Only users with full accounts can post comments. Log in, please.