В самом простом случае можно реализовать так же, как реализовано управление одним игроком. Достаточно регистрировать события нажатия/отпускания клавиш. В sfml функция
sf::Window::setKeyRepeatEnabled с аргументом false включает режим однократной регистрации нажатия клавиши. То есть событие keyPressed будет зарегистрировано однажды при нажатии клавиши, даже если она остается нажатой.
Можем считать, что между событиями keyPressed и keyReleased клавиша остается нажатой. Сохраним нажатые клавиши с списке и на каждом шаге игрового цикла будем менять соответственно нажатым клавишам состояние игры, в том числе игрока.
Пример реализации (C++17):
#include <SFML/Graphics.hpp>
#include <unordered_set>
#include <unordered_map>
using namespace std;
void normalize(sf::Vector2i& v) {
int length = sqrt(1ll * v.x * v.x + 1ll * v.y * v.y);
if (length != 0) {
v.x /= length;
v.y /= length;
}
}
class Input {
public:
using Keys = std::unordered_set<sf::Keyboard::Key>;
void registerKeyEvent(sf::Event e) {
if (e.type == sf::Event::KeyPressed)
pressedKeys_.insert(e.key.code);
else if (e.type == sf::Event::KeyReleased)
pressedKeys_.erase(e.key.code);
}
const Keys& pressedKeys() const {
return pressedKeys_;
}
private:
Keys pressedKeys_;
};
class Player {
public:
enum class Control {
moveUp, moveLeft, moveRight, moveDown
};
Player(sf::Vector2i initialPosition, sf::Color color)
: position_{ initialPosition } {
sprite_.setPosition(sf::Vector2f(initialPosition.x, initialPosition.y));
sprite_.setFillColor(color);
sprite_.setRadius(50);
}
Player& map(Control control, sf::Keyboard::Key key) {
keyControls_[key] = control;
return *this;
}
void process(const Input& input) {
velocity_ = {};
for (sf::Keyboard::Key k : input.pressedKeys()) {
if (auto it = keyControls_.find(k); it != keyControls_.end()) {
switch (it->second) {
case Control::moveDown:
velocity_.y = 1;
break;
case Control::moveUp:
velocity_.y = -1;
break;
case Control::moveLeft:
velocity_.x = -1;
break;
case Control::moveRight:
velocity_.x = 1;
break;
default:
break;
}
}
}
normalize(velocity_);
velocity_ *= speed_;
}
void move(sf::Vector2i position) {
position_ = position;
sprite_.setPosition(sf::Vector2f(position.x, position.y));
}
const sf::CircleShape& sprite() const {
return sprite_;
}
sf::Vector2i position() const {
return position_;
}
sf::Vector2i velocity() const {
return velocity_;
}
private:
int speed_ = 300; // pixels / s
sf::Vector2i velocity_;
sf::Vector2i position_;
sf::CircleShape sprite_;
std::unordered_map<sf::Keyboard::Key, Control> keyControls_;
};
class Game {
public:
Game(sf::RenderWindow* gameWindow)
: window_(gameWindow) {
window_->setKeyRepeatEnabled(false);
auto width = window_->getSize().x;
auto height = window_->getSize().y;
Player mrRed(sf::Vector2i(width / 3, height / 3 * 2), sf::Color::Red);
mrRed.map(Player::Control::moveUp, sf::Keyboard::W)
.map(Player::Control::moveDown, sf::Keyboard::S)
.map(Player::Control::moveLeft, sf::Keyboard::A)
.map(Player::Control::moveRight, sf::Keyboard::D);
players_.emplace_back(std::move(mrRed));
Player mrBlue(sf::Vector2i(width / 3 * 2, height / 3 * 2), sf::Color::Blue);
mrBlue.map(Player::Control::moveUp, sf::Keyboard::Up)
.map(Player::Control::moveDown, sf::Keyboard::Down)
.map(Player::Control::moveLeft, sf::Keyboard::Left)
.map(Player::Control::moveRight, sf::Keyboard::Right);
players_.emplace_back(std::move(mrBlue));
Player mrGreen(sf::Vector2i(width / 2, height / 3), sf::Color::Green);
mrGreen.map(Player::Control::moveUp, sf::Keyboard::I)
.map(Player::Control::moveDown, sf::Keyboard::K)
.map(Player::Control::moveLeft, sf::Keyboard::J)
.map(Player::Control::moveRight, sf::Keyboard::L);
players_.emplace_back(std::move(mrGreen));
}
void update() {
processInputEvents();
sf::Time dt = clock_.restart();
processMovement(dt.asMilliseconds());
drawScene();
}
private:
void processInputEvents() {
for (sf::Event e; window_->pollEvent(e); ) {
if (e.type == sf::Event::Closed) {
window_->close();
return;
}
input_.registerKeyEvent(e);
}
for (auto& player : players_)
player.process(input_);
}
void processMovement(sf::Int32 dt) {
for (int id = 0; id < players_.size(); ++id) {
auto& player = players_[id];
sf::Vector2i position = player.position() + sf::Vector2i(
player.velocity().x * 1e-3f * dt,
player.velocity().y * 1e-3f * dt
);
player.move(position);
}
}
void drawScene() {
window_->clear(sf::Color::White);
for (const auto& player : players_)
window_->draw(player.sprite());
window_->display();
}
std::vector<Player> players_;
Input input_;
sf::RenderWindow* window_;
sf::Clock clock_;
};
int main() {
sf::RenderWindow mainWindow(sf::VideoMode(600, 600), "Circles", sf::Style::Close);
mainWindow.setFramerateLimit(120);
Game game(&mainWindow);
while (mainWindow.isOpen())
game.update();
return 0;
}
Мы создаем три круга, которые могут двигаться независимо. Клавиши движений назначаются в конструкторе Game.
FrameLimit ограничен, потому что скорость и другие результаты расчетов целочисленные и привязаны к пискелям. При слишком высокой частоте кадров изменения расстояний на каждом шаге симуляции будут незначительными, округлятся к 0. Лучше заменить целочисленную арифметику на float-арифметику, чтобы не было грубых ошибок округления как в normalize, и промежуточных преобразований sf::Vector.
За поддержание актуальности списка нажатых клавиш отвечает class Input.