Ein Flappy Bird Game
Hier ist ein Projekt, an dem ich in den letzten Tagen gearbeitet habe.
Ich habe die SFMLBibliothek in C ++ verwendet, um selbst ein Flappy-Bird-Spiel zu erstellen. Ich habe dies als einen Schritt zum Erlernen der GUI in C ++ gemacht.
Das Programm ist objektorientiert, da es meiner Meinung nach die Wartung etwas erleichtert hat. Obwohl ich SFML nicht zum ersten Mal lerne, bin ich ziemlich verrostet, da ich nie versucht habe, etwas Ernstes daraus zu machen.
Game.h
#pragma once
#include <SFML/Graphics.hpp>
#include "Bird.h"
#include "Obstacle.h"
class Game
{
public:
Game(const char*);
~Game();
int score;
void mainloop();
private:
sf::RenderWindow window;
Bird bird;
Obstacle obstacle;
sf::Texture background_texture;
sf::Sprite background;
void handle_events(const sf::Event&);
inline void draw_objects();
inline void update_object_positions();
inline bool detect_loss();
};
inline bool Game::detect_loss()
{
const auto& bird_bounds = bird.body.getGlobalBounds();
if (bird_bounds.intersects(obstacle.top_obstacle.getGlobalBounds()))
return true;
if (bird_bounds.intersects(obstacle.bottom_obstacle.getGlobalBounds()))
return true;
return false;
}
inline void Game::update_object_positions()
{
bird.update_bird();
obstacle.update_obstacle();
if (obstacle.bottom_obstacle.getPosition().x < -89)
{
++score;
obstacle.new_rand_obstacle();
}
}
inline void Game::draw_objects()
{
window.draw(background);
window.draw(bird.body);
window.draw(obstacle.bottom_obstacle);
window.draw(obstacle.top_obstacle);
}
Game.cpp
#include "Game.h"
#include <iostream>
Game::~Game()
{
std::cout << "Well played ! Score : " << score << '\n';
}
Game::Game(const char* title)
: score{ 0 }
{
window.create(sf::VideoMode(800, 800), title);
if (!background_texture.loadFromFile("images//background.png"))
std::cout << "Failed to load background image\n";
background.setTexture(background_texture);
}
void Game::handle_events(const sf::Event& event)
{
switch (event.type)
{
case sf::Event::Closed:
window.close();
break;
case sf::Event::KeyPressed:
if (event.key.code == sf::Keyboard::Space || event.key.code == sf::Keyboard::Up)
bird.fly();
if (event.key.code == sf::Keyboard::N)
obstacle.new_rand_obstacle();
break;
}
}
void Game::mainloop()
{
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
handle_events(event);
}
if (detect_loss())
break;
update_object_positions();
window.clear();
draw_objects();
window.display();
}
}
Bird.h
#pragma once
#include <SFML/Graphics.hpp>
class Bird
{
public:
sf::Texture texture_wing_up;
sf::Texture texture_wing_down;
sf::Sprite body;
sf::Vector2f acceleration;
sf::Vector2f velocity;
Bird();
void fall();
void fly();
void reset();
void update_bird();
private:
int start_fall;
};
Bird.cpp
#include "Bird.h"
#include <iostream>
namespace
{
const sf::Vector2f fly_acc(0, -0.01f);
const sf::Vector2f fall_acc(0, 0.001f);
const float fly_rot{ -30.5f };
const float fall_rot{ 0.06f }; // offset is applied to current rotation
const sf::Vector2f middle(35,29);
const sf::Vector2f initial_bird_pos(320, 300);
const float max_fall_vel = 0.4f;
const float max_fly_vel = -0.5f;
}
void Bird::fly()
{
acceleration = ::fly_acc;
start_fall = static_cast<int>(body.getPosition().y-7);
body.setRotation(::fly_rot);
body.setTexture(texture_wing_down);
}
void Bird::fall()
{
acceleration = ::fall_acc;
body.rotate(::fall_rot);
body.setTexture(texture_wing_up);
}
void Bird::reset()
{
acceleration = { 0,0 };
velocity = { 0,0 };
body.setPosition(320, 300);
body.setRotation(0);
start_fall = 0;
}
void Bird::update_bird()
{
velocity += acceleration;
if (velocity.y > ::max_fall_vel) velocity.y = ::max_fall_vel;
if (velocity.y < ::max_fly_vel) velocity.y = ::max_fly_vel;
body.move(velocity);
const auto& position = body.getPosition().y;
if (position < start_fall) fall();
}
Bird::Bird()
{
if (!texture_wing_up.loadFromFile("images//bird_wing_up.png"))
throw std::runtime_error("Failed to load images//bird_wing_up.png\n");
if (!texture_wing_down.loadFromFile("images//bird_wing_down.png"))
throw std::runtime_error("Failed to load images//bird_wing_down.png");
body.setTexture(texture_wing_up);
body.setPosition(initial_bird_pos);
acceleration = { 0,0 };
velocity = { 0,0 };
body.setOrigin(middle); // Imporant as it also sets the point where the bird rotates at
start_fall = 0;
}
Hindernis.h
#pragma once
#include <SFML/Graphics.hpp>
class Obstacle
{
public:
sf::Texture texture;
sf::Sprite bottom_obstacle;
sf::Sprite top_obstacle;
sf::Vector2f velocity;
Obstacle();
void update_obstacle();
void new_rand_obstacle();
};
Obstacle.cpp
#include "Obstacle.h"
#include <stdlib.h>
#include <iostream>
Obstacle::Obstacle()
{
velocity = { -0.15f,0 };
if (!texture.loadFromFile("images//obstacle.png"))
throw std::runtime_error("Failed to load images//obstacle.png\n");
bottom_obstacle.setTexture(texture);
bottom_obstacle.setPosition(720, 300);
top_obstacle = bottom_obstacle;
top_obstacle.rotate(180);
const auto& bottom_position = bottom_obstacle.getPosition();
top_obstacle.setPosition(bottom_position.x+89, bottom_position.y - 250);
srand((unsigned)time(0));
}
void Obstacle::update_obstacle()
{
bottom_obstacle.move(velocity);
auto bottom_position = bottom_obstacle.getPosition();
top_obstacle.setPosition(bottom_position.x+89, bottom_position.y - 250);
}
void Obstacle::new_rand_obstacle()
{
const auto new_pos = rand() % 600 + 200;
bottom_obstacle.setPosition(800, (float)new_pos);
const auto& bottom_position = bottom_obstacle.getPosition();
top_obstacle.setPosition(bottom_position.x+89, bottom_position.y - 250);
}
main.cpp
#include "Game.h"
int main()
{
Game* game = new Game("Flappy Bird");
game->mainloop();
delete game;
game = nullptr;
return 0;
}
Die Physik für den Vogel
Die Physik des Vogels war der Teil, für den ich mir Zeit zum Codieren genommen habe, nicht weil es schwierig war, sondern weil ich versucht habe zu perfektionieren, wie der Vogel
fall()undfly(). Ich habe das verwendetacceleration, um zu ändernvelocity. Die angegebenen Werteaccelerationsind recht klein, aber jedes Bild summiert sich, sodass die Gesamtbewegung des Vogels wirklich gut aussieht. Jedes Malfly(), wenn aufgerufen wird, wird ein Punkt über dem Vogel festgelegt, an dem der Vogel zu bremsen beginnt . Daherstart_fall()bin ich glücklich darüber, wie der Vogel endlich geflogen ist 😁Es gibt zwei Bilder, eines mit geflatterten Vogelflügeln und eines normal. Wenn
fly()es aufgerufen wird, wechsle ich zu den Schlagflügeln, und wenn es anfängt zu fallen, wechsle ich zurück zu den normalen, dies trägt ebenfalls zum Effekt bei und gibt ein besseres Aussehen.Der Vogel dreht sich auch entsprechend seiner Geschwindigkeit.
Die Hindernisse sind ziemlich einfach.
Ein Hindernis am Boden hat eine konstante Geschwindigkeit und wird bei
y-axisjeder neuen Generation zufällig platziert .Das obere Hindernis wird gedreht
180 °und mit dem unteren Hindernis ausgerichtet.
Was ich von einer Bewertung erwarte
Allgemeine Codierungsaspekte
Dinge wie Beschleunigung, Ursprung, Positionen sind alle konstant und
bird.cpphaben viele davon. Zuerst habe ich beschlossen, nur die Plan-Floating-Konstanten zu verwenden, aber dann sahen die magischen Zahlen nicht sehr gut aus.
Daher habe ich beschlossen, sie in einem anonymen Namespace zu belassen, da sie nur in verwendet werdenbird.cpp. Ist das ein besserer Weg, dies zu tun? Wie bevorzugen Sie es normalerweise , solche Dinge aufzubewahren?
Antworten
privateWenn möglich, bevorzugen Sie Mitgliedsvariablen
Es gibt viele Mitgliedsvariablen public, die nicht außerhalb der Klasse selbst verwendet werden. Zum Beispiel Game::score, Bird::texture_wing_upund viele mehr. Dies sollte alles sein private, da dies verhindert, dass andere Klassen versehentlich auf diese Mitgliedsvariablen zugreifen.
Vermeiden Sie vorzeitiges Inlining von Funktionen
Warum sind detect_loss(), update_object_positions()und draw_objects()erklärt , wie inlineFunktionen in Game.h? Ich sehe keinen Grund, warum diese leistungskritisch wären. Deklarieren Sie sie als reguläre Mitgliedsfunktionen und definieren Sie sie in Game.cpp. Beachten Sie, dass der Compiler selbst weiterhin entscheiden kann, diese Funktionen zu integrieren, wenn sie von aufgerufen werden mainloop().
Wer ist für was verantwortlich?
Ihr Spiel ist recht einfach und Game::draw_objects()sieht absolut vernünftig aus. Aber was es wirklich tut , ist mit class GameErreichen in class Birdund class Obstacle, und deren Mitglied Zugriff auf variabels bodyund bottom/ top_obstacle. Dies bedeutet, dass es jetzt eine ziemlich enge Kopplung zwischen diesen Klassen gibt. Aber denken Sie jetzt daran, dass das Zeichnen des Vogels viel komplexer wäre als nur das Zeichnen eines einzelnen sf::Sprite. Vielleicht haben Sie viele separate Sprites, zum Beispiel einen für die Flügel, einen für den Körper, einen für den Kopf und so weiter, die alle unabhängig voneinander animieren. Möchten Sie in diesem Fall class Gamefür das Zeichnen eines verantwortlich sein Bird?
Es gibt verschiedene Möglichkeiten, um dieses Problem zu beheben. Sie können einfach eine Elementfunktion hinzufügen, die das gesamte Zeichnen ausführt class Bird:
void Game::draw_objects() {
...
bird.draw(window);
...
}
void Bird::draw(sf::RenderTarget &target) {
target.draw(body);
}
Ich habe die Tatsache verwendet, die sf::Windowvon abgeleitet ist sf::RenderTarget, und Bird::draw()ist jetzt allgemeiner, als wenn Sie einen Verweis auf eine übergeben würden sf::Window. Alternativ mit SFML, könnten Sie machen Birdeine werden sf::Drawable, etwa so:
void Game::draw_objects() {
...
window.draw(bird);
...
}
class Bird: public sf::Drawable {
...
Bird::draw(sf::RenderTarget &target, sf::RenderStates states) final;
...
};
void Bird::draw(sf::RenderTarget &target, sf::RenderStates states) {
target.draw(body);
}
Mit beiden Techniken können Sie die sf::SpriteVariablen erstellen private, und obwohl dies keine große Verbesserung darstellt Bird, wird es bereits interessanter Obstacle, wenn Game::draw_objects()Sie nur anrufen window.draw(obstacle)müssen , damit das Selbst Obstaclesowohl den unteren als auch den oberen Teil zeichnet .
(Für Spiele mit viel mehr Objekten möchten Sie möglicherweise ein Entity Component System wie EnTT verwenden , bei dem eine der Komponenten der zeichnbare Teil von Entities wie dem Vogel und dem Hindernis ist, aber das ist offensichtlich ein völliger Overkill für dieses Spiel. )
Habe Game::handle_events()die while-schleife implementiert
Trotz des Namens wird Game::handle_events()nur ein einzelnes Ereignis behandelt. Die whileSchleife , die sicherstellt, dass alle Ereignisse in der Warteschlange behandelt werden, befindet sich in Game::mainloop(). Verschieben Sie die whileSchleife nach handle_events().
Erstellen Sie eine Funktion render()zur weiteren Vereinfachungmainloop()
Erstellen Sie eine übergeordnete Funktion für das Rendern. Das bleibt mainloop()schön sauber:
void Game::render() {
window.clear();
draw_objects();
window.display();
}
void Game::mainloop() {
while (running)
{
handle_events();
update_object_positions();
render();
}
}
Erstellen Sie eine Mitgliedsvariable running, die auf false gesetzt werden kann, handle_events()wenn das Fenster geschlossen wird oder update_object_positions()wenn eine Kollision zwischen dem Vogel und dem Hindernis erkannt wird.
Machen Sie globale Konstanten constexpr
Es ist sehr gut, dass Sie magische Konstanten in Ihrem Code vermieden und ihnen klare Namen gegeben haben. Der anonyme Namespace macht dasselbe wie staticund stellt sicher, dass keine externe Verknüpfung besteht. Aber noch besser ist es, sie constexprstatt zu machen const. Dies macht dem Compiler klar, dass dies nicht als Variable gedacht ist (von der beispielsweise eine Adresse verwendet werden kann), sondern eigentlich nur als Literalkonstante.
Es sind noch magische Konstanten übrig
Es gibt immer noch viele magische Konstanten in Ihrem Code. Zum Beispiel die Fenstergröße, die Anfangsposition des Hindernisses und so weiter.
Aufruf reset()vom Konstruktor
Sie haben einige unnötige Codeduplizierungen, da Sie Mitgliedsvariablen manuell im Konstruktor von Birdund initialisieren Obstacle, den Sie auch von Bird::reset()und festlegen Obstacle::new_rand_obstacle(). Erwägen Sie, die beiden letztgenannten Funktionen von den Konstruktoren dieser Klassen aufzurufen.
Verwenden Sie die Zufallszahlengeneratorfunktionen von C ++
C ++ hat viel bessere Zufallszahlengeneratorfunktionen als C. Verwenden Sie dann statt rand(). Zum Beispiel können Sie std::uniform_int_distributionin verwenden Obstacle.
Verwenden Sie std::minundstd::max
Beispielsweise:
void Bird::update_bird()
{
velocity += acceleration;
velocity.y = std::min(std::max(velocity.y, max_fly_vel), max_fall_vel);
...
}
Oder noch besser, mit C ++ 17 können Sie schreiben:
velocity.y = std::clamp(velocity.y, max_fly_vel, max_fall_vel);
Vermeiden Sie es, Objekte auf dem Heap ohne guten Grund zuzuweisen
Es besteht keine Notwendigkeit zu verwenden , ist newin main(), können Sie einfach schreiben:
int main()
{
Game game("Flappy Bird");
game.mainloop();
}
Wenn Sie die Zuweisung eines Objekts auf dem Stapel vermeiden möchten oder müssen, sollten Sie die Verwendung von newund deletedirekt vermeiden , aber stattdessen so etwas verwenden std::unique_ptr, um die Lebensdauer automatisch für Sie zu verwalten. wie so:
int main()
{
auto game = std::make_unique<Game>("Flappy Bird");
game->mainloop();
}
Ich bin kein Experte für SFML, daher kann ich dazu keine wirklichen Ratschläge geben. Lassen Sie mich den Code so sehen, wie er ist.
Beginnen wir mit Game.h:
- Ihr Spiel nimmt ein
const char *Argument, ich würde empfehlen,std::string_viewwenn Sie mit C ++ 17 kompilieren würden. Es hat viele Funktionen vonstd::stringund es verhält sich wieconst char * - Mir gefällt, wie Sie mehrere Ihrer Mitglieder hinter relevanten Funktionen zusammenfassen. Warum ist dies
scoreöffentlich? - Ich kann verstehen, dass einige Funktionen inline sein müssen. Warum sollten Sie diese Funktionen jedoch im selben Header implementieren, wenn sie nur von Ihren anderen Methoden aus aufgerufen werden können (die alle in cpp implementiert sind)? Dies würde die Kompilierungszeiten verlängern (insbesondere bei großen Projekten) und die privaten Details in die öffentliche Datei aufnehmen.
- In einer der Funktionen, mit denen Sie x vergleichen
-89, ist dies für mich als Leser etwas seltsam. Wie lautet diese Zahl? Was stellt es dar. Eine Konstante würde viel dazu beitragen, zu verstehen, warum jeder Wert von -inf bis (und einschließlich) -90 akzeptiert wird.
Game.cpp::
- Wieder haben Sie einige magische Konstanten, in diesem Fall :
"images//background.png". Hier ist es sinnvoll, dies in eine Konstante zu setzen. Auf diese Weise können Sie später einen Codegenerator verwenden, um diese Konstanten basierend auf den tatsächlichen Bildern zu erstellen (oder sie sogar einzubetten), und Sie erhalten Kompilierungsfehler, wenn sie verloren gehen. Failed to load ...klingt wie ein Fehler, aber Sie streamen diesstd::coutanstelle vonstd::cerr. Infolgedessen kann die Konsole dies nicht anders einstellen.- Wenn Sie dieselbe Nachricht betrachten, wird Ihr Benutzer verwirrt sein. Wie sollen sie das lösen? Es könnte hilfreich sein, wenn Sie ihnen mitteilen, wo Sie das Bild erwartet haben, damit sie dort ein neues Bild einfügen können.
Bird.h::
start_fallwird nicht initialisiert, durch Schreibenint start_fall{0};kann man sagen, dass dies Null sein muss. Auf diese Weise können Sie es in der CPP nicht vergessen.
Bird.cpp::
- Schauen wir uns das Layout an, indem Sie
update_birddenifund den Code in eine Zeile setzen, im Ctor den Code in 2. - In der gleichen
update_birdFunktion scheinen Sie die Geschwindigkeit zu korrigieren. Y, ich würde etwas schreiben wie:velocity.y = std::clamp(velocity.y, ::max_fly_vel, ::max_fall_vel);Viel einfacher zu lesen, was los ist, weniger Chance, etwas falsch zu schreiben. - Im Ctor werfen Sie Ausnahmen aus, aber ich sehe keine Erwähnung
noexceptoder in diesem Fallnoexcept(false), um Ihre Benutzer darüber zu informieren, wann Ausnahmen zu erwarten sind.
Obstacle.cpp::
- In C ++ verwenden wir
<cstdlib>anstelle der C-Header<stdlib.h> srand((unsigned)time(0));tut meinen Augen weh, ich werde es nicht einmal erklären, es ist am besten, wenn Sie sich die CppCon 2016 ansehen : Walter E. Brown „Was C ++ - Programmierer über Header >random> wissen müssen“
main.cpp::
- Was bringt es, ein Spiel zuzuweisen, wenn Sie es auf den Stapel legen können? Ich habe dies einmal ausführlicher erklärt, siehe Stackoverflow
Also im Allgemeinen:
- Wirklich guter Code mit ein paar Bemerkungen
- Es ist offensichtlich, dass Sie mit den Details von C ++ 1 oder neueren Versionen nicht vertraut sind. Wenn Sie diese Dinge verwenden, kann dies dazu beitragen, dass dieser Code leichter zu lesen ist
- Ich habe nichts über Struktur erwähnt, lassen Sie mich das beheben: Das sieht nach wirklich schön aus OO!
Und um Ihre Frage zu den Konstanten zu beantworten: Ich verwende normalerweise constexprKonstanten. Ob es sich um einen unbenannten Namespace, Konstanten in einem separaten Header oder statische Konstanten in der Klasse handelt, hängt vom Anwendungsfall ab.