Um jogo Flappy bird

Oct 20 2020

Aqui está um projeto no qual tenho trabalhado nos últimos dias.

Eu usei a SFMLbiblioteca em C ++ para fazer meu próprio jogo flappy bird. Fiz isso como um passo para aprender a GUI em C ++.


O programa é orientado a objetos, pois acredito que isso o tornou um pouco mais fácil de manter. Embora não seja minha primeira vez aprendendo SFML, estou muito enferrujado, pois nunca tentei fazer algo sério com ele.


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;

}

Obstacle.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;
}

A física para o pássaro

  • A física do pássaro foi a parte que levei tempo para codificar, não porque fosse difícil, mas porque tentei aperfeiçoar o modo como o pássaro fall()e fly(). Eu usei accelerationisso para modificar velocity. Os valores fornecidos accelerationsão bem pequenos, mas cada quadro é adicionado de forma que o movimento geral do pássaro parece realmente bom. Cada vez que fly()é chamado, ele define um ponto acima do pássaro no qual o pássaro começará a descelerar . Por isso start_fall(). Estou feliz com a forma como o pássaro finalmente voou 😁

  • São duas imagens, uma com as asas do pássaro batidas e outra normal. Quando fly()é chamado eu mudo para as asas batidas, e quando começa a cair eu mudo de volta para as normais, isso também adiciona efeito e dá uma aparência melhor.

  • O pássaro também gira de acordo com sua velocidade.

Os obstáculos são bastante simples.

  • Um obstáculo no fundo tem velocidade constante e é colocado aleatoriamente a y-axiscada nova geração.

  • O obstáculo superior é girado 180 °e alinhado com o obstáculo inferior.

O que eu espero de uma revisão

  • Aspectos gerais de codificação

  • Coisas como aceleração, origem, posições são todas constantes e bird.cpptem muitas delas. No início, decidi usar apenas as constantes flutuantes do plano, mas depois os números mágicos não pareciam muito bons.
    Portanto, decidi mantê-los em um namespace anônimo, pois são usados ​​apenas em bird.cpp. Esta é a melhor maneira de fazer isso? Além disso, de que maneira você geralmente prefere armazenar esse tipo de coisa?

Respostas

5 G.Sliepen Oct 21 2020 at 02:49

Prefira que as variáveis ​​de membro sejam, privatese possível

Existem muitas variáveis ​​de membro publicque não são usadas fora da própria classe. Por exemplo, Game::score, Bird::texture_wing_up, e muitos mais. Todos devem ser private, pois isso evita que outras classes acessem acidentalmente essas variáveis ​​de membro.

Evite inlining prematuro de funções

Por que são detect_loss(), update_object_positions()e draw_objects()declarados como inlinefunções em Game.h? Não vejo nenhuma razão para que isso seja crítico para o desempenho. Declare-os como funções-membro regulares e defina-os em Game.cpp. Observe que o próprio compilador ainda pode decidir embutir essas funções quando elas são chamadas de mainloop().

Quem é responsável pelo que

Seu jogo é bastante simples e Game::draw_objects()parece perfeitamente razoável. Mas o que ele realmente faz é class Gamealcançar class Birde class Obstacle, e acessar suas variáveis ​​de membro bodye bottom/ top_obstacle. Isso significa que agora existe um acoplamento bastante estreito entre essas classes. Mas considere agora que desenhar o pássaro seria muito mais complexo do que apenas desenhar um único sf::Sprite. Talvez você tenha muitos sprites separados, por exemplo, um para as asas, um para o corpo, um para a cabeça e assim por diante, todos animados independentemente. Você quer class Gameser responsável por desenhar um Birdnesse caso?

Existem várias maneiras de resolver esse problema. Você pode simplesmente adicionar uma função-membro que faz todo o desenho em class Bird:

void Game::draw_objects() {
    ...
    bird.draw(window);
    ...
}

void Bird::draw(sf::RenderTarget &target) {
    target.draw(body);
}

Usei o fato que sf::Windowderiva de sf::RenderTarget, então Bird::draw()agora é mais genérico do que se você passasse uma referência a um sf::Window. Alternativamente, com SFML, você pode tornar- Birdse um sf::Drawable, assim:

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);
}

Com qualquer uma das técnicas, você pode fazer as sf::Spritevariáveis privatee, embora não pareça uma grande melhoria para Bird, já se torna mais interessante para Obstacle, em que Game::draw_objects()você só deve chamar window.draw(obstacle)para que o Obstaclepróprio desenhe suas partes inferior e superior .

(Para jogos com muito mais objetos, você pode querer usar um Entity Component System como o EnTT , onde um dos componentes seria a parte extraível de entidades como o pássaro e o obstáculo, mas isso é obviamente um exagero para este jogo. )

Tem Game::handle_events()implementar a while-loop

Apesar do nome, Game::handle_events()trata apenas de um único evento, o while-loop que garante que todos os eventos enfileirados sejam tratados está em Game::mainloop(). Considere mover o while-loop para handle_events().

Crie uma função render()para simplificar ainda maismainloop()

Crie uma função de alto nível para fazer a renderização. Isso mantém mainloop()bom e limpo:

void Game::render() {
    window.clear();
    draw_objects();
    window.display();
}

void Game::mainloop() {
    while (running)
    {
        handle_events();
        update_object_positions();
        render();
    }
}

Crie uma variável de membro runningque pode ser definida como falsa handle_events()se a janela for fechada ou update_object_positions()se for detectada uma colisão entre o pássaro e o obstáculo.

Faça constantes globais constexpr

