Часть 4 "Класс Player"

Предыдущая тема

Класс Player

Перед написанием класса Player, опишем задачи, с которыми должны справляться объекты данного класса:

  • создавать анимированного игрового персонажа в графическом окне;

  • перемещать игрового персонажа в графическом окне, не нарушая установленных границ;

  • иметь свойство состояния жизни и смерти игрового персонажа;

  • передавать во внешнюю среду методы, вычисляющие столкновение спрайта с другими объектами.

Рассмотрим закрытые свойства класса Player:

class Player
{
private:
	
	struct Borders 
{
	float x;
	float y;
	float maxx;
	float maxy;
};
	
	sf::RenderWindow& window;
	sf::Sprite SpritePlayer;
	Animator AnimPlayer = Animator(SpritePlayer);

	sf::Vector2f step;      
	  
	Borders borders{0};
    sf::Vector2f pos;
	sf::Time timeUpdate;
	bool dead=false;

Структура Borders определяет переменные задающие границы перемещения игрового объекта типа Player в графическом окне. По умолчанию границы задаются согласно размеру графического окна, которые передаются по ссылки window.

Объекты SpritePlayer и AnimPlayer создают анимированное отображение игрового персонажа. Свойство step устанавливает шаг перемещения игрового персонажа по оси Х и оси Y. Свойство pos – это начальные координаты игрового персонажа. timeUpdate – свойство подсчёта интервала времени, по окончанию которого периодически будет обновляться игровая логика. Свойство dead – устанавливает статус игрока жив или мёртв. 

Рассмотрим открытые методы класса Player:

public:
	enum class direction {up,down,right,left};

	Player(sf::RenderWindow& window,sf::Vector2f mypos=sf::Vector2f(90,365), 
		std::string const& texture = "image/SPRITESHEET.png", float time = 0.5,
		sf::Vector2i spriteSize=sf::Vector2i(100, 100), int frame=6, int stepy=100):window(window),pos(mypos)
	{
		SpritePlayer.setPosition(pos.x, pos.y);
		auto& idleForward = AnimPlayer.CreateAnimation("idleForward", texture, sf::seconds(time), true);
		idleForward.AddFrames(sf::Vector2i(0, 0), spriteSize, frame, 1);
		auto& idleBack = AnimPlayer.CreateAnimation("idleBack", texture, sf::seconds(time), true);
		idleBack.AddFrames(sf::Vector2i(0, stepy), spriteSize, frame, 1);

		borders.maxx = static_cast<float>(window.getSize().x);
		borders.maxy = static_cast<float>(window.getSize().y);
	}

	void setStepx(float x);
	void setStepy(float y);
	void move(direction direction, float mystep = 1.0f);
	void update(sf::Time const& deltaTime);
	void setBordersPlayer(float x,float y,float maxx,float maxy);
	void setDead(bool playrdead)
	{
		dead = playrdead;
	}
	bool getDead() const 
	{
		return dead;
	}
	sf::Sprite& getPlayer()
	{
		return SpritePlayer;
	}
};

В перечислении direction, создадим список направлений, в которых будет двигаться игровой объект типа Player.

В параметрах конструктора передаём: ссылку на графическое окно sf::RenderWindow& window, начальные координаты персонажа sf::Vector2f mypos=sf::Vector2f(90,365), имя загружаемой текстуры игрового персонажа std::string const& texture = "image/SPRITESHEET.png", время проигрывания всех кадров анимации float time = 0.5, размер кадров анимации sf::Vector2i spriteSize=sf::Vector2i(100, 100), количество кадров анимации int frame=6 и шаг перехода по вертикали на следующую анимацию int stepy=100. В теле конструктора создаём анимацию и инициализируем переменные, обозначающие максимальное значения границ перемещения персонажа по вертикали и горизонтали.

Методы SetStep(), изменяют шаг перемещения плеера.

Метод  move(), задаёт шаг и направление перемещения плеера.

Метод  update(), перемещает объект, учитывая интервал времени обновления игровой логики.

Далее идут вспомогательные методы. Метод setBordersPlayer() устанавливающий границы в которых может перемещаться плеер. Метод setDead() устанавливающий статус жизнь — смерть. Метод getDead() возвращающий текущий статус плеера. Метод getPlayer() возвращающий ссылку на спрайт плеера.

Разберём подробно все описанные выше методы.

