Skip to content

Pong-SFML-Game

Sau một vài tuần trải nghiệm C++ từ sách Book-Esenthel-2D-Basics thì tôi thấy làm game 2D sẽ phù hợp với kiến thức hiện tại. Ngoài ra Esenthel Engine thì có khá ít tài liệu nên tiếp phải dùng một library khác đó là SFML.

Introduction

Tôi đã học SFML được vài lần nhưng đã bỏ cuộc khi thấy quá nhàm chán khi tạo các game 2D theo sách. Tuy nhiên cuối cùng tôi vẫn phải quay lại với nó vì sẽ càng chán hơn nếu học C++ mà không có thực hành gì cả. Tôi sẽ cố gắng hoàn thành quyển sách này, vì đây là sách dạy C++ dựa trên làm game

Trong game này tôi sẽ học rõ hơn về OOP

Ý tưởng của Gane sẽ là các thanh có khả năng di chuyển hướng dọc hoặc ngang và phản ứng lại với quả banh khi chạm vào.

  • Thanh ngang có khả năng đi trái hoặc phải
  • Có speed
  • Có toạ độ

Tạo class

Tạo một class Bat có các chức năng như di chuyển, lấy vị trí

được gọi là Constructor functions

Header cho Bat

Tạo một header Bat.h

#pragma once
#include <SFML/Graphics.hpp>
//pragma once có nghĩa là chỉ đọc một lần từ compiler
using namespace sf; // viết tắt cho sf::function
// nhưng tốt nhất sử dụng sf:: để cho nhớ rõ function của từng thư viện


class Bat
{
private: // trong private là thuộc tính, ko gọi ra
    sf::Vector2f m_Position;// Tạo vector dạng float

    sf::RectangleShape m_Shape; // Tạo Shape hình chữ nhật

    float m_speed{ 1000.f };
    bool m_MovingRight{ false };
    bool m_MovingLeft{ false };
public: // Giới thiệu các functions sử dụng trong class Bat, gọi là constructor functions
    Bat(float startX, float startY); // Class bat sẽ tạo va variable gồm 2 variable là float
    sf::FloatRect getPosition();
    sf::RectangleShape getShape();

    void moveLeft();// thực hiện các lệnh bên trong void
    void moveRight();
    void stopLeft();
    void stopRight();

    void update(sf::Time dt);
};

Definition cho header Bat

Tạo một cpp file là Bat.cpp tương ứng cho header đã tạo Bat.h. Trong header này đã có constructor tức là khai báo cho các objects được tạo từ class Bat này.
Đó là trong public: Bat(float x, float y) nên trong cpp file cần phải định nghĩa vai trò của các arguement float x, float y

Bat::Bat(float startX, float startY)// đây là constructor
{
    m_Position.x = startX;// gán giá trị m_Position trong private bằng arguments của X và Y khi tạo một object từ class Bat
    m_Position.y = startY; // vì m_Position thuộc class Vector2f nên có (x,y)
    m_Shape.setSize(sf::Vector2f(50.f, 5.f));
    m_Shape.setPosition(m_Position);
}

Ngoài ra còn có các function khác trong class bat này cần được phải định nghĩa như moveLeft()

#include "Bat.h"

Bat::Bat(float startX, float startY)// đây là constructor
{
    m_Position.x = startX;// gán giá trị m_Position trong private bằng arguments của X và Y khi tạo một object từ class Bat
    m_Position.y = startY; // vì m_Position thuộc class Vector2f nên có (x,y)
    m_Shape.setSize(sf::Vector2f(50.f, 5.f));
    m_Shape.setPosition(m_Position);

}

sf::FloatRect Bat::getPosition()
{
    return m_Shape.getGlobalBounds();// cần phải cho ra 4  vị trí của Rect
}

sf::RectangleShape Bat::getShape()
{
    return m_Shape;
}

void Bat::moveLeft()
{
    m_MovingLeft = true;
}

void Bat::moveRight()
{
    m_MovingRight = true;
}

void Bat::stopLeft()
{
    m_MovingLeft = false;
}

