4
\$\begingroup\$

I started learning programming about a year ago, starting with Python. Half a year ago, I moved onto C++ and this is my first large project with that language. Have I understood the basics of the language?

In Main.cpp:

#pragma warning(disable : 4996)
#pragma warning(disable : 4244)

#include <memory>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#include <SFML\Graphics.hpp>
#include "Snake.h"
#include "SnakeFood.h"
#include "HighScoreFile.h"
class Snake;
class SnakeFood;
class HighScoreFile;

void displayScores(sf::RenderWindow& window, HighScoreFile& highScorefile, int score, const sf::Font& font);
void displayNewBest(sf::RenderWindow& window, const sf::Font& font);
bool playAgain(sf::RenderWindow& window);
std::unique_ptr<sf::Font> newFont(std::string&& fileName);
std::unique_ptr<sf::SoundBuffer> newSoundBuffer(std::string&& fileName);

int main()
{
    srand(time(NULL));

    static auto scoredSoundBuffer = newSoundBuffer("Sound Effects\\Scored.wav");
    static auto celebrationSoundbuffer = newSoundBuffer("Sound Effects\\Celebration.wav");
    static auto defeatSoundBuffer = newSoundBuffer("Sound Effects\\Defeat.wav");
    static auto startupSoundBuffer = newSoundBuffer("Sound Effects\\Startup.wav");
    sf::Sound scoredSoundEffect{ *scoredSoundBuffer };
    sf::Sound celebrationSoundEffect{ *celebrationSoundbuffer };
    sf::Sound defeatSoundEffect{ *defeatSoundBuffer };
    sf::Sound startupSoundEffect{ *startupSoundBuffer };
    scoredSoundEffect.setVolume(30.f);
    celebrationSoundEffect.setVolume(30.f);
    defeatSoundEffect.setVolume(30.f);
    startupSoundEffect.setVolume(30.f);

    static auto gameTextFont = newFont("Arcade Classic.ttf");

    sf::RenderWindow window(sf::VideoMode(665, 595), "Snake", sf::Style::Close | sf::Style::Titlebar);

    while (true)
    {
        Snake snake{};
        SnakeFood food{ window, snake };
        int score{ 0 };
        HighScoreFile highScoreFile{ "high-score-file.txt" };
        
        startupSoundEffect.play();

        while (window.isOpen())
        {
            sf::Event event;
            while (window.pollEvent(event))
            {
                switch (event.type)
                {
                case sf::Event::KeyPressed:

                    switch (event.key.code)
                    {
                    case sf::Keyboard::W:
                    case sf::Keyboard::Up:
                        snake.changeDirection(Direction::Up);
                        break;
                    case sf::Keyboard::S:
                    case sf::Keyboard::Down:
                        snake.changeDirection(Direction::Down);
                        break;
                    case sf::Keyboard::A:
                    case sf::Keyboard::Left:
                        snake.changeDirection(Direction::Left);
                        break;
                    case sf::Keyboard::D:
                    case sf::Keyboard::Right:
                        snake.changeDirection(Direction::Right);
                        break;
                    }
                    break;

                case sf::Event::Closed:
                    exit(0);
                    break;


                default:
                    //No need to handle unrecognised events
                    break;
                }
            }

            snake.checkIfOutOfBounds(window);
            snake.move();

            if (snake.isTouchingFood(food))
            {
                scoredSoundEffect.play();
                snake.grow();
                score++;
                food.setToRandomPosition(window, snake);
            }

            window.clear(sf::Color::Black);

            snake.drawBody(window);
            window.draw(food);

            sf::Text scoreText{ std::to_string(score), *gameTextFont, 30 };
            scoreText.setPosition(10.f, 5.f);

            window.draw(scoreText);;

            window.display();

            if (snake.isTouchingSelf())
            {
                if (score > highScoreFile.getHighScore())
                {
                    celebrationSoundEffect.play();

                    displayNewBest(window, *gameTextFont);

                    highScoreFile.editHighScore(score);

                    _sleep(1500);
                }
                else
                {
                    defeatSoundEffect.play();
                }
                _sleep(1000);
                displayScores(window, highScoreFile, score, *gameTextFont);
                if (!playAgain(window))
                {
                    exit(0);
                }
                break;
            }

        }
    }
    return 0;
}