void Player::move(direction direction,float mystep) 
{
	if (!dead) 
	{
	switch (direction)
	{
	case direction::up: {if (SpritePlayer.getPosition().y > borders.y) {	step.y = -mystep;} break;}
	case direction::down:{if (SpritePlayer.getPosition().y < borders.maxy) { step.y = mystep; } break;}
	case direction::right: {if (SpritePlayer.getPosition().x < borders.maxx) step.x = mystep;
		       if (AnimPlayer.GetCurrentAnimationName() != "idleForward") AnimPlayer.SwitchAnimation("idleForward");
		       break;}
	case direction::left: {if (SpritePlayer.getPosition().x > borders.x) step.x = -mystep;
		       if (AnimPlayer.GetCurrentAnimationName() != "idleBack") AnimPlayer.SwitchAnimation("idleBack");
			   break;
	          }
	default:
		break;
	}
	}
}

Метод move() устанавливает направление движения плеера, инициализируя значением соответствующий шаг перемещения. А также изменяет текущую анимацию персонажа, в зависимости от выбранного направления движения. 

void Player::update(sf::Time const& deltaTime)
{
	if (!dead) AnimPlayer.Update(deltaTime);
	
	
	timeUpdate += deltaTime;

	if (timeUpdate > sf::milliseconds(3))
	{
				
		timeUpdate = sf::milliseconds(0);
		
		if (!dead) {
		SpritePlayer.move(step.x, step.y);
		if (SpritePlayer.getPosition().x < borders.x) {
			SpritePlayer.setPosition(borders.x, SpritePlayer.getPosition().y);
		}
		if (SpritePlayer.getPosition().x > borders.maxx- SpritePlayer.getGlobalBounds().width) 
		{ SpritePlayer.setPosition(borders.maxx-SpritePlayer.getGlobalBounds().width, SpritePlayer.getPosition().y); }
		if (SpritePlayer.getPosition().y < borders.y) { SpritePlayer.setPosition(SpritePlayer.getPosition().x, borders.y); }
		if (SpritePlayer.getPosition().y > borders.maxy - SpritePlayer.getGlobalBounds().height) 
		{ SpritePlayer.setPosition(SpritePlayer.getPosition().x, borders.maxy- SpritePlayer.getGlobalBounds().height); }
					}
		else
		{
			if (SpritePlayer.getRotation() == 0) { 
				SpritePlayer.setPosition(SpritePlayer.getPosition().x+SpritePlayer.getGlobalBounds().height,SpritePlayer.getPosition().y); 
				SpritePlayer.setRotation(90); }
			SpritePlayer.move(0, 1);
			if (SpritePlayer.getPosition().y > borders.maxy) {
			SpritePlayer.setRotation(0);
			SpritePlayer.setPosition(pos.x, pos.y); dead = false; }
		}
	}

}

Метод update(), делает проверку статуса плеера dead, если статус установлен как жив, тогда показывает анимацию игрового персонажа и разрешает  перемещаться объекту в графическом окне.  В противном случае выполняет логику смерти. В данном варианте спрайт поворачивается на 90 градусов метод setRotation(90) и совершает движение вниз, пока не достигнет границы области перемещения. После чего обновит значение переменных на начальные. Выполнение данного кода происходит с интервалом в 3 миллисекунды.

void Player::setStepx(float x)
{
	step.x = x;
}

void Player::setStepy(float y)
{
	step.y = y;
}

void Player::setBordersPlayer(float x, float y, float maxx, float maxy) 
{
	borders.x = x;
	borders.y = y;
	borders.maxx = maxx;
	borders.maxy = maxy;
}

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

Давайте теперь используем класс Player в нашей разработке.

Player bee=Player(*window);
sf::RectangleShape drop;
sf::Time tm;

В классе игровой движок подключим заголовочный файл Player.h. Создадим объект типа Player и назовём его bee, все параметры оставим по умолчанию за исключением графического окна, которое передадим по ссылке. Далее создаём объект прямоугольник drop — это будет капля. Объявим переменную tm, подсчитывающую пройденный интервал времени, спустя который будет периодически выполняться игровая логика.

void Engine::input()
{
	// Объект события
	sf::Event event_play;

	while (window->pollEvent(event_play))  
	{
		if (event_play.key.code == sf::Keyboard::Escape)  { window->close();}
               
        if (event_play.type==sf::Event::KeyPressed)
        {
            switch (event_play.key.code)
            {
			case sf::Keyboard::Right:{bee.move(Player::direction::right); break;}
			case sf::Keyboard::Left:{bee.move(Player::direction::left); break;}
			case sf::Keyboard::Up:{bee.move(Player::direction::up); break;}
			case sf::Keyboard::Down:{bee.move(Player::direction::down); break;}

            default:
                break;
            }
        }
		if (event_play.type == sf::Event::KeyReleased)
		{
            switch (event_play.key.code)
            {
            case sf::Keyboard::Right:{bee.setStepx(0); break;}
            case sf::Keyboard::Left:{bee.setStepx(0); break;}
            case sf::Keyboard::Up:{bee.setStepy(0); break;}
            case sf::Keyboard::Down:{bee.setStepy(0); break;}

            default:
                break;
            }
        }
    }
}

