Пишем игры на 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 — конечные координаты части картинки. Этот метод рисования применяется, если используются атласы картинок. Вот пример атласа:
(изображение взято из веб-ресурса 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
Автор: Izaron