Wednesday, April 8, 2020

Tutorial 2.5: A Dusty Image Library


Last epside in Dragon Ball Z, Goku programmed a GxImage class, but now Vegeta wonders who is managing all those image instances. Of course, yet another GxImageLibrary class will do just that. It is used to add images (typically during the boot-up, or when loading a new level (section)), and future Materials (see next chapter) should be able to quickly find their image needs here. As the word says: a library.

Now I’m not really familiar with Java, so forgive me if I’m doing it all wrong. But I went looking into (alphabetic) sorted arrays. Since we may have quite a bunch of images (actually, we don’t, but anyway), we don’t want to loop through the whole shitpile every time some other object needs image “xyz”. Sorted lists, collections, hashes, or whatever they are called in Java, can reduce such searches. Turns out Java calls this a “Map”. I think. 

My point is, searching sucks kids. Avoid it when possible. And if you really have to, ask your wife. Or your mom. Because most likely, she has a better memory than you. And that kids, is because she organizes things, instead of just throwing it in a random corner.
public class GxImageLibrary {

    HashMap<String,GxImage> images;

    public GxImageLibrary()
    {
        this.images = new HashMap();
    } // create


    public void releaseAll()
    {
        Iterator it = this.images.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry pair = (Map.Entry)it.next();
            GxImage img = (GxImage)pair.getValue();
            img.releaseFromGPU();
            it.remove(); // avoids a ConcurrentModificationException
        } // while iterating

        this.images.clear();
    } // releaseAll



    public void addImage( String imageFilePath )
    {
        // Check if there is an Atlas file as well - assuming PNG and Atlas are named the same!
        String atlasFilePath    = imageFilePath.replace( ".png", ".atlas" );

        // Use fileName as idName
        int start               = imageFilePath.lastIndexOf( '/' )+1;
        int end                 = imageFilePath.lastIndexOf( '.' );

        String idName           = imageFilePath.substring( start, end );

        // Make image and add to the list
        GxImage img     = new GxImage( idName, imageFilePath, atlasFilePath );
        this.images.put( idName, img );
    } // addImage


    public GxImage get( String idName )
    {
        return this.images.get( idName );
    } // idName

} // GxImageLibrary
 

Just like how we added a global camera instance in the Engine, we also add the ImageLibrary as a part of our engine code. The engine will create it, destroys it, and makes it available so your "game-code" can use it.
<engine.java>
public class Engine {
    float   deltaSecs;

    GxCamera        camera;
    GxImageLibrary  imageLibrary;

    public Engine()
    {
        this.camera         = new GxCamera();
        this.imageLibrary   = new GxImageLibrary();
    } // create

    public void shutdown()
    {
        this.imageLibrary.releaseAll();
    } // shutdown

   ...

    public GxImage getImage(String idName )
    {
        return this.imageLibrary.get( idName );
    } // getImage

    public GxImageLibrary getImageLibrary()
    {
        return this.imageLibrary;
    } // getImageLibrary

} // Engine


In general, there are 3 ways here. First is to have your “game-code” doing that, using hard-coded lines. The second approach is to load a database or “package” file of some sort, which enlists all available images. Method 3 is on-demand. If the “mainMenu” class needs Image X, then it shall be loaded. The neat part about that, is that you can also unload it (making room for others) once the “MainMenu” is inactive for a while. However, this management is a bit more complicated, and still those other instances need to know material paths and such.


I hate hard-coded stuff, but since this is a small game, with relative few resources, I’m gonna do it anyway. Just adding a simple (game) file that is called in the initial phase, which then loads all the images. The same file will also load all Materials, Sounds and other resource files. But since we don’t have anything yet for that, those are just empty placeholders for now.
public class Resources {

    Engine engine;

    public void loadInitial( Engine engine )
    {
        // Load all the initial stuff, during boot-up
        // These resources are always available, thus consuming memory permanently
        this.engine = engine;

        this.loadInitial_Sounds();
        this.loadInitial_Images();
        this.loadInitial_Animations();
        this.loadInitial_LevelPack();
    } // loadInitial


    private void loadInitial_Images(  )
    {
        engine.getImageLibrary().addImage( "Images/Orb/Orb01.png" );

        engine.getImageLibrary().sort();
    } // loadInitial_Images


    private void loadInitial_Animations()
    {

    } // loadInitial_Animations


    private void loadInitial_Sounds()
    {

    } // loadInitial_Sounds


    private void loadInitial_LevelPack()
    {

    } // loadInitial_LevelPack


} // Resources

<yourGame.java>
public class JunkThrowerGame extends ApplicationAdapter {
   Engine        engine;
   Resources  resources;

    SpriteBatch batch;
   ...

   @Override
   public void create () {
      this.engine    = new Engine();
      this.resources = new Resources();
      this.resources.loadInitial( this.engine );

      ...
   } // create

You can see, as we slowly start deleting our initial test junk code, the game code gets more tidy, robust, readable and moreover: extendable. Chunks of test-code in the Game file are slowly transformed into engine classes. Next chapter, we’ll proceed sprite animations.


No comments:

Post a Comment