void displayScores(sf::RenderWindow& window, HighScoreFile& highScoreFile, int score, const sf::Font& font)
{
    window.clear(sf::Color::Black);

    sf::Text scoreText{ "SCORE: " + std::to_string(score), font, 90 };
    if (score < 10)
    {
        scoreText.setPosition(85.f, 85.f);
    }
    else
    {
        scoreText.setPosition(55.f, 85.f);
    }
    //scoreText.setPosition(85.f, 85.f);
    scoreText.setFillColor(sf::Color::Green);

    sf::Text highScoreText{ "HI SCORE: " + std::to_string(highScoreFile.getHighScore()), font, 80 };
    highScoreText.setFillColor(sf::Color::Green);
    if (highScoreFile.getHighScore() < 10)
    {
        highScoreText.setPosition(40.f, 375.f);
    }
    else
    {
        highScoreText.setPosition(10.f, 375.f);
    }
    //highScoreText.setPosition(40.f, 375.f);
    window.draw(scoreText);
    window.draw(highScoreText);
    window.display();
}

void displayNewBest(sf::RenderWindow& window, const sf::Font& font)
{
    sf::Text newBest{ "NEW BEST!", font, 75 };
    newBest.setPosition(110.f, 250.f);
    newBest.setFillColor(sf::Color::Red);
    window.draw(newBest);
    window.display();
}

bool playAgain(sf::RenderWindow& window)
{
    while (true)
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
            {
                window.close();
            }
            switch (event.key.code)
            {
            case sf::Keyboard::Q:
                return false;
                break;
            case sf::Keyboard::Z:
                return true;
                break;
            default:
                //No need to handle unrecognised events
                break;
            }
        }
    }
}


std::unique_ptr<sf::Font> newFont(std::string&& fileName)
{
    auto font = std::make_unique<sf::Font>();
    if (!font->loadFromFile(fileName))
    {
        exit(0);
    }
    return font;

}

std::unique_ptr<sf::SoundBuffer> newSoundBuffer(std::string&& fileName)
{
    auto buffer = std::make_unique<sf::SoundBuffer>();
    if (!buffer->loadFromFile(fileName))
    {
        exit(0);
    }
    return buffer;
}

In SnakeRect.h:

#pragma once
#include <SFML\Graphics.hpp>

enum class Direction { Left, Right, Up, Down };

class SnakeRect : public sf::RectangleShape 
{
    using RectangleShape::RectangleShape;
public:
    SnakeRect(Direction dir);
    Direction direction() const;
    Direction oppositeDirection() const;
private:
    Direction direction_;
};

In SnakeRect.cpp:

#include "SnakeRect.h"

SnakeRect::SnakeRect(Direction dir) : RectangleShape{}, direction_{ dir }
{
}

Direction SnakeRect::direction() 
const {
    return direction_;
}

Direction SnakeRect::oppositeDirection()
const {
    switch (direction_)
    {
    case Direction::Up:
        return Direction::Down;
        break;
    case Direction::Down:
        return Direction::Up;
        break;
    case Direction::Right:
        return Direction::Left;
        break;
    case Direction::Left:
        return Direction::Right;
        break;
    default:
        break;
    }
}

In Snake.h:

#pragma once
#include <vector>
#include <SFML/Audio.hpp>
#include "SnakeRect.h"
#include "SnakeFood.h"

class Snake 
{
public:
    Snake();
    Snake(sf::Vector2f startingPos, Direction startingDir);

    bool isTouchingFood(const SnakeFood& food);
    bool isTouchingSelf();
    void move();
    void changeDirection(Direction dir);
    void checkIfOutOfBounds(const sf::RenderWindow& window);
    void grow();
    void drawBody(sf::RenderWindow& window);

    friend class SnakeFood;
private:
    std::vector<SnakeRect> body_;

