In this chapter we take the basic resource management framework we built in the last chapter and add a few more things to it. We will refine our code and make the framework more generic so we can read resources from archives and allow loading of resources via resource groups. (This will allow us to make our framework more data-driven in later chapters.) Let’s first begin by looking at what an archive is and where it fits in our framework. In it’s simplest form, an archive is a collection of files. In our case the files are resource files that are loaded and shared between game entities. If you consider our directory structure, the res folder could be considered as an archive since it contains all our resource files. Most games will often pack such files into single file called an archive file, or sometimes into a set of separate archive files for each kind of resource (images, sounds, etc). Archive files are not to be confused with simple binary files. Archive files maintain a directory structure and also contain metadata along with other information, and can (optionally) store data in a compressed format. You may have come across the terms “zip archive” or “rar archive” — they are in fact compressed archives.
We will first start off with our uncompressed res folder as an archive and build archive support into our framework. At the same time however, we will keep our design flexible enough to add support for compressed archives. So, let’s see how you can create a basic archive class.
class Archive(): '''Manages the loading of the resources from a archive.''' def __init__(self, archivePath): '''Creates an archive object. Note the archive is not loaded or created.''' self.path = archivePath def Load(self): '''Loads the archive.''' def Unload(self): '''Unloads the archive''' def Open(self, fileName, mode='r'): '''Opens the archive file for access. The function returns a file type object.''' def GetSize(self, fileName): '''Returns the size in bytes of the file inside the archive.'''
The Load function loads an archive. In case of a folder this function will probably do nothing except check for the existence of the archive path. For compressed/single file archives, this function will load the archive file. The Open function opens a file inside the archive and returns a Python file type object. You can read the contents of the file inside the archive via this object. If you recollect from the previous chapter, our ResourceManager’s LoadResource function should call the Open function of the archive since that is the place where resource loading takes place. For our FolderArchive the code should be —
class FolderArchive(Archive): '''Manages the loading of the resources from the disk drive folder.''' def Load(self): '''Loads the archive.''' if os.path.exists(self.path) == False: return False def Open(self, fileName, mode='rb'): '''Opens the archive file for access. The function returns a file type object.''' f = open(self.path + fileName, mode) return f def GetSize(self, fileName): ''' Returns the size in bytes of the file inside the archive.''' return os.path.getsize(self.path + fileName)
Pointing the FolderArchive class to the res folder allows the class to load the files under that folder. However we still would have to use a relative path to our resource if we wanted to load our files. If you recollect, we used relative file paths in the previous chapter for our resources. I find that kinda unintuitive. Imagine names like ‘../res/images/squares.png’ everywhere in your code. That’s just doesn’t look right. An ideal solution would be to defer the relative paths to the resource system. Once initiated with the correct path, you are free to use the resource names directly within your code. Well that’s pretty easy to do — we just have add a small function to our ResourceManager code –
def SetSubPath(self, archivePath): '''Sets a path (folder) inside the archive realtive to the archive root where the manager searches for resources.''' self.__archivePath = archivePath
Our archive already points to the res folder, so the relative path (subpath) of the manager will be ‘images/’. The code for both the archive and the manager should look something like this –
# create a FolderArchive pointing it to our resources. archive = FolderArchive('../res/') # create a resource manager for images. self.imgResMan = ImageResouceManager() # set the sub-path where our images are located relative the the archive. self.imgResMan.SetSubPath('images/')
This should allow us to point our resource manager to the correct folder and directly use resources using their resource names. This however, doesn’t solve the problem with resource loading. In the previous chapter we passed a list of resources to the Cache function of the ImageResource manager, and we did that in the Init function of the GameManager. That’s not quite the correct way. Games require hundreds of resources and you can’t possibly pollute the GameManager class with names of all the resources required. In fact in trying to solve one problem we created another — that of resource loading.
What we need to do is allow each sprite class to request the resources it needs and then make the ResourceManager cache all the requested resources. To do that, we introduce a new class called ResourceGroup. A ResourceGroup, as it’s names suggests, is a set of resources names that are passed to the ResourceManager’s Cache function. At it’s heart is a set (unordered list of unique names), which contains the resource names that we want the ResourceManager to cache. The flow goes something like this — after the creation of each sprite, the GameManager allows each of the sprites to request the resources it needs by appending their names to the resource group. The resources are cached by the ResourceManager and after doing that, the Create function of the each individual sprite is called. The ResourceGroup code is something like this —
class ResourceGroup(): '''This class is used to tell the ResourceManager which batches of resources need to be cached.''' def __init__(self, archive, name=''): '''Construct a resource group with a name.''' self.archive = archive self.resourceNames = set() self.name = name def __iadd__(self, resource): '''Adds a resource to the group.''' self.resourceNames.add(resource) return self def __isub__(self, resource): '''Removes a resource from the group.''' self.resourceNames.remove(resource) return self def Clear(self): '''Removes all the resources from the group.''' self.resourceNames[:] = []
The flow for the Init function of the GameManager is —
def Init(self): .... .... # create a FolderArchive pointing it to our resources. archive = resmgr.FolderArchive('../res/') # create a resource manager for images. self.imgResMan = resmgr.ImageResouceManager() # create a resource group for images imgResGroup = resmgr.ResourceGroup(archive) # create our sprites. self.sprites = [] self.sprites.append(gamesprites.SquareSprite()); .... # load the archive. archive.Load() # set the sub-path where our images are located relative the the archive. self.imgResMan.SetSubPath('images/') for sprite in self.sprites : # call Setup on our sprites and pass the resource group to the sprites # allowing them to request their resources. sprite.Setup(imgResGroup) # cache the requested image resources. self.imgResMan.Cache(imgResGroup) for sprite in self.sprites : # call Create on our sprites. sprite.Create(self.imgResMan) # attach the draw handler to our drawEvent.
I have added a Setup function to the sprite class and where the name of the resource is appended.
class SquareSprite(GameSprite): def Setup(self, resGroup): resGroup += 'squares.png' def Create(self,mgr): img = mgr['squares.png'] self.sprite = sf.Sprite(img) self.sprite.SetPosition(100.0, 10.0)
The entire flow of the resource system can seem a bit complicated and that’s why I have included a sequence diagram of the process I explained above. (Click on the fig to enlarge.)
If you find this a bit confusing at a first read, step through the code via a debugger and try and see what happens where. You can try loading additional images and adding a few other sprites just to get the hang of things. If you look at the code for this chapter, I have made subtle changes to included exceptions to correctly handle error conditions. That’s in accordance with standard Python practices. I hope there will no trouble understanding the use of exceptions and what role they play within our framework. I have also added support for a compressed zip archive (ZipArchive), which is a drop in replacement for our FolderArchive. You can experiment with that too.
We have now our resource system in place. We will be using this system in all our later chapters. In the next chapter we take a look a something which everyone has been patiently waiting for — Game Entities and Objects. From then on, we will build upon this framework and go on to advanced topics like transformations, collisions, graphs, render passes, collections, GUIs and finally put all of this into a small working game.
One response to “A Dash of Game Development – 9. Archives and Resource Groups.”
Hi – I wanted to thank you for this great tutorial and I just hope it will continue. I am interested in 2D OpenGL supported game making, but so far everything has been too complex (like combining pygame and pyOpenGL) or with not enough tutorial (like pySFML). This tutorial helps a lot and it seems it also teaches quite a few interesting workflow practices that can be of a good use.
Yours truly,
Nya-chan Production