// A note: after a comment by nicholasclayg I've changed my code so that now it doesn't use "package.seeall" (now deprecated) and therefore doesn't use global namespace //
// Note 2: I've loaded my example code (for Windows) on GitHub; this version of DynResManager contains also some useful functions for determining actual screen size of your device and real values of visible x/y boundaries. Next time I'll explain these functions //
As you possibly know, Corona SDK provides a method of “dynamic image resolution” that allows you to automatically load a high-resolution image when your application works on a higher resolution device.
This feature is explained very well, for example, here:
http://blog.anscamobile.com/2011/01/dynamic-image-resolution-made-easy/
The only “but” of this method is that you have to use display.newImageRect() function instead of display.newImage() – and this means that you must provide image width and height as parameters.
This works fine when your images are a part of GUI – i.e. a part of your application.
But what if your images are a part of your data?
Now I’m working on a guidebook with a lot of text and pictures. All the image files’ names are loaded from the app’s documents (text files) and I don’t want to modify my code each time I add a new picture or change its size.
It’s not only difficult to do and maintain; this is a (very) bad style: as a general principle, your application must never depend on your data or contain any part of it hard-coded.
So I developed an alternative way to manage dynamic resolution images that doesn’t use display.newImageRect() but still doesn’t require loading images twice (this is, in fact, the reason you have to provide image dimensions to newImageRect: otherwise Corona would have to load both low-res and high-res images in order to calculate the scale).
It’s very easy to use: you just have to use DynResManager:loadImage(imageFileName [, forceLoadRes]) function instead of display.newImage. The second parameter forceLoadRes (values “high” / “low”) is optional and allows you to specify explicitly which of the images (low-res or high-res) to load.
How does it work?
1. There are 3 constants defined in DynResManager:
- constHiResPicScale is the ratio between the size of high-res images and that of low-res images. Of course, it’s your responsibility to provide images of correct size;
- constHiResSuffix is the suffix added to the file names of high-res pictures (just like standard dynamic resolution image naming);
- constHiResThreshold is the threshold value of image “magnification” starting from which high resolution images will be loaded.
If you need more than two resolution levels, it's possible to turn constHiResPicScale, constHiResSuffix and constHiResThreshold into tables (of course, this requires also changes in functions).
2. The program calculates the “magnification level” of current device -
deviceScaleX = 1.0 / display.contentScaleX
deviceScaleY = 1.0 / display.contentScaleY
- and, if it is equal to or greater than constHiResThreshold, it adds constHiResSuffix to the specified file name and tries to load a high resolution image (if it fails, the “base” low resolution image is loaded).
3. If a high resolution image was loaded, the program scales it down using constHiResPicScale; you’ll have to take the new image’s xScale and yScale into account if you want to scale it again.
That’s all! Here’s the code you can try to test DynResManager -
just prepare two pictures (low-res Pic.jpg and hi-res Pic@2x.jpg (no, it's not an email), its width and height exactly twice as big as those of Pic.jpg), mark the pictures somehow to see which one of them is loaded and open the example in simulator viewing it as devices of different screen sizes.
Main:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | ---------------------------------------------- -- DynResManager test -- ---------------------------------------------- local myImageLoader = require "DynResManager" -- 1) background: its dimensions ensure that "letterbox" margins are filled on known devices local background = display.newRect(0, 0, 380, 570) background.x = display.contentWidth * 0.5 background.y = display.contentHeight * 0.5 background:setFillColor( 240, 220, 210 ) -- 2) Loading a picture (lo-res "Pic.jpg" or hi-res "Pic@2x.jpg" depending on device resolution --pic = DynResManager:loadImage("Pic.jpg") pic = myImageLoader.loadImage("Pic.jpg") pic.x = display.contentWidth * 0.5 pic.y = display.contentHeight * 0.5 print ("pic.xScale "..pic.xScale) print ("pic.yScale "..pic.yScale) |
DynResManager:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | local public = {} -- This form of module uses only locals, -- as described in http://blog.anscamobile.com/2011/09/a-better-approach-to-external-modules/ -- ================================================================================================ -- DynResManager - a tool for handling dynamic resolution images. -- ------------- the images are loaded by means of DynResManager:loadImage function. -- -- written by Michael Magrilov, mmagrilov@gmail.com -- 08.09.2011 -- -- ================================================================================================ --------------------------------------- -- Constants for scalng -- --------------------------------------- local constHiResPicScale = 2.0 -- in this case Hi-res images must be twice as big as lo-res ones local constHiResSuffix = "@2x" -- Low: "pic.png"; High: "pic@2x.png" -- a note: in this version there can be only 1 dot in image name (names like abc.def.jpg are not allowed) local constHiResThreshold = 1.8 -- Hi-res pic will be loaded if the app is "magnified" by this or greater value; -- if width in config.lua is 320, then hi-res is loaded starting from device width = 320 * 1.8 = 576 -- (I assume that scale in config.lua = "letterbox" or "zoomeven" and device proportions are "taller" -- than those of the application) -- If you need more than two resolution levels, -- it's possible to turn constHiResPicScale, constHiResSuffix and constHiResThreshold into tables -- (of course, this requires also changes in functions) --------------------------------------------------------------- function haveToLoadHiRes() -- Here we decide if we have to load hi-res or lo-res images --------------------------------------------------------------- local deviceScaleX = 1.0 / display.contentScaleX -- App width on the device screen (pix) / app width set in config.lua local deviceScaleY = 1.0 / display.contentScaleY -- App height on the device screen (pix) / app height set in config.lua if deviceScaleX >= constHiResThreshold or deviceScaleY >= constHiResThreshold then return true else return false end end -- haveToLoadHiRes public.haveToLoadHiRes = haveToLoadHiRes --------------------------------------------------------------- function loadImage(imageFileName, forceLoadRes) -- The main method; optional parm forceLoadRes = "high" / "low" or empty --------------------------------------------------------------- local imageFileName = tostring(imageFileName) or false if imageFileName == "" then return nil end local forceLoadRes = string.lower(tostring(forceLoadRes)) or false if forceLoadRes ~= "high" and forceLoadRes ~= "low" then forceLoadRes = "" end local hiResImageFileName local tempImage = nil -- 1) Load a low-res (base) image if we don't have to load hi-res if (not haveToLoadHiRes() and forceLoadRes ~= "high") or forceLoadRes == "low" then tempImage = display.newImage (imageFileName) tempImage.x = 0 tempImage.y = 0 return tempImage end -- 3) Loading a HiRes image - first build its name, then load, if not OK, load base image hiResImageFileName = string.gsub(imageFileName, "%.", constHiResSuffix..".") -- abc.jpg --> abc@2x.jpg tempImage = display.newImage (hiResImageFileName) -- 4) If we failed to load a hi-res image, load a lo-res; if tempImage == nil then tempImage = display.newImage (imageFileName) tempImage.x = 0 tempImage.y = 0 return tempImage end -- 4) Scaling; if a hi-res image was loaded, its xScale and yScale will not be equal to 1; -- take it into account if you want to scale the image again! tempImage.xScale = tempImage.xScale / constHiResPicScale tempImage.yScale = tempImage.yScale / constHiResPicScale tempImage.x = 0 tempImage.y = 0 return tempImage end -- loadImage public.loadImage = loadImage --------------------------------------------------------------------------------- -- "Get" methods to access constants used by DynResManager --------------------------------------------------------------------------------- ----------------------------------------- function getHiResPicScale() ----------------------------------------- return constHiResPicScale end public.getHiResPicScale = getHiResPicScale --------------------------------------- function getHiResSuffix() --------------------------------------- return constHiResSuffix end public.getHiResSuffix = getHiResSuffix ------------------------------------------ function getHiResThreshold() ------------------------------------------ return constHiResThreshold end public.getHiResThreshold = getHiResThreshold return public |
Though it doesn't deal with images,it’s also possible to use similar technique to display text with a greater font size on a higher-resolution devices (add this function before "return public"):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | --------------------------------------------------------------- -- A function for rendering text with a font size set dynamically according to device resolution. --------------------------------------------------------------- function displayText(text, x, y, font, size, refPoint ) --------------------------------------------------------------- local text = text or "xxx" local x = x or 0 local y = y or 0 local font = font or native.systemFont local size = size or 12 local refPoint = refPoint or "" local sizeAtRealResolution local tempText local W0, H0 sizeAtRealResolution = size / display.contentScaleY tempText = display.newText(text, 0, 0, font, sizeAtRealResolution ) refPoint = string.lower(refPoint) if refPoint == "center" then tempText:setReferencePoint( display.CenterReferencePoint ) elseif refPoint == "topleft" then tempText:setReferencePoint( display.TopLeftReferencePoint ) elseif refPoint == "topcenter" then tempText:setReferencePoint( display.TopCenterReferencePoint ) elseif refPoint == "topright" then tempText:setReferencePoint( display.TopRightReferencePoint ) elseif refPoint == "centerright" then tempText:setReferencePoint( display.CenterRightReferencePoint ) elseif refPoint == "bottomright" then tempText:setReferencePoint( display.BottomRightReferencePoint ) elseif refPoint == "bottomcenter" then tempText:setReferencePoint( display.BottomCenterReferencePoint ) elseif refPoint == "bottomleft" then tempText:setReferencePoint( display.BottomLeftReferencePoint ) elseif refPoint == "centerleft" then tempText:setReferencePoint( display.CenterLeftReferencePoint ) else tempText:setReferencePoint( display.TopLeftReferencePoint ) -- default end tempText.yScale = tempText.yScale * display.contentScaleY tempText.xScale = tempText.xScale * display.contentScaleX tempText.x = x tempText.y = y return tempText end -- displayText public.displayText = displayText |
Hi Nicholas, thank you very much for this important remark.
Honestly - I just didn't notice that seeall died.
I've already modified my code according to Jon's post you've mentioned and it works fine! Very soon I will update my example here.
Thanks a lot,
Michael
P.S. I have updated the code, now you can try it!
@mmagrilov
No problem. I wasn't trying to be picky or anything, your effort in coding is far beyond my talent set haha.
A lot of people when I tell them this, they look at the post and go "Wow, that's a crazy way to do things". Jonathan Beebe really laid out some really unique ideas on how to tackle memory management (through the use of going local vs the old way.Dealing with retina vs non retina (iphone vs iphone 4) is a pain right now!
Luckily all my games are built using system shapes (display.newRect etc) for prototyping. When I get to actually putting images in this will be VERY VERY HANDY
Glad you found the link helpful. I'm hoping (if it really is the way to go) that other code like Director, particle candy, crawlspace lib etc will go this route.
Also, thanks for the name mention at the top. I'm really not that talented with coding, but I do pay attention very closely. I figured I didn't want you to go all out and write this huge code library then find out it's all for nothing :)
Happy times!
oh and one more thing - you should post your code on github or somewhere that people can just download the .lua files :) Just a happy suggestion hehe.
*edit* (Just tagging on to this post so there isn't many many posts asking different questions....
One more one more thing.
Ok so the first code post is the main.lua
The 2nd post is the DynResManager.lua
And the 3rd post is a different way to do things?
ng
Hi ng, you're right, posting code on github is a better way but I just have to learn how to do it (I suppose it's not very difficult)...
As to the third piece of code, it doesn't deal with images, it applies the same scaling method to text. There were complaints that if you write 16-points text on a device with double screen resolution, you get just - an upscaled 16 points font while in this case it would be better to display the text at 32 points - that's what this function does.
Michael
Anyone having problems with this running on the latest daily build?
My background images on the iPhone4 simulator are using the 320 x 480 sized images instead of the higher resolution ones.
Thanks a lot mmagrilov!
I was just reading the other excellent posts on 'Content Scaling made easy' and 'Dynamic Scaling made easy'...
I was actually a bit surprised when I realised that there wasn't any automatic technique to load any image regardless of whether or not I knew the dimensions of it, dynamically in this manner.
But what if your images are a part of your data?
I was thinking that this would happen more frequently than the opposite? :)
Anyway, really appreciate the sneaky fix you made. Thanks!
I think it would be nice if something like this was implemented as a config setting (true/false ?) in the core... it should go hand in hand with the dynamic/content scaling already in place there, shouldn't it?
It just felt a bit funky to have to search for and download a non-core script for this feature as for me it felt really essential when I started to look into the dynamic/content scaling.
Cheers!
Hi Crowye, thanks a lot for appreciation!
I'm very glad you've found my module useful;
I would be happy if more people would use it.
Indeed - it can save much headache when you work with images.
And - in general - having to provide exact dimensions of every picture is a bit like programming in absolute addresses :-)
Did you notice that there is another strange thing: no standard way to know the screen size of the device; when you work in letterbox mode it's also hard to determine the actual x/y boundaries of visible screen.
I'm preparing another example showing how to do it by means of the same DynResManager module (of course, it is based on screenOriginX, screenOriginY and scale values). I'm abroad now but when I return home I will post it.
Michael
Very cool, but my only concern since the module package...seeall is now deprecated I won't use any module library that uses this.
The whole Lua/corona community is basically doing everything local vs global (memory leaks).
I'm sure others will chime in. I know Ricardo (dude who wrote director) is considering re writing his entire code set to accommodate this change, but other libraries (LIME, Particle Candy, CrawlSpace lib) are still rolling with the seeall shenanigans.
I'm still digesting the information, and from what others have tested so far this seems to be the direction to take.
Here are the links for your reference. There are some very very good pieces of information, and people challenging what's being said. All good things to make things better!
"A better approach to external modules"
http://blog.anscamobile.com/2011/09/a-better-approach-to-external-modules/
"Modular Classes in Corona"
http://blog.anscamobile.com/2011/09/tutorial-modular-classes-in-corona/
I guess we shall see what happens.
ng