Ответы пользователя по тегу Разработка игр
  • Как в игровых движках реализованы отскоки?

    @code_panik
    Какие способы реализации взаимодействия двух тел реально используются в игровых движках?

    Отскок - один из вариантов разрешения коллизий. Тема очень большая, ответы можно найти по запросам collision detection, collision resolution, collision response. Примеры материала:
    https://blog.winter.dev/2020/designing-a-physics-e...
    https://realtimecollisiondetection.net/books/rtcd/
    Интересно, что каждое столкновение генерирует сигнал-событие, который должен быть обработан движком. Представьте, что у вас множество шариков падают на пол в ограниченном стенами пространстве. Шарики ударяются друг об друга, теряя энергию. Расстояния между ними становятся всё меньше, и в конечном итоге движок регистрирует такое количество событий, что всё виснет, и сам движок занимается только обработкой коллизий. Такое поведение нужно иметь в виду и обрабатывать соответствующим образом. То есть разрешение коллизий зависит от желаемых результата и точности симуляции. Например, в sokoban или тетрисе достаточно останавливаться в момент регистрации коллизии, в примере с шарами у нас continuous collision.
    Примеры того, как сделано в реальных движках можно найти в выступлениях разработчиков на gdc, напр. https://www.gdcvault.com/play/1017644/Physics-for-...
    Ответ написан
  • Как сделать коллизии для вращающейся фигуры SFML?

    @code_panik
    Upd: sfml не предоставляет средств для работы с физикой. Разрешение коллизий придется реализовать самому, пример.

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

    В двумерном платформере коллайдером может быть прямоугольник. Если объект имеет квадратную форму, и мы можем пожертвовать столкновением с углами, то коллайдером можно выбрать аккуратно подогнанную по размеру окружность. Распространенный вид коллайдера - AABB (axis aligned bounding box), прямоугольник, стороны которого всегда параллельны осям координат. При повороте игрового объекта достаточно изменить размеры такого прямоугольника, чтобы объект оставался в его границах.

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

    Если нужно реализовать именно игру, то посмотрите в сторону библиотек с готовыми реализациями. Если проект учебный, выберите простую форму коллайдера, например aabb или окружность, и поищите для них примеры разрешения коллизий. Примеров множество.
    Ответ написан
  • Как реализовать управление для двух игроков на одной клавиатуре SFML?

    @code_panik
    В самом простом случае можно реализовать так же, как реализовано управление одним игроком. Достаточно регистрировать события нажатия/отпускания клавиш. В 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.
    Ответ написан
    6 комментариев