Deriving but keeping data

Started by
17 comments, last by Tallkotten 11 years, 7 months ago

What about something like this:

type Loader
{
def Image load(string name) { /* ... */ }


private Cache<Image> cache;
}

type Tile
{
def Tile(Loader loader)
{
image = loader.load("tile");
}


def draw(Screen screen)
{
screen.draw(image);
}

private Image image;
}

type Player
{
def Player(Loader loader)
{
image = loader.load("player");
}


def draw(Screen screen)
{
screen.draw(image);
}

private Image image;
}

def main()
{
Loader loader = new Loader();
Tile tile = new Tile(loader);
Player player = new Player(loader);


// ...

tile.draw(screen);
player.draw(screen);
}



Yeah i guess i'll have to go for something like that. Maybe smarter to use pointers thought. Since i want the image to change during the game.

Why is it bad to use a static function?
This is the simple example my dad drew up for me (makes me sound really young, I'm 19 if anybody cares)

class Loader
{

private static Loader myLoader;
public static &Loader files(); //loads all the files, so this is called when myLoader is created
Loader()
{
if(myLoader == NULL)
{
myLoader = new Loader();
myLoader.files();
return myLoader;
}
}
}


Nowadays he codes in c# so something might be wrong in the code and i haven't executed it to look for errors, but you get the idea.
Advertisement
return myLoader should be outside that IF I think; and it looks like the Singleton pattern.

It does work, but can cause complications in testing and multithreading in some cases.

return myLoader should be outside that IF I think; and it looks like the Singleton pattern.

It does work, but can cause complications in testing and multithreading in some cases.


So you'd rather recommend me assigning a pointer to the "loader" upon creation of the object?
IMO, the easiest way to solve this is to don't even worry about classes. You can use basic C-style functions to load your images and return the pointers to them in a generic Utility.cpp file, like this:


// in Utility.h define this
SDL_Surface GetImage(std::string imageName);

// in Utility.cpp, do this
std::map<std::string, SDL_Surface*> ImageMap;

SDL_Surface *GetImage(std::string imageName)
{
// check if this image is in the map
if (ImageMap.find(imageName) == std::map::end) {
// load it once into the map
ImageMap[imageName] = IMG_Load(imageName);
}
return ImageMap[imageName];
}

// Then, when you want to load an image, #include "Utility.h" and call
SDL_Surface *PlayerImage = GetImage("player.png");

You can put Utility into a Utility namespace if you want, but that's just another option.
Good Luck.

My Gamedev Journal: 2D Game Making, the Easy Way

---(Old Blog, still has good info): 2dGameMaking
-----
"No one ever posts on that message board; it's too crowded." - Yoga Berra (sorta)


IMO, the easiest way to solve this is to don't even worry about classes. You can use basic C-style functions to load your images and return the pointers to them in a generic Utility.cpp file, like this:


// in Utility.h define this
SDL_Surface GetImage(std::string imageName);

// in Utility.cpp, do this
std::map<std::string, SDL_Surface*> ImageMap;

SDL_Surface *GetImage(std::string imageName)
{
// check if this image is in the map
if (ImageMap.find(imageName) == std::map::end) {
// load it once into the map
ImageMap[imageName] = IMG_Load(imageName);
}
return ImageMap[imageName];
}

// Then, when you want to load an image, #include "Utility.h" and call
SDL_Surface *PlayerImage = GetImage("player.png");

You can put Utility into a Utility namespace if you want, but that's just another option.
Good Luck.


Thanks! Will that code only run once? No matter how many times i include the file?
No code runs when you include a file. C++ is compiled, which means that there is a tool which translates your code into something that will execute.

At runtime, the image will be loaded only once. This is because the first time a file name is referenced, the ImageMap has no entry with that name. Thus the test to determine if the entry is in the map will fail, and the image is then loaded and inserted into the map. Subsequent times, the ImageMap will have an entry with the name, so the existence test will pass and the early image will be returned.

Of course, care now needs to be taken of these shared surfaces. It would be incorrect to call SDL_FreeSurface() until you know that all other references to this surface are out of scope.

Of course, care now needs to be taken of these shared surfaces. It would be incorrect to call SDL_FreeSurface() until you know that all other references to this surface are out of scope.


This is true. In general, the example I gave, you would only call a FreeAllImages() (which loops through the ImageMap and call SDL_FreeSurface()) either at the end of the program, or possibly when changing levels and you know no-one is holding onto old images.

My Gamedev Journal: 2D Game Making, the Easy Way

---(Old Blog, still has good info): 2dGameMaking
-----
"No one ever posts on that message board; it's too crowded." - Yoga Berra (sorta)

To follow on from my last point, we need to control the lifetime of the objects in the map. This is especially important because the map is global, so it will be destroyed after main() ends. If you call SDL_Quit() during main, it is undefined whether you can safely call SDL_FreeSurface() on the pointers in the map.

This is one good reason to not use a global for this.

If we ignore that aspect for the moment, one way to achieve the cleaning up of the surfaces is to use smart pointers. Smart pointers try to automatically clean up after they are done. They are fairly clever, but you can confuse them in certain circumstances that won't arise in the context of this (if you're curious, they don't work with cyclical references).

If your compiler supports it, you can use the standard smart pointers std::shared_ptr or std::weak_ptr in the map. If your compiler doesn't support these new classes, consider upgrading! If you cannot or will not upgrade, the boost libraries can provide these classes for you.

Shared pointer is a really nice class that manages shared ownership of an interesting object. When the last owner object that needs an object is finished using it, the shared pointer will cleanup the owned object. The clients would store a shared_ptr. You can specify SDL_FreeSurface() as the "cleanup" function when you create the smart pointer. This is important, because shared_ptr defaults to trying to delete the object which was passed, which is incorrect for objects that were not allocated with new.

Something like this:

#include <map>
#include <string>
#include <memory>

#include "SDL.h"
#include "SDL_image.h"

typedef std::shared_ptr<SDL_Surface> SurfacePtr;
typedef std::map<std::string, SurfacePtr> SurfaceCache;
SurfaceCache surfaceCache;

SurfacePtr load(const std::string &name)
{
SurfaceCache::iterator i = surfaceCache.find(name);
if(i != surfaceCache.end())
{
return i->second;
}

SDL_Surface *loaded = IMG_Load(name.c_str());
if(loaded == NULL)
{
// TODO: handle this here?
}
SurfacePtr surface(loaded, &SDL_FreeSurface);
surfaceCache.insert(std::make_pair(name, surface));
return surface;
}

Note this code shows a reasonably idiomatic search for and add entries to a std::map. It avoids extra, unnecessary lookups in the best case. In constrast, BeerNutts's simpler code performs at least two, and worst case three searches for an element.

But we are still left with the situation I mentioned earlier, where the map is destroyed after main. In general, running complicated code like constructors and destructors outside main isn't a good idea. You cannot depend on exactly when these functions will be called relative to the rest of your program. If you aren't careful, it is easy to trigger undefined behaviour.

One solution is to manage the lifetime of the global in main(). main() creates the global and exports it in a controlled manner. Something like the following:

// image.h

#ifndef IMAGE_H
#define IMAGE_H

#include <map>
#include <string>
#include <memory>

#include "SDL.h"
#include "SDL_image.h"

typedef std::shared_ptr<SDL_Surface> SurfacePtr;
typedef std::map<std::string, SurfacePtr> SurfaceCache;

SurfacePtr load(const std::string &name);

#endif


// image.cpp

#include "image.h"

SurfaceCache &globalSurfaceCache();

SurfacePtr load(const std::string &name)
{
SurfaceCache &surfaceCache = globalSurfaceCache();

SurfaceCache::iterator i = surfaceCache.find(name);
if(i != surfaceCache.end())
{
return i->second;
}

SDL_Surface *loaded = IMG_Load(name.c_str());
if(loaded == NULL)
{
// TODO: handle this here?
}
SurfacePtr surface(loaded, &SDL_FreeSurface);
surfaceCache.insert(std::make_pair(name, surface));
return surface;
}


main.cpp

#include <iostream>
#include <cstdlib>

#include "image.h"

namespace {
static SurfaceCache *hiddenSurfaceCache = 0;
}

SurfaceCache &globalSurfaceCache() {
return *hiddenSurfaceCache;
}

int main(int, char**)
{
if(SDL_Init(SDL_INIT_VIDEO) < 0)
{
std::cerr << "Failed to initialise SDL: " << SDL_GetError() << '\n';
return 1;
}

std::atexit(&SDL_Quit);

SDL_Surface *screen = SDL_SetVideoMode(800, 600, 0, SDL_SWSURFACE);
if(!screen)
{
std::cerr << "Failed to set video mode: " << SDL_GetError() << '\n';
return 1;
}

SurfaceCache surfaceCache;
hiddenSurfaceCache = &surfaceCache;

bool running = true;
while(running)
{
// ...
}
hiddenSurfaceCache = 0;
return 0;
}

This offsets most of the damage of the global. The lifecycle of the cache is automatically controlled in main(), and the pointer is nullified before it is invalidated (by falling off the end of main). I've also hidden the declaration of globalSurfaceCache() inside image.cpp, so client code will not directly interact with it.
Thanks for the answers!!

About the lifetime. The images I'm loading are sheets (meaning there are several images per file i load). This enables me to load one file for all the different tiles instead of loading everyone individually.

I am planning on having them sorted kind of like this. inhouseTilesSheet which contains all the tiles to use in houses. Then I'll have the game check if there is a need for them to be loaded, if not they don't. So if i am just out and wandering I've only got loaded what that scene has in it. Hope i make myself clear, just woke up.
Going to have to re-read your response to fully grasp it later ;)

This topic is closed to new replies.

Advertisement