Resource Manager with Multi-Res support Tutorial

New to Moai? Get started here with our best tips and tutorials.

Moderator: naturally

Resource Manager with Multi-Res support Tutorial

Postby dooms101 » Tue Oct 30, 2012 1:38 pm

I've noticed a lot of great beginner's tutorials around but not much at the intermediate level, so I figured I'd write my own tutorial aimed at developers looking for the next step.

Tutorial: A Simple Resource Manager with Caching Functionality, Locale Handling, and Multi-Resolution Handling

Introduction
The resource manager (or similar component) is an essential part of any great game, especially graphics intense mobile games where memory is scarce and I/O is expensive. The resource manager can take on many different responsibilities but the most common is for providing a data abstraction layer to the rest of the application. What exactly does this mean? Well, you can think of the resource manager as a middleman between the application and data sources. It allows the application to simply ask the resource manager for some data and not care where it comes from or how it was made. This type of application structure where application logic is separated from data is not unique to games. Those coming from an Android background may be familiar with the resource manager in place there. The resource manager can be as simple as a few functions that handle reading in files or may be as complex as providing a common interface that bridges between remote and local databases, file systems, network datastreams, and hardware data sources. The level of complexity is determined by the needs of the application; for a game this usually means handling image resources, meshes, textures, audio, fonts, and game data (items, levels, etc.)

So now that you have a general idea of what a resource manager is, we can get more specific as it relates to a mobile game. The resource manager that we will be creating will actually be divided into several parts, each adding specific functionality and some really handy features. As far as the application (your game logic) is concerned, there is a namespace called 'ResMan' that provides functions for retrieving any type of data that it needs. This tutorial will start with a simple interface in the ResMan namespace for retrieving images and fonts, as the tutorial progresses the interface will be expanded to include more resource types as well as implement some very handy functionality.