void Bat::stopRight()
{
    m_MovingRight = false;
}

void Bat::update(sf::Time dt)
{
    if (m_MovingLeft) {
        m_Position.x = m_Position.x - m_speed * dt.asSeconds();
    }
    if (m_MovingRight) {
        m_Position.x += m_speed * dt.asSeconds();
    }
    m_Shape.setPosition(m_Position);

}

Check Main.cpp

File này sẽ kiểm soát toàn bộ các functions chính trong game, chính là game loop. Bây giờ sẽ sử dụng class Bat vừa tạo để có nhân vật là thanh hình chữ nhật, có khả năng di chuyển trái/phải

#include "Bat.h"
#include <sstream>
#include <cstdlib>
#include <SFML/Graphics.hpp>

int main() {
    sf::VideoMode vm(1280, 720);

    sf::RenderWindow window(vm, "Pong Game", sf::Style::Default);

    int score = 0;
    int lives = 3;

    // Sử dụng class Bat vừa tạo để tạo một object tên là bat
    Bat bat(1280 / 2, 720 - 20);// khai báo 2 arguments là toạ độ x và y

    // Tạo các text từ SF
    sf::Text hud;
    sf::Font font;
    font.loadFromFile("fonts/DS-DIGI.TTF");
    hud.setFont(font);
    // Make it nice and big
    hud.setCharacterSize(75);
    // Choose a color
    hud.setFillColor(Color::White);
    hud.setPosition(20, 20);

    // Sử dụng clock để xác định thời gian trong game
    sf::Clock clock;

    // Tạo một game loop để update các hoạt động trong game
    while (window.isOpen())
    {
        // Update các input từ keyboard

        sf::Event event; // tạo các event cho game
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed)window.close();
        }

        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Escape))window.close();
        // Update từ phản hồi của game
        if (Keyboard::isKeyPressed(Keyboard::Right)) {
            bat.moveRight();
        }
        else {
            bat.stopRight();
        }
        if (Keyboard::isKeyPressed(Keyboard::Left)) {
            bat.moveLeft();
        }
        else {
            bat.stopLeft();
        }

        // Update time, text in game
        Time dt = clock.restart();
        bat.update(dt);
        std::stringstream ss;
        ss << "Score: " << score << " Lives: " << lives;
        // Sử dụng stringstream để gán cả text và value vào một variable ss
        // Sau đó dùng sf::Text để print text ra game, nếu ko thì ra console
        hud.setString(ss.str()); // Sử dụng setString, chú ý ss phải sử dụng .str()

        // Draw
        window.clear();
        window.draw(hud);
        window.draw(bat.getShape());
        window.display();
    }
    return 0;
}

Hiện tại thì game chỉ có thể cho Pong đi qua lại trái /phải.

Ball class

Đây là class để tạo quả banh có khả năng phản hồi, xác định va chạm collision và tính điểm.
Cũng tương tự như Bat class, chỉ cần sử dụng Graphics class của SFML là đủ để vẽ các hình.
Các điểm chung như sau:

  • Trong private:
  • Có vị trí X, Y
  • Có khai báo Shape
  • Có thêm Direction X và Direction Y vì Ball có thể văng theo hệ toạ độ 2D
  • Trong public:
  • function getposition
  • getShape()
  • getVelocity()
  • rebound() phản hồi của ball khi va chạm
  • update() vị trí
#include "Bat.h"

Bat::Bat(float startX, float startY)// đây là constructor
{
    m_Position.x = startX;// gán giá trị m_Position trong private bằng arguments của X và Y khi tạo một object từ class Bat
    m_Position.y = startY; // vì m_Position thuộc class Vector2f nên có (x,y)
    m_Shape.setSize(sf::Vector2f(100.f, 10.f));
    m_Shape.setPosition(m_Position);

}

sf::FloatRect Bat::getPosition()
{
    return m_Shape.getGlobalBounds();// cần phải cho ra 4  vị trí của Rect
}

sf::RectangleShape Bat::getShape()
{
    return m_Shape;
}

void Bat::moveLeft()
{
    m_MovingLeft = true;
}

