Search code examples
c++sdl-2

SDL2 C++ Snake Game Self Collision


As a school project, I've made the classic snake game using SDL2 and C++. I've already implemented the growing, moving features for the Snake but it was required to make the movement based on a grid, but when I implemented the grid feature, the self-collision was always triggering whenever grow one part, so every time I start the game, and eat the first fruit, the snake dies.

I've been trying for a while now, from placing a delay to the adding of the tail and delaying the collision check, but to no avail, it's always colliding with itself even though it is not.

I can't see what is wrong with the self collision, I would gladly appreciate it if someone can point out what's wrong.

Snake.h

#include "GameObject.h"
#include "common.h"
#include "draw.h"
#include "Food.h"
#include "util.h"
#include <vector>

struct Segment {

    int x;
    int y;
    Segment(int posx, int posy) {

        x = posx;
        y = posy;

    }

};

class Snake :
    public GameObject

{
public:

    ~Snake();
    void start();
    void update();
    void draw();

    void outOfBoundsCheck();
    void move();
    void addSegment(int x, int y);
    void selfCollisionCheck(bool hasEaten);
    void setHasMoved(bool a);
    void setIsAlive(bool a);

    
    int getX();
    int getY();
    int getWidth();
    int getHeight();
    bool getIsAlive();
    bool getHasMoved();
    
    std::vector<Segment*> const& getV() const;


private:

    std::vector<Segment*> body;
    SDL_Texture* headTexture;
    SDL_Texture* bodyTexture;
    
    int x;
    int y;
    int width;
    int height;
    int dx;
    int dy;
    int tempX;
    int tempY;
    bool isAlive;
    bool hasMoved;
    
};

Snake.cpp


Snake::~Snake()
{
    
}

void Snake::start()
{
    // Load Texture
    headTexture = loadTexture("gfx/player.png");
    bodyTexture = loadTexture("gfx/body.png");
    
    tempX = 0;
    tempY = 0;

    x = 0;
    y = 0;
    
    dx = 0;
    dy = 0;

    isAlive = true;
    hasMoved = false;

    width = 0;
    height = 0;

    SDL_QueryTexture(headTexture, NULL, NULL, &width, &height);
    
    addSegment(x, y);
}

void Snake::update()
{   
        std::cout << "Head" << body[0]->x << std::endl;
        if (body.size() > 1) {
            std::cout << "2nd Segment" << body[1]->x << std::endl;
            if (body.size() > 2) {
                std::cout << "3nd Segment" << body[2]->x << std::endl;
            }
        }

        move();
        outOfBoundsCheck(); 
}

void Snake::draw()
{
    if (!isAlive) return; // Cancel the render if player dies
    
    for (int i = 0; i < body.size(); i++) {
        
        blit(headTexture, body[i]->x, body[i]->y);
    }
    
}

void Snake::outOfBoundsCheck()
{
    for (int i = 0; i < body.size(); i++) {
        if (body[i]->x > SCREEN_WIDTH) {

            body[i]->x = 0;

        }
        if (body[i]->x < 0) {

            body[i]->x = SCREEN_WIDTH;

        }

        if (body[i]->y > SCREEN_HEIGHT) {

            body[i]->y = 0;

        }

        if (body[i]->y < 0) {

            body[i]->y = SCREEN_HEIGHT;

        }
    }
}

void Snake::move()
{
    
    if (app.keyboard[SDL_SCANCODE_W] && dy != 5) {
        
            dx = 0;
            dy = -5;
        
    }

    if (app.keyboard[SDL_SCANCODE_A] && dx != 5) {
        
            dx = -5;
            dy = 0;
            
    }

    if (app.keyboard[SDL_SCANCODE_S] && dy != -5) {
        
            dx = 0;
            dy = 5;
            
    }

    if (app.keyboard[SDL_SCANCODE_D] && dx != -5) {
        
            dx = 5;
            dy = 0;
            
    }

    Segment* snakeHead = *(body.begin()); //Grid

    tempX += dx;
    tempY += dy;

    if (tempX % 25 == 0) {
        snakeHead->x += tempX;
        tempX = 0;
    }

    if (tempY % 25 == 0) {
        snakeHead->y += tempY;
        tempY = 0;
    }


    for (int i = body.size() - 1; i > 0; i--) { //For the other parts to follow

        body[i]->x = body[i - 1]->x;
        body[i]->y = body[i - 1]->y;

    }

    
}

void Snake::addSegment(int x, int y)
{
    Segment* seg = new Segment(x, y );
    body.push_back(seg);
}

void Snake::selfCollisionCheck(bool hasEaten) // Fail
{
    Segment* head = body[0];

    if (hasEaten == false) {
        for (int i = 1; i < body.size(); i++) {
            if (head->x == body[i]->x && head->y == body[i]->y) {
                isAlive = false;
                break;
            }
        }
    }

    else {
        return;
    }
    
}

void Snake::setHasMoved(bool a)
{
    hasMoved = a;
}


void Snake::setIsAlive(bool a)
{
    isAlive = a;
}

int Snake::getX()
{
    return x;
}

int Snake::getY()
{
    return y;
}

int Snake::getWidth()
{
    return width;
}