    static const float thickness;
    static const float speed;
    static const sf::Color color;
    static const float startingLength;
    static const sf::Vector2f defaultStartingPos;
    static const Direction defaultStartingDir;
};

In Snake.cpp:

#pragma warning(disable : 4996)

#include <chrono>
#include "Snake.h"

const float Snake::thickness{ 35.f };
const float Snake::speed{ 35.f };
const sf::Color Snake::color{ sf::Color::Green };
const float Snake::startingLength{ 3.f };
const sf::Vector2f Snake::defaultStartingPos{280.f, 280.f};
const Direction Snake::defaultStartingDir{Direction::Right};

Snake::Snake() : Snake{defaultStartingPos, defaultStartingDir}
{
}

Snake::Snake(sf::Vector2f startingPos, Direction startingDir)
{
    SnakeRect newRect{ startingDir };
    newRect.setSize(sf::Vector2f(startingLength*speed, (float)thickness));
    newRect.setPosition(startingPos);
    newRect.setFillColor(color);
    body_.push_back(newRect);
}

bool Snake::isTouchingFood(const SnakeFood& food)
{
    const SnakeRect& frontRect{ (body_.at(body_.size() - 1)) };
    return (frontRect.getGlobalBounds().intersects(food.getGlobalBounds()));
}

bool Snake::isTouchingSelf()
{
    SnakeRect& frontRect{ body_.at(body_.size() - 1) };
    for (auto it = body_.begin(); it != std::prev(body_.end()); it++)
    {
        if (frontRect.getGlobalBounds().intersects(it->getGlobalBounds()))
        {
            return true;
        }
    }
    return false;
}

void Snake::move() 
{
    SnakeRect& backRect{ body_.at(0) };
    SnakeRect& frontRect{ body_.at(body_.size() - 1) };
    for (int i{ 0 }; i < 2; i++) {
        SnakeRect& currentRect{ (i == 0) ? backRect : frontRect };
        float modifier{ (i == 0) ? -(float)speed : (float)speed };

        switch (currentRect.direction())
        {

        case Direction::Up:
            currentRect.setSize(sf::Vector2f(currentRect.getSize().x, (currentRect.getSize().y) + modifier));
            currentRect.move(0, (i == 1) ? -modifier : 0);
            break;

        case Direction::Down:
            currentRect.setSize(sf::Vector2f(currentRect.getSize().x, (currentRect.getSize().y) + modifier));
            currentRect.move(0, (i == 0) ? fabs(modifier) : 0);
            break;

        case Direction::Left:
            currentRect.setSize(sf::Vector2f((currentRect.getSize().x) + modifier, currentRect.getSize().y));
            currentRect.move((i == 1) ? -modifier : 0, 0);
            break;

        case Direction::Right:
            currentRect.setSize(sf::Vector2f((currentRect.getSize().x) + modifier, currentRect.getSize().y));
            currentRect.move((i == 0) ? fabs(modifier) : 0, 0);
            break;

        default:
            //Will never execute since Direction is an enum
            break;

        }
    }
    if (backRect.getSize().x <= 0 || backRect.getSize().y <= 0)
    {
        body_.erase(body_.begin() + 0);
    }
    _sleep(150);
}