void Bat::moveRight()
{
    m_MovingRight = true;
}

void Bat::stopLeft()
{
    m_MovingLeft = false;
}

void Bat::stopRight()
{
    m_MovingRight = false;
}

void Bat::update(sf::Time dt)
{
    if (m_MovingLeft) {
        m_Position.x = m_Position.x - m_speed * dt.asSeconds();
    }
    if (m_MovingRight) {
        m_Position.x += m_speed * dt.asSeconds();
    }
    m_Shape.setPosition(m_Position);

}

main.cpp update Ball

Bây giờ áp dụng class ball vào cho main game

#include "Bat.h"
#include "Ball.h"
#include <sstream>
#include <cstdlib>
#include <SFML/Graphics.hpp>

int main() {
    sf::VideoMode vm(1280, 720);

    sf::RenderWindow window(vm, "Pong Game", sf::Style::Default);

    int score = 0;
    int lives = 3;

    // Sử dụng class Bat vừa tạo để tạo một object tên là bat
    Bat bat(1280 / 2, 720 - 20);// khai báo 2 arguments là toạ độ x và y
    Ball ball(1280 / 2, 0); // class ball tạo tại top màn hình

    // Tạo các text từ SF
    sf::Text hud;
    sf::Font font;
    font.loadFromFile("fonts/DS-DIGI.TTF");
    hud.setFont(font);
    // Make it nice and big
    hud.setCharacterSize(50);
    // Choose a color
    hud.setFillColor(Color::White);
    hud.setPosition(20, 20);

    // Sử dụng clock để xác định thời gian trong game
    sf::Clock clock;

    // Tạo một game loop để update các hoạt động trong game
    while (window.isOpen())
    {
        // Update các input từ keyboard

        sf::Event event; // tạo các event cho game
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed)window.close();
        }

        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Escape))window.close();
        // Update từ phản hồi của game
        if (Keyboard::isKeyPressed(Keyboard::Right)) {
            bat.moveRight();
        }
        else {
            bat.stopRight();
        }
        if (Keyboard::isKeyPressed(Keyboard::Left)) {
            bat.moveLeft();
        }
        else {
            bat.stopLeft();
        }

        //Phản hồi, di chuyển ball
        // Nếu rơi ra ngoài
        if (ball.getPosition().top > window.getSize().y) {
            lives--; // trừ 1 mạng
            ball.reboundBottom();// reset lại vị trí ball

            //Check nếu lives bị âm thì reset lại game
            if (lives < 1) {
                score = 0;
                lives = 3;
            }
        }
        // Nếu chạm vào top, được phản hồi và tăng điểm
        if (ball.getPosition().top <0 ) {
            ball.reboundBatOrTop();
            score++;
        }

        // Nếu bạn vào bat
        if (ball.getPosition().intersects(bat.getPosition())) {
            ball.reboundBatOrTop();
        }
        // Nếu chạm vào thành
        if (ball.getPosition().left <=0 || (ball.getPosition().left+ball.getShape().getSize().x >window.getSize().x)) {
            ball.reboundSides();
        }


        // Update time, text in game
        Time dt = clock.restart();
        bat.update(dt);
        ball.update(dt);

        // reset bat position if out of bound
        if (bat.getPosition().left + bat.getShape().getSize().x <=0) {
            bat.newPosition(window.getSize().x);
        }

        if (bat.getPosition().left >= window.getSize().x) {
            bat.newPosition(0.f);
        }

        std::stringstream ss;
        ss << "Score: " << score << " Lives: " << lives;
        // Sử dụng stringstream để gán cả text và value vào một variable ss
        // Sau đó dùng sf::Text để print text ra game, nếu ko thì ra console
        hud.setString(ss.str()); // Sử dụng setString, chú ý ss phải sử dụng .str()

        // Draw
        window.clear();
        window.draw(hud);
        window.draw(bat.getShape());
        window.draw(ball.getShape());
        window.display();
    }
    return 0;
}

Sản phẩm cuối cùng như sau:

The following pages link to this page:



Created : Mar 15, 2022