int Snake::getHeight()
{
    return height;
}

bool Snake::getIsAlive()
{
    return isAlive;
}

bool Snake::getHasMoved()
{
    return hasMoved;
}


std::vector<Segment*> const& Snake::getV() const
{
    // TODO: insert return statement here
    return body;
}

GameScene.h

#include "Scene.h"
#include "GameObject.h"
#include "Snake.h"
#include "Food.h"
#include "util.h"
#include "text.h"
#include "SoundManager.h"

class GameScene : public Scene
{
public:

    GameScene();
    ~GameScene();
    void start();
    void draw();
    void update();

    void spawnFood();
    void collisionLogic();
    void selfCollision();
    void despawnFood(Food* food);

private:

    Snake* snake;
    Food* food;

    int points;

    std::vector<Food*> spawnedFood;
    
};

GameScene.cpp

#include "GameScene.h"

GameScene::GameScene()
{
    // Register and add game objects on constructor
    snake = new Snake();
    this->addGameObject(snake);

    points = 0;
}

GameScene::~GameScene()
{
    delete snake;
    delete food;
}

void GameScene::start()
{
    Scene::start();
    // Initialize any scene logic here
    initFonts();
    spawnFood();
    
}

void GameScene::draw()
{
    Scene::draw();

    drawText(110, 20, 255, 255, 255, TEXT_CENTER, "POINTS: %03d", points);

    if (snake->getIsAlive() == false) {

        drawText(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 255, 255, 255, TEXT_CENTER, "GAME OVER!");
    }


}

void GameScene::update()
{
    Scene::update();

    if (spawnedFood.size() == 0 && spawnedFood.size() != 1) {
            spawnFood();
    }
    collisionLogic();
    selfCollision();
    
}

void GameScene::spawnFood()
{
    int random = rand() % 720;
    if (random % 25 != 0) {
        random = rand() % 720;
    }

    else {
        Food* food = new Food();
        this->addGameObject(food);

        food->setPosition(rand() % SCREEN_WIDTH, rand() % SCREEN_HEIGHT);

        spawnedFood.push_back(food);
    }
}

void GameScene::collisionLogic()
{
    Segment* head = snake->getV()[0];
    std::vector<Segment*> snakeBody = snake->getV();
    

    for (int i = 0; i < objects.size(); i++) {

        Food* food = dynamic_cast<Food*>(objects[i]);
    
        if (food != NULL) {

            int collision = checkCollision(
                head->x, head->y, snake->getWidth(), snake->getHeight(),
                food->getX(), food->getY(), food->getWidth(), food->getHeight()
            );

            if (collision == 1) {

                despawnFood(food);
                snake->addSegment(snakeBody[snakeBody.size() - 1]->x, snakeBody[snakeBody.size() - 1]->y); //Adds a part to the snake
                points++;

                break;

            }

        }

    }
}

void GameScene::selfCollision()
{
    std::vector<Segment*> body = snake->getV();
    Segment* head = snake->getV()[0];

    for (int i = 1; i < snake->getV().size(); i++) {
        if (head->x == body[i]->x && head->y == body[i]->y) {
                snake->setIsAlive(false);
                break;
        }
    }
    
}

void GameScene::despawnFood(Food* food)
{
    int index = -1;
    for (int i = 0; i < spawnedFood.size(); i++) {

        if (food == spawnedFood[i]) {

            index = i;
            break;

        }

    }

    if (index != -1) {

        spawnedFood.erase(spawnedFood.begin() + index);
        delete food;
    }
}

Solution

  • It seems that I had some logical errors when it comes to the grid movement because when I re-coded everything and changed the grid movement into cell based instead of using modulo condition by dividing the screen width and height to the pixel size of my snake and using that as the coordinates for my movement, everything went back to normal and the collision bug disappeared.

    Instead of doing this for the grid movement

    Old Grid Movement Code

        tempX += dx;
        tempY += dy;
    
        if (tempX % 25 == 0) {
            snakeHead->x += tempX;
            tempX = 0;
        }
    
        if (tempY % 25 == 0) {
            snakeHead->y += tempY;
            tempY = 0;
        }
     
    

    I defined this as a permanent value in my defs.h

    defs.h

    #define CELL_SIZE 25 // Size of the segment
    #define CELL_WIDTH SCREEN_WIDTH / CELL_SIZE
    #define CELL_HEIGHT SCREEN_HEIGHT / CELL_SIZE
    
    

    After that, since I'm still going to render the picture with the default resolution, I multiplied CELL_SIZE to the dest variable of my blit function

    draw.cpp

    void blit(SDL_Texture* texture, int x, int y)
    {
        SDL_Rect dest;
    
        dest.x = x * CELL_SIZE;
        dest.y = y * CELL_SIZE;
        SDL_QueryTexture(texture, NULL, NULL, &dest.w, &dest.h);
    
        SDL_RenderCopy(app.renderer, texture, NULL, &dest);
    }
    

    This results to the snake and any other thing that I'm going to render to follow a grid and by assigning the x and y values with the CELL_WIDTH and CELL_HEIGHT as substitution to the resolution, I accomplished the grid movement with no conflict with my collision check.