Part 1: A Beginner Resource Manager
As is the style with some other tutorials that I like, I will begin by showing all the code and then explaining each part.
ResMan.lua
Code: Select all
  1. --[[

  2.         ResMan.lua

  3.         Version 1.0

  4. ]]--

  5.  

  6. -- make namespace global

  7. _G.ResMan = {}

  8.  

  9. -- constants:

  10. --              resource folder path

  11. ResMan.RES_PATH = './res/'

  12. --              images folder path

  13. ResMan.IMAGES_PATH = ResMan.RES_PATH .. 'images/'

  14. --              fonts folder path

  15. ResMan.FONTS_PATH = ResMan.RES_PATH .. 'fonts/'

  16.  

  17. local charCode = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _.,?!;:()[]{}+-/*^@#$%&\\\'"<>`|'

  18.  

  19. -- create a cache for fonts and images

  20. local imgCache = {}

  21. local fntCache = {}

  22.  

  23. -- retrieve an image resource

  24. function ResMan.getImage( fileName )

  25.  

  26.         -- look if image is already cached

  27.         if imgCache[ fileName ] then

  28.                 -- instance exists

  29.                 return imgCache[ fileName ]

  30.         end

  31.        

  32.         -- make a new image

  33.         local img = MOAIImage.new()

  34.         -- load file

  35.         img:load( ResMan.IMAGES_PATH .. fileName )

  36.         -- add to cache

  37.         imgCache[ fileName ] = img

  38.        

  39.         return img

  40. end

  41.  

  42. -- release an image from cache

  43. function ResMan.releaseImage( fileName )

  44.        

  45.         -- look for fileName

  46.         if imgCache[ fileName ] then

  47.                 -- release image from memory

  48.                 imgCache[ fileName ]:release()

  49.                 imgCache[ fileName ] = nil

  50.         end

  51. end

  52.  

  53. -- retrieve a font resource

  54. function ResMan.getFont( fileName, size )

  55.  

  56.         -- look if font is already cached

  57.         if fntCache[ fileName .. size ] then

  58.                 -- instance exists

  59.                 return fntCache[ fileName .. size ]

  60.         end

  61.        

  62.         -- make a new font

  63.         local fnt = MOAIFont.new()

  64.         -- load file

  65.         fnt:load( ResMan.FONTS_PATH .. fileName )

  66.         -- preload charcode and size

  67.         fnt:preloadGlyphs( charCode, size, 72 )

  68.         -- add to cache

  69.         fntCache[ fileName .. size ] = fnt

  70.        

  71.         return fnt

  72. end

  73.  

  74. -- release a font from cache

  75. function ResMan.releaseFont( fileName, size )

  76.        

  77.         fntCache[ fileName .. size ] = nil

  78. end



Hopefully this code should be pretty straight forward and wont require too much explaining. In this version of ResMan the only functionality is the use of a cache for images and fonts. This is done simply by creating two local tables:
Code: Select all
  1. local imgCache = {}

  2. local fntCache = {}


These two tables store cached images and fonts by making a pairing between a file name and a data object (either a MOAIImage or MOAIFont). This is an excellent way to keep memory use low by only keeping one instance of a data object in memory even if it is accessed multiple times and in multiple places. Since MOAI stores render size in decks and not in the MOAIImage itself, a single image instance can be shared by multiple props of different sizes simultaneously.

ResMan further simplifies things by keeping track of a resource file hierarchy as presented by these lines:
Code: Select all
  1. --              resource folder path

  2. ResMan.RES_PATH = './res/'

  3. --              images folder path

  4. ResMan.IMAGES_PATH = ResMan.RES_PATH .. 'images/'

  5. --              fonts folder path

  6. ResMan.FONTS_PATH = ResMan.RES_PATH .. 'fonts/'


As more types of resources are managed by ResMan we can add their folder structures here. This is convenient in that resources can be identified by just their file name instead of their entire file path; this makes application code easier to read and less error prone. As you will see later on, this strategy will be helpful when implementing locale handling and multi-resolution handling.

The four methods introduced here are apart of that interface I was talking about in the introduction. All the application needs to know is that it will get an image or font object by calling the respective 'getter' method. The application does not need to know about the cache. This concludes the first part of this tutorial. The next part will implement multi-resolution handling.

Part 2: Adding The Basis For Multi-Resolution Handling
In this part of the series, we will add robustness to the code and strengthen the 'res' folder file structure concept. The major focus is to add a basis for multi-resolution handling. The changes made here will also aid us in locale handling later on in the series.
ResMan.lua
Code: Select all
  1. --[[

  2.         ResMan.lua

  3.         Version 2.0

  4. ]]--

  5.  

  6. -- make namespace

  7. _G.ResMan = {}

  8.  

  9. -- constants

  10. ResMan.RES_CLASS_MIN_MID_DIVIDE = 340

  11. ResMan.RES_CLASS_MID_MAX_DIVIDE = 680

  12.  

  13. -- represents the resources folder system

  14. local Res = {

  15.         Path = './res/',

  16.         -- images folder

  17.         Images = {

  18.                 Name = 'images',

  19.                 Path = './res/images/',

  20.                 -- min resolution class

  21.                 Min = {

  22.                         Name = 'min',

  23.                         Path = './res/images/min/'

  24.                 },

  25.                 -- mid resolution class

  26.                 Mid = {

  27.                 Name = 'mid',

  28.                         Path = './res/images/mid/'

  29.                 },

  30.                 -- max resolution class

  31.                 Max = {

  32.                         Name = 'max',

  33.                         Path = './res/images/max/'

  34.                 }

  35.         },

  36.         -- fonts folder

  37.         Fonts = {

  38.                 Name = 'fonts',

  39.                 Path = './res/fonts/'

  40.         }

  41. }

  42.  

  43. -- determine resolution class of device

  44. function initImagesResPath()

  45.         if Config.ScreenHeight <= ResMan.RES_CLASS_MIN_MID_DIVIDE then

  46.                 -- min res class

  47.                 return Res.Images.Min.Path

  48.         elseif Config.ScreenHeight <= ResMan.RES_CLASS_MID_MAX_DIVIDE then

  49.                 -- mid res class

  50.                 return Res.Images.Mid.Path

  51.         else

  52.                 -- max res class

  53.                 return Res.Images.Max.Path

  54.         end

  55. end

  56.  

  57. local imagesResPath = initImagesResPath()

  58.  

  59. local charCode = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _.,?!;:()[]{}+-/*^@#$%&\\\'"<>`|'

  60.  

  61. -- create a cache for fonts and images

  62. local imgCache = {}

  63. local fntCache = {}

  64.  

  65. -- attempts to resolve the file path for a resource file

  66. function ResMan.resolveResourceFileName( fileName, resType )

  67.  

  68.         local path

  69.         -- image type resource

  70.         if resType == Res.Images.Name then

  71.                 -- look in res class

  72.                 if MOAIFileSystem.checkFileExists( imagesResPath .. fileName ) then

  73.                         path = imagesResPath .. fileName

  74.                 -- look in images folder

  75.                 elseif MOAIFileSystem.checkFileExists( Res.Images.Path .. fileName ) then

  76.                         path = Res.Images.Path .. fileName

  77.                 end

  78.         -- font type resource

  79.         elseif resType == Res.Fonts.Name then

  80.                 -- look in fonts folder

  81.                 if MOAIFileSystem.checkFileExists( Res.Fonts.Path .. fileName ) then

  82.                         path = Res.Fonts.Path .. fileName

  83.                 end

  84.         end

  85.         -- look for file in res folder

  86.         if not path then

  87.                 if MOAIFileSystem.checkFileExists( Res.Path .. fileName ) then

  88.                         path = Res.Path .. fileName

  89.                 end

  90.         end

  91.         -- file wasn't found in res folder

  92.         -- * good place to put some exception handling...

  93.         return path

  94. end

  95.  

  96. -- retrieve an image resource

  97. function ResMan.getImage( fileName )

  98.  

  99.         -- look if image is already cached

  100.         if imgCache[ fileName ] then

  101.                 -- instance exists

  102.                 return imgCache[ fileName ]

  103.         end

  104.        

  105.         -- make a new image

  106.         local img = MOAIImage.new()

  107.         -- load file

  108.         img:load( ResMan.resolveResourceFileName( fileName, Res.Images.Name ) )

  109.         -- add to cache

  110.         imgCache[ fileName ] = img

  111.        

  112.         return img

  113. end

  114.  

  115. -- release an image from cache

  116. function ResMan.releaseImage( fileName )

  117.        

  118.         -- look for fileName

  119.         if imgCache[ fileName ] then

  120.                 -- release image from memory

  121.                 if type( imgCache[ fileName ].release ) == 'function' then

  122.                         imgCache[ fileName ]:release()

  123.                 end

  124.                 imgCache[ fileName ] = nil

  125.         end

  126. end

  127.  

  128. -- retrieve a font resource

  129. function ResMan.getFont( fileName, size )

  130.  

  131.         -- look if font is already cached

  132.         if fntCache[ fileName .. size ] then

  133.                 -- instance exists

  134.                 return fntCache[ fileName .. size ]

  135.         end

  136.        

  137.         -- make a new font

  138.         local fnt = MOAIFont.new()

  139.         -- load file

  140.         fnt:load( ResMan.resolveResourceFileName( fileName, Res.Fonts.Name ) )

  141.         -- preload charcode and size

  142.         fnt:preloadGlyphs( charCode, size, 72 )

  143.         -- add to cache

  144.         fntCache[ fileName .. size ] = fnt

  145.        

  146.         return fnt

  147. end

  148.  

  149. -- release a font from cache

  150. function ResMan.releaseFont( fileName, size )

  151.        

  152.         fntCache[ fileName .. size ] = nil

  153. end



The mechanism for handling screens of varying resolutions in this resource manager is half of the equation to automatically handling varying screen types. For mobile developers, building an app that can gracefully adjust to any screen size and density is a difficult challenge. Thankfully, building a resource manager with the ability to automatically select an appropriate size image resource isn't that difficult and helps a lot. The second part of the solution is to implement a UI layer (possible to build into resource manager too but makes more sense to separate) that maintains a common 'stage' size and scale. I am planning on a follow up tutorial series on a companion UI module.

The first changes made are the addition of some new constants, locals, and an initialization function. The two constants up at the top named RES_CLASS_MIN_MID_DIVIDE and RES_CLASS_MID_MAX_DIVIDE are apart of the multi-resolution handling system; so is the local variable imagesResPath and its initialization function initImagesResPath(). Since my UI layer scales the width of its stage to adjust to different aspect ratios and keeps the stage height constant, it made the most sense to determine the resolution class of a device by its screen height. By resolution class, I mean either 'min', 'mid', or 'max'; each class represents a range of screen resolutions. The use of the resolution class will become more apparent in a moment.

Code: Select all
  1. -- determine resolution class of device

  2. function initImagesResPath()

  3.         if Config.ScreenHeight <= RES_CLASS_MIN_MID_DIVIDE then

  4.                 -- min res class

  5.                 return Res.Images.Min.Path

  6.         elseif Config.ScreenHeight <= RES_CLASS_MID_MAX_DIVIDE then

  7.                 -- mid res class

  8.                 return Res.Images.Mid.Path

  9.         else

  10.                 -- max res class

  11.                 return Res.Images.Max.Path

  12.         end

  13. end


The code above compares the screen height (Config.ScreenHeight is assumed to be initialized to MOAIEnvironment.verticalResolution) to each of the 'divide' constants to determine which resolution class it belongs to. Then the local variable imagesResPath is set to that value.

Code: Select all
  1. -- represents the resources folder system

  2. local Res = {

  3.         Path = './res/',

  4.         -- images folder

  5.         Images = {

  6.                 Name = 'images',

  7.                 Path = './res/images/',

  8.                 -- min resolution class

  9.                 Min = {

  10.                         Name = 'min',

  11.                         Path = './res/images/min/'

  12.                 },

  13.                 -- mid resolution class

  14.                 Mid = {

  15.                 Name = 'mid',

  16.                         Path = './res/images/mid/'

  17.                 },

  18.                 -- max resolution class

  19.                 Max = {

  20.                         Name = 'max',

  21.                         Path = './res/images/max/'

  22.                 }

  23.         },

  24.         -- fonts folder

  25.         Fonts = {

  26.                 Name = 'fonts',

  27.                 Path = './res/fonts/'

  28.         }

  29. }


This data structure represents the hierarchy of the resource folder system. It provides an intuitive way to store information of the folder structure. It goes along with the following function:
Code: Select all
  1. -- attempts to resolve the file path for a resource file

  2. function ResMan.resolveResourceFileName( fileName, resType )

  3.  

  4.         local path

  5.         -- image type resource

  6.         if resType == Res.Images.Name then

  7.                 -- look in res class

  8.                 if MOAIFileSystem.checkFileExists( imagesResPath .. fileName ) then

  9.                         path = imagesResPath .. fileName

  10.                 -- look in images folder

  11.                 elseif MOAIFileSystem.checkFileExists( Res.Images.Path .. fileName ) then

  12.                         path = Res.Images.Path .. fileName

  13.                 end

  14.         -- font type resource

  15.         elseif resType == Res.Fonts.Name then

  16.                 -- look in fonts folder

  17.                 if MOAIFileSystem.checkFileExists( Res.Fonts.Path .. fileName ) then

  18.                         path = Res.Fonts.Path .. fileName

  19.                 end

  20.         end

  21.         -- look for file in res folder

  22.         if not path then

  23.                 if MOAIFileSystem.checkFileExists( Res.Path .. fileName ) then

  24.                         path = Res.Path .. fileName

  25.                 end

  26.         end

  27.         -- file wasn't found in res folder

  28.         -- * good place to put some exception handling...

  29.         log( 'ResMan', 'resolveResourceFileName\tpath: ' .. path )

  30.         return path

  31. end


Now, instead of looking for image or font files in the 'res' folder, the file is searched for in the Res folder system according to the type of resource. Image files are first looked for in their resolution class folder (example: a small screen would look under './res/images/min/' and a large screen under './res/images/max/'); then they are looked for under './res/images/' and lastly under just './res/'. This allows you to place a high res version of an image under 'max', a normal res version under 'mid', and a low res version under 'min'. If you have an image that will be the same resolution no matter the device's resolution you can place it in 'images'. A similar process is performed for font files, first looked for under 'font' and then under 'res'. The function returns the full validated path for a give file and resource type. This system will make it easy to add in many different kinds of resources later on.

The only other changes made are now the functions getImage and getFont use the above function to resolve the resource filename.

In the next part, we will add some basic locale handling and string resource management.

Part 3: Adding Localized String Support
An often overlooked but vitally important aspect to developing an international mobile game is adding localization support. A simple and very effective method of implementing a localization system is to eliminate hard coded string literals and avoiding saving text in image resources. The resource manager is the obvious location for string resource management. All the application needs to know is that all the strings that it needs (for titles, character names, dialog, etc.) can be retrieved from the resource cache, it doesn't need to worry about localization - it only needs to know an internal key for a given string. The resource manager will automatically load in the string data from the most appropriate place.

Not just text to be displayed can be stored as a resource, the names of localized image files can be saved there too. For instance, a complex image of your title might be best saved as an image file but you want your game to have a slightly different title for each language. This can be easily achieved by storing a string key called 'titleImageName' and giving it the appropriate value in each language file. Speaking of language files, here's an example of some language files:
./res/strings/strings.lua
Code: Select all
  1. local strings = {

  2.         -- Main Menu

  3.         mainMenu_storyMode = 'Story Mode',

  4.         mainMenu_arcadeMode = 'Arcade Mode',

  5.         mainMenu_extrasMenu = 'Extras Menu',

  6.         mainMenu_title = 'AwesomeGame!'

  7. }

  8. return strings



./res/strings/es/strings.lua
Code: Select all
  1. local strings = {

  2.         -- char code

  3.         _charCode = 'ú',

  4.         -- Main Menu

  5.         mainMenu_storyMode = 'El Modo Historia',

  6.         mainMenu_arcadeMode = 'Arcarda Modo',

  7.         mainMenu_extrasMenu = 'Extras Menú',

  8. }

  9. return strings


*note: I used google translate to get these values, they might not be accurate...

The structure of the language files should be pretty obvious. The only ambiguous field might be _charCode. It is an optional field that when included will be appended on to the default char code string. This is useful for including special language characters (like in the Spanish example above).

Here's the new code for ResMan:
ResMan.lua
Code: Select all
  1. --[[

  2.         ResMan.lua

  3.         Version 3.0

  4. ]]--

  5.  

  6. -- make namespace

  7. _G.ResMan = {}

  8.  

  9. -- constants

  10. ResMan.RES_CLASS_MIN_MID_DIVIDE = 340

  11. ResMan.RES_CLASS_MID_MAX_DIVIDE = 680

  12. ResMan.STRING_DATA_FILENAME = 'strings.lua'

  13.  

  14. -- create a cache for each resource type

  15. local imgCache = {}

  16. local fntCache = {}

  17. local strCache = {}

  18.  

  19. -- represents the resources folder system

  20. local Res = {

  21.         Path = './res/',

  22.         -- images folder

  23.         Images = {

  24.                 Name = 'images',

  25.                 Path = './res/images/',

  26.                 -- min resolution class

  27.                 Min = {

  28.                         Name = 'min',

  29.                         Path = './res/images/min/'

  30.                 },

  31.                 -- mid resolution class

  32.                 Mid = {

  33.                 Name = 'mid',

  34.                         Path = './res/images/mid/'

  35.                 },

  36.                 -- max resolution class

  37.                 Max = {

  38.                         Name = 'max',

  39.                         Path = './res/images/max/'

  40.                 }

  41.         },

  42.         -- fonts folder

  43.         Fonts = {

  44.                 Name = 'fonts',

  45.                 Path = './res/fonts/'

  46.         },

  47.         -- strings folder

  48.         Strings = {

  49.                 Name = 'strings',

  50.                 Path = './res/strings/',

  51.                 -- en locale

  52.                 English = {

  53.                         Name = 'en',

  54.                         Path = './res/strings/en/'

  55.                 },

  56.                 -- es locale

  57.                 Spanish = {

  58.                         Name = 'es',

  59.                         Path = './res/strings/es/'

  60.                 },

  61.                 -- fr locale

  62.                 French = {

  63.                         Name = 'fr',

  64.                         Path = './res/strings/fr/'

  65.                 },

  66.                 -- de locale

  67.                 German = {

  68.                         Name = 'de',

  69.                         Path = './res/strings/de/'

  70.                 }

  71.         }

  72. }

  73.  

  74. -- determine resolution class of device

  75. function initImagesResPath()

  76.         if Config.ScreenHeight <= ResMan.RES_CLASS_MIN_MID_DIVIDE then

  77.                 -- min res class

  78.                 return Res.Images.Min.Path

  79.         elseif Config.ScreenHeight <= ResMan.RES_CLASS_MID_MAX_DIVIDE then

  80.                 -- mid res class

  81.                 return Res.Images.Mid.Path

  82.         else

  83.                 -- max res class

  84.                 return Res.Images.Max.Path

  85.         end

  86. end

  87.  

  88. local imagesResPath = initImagesResPath()

  89.  

  90. -- load the strings cache from file system

  91. function buildStringsCache()

  92.         local default = {}

  93.         local localized = {}

  94.         -- load default string data if it exists

  95.         if MOAIFileSystem.checkFileExists( Res.Strings.Path .. ResMan.STRING_DATA_FILENAME ) then

  96.                 default = dofile( Res.Strings.Path .. ResMan.STRING_DATA_FILENAME )

  97.         end

  98.         -- resolve local path

  99.         local localePath

  100.         if Config.locale == Res.Strings.English.Name then

  101.                 localePath = Res.Strings.English.Path

  102.         elseif Config.locale == Res.Strings.Spanish.Name then

  103.                 localePath = Res.Strings.Spanish.Path

  104.         elseif Config.locale == Res.Strings.French.Name then

  105.                 localePath = Res.Strings.French.Path

  106.         elseif Config.locale == Res.Strings.German.Name then

  107.                 localePath = Res.Strings.German.Path

  108.         end

  109.         -- load localized string data if it exists

  110.         if localePath and MOAIFileSystem.checkFileExists( localePath .. ResMan.STRING_DATA_FILENAME ) then

  111.                 localized = dofile( localePath .. ResMan.STRING_DATA_FILENAME )

  112.         end

  113.         -- merge tables

  114.         for k,v in pairs( localized ) do

  115.                 default[ k ] = v

  116.         end

  117.         return default

  118. end

  119.  

  120. strCache = buildStringsCache()

  121.  

  122. local charCode = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _.,?!;:()[]{}+-/*^@#$%&\\\'"<>`|'

  123.  

  124. -- append localized charcode

  125. if strCache[ '_charCode' ] then

  126.         charCode = charCode .. strCache[ '_charCode' ]

  127. end

  128.  

  129. -- get a localized string resource

  130. function ResMan.getString( key )

  131.         return strCache[ key ] or ''

  132. end

  133.  

  134. -- attempts to resolve the file path for a resource file

  135. function ResMan.resolveResourceFileName( fileName, resType )

  136.  

  137.         local path

  138.         -- image type resource

  139.         if resType == Res.Images.Name then

  140.                 -- look in res class

  141.                 if MOAIFileSystem.checkFileExists( imagesResPath .. fileName ) then

  142.                         path = imagesResPath .. fileName

  143.                 -- look in images folder

  144.                 elseif MOAIFileSystem.checkFileExists( Res.Images.Path .. fileName ) then

  145.                         path = Res.Images.Path .. fileName

  146.                 end

  147.         -- font type resource

  148.         elseif resType == Res.Fonts.Name then

  149.                 -- look in fonts folder

  150.                 if MOAIFileSystem.checkFileExists( Res.Fonts.Path .. fileName ) then

  151.                         path = Res.Fonts.Path .. fileName

  152.                 end

  153.         end

  154.         -- look for file in res folder

  155.         if not path then

  156.                 if MOAIFileSystem.checkFileExists( Res.Path .. fileName ) then

  157.                         path = Res.Path .. fileName

  158.                 end

  159.         end

  160.         -- file wasn't found in res folder

  161.         -- * good place to put some exception handling...

  162.         log( 'ResMan', 'resolveResourceFileName\tpath: ' .. path )

  163.         return path

  164. end

  165.  

  166. -- retrieve an image resource

  167. function ResMan.getImage( fileName )

  168.  

  169.         -- look if image is already cached

  170.         if imgCache[ fileName ] then

  171.                 -- instance exists

  172.                 return imgCache[ fileName ]

  173.         end

  174.        

  175.         -- make a new image

  176.         local img = MOAIImage.new()

  177.         -- load file

  178.         img:load( ResMan.resolveResourceFileName( fileName, Res.Images.Name ) )

  179.         -- add to cache

  180.         imgCache[ fileName ] = img

  181.        

  182.         return img

  183. end

  184.  

  185. -- release an image from cache

  186. function ResMan.releaseImage( fileName )

  187.        

  188.         -- look for fileName

  189.         if imgCache[ fileName ] then

  190.                 -- release image from memory

  191.                 if type( imgCache[ fileName ].release ) == 'function' then

  192.                         imgCache[ fileName ]:release()

  193.                 end

  194.                 imgCache[ fileName ] = nil

  195.         end

  196. end

  197.  

  198. -- retrieve a font resource

  199. function ResMan.getFont( fileName, size )

  200.  

  201.         -- look if font is already cached

  202.         if fntCache[ fileName .. size ] then

  203.                 -- instance exists

  204.                 return fntCache[ fileName .. size ]

  205.         end

  206.        

  207.         -- make a new font

  208.         local fnt = MOAIFont.new()

  209.         -- load file

  210.         fnt:load( ResMan.resolveResourceFileName( fileName, Res.Fonts.Name ) )

  211.         -- preload charcode and size

  212.         fnt:preloadGlyphs( charCode, size, 72 )

  213.         -- add to cache

  214.         fntCache[ fileName .. size ] = fnt

  215.        

  216.         return fnt

  217. end

  218.  

  219. -- release a font from cache

  220. function ResMan.releaseFont( fileName, size )

  221.        

  222.         fntCache[ fileName .. size ] = nil

  223. end



Much of the old code remains untouched but a lot of new stuff has been added near the top. The first change is the inclusion of a string constant that identifies what a string data file name should be. It is expected that all string values are to be stored in a single file for each language using this name constant. The next change is this addition to the Res folder structure:
Code: Select all
  1. -- represents the resources folder system

  2. local Res = {

  3.         -- other folders here...

  4.         -- strings folder

  5.         Strings = {

  6.                 Name = 'strings',

  7.                 Path = './res/strings/',

  8.                 -- en locale

  9.                 English = {

  10.                         Name = 'en',

  11.                         Path = './res/strings/en/'

  12.                 },

  13.                 -- es locale

  14.                 Spanish = {

  15.                         Name = 'es',

  16.                         Path = './res/strings/es/'

  17.                 },

  18.                 -- fr locale

  19.                 French = {

  20.                         Name = 'fr',

  21.                         Path = './res/strings/fr/'

  22.                 },

  23.                 -- de locale

  24.                 German = {

  25.                         Name = 'de',

  26.                         Path = './res/strings/de/'

  27.                 }

  28.         }

  29. }


This file structure should also be pretty obvious. So now there is a new folder in 'res' called 'strings' and in it there are folders named after the language code they represent (I use the usual codes 'en' for English, 'es' for Spanish, etc.) You will also notice a new cache field called strCache that is initialized using this function:
Code: Select all
  1. -- load the strings cache from file system

  2. function buildStringsCache()

  3.         local default = {}

  4.         local localized = {}

  5.         -- load default string data if it exists

  6.         if MOAIFileSystem.checkFileExists( Res.Strings.Path .. ResMan.STRING_DATA_FILENAME ) then

  7.                 default = dofile( Res.Strings.Path .. ResMan.STRING_DATA_FILENAME )

  8.         end

  9.         -- resolve local path

  10.         local localePath

  11.         if Config.locale == Res.Strings.English.Name then

  12.                 localePath = Res.Strings.English.Path

  13.         elseif Config.locale == Res.Strings.Spanish.Name then

  14.                 localePath = Res.Strings.Spanish.Path

  15.         elseif Config.locale == Res.Strings.French.Name then

  16.                 localePath = Res.Strings.French.Path

  17.         elseif Config.locale == Res.Strings.German.Name then

  18.                 localePath = Res.Strings.German.Path

  19.         end

  20.         -- load localized string data if it exists

  21.         if localePath and MOAIFileSystem.checkFileExists( localePath .. ResMan.STRING_DATA_FILENAME ) then

  22.                 localized = dofile( localePath .. ResMan.STRING_DATA_FILENAME )

  23.         end

  24.         -- merge tables

  25.         for k,v in pairs( localized ) do

  26.                 default[ k ] = v

  27.         end

  28.         return default

  29. end


All this function does is define two string data tables, one for the default values and one for the localized values. The default values are meant to be values that you want accessible through the resource manager but are not localizable or are common to all languages. These values are stored in the file './res/strings/strings.lua'. Determining the locale code can be tricky so I left that part out (can sometimes be determined from MOAIEnvironment.languageCode but doesn't work for every device so making it a game setting always works). The localized data is loaded from the appropriate file if it exists. Then, the default data table and localized data table are merged together with localized values overwriting default values.

That leaves one last part - accessing the values stored in the string cache. This is done with the last addition to the code:
Code: Select all
  1. -- get a localized string resource

  2. function ResMan.getString( key )

  3.         return strCache[ key ] or ''

  4. end



That wraps up this part of the tutorial. Hopefully this should be enough to give you a good understanding of how the resource manager works and ways to implement one. I will not be adding any more parts to this tutorial series but the code used here will be reused (and modified) in later tutorial series. The next series will be on a simple UI module.

Happy Coding! :mrgreen:
Last edited by dooms101 on Thu Nov 01, 2012 4:09 pm, edited 3 times in total.
User avatar
dooms101
 
Posts: 74
Joined: Tue May 01, 2012 7:04 pm
Location: Appalachian State University

Re: Resource Manager with Multi-Res support Tutorial

Postby todd » Tue Oct 30, 2012 3:32 pm

Awesome - thanks!
User avatar
todd
 
Posts: 262
Joined: Fri Mar 25, 2011 1:11 pm

Re: Resource Manager with Multi-Res support Tutorial

Postby rhoster » Thu Nov 01, 2012 8:56 am

Great writeup! This is a similar concept to the asset control code we use in our engine. I second the idea of abstracting resource management into a module. Thanks for sharing the info dooms101 :)
User avatar
rhoster
 
Posts: 230
Joined: Sat May 26, 2012 3:59 pm
Location: Santa Barbara, CA

Re: Resource Manager with Multi-Res support Tutorial

Postby dooms101 » Thu Nov 01, 2012 10:55 am

Thanks! Glad to hear this is useful. I am currently working on the second tutorial as well as writing some companion code to demonstrate its use.
User avatar
dooms101
 
Posts: 74
Joined: Tue May 01, 2012 7:04 pm
Location: Appalachian State University

Re: Resource Manager with Multi-Res support Tutorial

Postby Serapth » Thu Nov 01, 2012 11:30 am

I am making a post linking this thread (and the extending Moai one), are you intending to post your second tutorial in the same thread?
Serapth
 
Posts: 50
Joined: Wed Aug 22, 2012 5:24 pm

Re: Resource Manager with Multi-Res support Tutorial

Postby dooms101 » Thu Nov 01, 2012 11:50 am

Yeah, I'll just keep editing the first post to include each part as I make them.

Edit: added second part to OP
User avatar
dooms101
 
Posts: 74
Joined: Tue May 01, 2012 7:04 pm
Location: Appalachian State University

Re: Resource Manager with Multi-Res support Tutorial

Postby dooms101 » Thu Nov 01, 2012 4:09 pm

Added final part. Look out for more tutorials soon!
User avatar
dooms101
 
Posts: 74
Joined: Tue May 01, 2012 7:04 pm
Location: Appalachian State University

Re: Resource Manager with Multi-Res support Tutorial

Postby Kyrano » Sat Nov 03, 2012 2:43 am

Thanks dooms101, I appreciate your willingness to cover more theoretical subjects and thank you for your efforts.

I would very much like to see how such a thing might be implemented in C++ and integrated with MOAI and Lua - after all this ability is the greatest selling point of MOAI; and particularly as the focus of this tut is performance.

Thanks.
Kyrano
 
Posts: 5
Joined: Sat May 12, 2012 2:34 pm

Re: Resource Manager with Multi-Res support Tutorial

Postby dooms101 » Sat Nov 03, 2012 11:08 am

@Kyrano

Implementing a resource manager with the same functionality as the Lua version above wouldn't be all too difficult in C++. The great thing about MOAI is that all the Lua MOAI objects are just a way to access the C++ MOAI objects. I would think that you could implement a resource manager just like the one above in C++ utilizing the MOAI C++ objects and expose it through a Lua object in a similar way. That might be a poor explanation... but I think you get my point. This would have an advantage in performance but a disadvantage in that you'd have to rebuild every time you make a change; of course, that's true for any extensions you make to the MOAI host.

I've been thinking lately about implementing some of my lower level systems (ResManager, ConfigManager, SaveManager) as extensions to the MOAI host. I've decided against it though mostly because it would make upgrading to newer versions of MOAI a pain and I make frequent changes at all levels of my code. I use an iterative approach to developing games, so I only add functionality as I need it each iteration. However, if you have well established systems that you know will change infrequently, then maybe bringing them down to the C++ level is practical.
User avatar
dooms101
 
Posts: 74
Joined: Tue May 01, 2012 7:04 pm
Location: Appalachian State University

Re: Resource Manager with Multi-Res support Tutorial

Postby Kyrano » Sun Nov 04, 2012 9:49 am

Very interesting thoughts dooms101, thank you.

You raise some valid points which have changed my attitude toward lua. I can definitely agree with your rational during experimental/learning phases of a software - and for parts that are not resource intensive. Also, I imagine a good C++ automated testing framework(and some well written tests) will make managing changes to MOAI much easier.

In addition, I have to declare a bias which comes from the wonder and novelty of working with C++ and the possibility to bind it to a command interpreter - that is, coming from Actionscript/Scheme/PHP.
Kyrano
 
Posts: 5
Joined: Sat May 12, 2012 2:34 pm

Re: Resource Manager with Multi-Res support Tutorial

Postby ezraanderson » Thu Dec 13, 2012 6:54 pm

great tutorial.
Dead Dark: A roguelike, zombie apocalypse survival game
www.dead-dark.com
User avatar
ezraanderson
 
Posts: 778
Joined: Wed Nov 21, 2012 2:24 pm
Location: Canada


Return to Questions and Tips

Who is online

Users browsing this forum: No registered users and 0 guests

x