void Snake::changeDirection(Direction dir) 
{
    SnakeRect frontRect{ body_.at(body_.size() - 1) };
    float frontRectX{ frontRect.getPosition().x };
    float frontRectY{ frontRect.getPosition().y };
    if (dir != frontRect.direction() && dir != frontRect.oppositeDirection())
    {
        float xPosition{};
        float yPosition{};
        switch (frontRect.direction()) //Can shorten this down, will look into it
        {
        case Direction::Up:
            xPosition = (dir == Direction::Left ? frontRectX : frontRectX + (float)thickness);
            yPosition = frontRectY;
            break;

        case Direction::Down:
            xPosition = (dir == Direction::Left ? frontRectX : frontRectX + float(thickness));
            yPosition = frontRectY + frontRect.getSize().y - (float)thickness;
            break;

        case Direction::Right:
            xPosition = frontRectX + frontRect.getSize().x - (float)thickness;
            yPosition = (dir == Direction::Up ? frontRectY : frontRectY + (float)thickness);
            break;

        case Direction::Left:
            xPosition = frontRectX;
            yPosition = (dir == Direction::Up ? frontRectY : frontRectY + (float)thickness);
            break;

        default:
            break;
            //Will never execute
        }
        float xSize{ (dir == Direction::Up || dir == Direction::Down) ? (float)thickness : 0.f };
        float ySize{ (dir == Direction::Up || dir == Direction::Down) ? 0.f : (float)thickness };
        SnakeRect newRect{dir};
        newRect.setSize(sf::Vector2f(xSize, ySize));
        newRect.setPosition(xPosition, yPosition);
        newRect.setFillColor(sf::Color::Green);

        body_.push_back(newRect);
    }
}

void Snake::checkIfOutOfBounds(const sf::RenderWindow& window) 
{
    

    const SnakeRect& frontRect{ body_.at(body_.size() - 1) };

    float xPositionWithSize{ frontRect.getPosition().x + frontRect.getSize().x };
    float yPositionWithSize{ frontRect.getPosition().y + frontRect.getSize().y };

    bool isLeft{ frontRect.direction() == Direction::Left };
    bool isRight{ frontRect.direction() == Direction::Right };
    bool isUp{ frontRect.direction() == Direction::Up };
    bool isDown{ frontRect.direction() == Direction::Down };
    
    bool xOutOfBounds{ (frontRect.getPosition().x - (isLeft ? (float)speed : 0.f)) < 0 || xPositionWithSize + (isRight ? (float)speed : 0.f) > window.getSize().x };
    bool yOutOfBounds{ (frontRect.getPosition().y - (isUp ? (float)speed : 0.f)) < 0 || yPositionWithSize + (isDown ? (float)speed : 0.f) > window.getSize().y };
    
    
    if (xOutOfBounds || yOutOfBounds)
    {
        SnakeRect newRect{frontRect.direction()};
        newRect.setFillColor(sf::Color::Green);
        sf::Vector2f newRectSize{};
        sf::Vector2f newRectPos{};
        switch (frontRect.direction()) {
        case Direction::Up:
            newRectSize = sf::Vector2f((float)thickness, 0.f);
            newRectPos = sf::Vector2f(frontRect.getPosition().x, (float)window.getSize().y);
            break;
        case Direction::Down:
            newRectSize = sf::Vector2f((float)thickness, 0.f);
            newRectPos = sf::Vector2f(frontRect.getPosition().x, 0.f);
            break;
        case Direction::Right:
            newRectSize = sf::Vector2f(0.f, (float)thickness);
            newRectPos = sf::Vector2f(0.f, frontRect.getPosition().y);
            break;
        case Direction::Left:
            newRectSize = sf::Vector2f(0.f, (float)thickness);
            newRectPos = sf::Vector2f((float)window.getSize().x, frontRect.getPosition().y);
            break;
        default:
            break;
        }
        newRect.setSize(newRectSize);
        newRect.setPosition(newRectPos);
        body_.push_back(newRect);
    }
}

void Snake::grow()
{

    SnakeRect& backRect{ body_.at(0) };

    switch (backRect.direction())
    {

    case Direction::Up:
        backRect.setSize(sf::Vector2f(backRect.getSize().x, (backRect.getSize().y) + (float)speed));
        break;

    case Direction::Down:
        backRect.setSize(sf::Vector2f(backRect.getSize().x, (backRect.getSize().y) + (float)speed));
        backRect.move(0, -(float)speed);
        break;

    case Direction::Left:
        backRect.setSize(sf::Vector2f((backRect.getSize().x) + (float)speed, backRect.getSize().y));
        break;

    case Direction::Right:

        backRect.setSize(sf::Vector2f((backRect.getSize().x) + (float)speed, backRect.getSize().y));
        backRect.move(-(float)speed, 0);
        break;

    default:
        //Will never execute since Direction is an enum
        break;
    }
}

