Game Burung Flappy
Ini adalah proyek yang telah saya kerjakan selama beberapa hari terakhir.
Saya telah menggunakan SFMLperpustakaan dalam C ++ untuk membuat permainan flappy bird saya sendiri. Saya menjadikan ini sebagai langkah untuk mempelajari GUI di C ++.
Program ini Berorientasi Objek karena saya yakin ini membuatnya sedikit lebih mudah untuk dipelihara. Meskipun ini bukan pertama kalinya saya mempelajari SFML, saya cukup berkarat karena saya tidak pernah mencoba membuat sesuatu yang serius dengannya.
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;
}
Rintangan.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;
}
Fisika untuk burung itu
Fisika burung adalah bagian saya mengambil waktu untuk kode, bukan karena itu sulit tapi aku mencoba untuk sempurna bagaimana burung
fall()danfly(). Saya menggunakanaccelerationitu akan memodifikasivelocity. Nilai yang diberikanaccelerationcukup kecil, tetapi setiap frame ditambahkan sehingga pergerakan keseluruhan burung terlihat sangat bagus. Setiap kalifly()dipanggil, ia menetapkan titik di atas burung di mana burung itu akan mulai menurun kecepatannya . Oleh karena itu,start_fall()saya senang dengan bagaimana burung itu akhirnya terbang 😁Ada dua gambar, satu dengan sayap burung mengepak dan satu lagi normal. Saat
fly()dipanggil, saya beralih ke sayap yang dikepakkan, dan saat mulai jatuh saya beralih kembali ke sayap normal, ini juga menambah efek dan memberikan tampilan yang lebih baik.Burung juga berputar sesuai dengan kecepatannya.
Hambatannya cukup lurus ke depan.
Satu rintangan di bagian bawah memiliki kecepatan yang konstan dan ditempatkan secara acak pada
y-axissetiap generasi baru.Rintangan atas diputar
180 °dan disejajarkan dengan rintangan bawah.
Apa yang saya harapkan dari ulasan
Aspek pengkodean umum
Hal-hal seperti percepatan, asal, posisi semuanya konstan, dan
bird.cppmemiliki banyak di antaranya. Pada awalnya, saya memutuskan untuk hanya menggunakan konstanta mengambang rencana, tetapi kemudian angka ajaib tidak terlihat sangat bagus.
Oleh karena itu, saya memutuskan untuk menyimpannya di namespace anonim karena hanya digunakan dibird.cpp. Apakah ini cara yang lebih baik untuk melakukan ini? Selain itu, cara apa yang biasanya Anda sukai untuk menyimpan barang seperti ini?
Jawaban
Lebih suka variabel anggota menjadi privatejika memungkinkan
Ada banyak variabel anggota publicyang tidak digunakan di luar kelas itu sendiri. Sebagai contoh, Game::score, Bird::texture_wing_up, dan banyak lagi. Ini semua harus private, karena ini mencegah kelas lain mengakses variabel anggota ini secara tidak sengaja.
Hindari inline fungsi yang terlalu dini
Mengapa detect_loss(), update_object_positions()dan draw_objects()dideklarasikan sebagai inlinefungsi di Game.h? Saya tidak melihat alasan mengapa ini akan menjadi kinerja yang penting. Deklarasikan mereka sebagai fungsi anggota biasa, dan definisikan di Game.cpp. Perhatikan bahwa kompilator itu sendiri masih bisa memutuskan untuk menyebariskan fungsi-fungsi itu saat dipanggil mainloop().
Siapa yang bertanggung jawab atas apa
Game Anda cukup sederhana, dan Game::draw_objects()terlihat sangat masuk akal. Tapi yang sebenarnya dilakukannya adalah class Gamemenjangkau class Birddan class Obstacle, dan mengakses variabel anggotanya bodydan bottom/ top_obstacle. Ini berarti bahwa sekarang ada hubungan yang cukup erat antara kelas-kelas tersebut. Tetapi pertimbangkan sekarang bahwa menggambar burung akan jauh lebih rumit daripada hanya menggambar satu burung sf::Sprite. Mungkin Anda memiliki banyak sprite yang terpisah, misalnya satu untuk sayap, satu untuk badan, satu untuk kepala dan seterusnya, yang semuanya bergerak sendiri-sendiri. Apakah Anda ingin class Gamebertanggung jawab untuk menggambar Birddalam kasus itu?
Ada beberapa cara untuk mengatasi masalah ini. Anda cukup menambahkan fungsi anggota yang melakukan semua gambar class Bird:
void Game::draw_objects() {
...
bird.draw(window);
...
}
void Bird::draw(sf::RenderTarget &target) {
target.draw(body);
}
Saya menggunakan fakta yang sf::Windowberasal dari sf::RenderTarget, jadi Bird::draw()sekarang lebih umum daripada jika Anda meneruskan referensi ke file sf::Window. Atau, dengan SFML, Anda bisa membuat Birdmenjadi sf::Drawable, seperti:
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);
}
Dengan salah satu teknik, Anda dapat membuat sf::Spritevariabel private, dan meskipun tidak terlihat seperti banyak perbaikan Bird, itu sudah menjadi lebih menarik Obstacle, di mana Game::draw_objects()Anda hanya perlu memanggil window.draw(obstacle)untuk Obstaclemenggambar sendiri bagian bawah dan atas. .
(Untuk game dengan lebih banyak objek, Anda mungkin ingin melihat menggunakan Sistem Komponen Entitas seperti EnTT , di mana salah satu komponennya adalah bagian yang dapat digambar dari entitas seperti burung dan penghalang, tetapi itu jelas berlebihan untuk game ini. )
Telah Game::handle_events()menerapkan while-loop
Terlepas dari namanya, Game::handle_events()hanya menangani satu acara, while-loop yang memastikan semua antrean acara ditangani Game::mainloop(). Pertimbangkan untuk memindahkan while-loop ke handle_events().
Buat fungsi render()untuk lebih menyederhanakanmainloop()
Buat satu fungsi tingkat tinggi untuk melakukan rendering. Ini tetap mainloop()bagus dan bersih:
void Game::render() {
window.clear();
draw_objects();
window.display();
}
void Game::mainloop() {
while (running)
{
handle_events();
update_object_positions();
render();
}
}
Buat variabel anggota runningyang dapat disetel ke false handle_events()jika jendela ditutup, atau update_object_positions()jika tabrakan antara burung dan penghalang terdeteksi.
Buat konstanta global constexpr
Sangat baik bahwa Anda menghindari konstanta ajaib dalam kode Anda, dan memberinya nama yang jelas. Ruang nama anonim melakukan hal yang sama static, dan memastikan mereka tidak memiliki tautan eksternal. Tetapi yang lebih baik adalah membuatnya constexprdaripada const. Ini menjelaskan kepada compiler bahwa ini tidak dimaksudkan untuk digunakan sebagai variabel (di mana alamat dapat diambil misalnya), tetapi sebenarnya hanya sebagai konstanta literal.
Masih ada konstanta ajaib yang tersisa
Masih ada banyak konstanta ajaib dalam kode Anda. Misalnya ukuran jendela, posisi awal halangan, dan lain sebagainya.
Panggilan reset()dari konstruktor
Anda memiliki beberapa duplikasi kode yang tidak perlu, karena Anda menginisialisasi variabel anggota secara manual dalam konstruktor Birddan Obstacle, yang juga Anda setel dari Bird::reset()dan Obstacle::new_rand_obstacle(). Pertimbangkan untuk memanggil dua fungsi terakhir dari konstruktor kelas tersebut.
Gunakan fungsi generator bilangan acak C ++
C ++ memiliki fasilitas penghasil bilangan acak yang jauh lebih baik daripada C. Gunakan saja daripada rand(). Misalnya, Anda dapat menggunakan std::uniform_int_distributiondalam Obstacle.
Gunakan std::mindanstd::max
Sebagai contoh:
void Bird::update_bird()
{
velocity += acceleration;
velocity.y = std::min(std::max(velocity.y, max_fly_vel), max_fall_vel);
...
}
Atau bahkan lebih baik, dengan C ++ 17 Anda dapat menulis:
velocity.y = std::clamp(velocity.y, max_fly_vel, max_fall_vel);
Hindari mengalokasikan objek di heap tanpa alasan yang jelas
Tidak perlu menggunakan newdi main(), Anda cukup menulis:
int main()
{
Game game("Flappy Bird");
game.mainloop();
}
Jika Anda benar-benar ingin atau perlu menghindari pengalokasian objek pada stack, maka Anda tetap harus menghindari penggunaan newdan deletesecara langsung, tetapi gunakan sesuatu seperti std::unique_ptruntuk mengatur masa hidup Anda secara otomatis. seperti ini:
int main()
{
auto game = std::make_unique<Game>("Flappy Bird");
game->mainloop();
}
Saya bukan ahli SFML, jadi saya tidak bisa memberikan saran apa pun tentang itu. Padahal, izinkan saya melihat kode apa adanya.
Mari kita mulai dengan Game.h:
- Game Anda mengambil
const char *argumen, saya akan merekomendasikanstd::string_viewjika Anda akan mengkompilasi dengan C ++ 17. Ini memiliki banyak fiturstd::stringdan berperilaku sebagaiconst char * - Saya suka bagaimana Anda merangkum beberapa anggota Anda di balik fungsi yang relevan, mengapa
scorepublik? - Saya dapat memahami kebutuhan untuk beberapa fungsi agar sejajar. Namun, mengapa Anda mengimplementasikan fungsi-fungsi itu di header yang sama jika mereka hanya dapat dipanggil dari dalam metode Anda yang lain (yang semuanya diimplementasikan di cpp). Ini akan meningkatkan waktu kompilasi (terutama pada proyek-proyek besar) dan menempatkan detail pribadi di file publik.
- Di salah satu fungsi yang Anda bandingkan dengan x
-89, ini agak aneh bagi saya sebagai pembaca, berapa angka ini? Mewakili apa itu. Menempatkannya dalam konstanta akan sangat membantu dalam memahami mengapa setiap nilai dari -inf hingga (dan termasuk) -90 akan diterima.
Game.cpp:
- Sekali lagi Anda memiliki beberapa konstanta ajaib, dalam hal ini:
"images//background.png". Di sini masuk akal untuk meletakkan ini dalam sebuah konstanta, dengan begitu, Anda nantinya dapat menggunakan generator kode untuk membuat konstanta ini berdasarkan gambar yang sebenarnya (atau bahkan menyematkannya) dan Anda mendapatkan kegagalan kompilasi jika hilang. Failed to load ...terdengar seperti kesalahan, namun, Anda mengalirkan ini kestd::coutalih-alihstd::cerr. Akibatnya, konsol tidak dapat menyusunnya secara berbeda.- Melihat pesan yang sama, pengguna Anda akan bingung, bagaimana mereka harus menyelesaikannya? Akan membantu jika Anda menyebutkan di mana Anda mengharapkan gambar itu sehingga mereka dapat meletakkan gambar baru di sana.
Bird.h:
start_falltidak diinisialisasi, dengan menulisint start_fall{0};Anda dapat mengatakan ini harus nol. Dengan begitu, Anda tidak bisa melupakannya di cpp.
Bird.cpp:
- Mari kita lihat tata letaknya,
update_birdAnda meletakkanifdan kode pada 1 baris, di Ctor, Anda meletakkannya di 2. - Dalam
update_birdfungsi yang sama , Anda sepertinya mengoreksi kecepatan. Y, saya akan menulis sesuatu seperti:velocity.y = std::clamp(velocity.y, ::max_fly_vel, ::max_fall_vel);Jauh lebih mudah untuk membaca apa yang terjadi, lebih sedikit kesempatan untuk menulis sesuatu yang salah. - Di Ctor, Anda melempar pengecualian, namun saya tidak melihat ada penyebutan
noexceptatau dalam kasus ininoexcept(false)untuk memberi tahu pengguna Anda kapan harus mengharapkan pengecualian.
Obstacle.cpp:
- Di C ++, kami menggunakan
<cstdlib>sebagai pengganti header C.<stdlib.h> srand((unsigned)time(0));sakit mata saya, saya bahkan tidak akan menjelaskannya, sebaiknya Anda menonton CppCon 2016: Walter E. Brown “Yang Perlu Diketahui Pemrogram C ++ tentang Header <random>"
main.cpp:
- Apa gunanya mengalokasikan game jika Anda bisa meletakkannya di tumpukan? Saya pernah menjelaskan ini secara lebih rinci, lihat stackoverflow
Jadi secara umum:
- Kode yang sangat bagus dengan beberapa komentar
- Jelas Anda tidak terbiasa dengan detail C ++ 1 atau yang lebih baru, menggunakan hal-hal itu dapat membantu membuat kode ini lebih mudah dibaca
- Saya tidak benar-benar menyebutkan apa pun tentang struktur, izinkan saya memperbaikinya: Ini terlihat sangat bagus OO!
Dan untuk menjawab pertanyaan Anda tentang konstanta: Saya biasanya menggunakan constexprkonstanta. Baik itu dalam namespace tanpa nama, konstanta di header terpisah atau konstanta statis di kelas bergantung pada kasus penggunaan.