Skip to content

Resources and Assets

Adrian edited this page Feb 5, 2021 · 26 revisions

Overview

No game, and verily no graphical application in general, is complete without assets such as images, sounds and any other kind of data that the application can serialize or read from disk. Assets are represented by Zenith as resources, and it would be wasteful and time consuming to read the same resource form disk every time we need it, as disk access times can be cumbersome for real time applications. For this reason, Zenith relies heavily on the resource cache, and we should use it whenever possible.

Zenith recognizes the root level Assets folder as it's go to location for all asset-like things. You can also register your own resource files with the engine, as described in the next section.

Note that it is preferred that you work on a development build of the engine, so that you don't have continually zip and unzip your assets to work with them. Automatically packaging files based on your configuration is planned for a future build of the Engine. For now it is recommended that you use the the -development command line flag (or --development on Unix) for any of the build or rebuild commands developing with the engine. In development builds, Zenith will pull from a regular, uncompressed Assets directory residing in the root Zenith location, or from any user registered directory.

Resource Cache

We can create a resource by using the ZResource class, which is a simple container that points to a resource and contains only the resource path:

    ZResource imageResource("my_image.png");

This class, along with the ZResourceCache, itself an engine subsystem, allow us to load our assets in an efficient manner. We can pass in a raw ZResource pointer to the GetHandle resource cache method to obtain the data we need:

    auto imageHandle = ZServices::ResourceCache()->GetHandle(&imageResource);

The above will return a ZResourceHandle pointer, described in more detail below. This will load the resource into main memory if it wasn't already there, but will otherwise fetch the cached resource, which is much faster than going out to disk every time. This is not guaranteed to prevent cache misses. It is recommended that you play around with the cache size according to how much memory you want to preallocate.

The resource system is abstract and capable enough to handle many different types of resource files. Have a look at the section on Resource Files for more information. We can create our own resource files using the ZResourceFile interface, and add as many as we want to the resource cache:

    std::shared_ptr<MyResourceFile> bundle;
    // Initialize the resource file...
    ZServices::ResourceCache()->RegisterResourceFile(bundle);

We can request resource files asynchronously using the RequestHandle method, which takes in a ZResource instance and returns the requested resource at a later time via a ZResourceLoadedEvent. Take a look at the event system for more on that.

    ZResource iconResource("/Images/icon.png", ZResourceType::Texture);
    ZServices::ResourceCache()->RequestHandle(iconResource);

We can preload certain assets of a particular resource file into memory using the Preload method. This is useful for pinch point or level chunking:

    ZServices::ResourceCache()->Preload(pattern, &myProgressCallback);

The first argument is a regular expression pattern to match file names against, and the second is a function pointer to a progress callback, so we can see how much has been loaded so far. It might be useful to have separate folders for different levels or pinch points within a resource file, and use the folder paths as the pattern to match against:

    ZEngine::ResourceCache()->Preload("Assets/Level1/.*", &myProgressCallback);

Note that the Preload method is still experimental and under development.

If necessary, we can flush the cache using the Flush method:

    ZServices::ResourceCache()->Flush();

Resource Loaders

We can register our own resource loaders with the system to load the resources we are expecting. In order to do this, we must subclass the ZResourceLoader interface and override the relevant methods. Take a look at the currently available loaders to get a feel for how this should be done. Once we have our resource loader, we can register it with the AddResourceLoader method:

    std::shared_ptr<MyResourceLoaderType> myLoader;
    // ... Do your loader initialization ...
    ZServices::ResourceCache()->RegisterLoader(myLoader);

Loaders are responsible for parsing specific file or package types, so foreign file types coming from different DCC platforms will need their own loaders. Currently Zenith only supports a tiny fraction of all the digital resource types in the universe.

Resource Files

Resource files are asset containers. They bundle our assets and may or may not compress them into a compact, game-ready format. New resource files can be defined by subclassing the ZResourceFile interface and overriding the appropriate methods. Zenith currently uses development and ZIP files as the primary engine resource file. Take a look at ZZipFile to get a feel for how resource files work.

Resource Handles

We use resource handles to interface with the underlying file data we are after. ZResourceHandle is a simple wrapper with a few methods to help us access the file's contents.

Using the Size method, we can easily get the size, in bytes, of the file in question:

    unsigned int size = myResourceHandle->Size();

We can access the actual file contents using the Buffer and FluidBuffer methods. The former returns a read-only data buffer, while the latter returns a mutable buffer with the file's contents.

    // Cannot write to this buffer... 
    const char* buffer = myResourceFile->Buffer();
    // ...but we can write to this one.
    char* mutableBuffer = myResourceFile->FluidBuffer();

We seldom need to task ourselves with creating resource handles, as these handles are automatically created and populated when we call the resource cache GetHandle method for a resource.

If you are creating a new asset type to be loaded within the engine and have created the appropriate resource loader, it may be useful to include extra data with the asset's resource handles. To this end, there is a ZResourceExtraData class which can be subclassed and passed into a resource handle using the SetExtra method once the handle is created.

Here's an example of how it's done for ZOF files within the engine

    ZOFParser parser;
    std::shared_ptr<ZZOFResourceExtraData> extraData = std::make_shared<ZZOFResourceExtraData>();
    extraData->objectTree_ = parser.Parse(std::string((const char*)handle->Buffer()));
    handle->SetExtra(extraData);
Clone this wiki locally