В методе input() создадим обработчик событий - нажатия на клавиши стрелки.

В этом обработчике метод плеера move(), будет получать соответствующий вектор направления движения, в зависимости от нажатой клавиши.   Если клавиша будет отпущена, обозначенный вектор инициализируется нулевым значением.

void Engine::update(sf::Time const& deltaTime) 
{
	bee.update(deltaTime);
    
	tm += deltaTime; 
	if (tm > sf::milliseconds(3))
	{
		auto mybee = bee.getPlayer(); 
		drop.move(0, 1);
		
		if (drop.getPosition().y > window->getSize().y - drop.getGlobalBounds().height) drop.setPosition(drop.getPosition().x,0);
		
		if (drop.getGlobalBounds().intersects(mybee.getGlobalBounds()) && (!bee.getDead()))
		{
		drop.setPosition(drop.getPosition().x, 0);
		bee.setDead(true); 
		}
		
	tm = sf::milliseconds(0);
	    
	}
}

 В методе update() обновляем игровую логику объекта плеер. Высчитываем нужный интервал времени tm, после чего получаем ссылку на спрайт объекта плеер mybee.  Перемещаем объект drop по вертикали вниз каждый пройденный интервал времени на один пиксель используя метод move(0,1). Если положение объекта drop превышает установленные максимальные границы, устанавливаем его координаты в начальные. В случае пересечения прямоугольника с объектом типа спрайт плеера drop.getGlobalBounds().intersects(mybee.getGlobalBounds() устанавливаем координаты для прямоугольника в начальное положение rop.setPosition(drop.getPosition().x, 0), а плееру статус мёртв bee.setDead(true). 

Разберём более детально процесс определения момента столкновения двух
графических объектов.

Каждый графический объект SFML в своём интерфейсе имеет метод возвращающий глобальный ограничивающий прямоугольник - метод getGlobalBounds().

Метод intersects() проверяет - существует ли пересечение между двумя ограничивающими прямоугольниками, границы которых возвращает метод getGlobalBounds().

В параметрах метод intersects() получает глобальный ограничивающий прямоугольник проверяемого объекта. В случае пересечения прямоугольников данный метод возвращает значение true.

void Engine::draw()
{
	window->clear(sf::Color::Green);
	window->draw(background);
	auto drawBee = bee.getPlayer();
	window->draw(drawBee);
	window->draw(drop);
	window->display();
}

В методе draw() рисуем пчелу и падающую каплю объект drop в графическом окне window. Чтобы нарисовать пчелу сначала получаем ссылку на спрайт объекта плеер drawBee и передаём её в метод draw() объекта window.

В конструкторе класса Engine незабываем установить начальные настройки объекта drop().

Engine::Engine()
{
	drop.setTexture(&AssetManager::GetTexture("image/blob.png"));
	drop.setSize(sf::Vector2f(10,20));
	drop.setPosition(500,0);
}

Более подробную инструкцию вы можете получить, посмотрев видео «Игра на SFML C++ Пчела на работе часть 4 класс Player»

Телеграмм канал "Программирование игр С++/С#

Предыдущая тема

Комментарии (1)


  1. domix32
    00.00.0000 00:00
    +2

    Господе, успокойте ваши кейсы.

    sf::Sprite SpritePlayer;
    Animator AnimPlayer = Animator(SpritePlayer);
    
    float stepx = 0.0f;      
    float stepy = 0.0f;   
    Borders borders{0};

    Пять строчек, а столько проблем. Почему одни члены класса с маленькой, а другие с большой? Если у вас инициализация в конструкторе, нафига вам инициализация дефолтных значения в заголовочнике? Если у вас есть Vec2 примитивы, зачем вам stepX/stepY как отдельные переменные? Спрайты всегда будут жить на стеке или Animator таки выделяет это всё в куче внутри своих пулов? Выглядит как минимум подозрительно.

    class Player
    {
    private:
    	
    	struct Borders 
    {
    ...
    
    case sf::Keyboard::Right:bee.move(Player::direction::right); break;

    У вас там какой-то дефицит пробелов/табов случился, что у вас обработки выглядят как один длинный неймспейс, а структуры только номинально делают отступ.