Trò chơi Flappy bird
Đây là một dự án mà tôi đã thực hiện trong vài ngày qua.
Tôi đã sử dụng SFMLthư viện trong C ++ để tạo một trò chơi flappy bird của riêng mình. Tôi đã thực hiện điều này như một bước hướng tới việc học GUI trong C ++.
Chương trình hướng đối tượng vì tôi tin rằng điều này làm cho nó dễ bảo trì hơn một chút. Mặc dù đây không phải là lần đầu tiên tôi học SFML, nhưng tôi cũng khá mệt mỏi vì tôi chưa bao giờ cố gắng làm điều gì đó nghiêm túc với nó.
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;
}
Chướng ngại vật.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();
};
Chướng ngại vật.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;
}
Vật lý của loài chim
Vật lý của con chim là phần tôi mất thời gian để viết mã, không phải vì nó khó mà tôi đã cố gắng hoàn thiện cách con chim
fall()vàfly(). Tôi đã sử dụngaccelerationđiều đó sẽ sửa đổivelocity. Các giá trị được đưa raaccelerationlà khá nhỏ, nhưng mỗi khung hình nó cộng lại để chuyển động tổng thể của con chim trông thực sự tốt. Mỗi khifly()được gọi, nó đặt một điểm phía trên con chim mà tại đó con chim sẽ bắt đầu giảm tốc độ . Do đó,start_fall()tôi hài lòng với cách con chim bay cuối cùng 😁Có hai hình ảnh, một hình có cánh chim vỗ và một hình bình thường. Khi
fly()được gọi, tôi chuyển sang cánh vỗ, và khi nó bắt đầu rơi xuống, tôi chuyển trở lại những cánh bình thường, điều này cũng làm tăng thêm hiệu ứng và trông đẹp hơn.Con chim cũng quay theo vận tốc của nó.
Các chướng ngại vật là khá thẳng về phía trước.
Một chướng ngại vật ở phía dưới có vận tốc không đổi và được đặt ngẫu nhiên trên
y-axismỗi thế hệ mới.Vật cản trên cùng được xoay
180 °và căn chỉnh với vật cản phía dưới.
Những gì tôi mong đợi từ một bài đánh giá
Các khía cạnh mã hóa chung
Những thứ như gia tốc, điểm gốc, vị trí đều không đổi và
bird.cppcó rất nhiều thứ trong số đó. Lúc đầu, tôi quyết định chỉ sử dụng các hằng số động của kế hoạch, nhưng sau đó các con số kỳ diệu trông không đẹp cho lắm.
Do đó, tôi quyết định giữ chúng trong một không gian tên ẩn danh vì chúng chỉ được sử dụng trong đóbird.cpp. Đây có phải là cách tốt hơn để làm điều này? Ngoài ra, bạn thường thích lưu trữ những thứ như thế này theo cách nào?
Trả lời
Ưu tiên các biến thành viên privatenếu có thể
Có rất nhiều biến thành viên publickhông được sử dụng bên ngoài lớp. Ví dụ, Game::score, Bird::texture_wing_up, và nhiều hơn nữa. Tất cả đều phải privatenhư vậy, vì điều này ngăn các lớp khác vô tình truy cập vào các biến thể thành viên này.
Tránh nội tuyến sớm các chức năng
Tại sao detect_loss(), update_object_positions()và draw_objects()khai báo là inlinechức năng trong Game.h? Tôi không thấy bất kỳ lý do nào tại sao những điều này lại là yếu tố quan trọng về hiệu suất. Khai báo chúng dưới dạng các hàm thành viên thông thường và định nghĩa chúng trong Game.cpp. Lưu ý rằng bản thân trình biên dịch vẫn có thể quyết định nội tuyến các hàm đó khi chúng được gọi từ mainloop().
Ai chịu trách nhiệm về những gì
Trò chơi của bạn khá đơn giản và có Game::draw_objects()vẻ hoàn toàn hợp lý. Nhưng những gì nó thực sự làm là class Gametiếp cận class Birdvà class Obstaclevà truy cập vào các biến thể thành viên bodyvà bottom/ top_obstacle. Điều này có nghĩa là bây giờ có một sự kết hợp khá chặt chẽ giữa các lớp đó. Nhưng bây giờ hãy cân nhắc rằng việc vẽ con chim sẽ phức tạp hơn nhiều so với việc chỉ vẽ một con đơn lẻ sf::Sprite. Có thể bạn có nhiều sprite riêng biệt, ví dụ như một cái cho cánh, một cái cho thân, một cái cho đầu, v.v., tất cả đều hoạt động độc lập. Bạn có muốn class Gamechịu trách nhiệm vẽ một Birdtrong trường hợp đó?
Có một số cách để giải quyết vấn đề này. Bạn có thể chỉ cần thêm một hàm thành viên thực hiện tất cả các bản vẽ trong class Bird:
void Game::draw_objects() {
...
bird.draw(window);
...
}
void Bird::draw(sf::RenderTarget &target) {
target.draw(body);
}
Tôi đã sử dụng dữ kiện có sf::Windownguồn gốc từ sf::RenderTarget, vì vậy Bird::draw()bây giờ chung chung hơn là nếu bạn chuyển một tham chiếu đến một sf::Window. Ngoài ra, với SFML, bạn có thể Birdtrở thành một sf::Drawable, như vậy:
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);
}
Với một trong hai kỹ thuật, bạn có thể làm cho sf::Spritevariabels private, và trong khi nó không giống như nhiều sự cải thiện cho Bird, nó đã trở nên thú vị hơn cho Obstaclenơi ở Game::draw_objects()bạn chỉ nên phải gọi window.draw(obstacle)để có Obstacletự vẽ cả hai đáy của nó và các bộ phận đầu .
(Đối với các trò chơi có nhiều đối tượng hơn, bạn có thể muốn xem xét bằng cách sử dụng Hệ thống thành phần thực thể như EnTT , trong đó một trong các thành phần sẽ là phần có thể vẽ được của các thực thể như chim và chướng ngại vật, nhưng điều đó rõ ràng là quá mức cần thiết cho trò chơi này. )
Có Game::handle_events()triển khai while-loop
Mặc dù có tên, Game::handle_events()chỉ xử lý một sự kiện duy nhất, while-loop đảm bảo tất cả các sự kiện được xếp hàng đợi được xử lý đều ở trong Game::mainloop(). Cân nhắc chuyển while-loop sang handle_events().
Tạo một hàm render()để đơn giản hóa hơn nữamainloop()
Tạo một chức năng cấp cao để thực hiện kết xuất. Điều này giữ cho mainloop()đẹp và sạch sẽ:
void Game::render() {
window.clear();
draw_objects();
window.display();
}
void Game::mainloop() {
while (running)
{
handle_events();
update_object_positions();
render();
}
}
Tạo một biến thành viên runningcó thể được đặt thành false handle_events()nếu cửa sổ đang đóng hoặc update_object_positions()khi phát hiện ra va chạm giữa chim và chướng ngại vật.
Tạo hằng số chung constexpr
Rất tốt khi bạn tránh được các hằng số ma thuật trong mã của mình và đặt tên rõ ràng cho chúng. Không gian tên ẩn danh thực hiện tương tự như staticvà đảm bảo chúng không có liên kết bên ngoài. Nhưng tốt hơn nữa là làm cho chúng constexprthay vì const. Điều này làm cho trình biên dịch hiểu rõ rằng điều này không được sử dụng như một biến (trong đó có thể lấy một địa chỉ chẳng hạn), mà thực sự chỉ là một hằng số theo nghĩa đen.
Vẫn còn những hằng số ma thuật
Vẫn còn rất nhiều hằng số ma thuật trong mã của bạn. Ví dụ: kích thước cửa sổ, vị trí ban đầu của chướng ngại vật, v.v.
Gọi reset()từ hàm tạo
Bạn có một số mã trùng lặp không cần thiết, vì bạn đang khởi tạo các biến thành viên theo cách thủ công trong hàm tạo của Birdvà Obstacle, mà bạn cũng đặt từ Bird::reset()và Obstacle::new_rand_obstacle(). Hãy xem xét việc gọi hai hàm sau từ các hàm tạo của các lớp đó.
Sử dụng các hàm tạo số ngẫu nhiên của C ++
C ++ có cơ sở tạo số ngẫu nhiên tốt hơn nhiều so với C. Sử dụng then thay vì rand(). Ví dụ, bạn có thể sử dụng std::uniform_int_distributiontrong Obstacle.
Sử dụng std::minvàstd::max
Ví dụ:
void Bird::update_bird()
{
velocity += acceleration;
velocity.y = std::min(std::max(velocity.y, max_fly_vel), max_fall_vel);
...
}
Hoặc thậm chí tốt hơn, với C ++ 17 bạn có thể viết:
velocity.y = std::clamp(velocity.y, max_fly_vel, max_fall_vel);
Tránh phân bổ các đối tượng trên heap mà không có lý do chính đáng
Không cần sử dụng newtrong main(), bạn chỉ cần viết:
int main()
{
Game game("Flappy Bird");
game.mainloop();
}
Nếu bạn muốn hoặc cần tránh phân bổ một đối tượng trên ngăn xếp, thì bạn vẫn nên tránh sử dụng newvà deletetrực tiếp, mà thay vào đó hãy sử dụng thứ gì đó như std::unique_ptrđể quản lý thời gian tồn tại tự động cho bạn. như vậy:
int main()
{
auto game = std::make_unique<Game>("Flappy Bird");
game->mainloop();
}
Tôi không phải là chuyên gia về SFML, vì vậy tôi thực sự không thể đưa ra bất kỳ lời khuyên nào về điều đó. Mặc dù vậy, hãy để tôi xem mã như hiện tại.
Hãy bắt đầu với Game.h:
- Trò chơi của bạn đang được coi là một
const char *đối số, tôi sẽ khuyên bạnstd::string_viewnếu bạn muốn biên dịch với C ++ 17. Nó có rất nhiều tính năngstd::stringvà nó hoạt động nhưconst char * - Tôi thích cách bạn đóng gói một số thành viên của mình đằng sau các chức năng có liên quan, tại sao lại là
scorecông khai? - Tôi có thể hiểu sự cần thiết của một số chức năng được nội tuyến. Tuy nhiên, tại sao bạn lại triển khai các hàm đó trong cùng một tiêu đề nếu chúng chỉ có thể gọi từ bên trong các phương thức khác của bạn (tất cả đều được triển khai trong cpp). Nó sẽ tăng thời gian biên dịch (đặc biệt là trên các dự án lớn) và nó đưa các chi tiết riêng tư vào tệp công khai.
- Trong một trong những hàm mà bạn so sánh với x
-89, điều này hơi lạ đối với tôi với tư cách là một độc giả, con số này là gì? Nó đại diện cho điều gì. Đặt nó trong một hằng số sẽ giúp ích rất nhiều trong việc hiểu tại sao mọi giá trị từ -inf đến (và bao gồm cả) -90 sẽ được chấp nhận.
Game.cpp:
- Một lần nữa bạn có một số hằng số kỳ diệu, trong trường hợp này:
"images//background.png". Ở đây, thật hợp lý khi đặt điều này trong một hằng số, theo cách đó, sau này bạn có thể sử dụng trình tạo mã để tạo các hằng số này dựa trên các hình ảnh thực tế (hoặc thậm chí nhúng chúng) và bạn sẽ gặp lỗi biên dịch nếu chúng bị thiếu. Failed to load ...nghe có vẻ như là một lỗi, tuy nhiên, bạn phát trực tuyến nàystd::coutthay vìstd::cerr. Do đó, bảng điều khiển không thể đối chiếu điều này theo cách khác.- Nhìn vào cùng một thông báo, người dùng của bạn sẽ phân vân, họ nên giải quyết vấn đề này như thế nào? Sẽ có ích nếu bạn đề cập với họ nơi bạn mong đợi hình ảnh ở đó để họ có thể đặt một bức tranh mới ở đó.
Bird.h:
start_fallkhông được khởi tạo, bằng cách viết,int start_fall{0};bạn có thể nói giá trị này cần bằng 0. Bằng cách đó, bạn không thể quên nó trong cpp.
Bird.cpp:
- Hãy xem bố cục,
update_birdbạn đặtifvà mã trên 1 dòng, trong Ctor, bạn đặt nó trên 2. - Trong cùng một
update_birdchức năng, bạn dường như đang điều chỉnh vận tốc.y, tôi sẽ viết một cái gì đó như:velocity.y = std::clamp(velocity.y, ::max_fly_vel, ::max_fall_vel);Dễ dàng hơn để đọc những gì đang xảy ra, ít cơ hội viết sai hơn. - Trong Ctor, bạn đưa ra các ngoại lệ, nhưng tôi không thấy bất kỳ đề cập nào
noexcepthoặc trong trường hợp nàynoexcept(false)là thông báo cho người dùng của bạn khi nào có ngoại lệ.
Obstacle.cpp:
- Trong C ++, chúng tôi sử dụng
<cstdlib>thay vì tiêu đề C<stdlib.h> srand((unsigned)time(0));làm đau mắt tôi, tôi thậm chí sẽ không giải thích nó, tốt nhất bạn nên xem CppCon 2016: Walter E. Brown "Những gì các lập trình viên C ++ cần biết về Header <random>"
main.cpp:
- Điểm của việc phân bổ một trò chơi nếu bạn có thể đặt nó vào ngăn xếp là gì? Tôi đã từng giải thích điều này chi tiết hơn, hãy xem stackoverflow
Vì vậy, nói chung:
- Mã thực sự tốt với một vài nhận xét
- Rõ ràng là bạn chưa quen với các chi tiết của C ++ 1 hoặc mới hơn, việc sử dụng những thứ đó có thể giúp làm cho mã này dễ đọc hơn
- Tôi đã không thực sự đề cập bất cứ điều gì về cấu trúc, hãy để tôi sửa lại rằng: Điều này trông giống như rất đẹp OO!
Và để trả lời câu hỏi của bạn về hằng số: Tôi thường sử dụng constexprhằng số. Cho dù đó là trong không gian tên chưa được đặt tên, các hằng số trong tiêu đề riêng biệt hoặc các hằng số tĩnh trong lớp phụ thuộc vào trường hợp sử dụng.