Search code examples
c++arraysobjectsdl

Garbage values when accessing SDL_Rect members from an array?


I've been following LazyFoo's SDL tutorials (and also adding my own organization and coding style). When I got to his animation tutorial I decided to make a separate class to store the variables and methods related to the animation algorithm, rather than having global variables. He uses an array of SDL_Rects to define the boundaries of different sprites on a sprite sheet, so I used an SDL_Rect pointer to store the array in my custom class. When I compiled everything I didn't see an animation, when I compiled the original source code I did. When I started debugging things, I found that when I was rendering the sprites, the rects were actually full of garbage, even though when I initialize them the rects are just fine. I've tried to simplify the problem so many times, but every approach I take to recreate the bug in a simpler environment actually works as expected! So with that in mind I apologize for the large amount of code, because I can't seem to reduce the problem.

texture.h

#ifndef TEXTURE_H
#define TEXTURE_H

#include <SDL2/SDL.h>
#include <string>

class Animation {
public:
    Animation(SDL_Renderer* renderer);
    ~Animation();

    void load(std::string path, int frames, SDL_Rect* clips),
         free(),
         render(int x, int y),
         next_frame();
private:
    SDL_Renderer* _renderer=NULL;
    SDL_Rect* _clips=NULL;
    SDL_Texture* _texture=NULL;
    int _frame=0, _frames=0, _width=0, _height=0;
};

#endif

texture.cpp

#include <stdio.h>
#include <SDL2/SDL_image.h>
#include "texture.h"
#include "error.h"

Animation::Animation(SDL_Renderer* renderer) {
    _renderer = renderer;
}

Animation::~Animation() {
    free();
    _renderer = NULL;
}

void Animation::load(std::string path, int frames, SDL_Rect* clips) {
    free();
    SDL_Texture* texture = NULL;

    SDL_Surface* surface = IMG_Load(path.c_str());
    if (!surface)
        throw ErrorIMG("Could not load image "+path);
    SDL_SetColorKey(surface, SDL_TRUE,
                    SDL_MapRGB(surface->format, 0, 0xFF, 0xFF));

    texture = SDL_CreateTextureFromSurface(_renderer, surface);
    if (!texture)
        throw ErrorSDL("Could not create texture from image "+path);

    _width = surface->w;
    _height = surface->h;
    SDL_FreeSurface(surface);

    _frames = frames;
    _clips = clips;
    printf("clips[%d]: w: %d h: %d\n", 0, _clips[0].w, _clips[0].h);
}

void Animation::free() {
    if (_texture) {
        SDL_DestroyTexture(_texture);
        _texture = NULL;
        _clips = NULL;
        _frames = 0;
        _frame = 0;
        _width = 0;
        _height = 0;
    }
}

void Animation::render(int x, int y) {
    SDL_Rect crect = _clips[_frame/4];
    printf("in render (clips[%d]): w: %d, h: %d\n", _frame/4, crect.w, crect.h);
    SDL_Rect render_space = {x, y, crect.w, crect.h};
    SDL_RenderCopy(_renderer, _texture, &_clips[_frame], &render_space);
}

void Animation::next_frame() {
    SDL_Rect crect = _clips[_frame/4];
    printf("in next frame (clips[%d]): w: %d, h: %d\n", _frame/4, crect.w, crect.h);
    ++_frame;
    if (_frame/4 >= _frames)
        _frame = 0;
}

game.h

#ifndef GAME_H
#define GAME_H

#include "texture.h"

class Game {
public:
    Game();
    ~Game();
    void main();
private:
    void load_media();

    SDL_Window* _window=NULL;
    SDL_Renderer* _renderer=NULL;
    Animation* _anim=NULL;

    const int SCREEN_WIDTH=640, SCREEN_HEIGHT=480;
};

#endif

game.cpp

#include <SDL2/SDL_image.h>
#include "game.h"
#include "error.h"

void Game::main() {
    load_media();

    bool has_quit = false;
    SDL_Event event;

    while (!has_quit) {
        while (SDL_PollEvent(&event))
            if (event.type == SDL_QUIT)
                has_quit = true;

        SDL_SetRenderDrawColor(_renderer, 0xff, 0xff, 0xff, 0xff);
        SDL_RenderClear(_renderer);

        _anim->render(100, 100);
        _anim->next_frame();
        SDL_RenderPresent(_renderer);
    }
}

Game::Game() {
    if (SDL_Init(SDL_INIT_VIDEO))
        throw ErrorSDL("SDL could not initialize");

    _window = SDL_CreateWindow("SDL Tutorial", SDL_WINDOWPOS_UNDEFINED,
                               SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH,
                               SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
    if (!_window)
        throw ErrorSDL("Window could not be created");

    Uint32 render_flags = SDL_RENDERER_ACCELERATED;
    render_flags |= SDL_RENDERER_PRESENTVSYNC;
    _renderer = SDL_CreateRenderer(_window, -1, render_flags);
    if (!_renderer)
        throw ErrorSDL("Renderer could not be created");
    SDL_SetRenderDrawColor(_renderer, 0xff, 0xff, 0xff, 0xff);

    if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG))
        throw ErrorIMG("SDL_image could not initialize");
}

Game::~Game() {
    delete _anim;

    SDL_DestroyRenderer(_renderer);
    SDL_DestroyWindow(_window);
    _renderer = NULL;
    _window = NULL;

    IMG_Quit();
    SDL_Quit();
}

void Game::load_media() {
    const int nclips = 4;
    SDL_Rect clips[nclips];

    for (int i=0; i < nclips; i++) {
        clips[i].x = i*64;
        clips[i].y = 0;
        clips[i].w = 64;
        clips[i].h = 164;
    }

    _anim = new Animation(_renderer);
    _anim->load("sheet.png", nclips, &clips[0]);
}

Solution

  • You're storing a pointer to a temporary. The SDL_Rect* clips pointer that you pass to Animation::load is assigned to the member _clips used after the function returns. For this to work correctly, the data that is pointed to needs to live for as long as the Animation class is using it. The problem arises here:

    void Game::load_media() {
        const int nclips = 4;
        SDL_Rect clips[nclips];
        ...
        _anim->load("sheet.png", nclips, &clips[0]);
    }
    

    In this piece of code, clips is a local variable. That means it gets destroyed at the end of load_media(), and the memory contents at that location will become garbage.

    There are a number of ways you could fix this. A simple one would be to use std::vector<SDL_Rect> instead of SDL_Rect*. std::vector can safely be copied and manages its internals for you. Your new code could look like:

    class Animation {
        ...
        std::vector<SDL_Rect> _clips;
        ...
    }
    
    
    void Animation::load(std::string path, int frames, std::vector<SDL_Rect> clips) {
        ...
        _clips = clips;
        ...
    }
    
    void Game::load_media() {
        const int nclips = 4;
        std::vector<SDL_Rect> clips;
        clips.resize(nclips);
        ...
        _anim->load("sheet.png", nclips, clips);
    }
    

    And dont forget to #include <vector>. Documentation for std::vector is here. Note that std::vector has a size() method that can probably replace frames everywhere it appears.