void Snake::drawBody(sf::RenderWindow& window)
{
    for (const SnakeRect& rect : body_)
    {
        window.draw(rect);
    }
}

In SnakeFood.h:

#pragma once
#include <SFML\Graphics.hpp>
class Snake;
class SnakeFood : public sf::RectangleShape {
    using RectangleShape::RectangleShape;
public:
    SnakeFood(const sf::RenderWindow& window, const Snake& snake);

    bool isTouching(const Snake& snake);
    void setToRandomPosition(const sf::RenderWindow& window, const Snake& s);
};

In SnakeFood.cpp:

#include "SnakeFood.h"
#include "Snake.h"
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

SnakeFood::SnakeFood(const sf::RenderWindow& window, const Snake& snake) : RectangleShape{}
{
    setSize(sf::Vector2f(15.f, 15.f));
    setFillColor(sf::Color::Red);
    setToRandomPosition(window, snake);
}

void SnakeFood::setToRandomPosition(const sf::RenderWindow& window, const Snake& snake) 
{
    do
    {
        float xPosition, yPosition;
        xPosition = float(rand() % (window.getSize().x - int(getSize().x)));
        yPosition = float(rand() % (window.getSize().y - int(getSize().y)));
        setPosition(xPosition, yPosition);
    } while (isTouching(snake));
    
}

bool SnakeFood::isTouching(const Snake& s)
{
    for (const SnakeRect& rect : s.body_)
    {
        if (rect.getGlobalBounds().intersects(this->getGlobalBounds()))
        {
            return true;
        }
    }
    return false;
}

In HighScoreFile.h:

#pragma once
#include <fstream>
class HighScoreFile
{
public:
    HighScoreFile(std::string fileName);
    int getHighScore();
    void editHighScore(int score);
private:
    const std::string highScoreFileName_;
    std::fstream highScoreFile_;
};

In HighScoreFile.cpp:

#include "HighScoreFile.h"

HighScoreFile::HighScoreFile(const std::string fileName)
: highScoreFileName_{fileName}
{
}

int HighScoreFile::getHighScore()
{
    highScoreFile_.open(highScoreFileName_, std::ios::in);
    if (!highScoreFile_){ exit(0); }

    int highScore{};
    highScoreFile_ >> highScore;
    highScoreFile_.close();

    return highScore;
}