É muito bom que você evitou constantes mágicas em seu código e deu-lhes nomes claros. O namespace anônimo faz o mesmo statice garante que eles não tenham vínculo externo. Mas ainda melhor é fazê-los em constexprvez de const. Isso deixa claro para o compilador que não deve ser usado como uma variável (da qual um endereço pode ser usado, por exemplo), mas apenas como uma constante literal.

Ainda há constantes mágicas restantes

Ainda há muitas constantes mágicas em seu código. Por exemplo, o tamanho da janela, a posição inicial do obstáculo e assim por diante.

Chamada reset()do construtor

Você tem alguma duplicação de código desnecessária, uma vez que está inicializando variáveis ​​de membro manualmente no construtor de Birde Obstacle, que você também define a partir de Bird::reset()e Obstacle::new_rand_obstacle(). Considere chamar as duas últimas funções dos construtores dessas classes.

Use as funções geradoras de números aleatórios do C ++

C ++ tem recursos de gerador de números aleatórios muito melhores do que C. Use then em vez de rand(). Por exemplo, você pode usar std::uniform_int_distributionem Obstacle.

Use std::minestd::max

Por exemplo:

void Bird::update_bird()
{

    velocity += acceleration;
    velocity.y = std::min(std::max(velocity.y, max_fly_vel), max_fall_vel);
    ...
}

Ou melhor ainda, com C ++ 17 você pode escrever:

    velocity.y = std::clamp(velocity.y, max_fly_vel, max_fall_vel);

Evite alocar objetos na pilha sem um bom motivo

Não há necessidade de usar newem main(), você pode apenas escrever:

int main()
{
    Game game("Flappy Bird");
    game.mainloop();
}

Se você deseja ou precisa evitar a alocação de um objeto na pilha, ainda deve evitar usar newe deletediretamente, mas, em vez disso, usar algo como std::unique_ptrgerenciar a vida útil para você automaticamente. igual a:

int main()
{
    auto game = std::make_unique<Game>("Flappy Bird");
    game->mainloop();
}
4 JVApen Oct 21 2020 at 02:56

Não sou um especialista em SFML, então não posso dar nenhum conselho sobre isso. Porém, deixe-me ver o código como está.

Vamos começar com Game.h:

  • Seu jogo está usando um const char *como argumento, eu recomendaria std::string_viewse você compilasse com C ++ 17. Tem muitos recursos std::stringe se comporta comoconst char *
  • Gosto de como você encapsula vários de seus membros em funções relevantes, porém, por que é scorepúblico?
  • Posso entender a necessidade de algumas funções estarem embutidas. No entanto, por que você implementaria essas funções no mesmo cabeçalho se elas só podem ser chamadas de dentro de seus outros métodos (que são implementados no cpp). Isso aumentaria o tempo de compilação (especialmente em projetos grandes) e colocaria os detalhes privados no arquivo público.
  • Em uma das funções com a qual você compara x -89, isso é um pouco estranho para mim como leitor, que número é esse? O que isso representa. Colocá-lo em uma constante ajudaria muito a entender por que todos os valores de -inf a (e incluindo) -90 seriam aceitos.

Game.cpp:

  • Mais uma vez você tem algumas constantes mágicas, neste caso: "images//background.png". Aqui faz sentido colocar isso em uma constante, dessa forma, você mais tarde poderia usar um gerador de código para criar essas constantes com base nas imagens reais (ou mesmo incorporá-las) e você obteria falhas de compilação se elas desaparecerem.
  • Failed to load ...soa como um erro, mas você transmite isso para em std::coutvez de std::cerr. Como resultado, o console não pode ser colorido de maneira diferente.
  • Olhando para a mesma mensagem, seu usuário ficará intrigado, como eles deveriam resolver isso? Pode ajudar se você mencionar onde você espera que a imagem esteja, para que possam colocar uma nova imagem lá.

Bird.h:

  • start_fallnão foi inicializado, ao escrever int start_fall{0};você pode dizer que precisa ser zero. Dessa forma, você não pode esquecer no cpp.

Bird.cpp:

  • Vamos dar uma olhada no layout, em update_birdvocê coloca o ife o código em 1 linha, no Ctor, você coloca em 2.
  • Na mesma update_birdfunção, você parece estar corrigindo a velocidade. Y, eu escreveria algo como: velocity.y = std::clamp(velocity.y, ::max_fly_vel, ::max_fall_vel);Muito mais fácil de ler o que está acontecendo, menos chance de escrever algo errado.
  • No Ctor, você lança exceções, mas não vejo qualquer menção de noexceptou, neste caso, noexcept(false)para informar seus usuários quando esperar exceções.

Obstacle.cpp:

  • Em C ++, usamos em <cstdlib>vez dos cabeçalhos C<stdlib.h>
  • srand((unsigned)time(0));machuca meus olhos, eu nem vou explicar, é melhor você assistir CppCon 2016: Walter E. Brown “What C ++ Programmers Need to Know about Header <random>"

main.cpp:

  • De que adianta alocar um jogo se você pode colocá-lo na pilha? Uma vez eu expliquei isso com mais detalhes, consulte stackoverflow

Portanto, em geral:

  • Código realmente bom, com alguns comentários
  • É óbvio que você não está familiarizado com os detalhes do C ++ 1 ou mais recente, usar essas coisas pode ajudar a tornar este código mais fácil de ler
  • Eu realmente não mencionei nada sobre estrutura, deixe-me corrigir isso: Isso parece muito bom OO!

E para responder sua pergunta sobre as constantes: Eu costumo usar constexprconstantes. Seja em um namespace sem nome, constantes em um cabeçalho separado ou constantes estáticas na classe dependem do caso de uso.