void HighScoreFile::editHighScore(int score)
{
    highScoreFile_.open(highScoreFileName_, std::ios::out, std::ios::trunc);
    if (!highScoreFile_) { exit(0); }
    
    highScoreFile_ << score;
    highScoreFile_.close();
}
```
\$\endgroup\$
  • \$\begingroup\$ Why did you disable these warning numbers? What exactly are they telling you? \$\endgroup\$ – πάντα ῥεῖ Aug 9 at 11:06
  • \$\begingroup\$ @πάνταῥεῖ One of them just warned me about the loss of data occouring with srand(time(NULL)), and the other warned me about using _sleep() but I have read online that using _sleep() works just fine. So I figured I didn't need those warnings \$\endgroup\$ – JensB Aug 9 at 11:20
  • \$\begingroup\$ Well, there are casts and std::thread::sleep; fixing warnings is always better than disabling them. It also makes the code unportable, not all c++ compilers use the same warning numbers. \$\endgroup\$ – πάντα ῥεῖ Aug 9 at 11:57
  • \$\begingroup\$ @πάνταῥεῖ Okay, got it. I'll remove the warning disablers and fix the actual warnings. Thanks! \$\endgroup\$ – JensB Aug 9 at 13:04
5
\$\begingroup\$

1) Remove unused headers

You don't need stdlib.h and stdio.h. These are C headers, and you'll rarely use them in C++ (and if you need to use them, use cstdlib and cstdio). Similarly, don't use time.h in C++; C++ provides much better functionality in the form of the chrono library.

2)Forward declarations

You don't need to forward declare your classes, since you're already including them.

3) Random number generation

Don't use srand and rand. These are C methods for random number generations, and truthfully aren't that random at all. Prefer using random library provided by the STL.

4) Static

Your use of static in the main method doesn't make sense, since main is not a function you will be calling repeatedly.

5) while(true)

The while(true) doesn't make any sense; it's not doing anything. You can safely remove it from the code.

6) Don't use exit

I suspect you're using exit because the outer infinite loop; once you've removed the loop, you should use window.Close() method. This exits the game loop, and allows you to do any resource cleanup or post game-loop activity.

7) Separate simulation and render logic

Your simulation and render logic are interspersed together. You first check if the snake is in contact with the food, then render the frame, and then check if the snake is biting itself. Ideally, you'd want the simulation and render logic grouped together, possibly as separate functions.

8) Use std::this_thread::sleep_for instead of _sleep.

9) Call sf::display only once per frame.

You have multiple display calls per frame. You only want to call display once per frame, after you've sent all data to be displayed by using sf::draw.

10) playAgain

playAgain can be consolidated into the main game loop, instead of running a separate infinite loop. Just something for you to look into.

11) Better error messages

Suppose your newFont methods cannot find the font. It just silently exits. The developer has no idea what happened. Instead, provide the developer with a complete error message explaining what failed. Something like "Unable to allocate font: <font_path>". This allows the developer to fix the issue. Better yet, have a backup font in case font allocation fails; this allows the game to run even if it can't find the font.

12) You don't need a break statement in the switch body if you're returning a value.

13) static data members in Snake

The use of static data members in the Snake class binds all instances to a particular configuration for Snake. If I want to have multiple snakes (I don't know; maybe you're creating a local multiplayer version), each with different colors or thickness, I'm out of luck. Consider making them instance data members.

14) SnakeFood::isTouching() should be const. Similarly, Snake::isTouchingFood and Snake::isTouchingSelf should be const.

15) body.begin() + 0 is the same as body.begin().

16) General advice

One way you can improve your design is to have snake contain a simulate or update method, which simulate the snake i.e. moving, checking if out of bounds, check if eating the food or biting itself; then inside your game loop, you can simply do snake.simulate(), it's much cleaner code.

Learn to use STL features, instead of C library features; the former is much more robust than the latter.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you so much for taking the time to go through my code! Just a few questions about 4 and 5.The reason I declared the sound buffers and fonts with static is because I wanted to make sure that they would be destructed when I called exit(). I was afraid that the data allocated by std::make_unique() wouldn't be deallocated otherwise. But if I remove exit() they don't need to be static anymore. Also, are you sure that I don't need the while(true) loop? If playAgain() returns true then we break out of the window loop, and the while(true) restarts. Are you saying I should do this some other way? \$\endgroup\$ – JensB Aug 10 at 8:03
  • \$\begingroup\$ 4) In a normal program flow, the memory is deallocated when the scope exits. In an abnormal program flow (such as exit), the memory is claimed back by the operating system. Regardless, all heap allocations are deallocated when the program finishes. 5) I'm sure there are better ways. One thing you might look into is the concept of game states. Each game states defines a separate scene (such as Main Menu, Playing, Paused, Game Finished), with each state managing their own resources and inputs. Here's a good introduction: gamedevgeek.com/tutorials/managing-game-states-in-c \$\endgroup\$ – Rish Aug 10 at 17:00
  • \$\begingroup\$ So you're saying that the unique_ptr's will be destructed when I call exit regardless of whether they're static or not? (Will look into game states) \$\endgroup\$ – JensB Aug 10 at 17:48
  • \$\begingroup\$ The destructor will not be called. Here's a quick example: godbolt.org/z/9KvW3d In the call to exit, the memory held by the unique_ptr is claimed by the operating system, even if it is not released by the object. See this: stackoverflow.com/questions/2975831/… \$\endgroup\$ – Rish Aug 10 at 18:23
  • \$\begingroup\$ But if allocated memory gets claimed by the operating system regardless of whether or not delete has been called on the pointer, how do memory leaks happen? \$\endgroup\$ – JensB Aug 13 at 9:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.