Async image loading
Monkey Forums/Monkey Programming/Async image loading
| ||
Hi all, I'm just trying to find a way to replace normal image loading with async loading. It works fine with Mark's sample on http://marksibly.blogspot.co.nz/2012/09/ok-this-got-bit-longwinded-so-its_14.html , but the problem there is that an image field also needs to be in the OnLoadImageComplete method. That's a bit inconvenient. So I'm trying to replace the usual LoadImage with some async workaround. I tried the following: [monkeycode] Strict Import mojo Function Main:Int() New AsyncTestApp Return 0 End Class AsyncTestApp Extends App Implements IOnLoadImageComplete Field testImage:Image Field asyncImages:StringMap<Image> Method OnCreate:Int() SetUpdateRate(60) asyncImages = New StringMap<Image> testImage = LoadImage("myimage.png") Return 0 End Method LoadImage:Image(path$, frames%=1, flags%=Image.DefaultFlags) Local image:Image = null asyncImages.Add(path, image) LoadImageAsync(path, frames, flags, Self) Return image End Method OnLoadImageComplete:Void(image:Image, path$, source:IAsyncEventSource) If image Then asyncImages.Set(path, image) ' works but doesn't change testImage 'If image Then asyncImages.Get(path) = image ' error message: cannot convert from image to string End Method OnUpdate:Int() UpdateAsyncEvents() Return 0 End Method OnRender:Int() Cls() ' works If asyncImages.Get("myimage.png") Then DrawImage(asyncImages.Get("myimage.png"), 0, 0) ' doesn't work If testImage Then DrawImage(testImage, 100, 0) Return 0 End End [/monkeycode] So basically I store async images in a StringMap and I don't need to worry about including individual images into OnLoadImageComplete. I would, however, much prefer to be able to use the testImage Field, e.g. [monkeycode] DrawImage(testImage, 0, 0) [/monkeycode] than [monkeycode] DrawImage(asyncImages.Get("myimage.png")) ' this works [/monkeycode] So I think something in my OnLoadImageComplete method should be different to achieve this, but I just don't find a solution. I'm not very surprised that my code doesn't update testImage when the image is loaded, but any other method I tried failed completely. I would appreciate any help. Maybe I'm completely on the wrong track. Thanks! Anatol |
| ||
OK, this is not really what I was looking for, but I'm loading async resources like this for now, unless I find a better way. [monkeycode] Strict Import mojo Function Main:Int() New AsyncTestApp Return 0 End Class AsyncTestApp Extends App Implements IOnLoadImageComplete, IOnLoadSoundComplete Field asyncImages:StringMap<Image> Field asyncSounds:StringMap<Sound> Method OnCreate:Int() SetUpdateRate(60) asyncImages = New StringMap<Image> asyncSounds = New StringMap<Sound> LoadImageAsync("myimage.png") LoadSoundAsync("mysound.ogg") Return 0 End Method AllAsyncImagesLoaded:Bool() For Local image:= Eachin asyncImages.Values() If Not image Then Return False Next Return True End Method AllAsyncSoundsLoaded:Bool() For Local sound:= Eachin asyncSounds.Values() If Not sound Then Return False Next Return True End Method AllAsyncResourcesLoaded:Bool() Return AllAsyncImagesLoaded() And AllAsyncSoundsLoaded() End Method LoadImageAsync:Void(path$, frames%=1, flags%=Image.DefaultFlags) If asyncImages.Contains(path) Then Return ' the image is already in the LoadImageAsync map asyncImages.Add(path, null) asyncloaders.LoadImageAsync(path, frames, flags, Self) End Method LoadSoundAsync:Void(path$) If asyncSounds.Contains(path) Then Return ' the sound is already in the LoadSoundAsync map asyncSounds.Add(path, Null) asyncloaders.LoadSoundAsync(path, Self) End Method OnLoadImageComplete:Void(image:Image, path$, source:IAsyncEventSource) If image Then asyncImages.Set(path, image) End Method OnLoadSoundComplete:Void(sound:Sound, path$, source:IAsyncEventSource) If sound Then asyncSounds.Set(path, sound) End Method DrawAsyncImage:Void(path$, x#, y#) If asyncImages.Get(path) Then DrawImage(asyncImages.Get(path), x, y) End Method PlayAsyncSound:Void(path$, channel%=0, flags%=0) If asyncSounds.Get(path) Then PlaySound(asyncSounds.Get(path), channel, flags) End Method OnUpdate:Int() UpdateAsyncEvents() If AllAsyncResourcesLoaded() If TouchDown() Then PlayAsyncSound("mysound.ogg") EndIf Return 0 End Method OnRender:Int() Cls() DrawAsyncImage("myimage.png", 0, 0) Return 0 End End [/monkeycode] Ideally I wanted to get some code that's somewhat "backwards compatible" to the usual DrawImage with an image field so that I don't need to go through all my code and change it. But this also works well and saves me to declare Fields for every image and sound as these get simply added to the StringMaps asyncImages and asyncSounds. A positive side effect is that any image/sound that's already in these maps won't get loaded twice. This is just some quick code (but it works) and may well need some refinement, e.g. a method that returns True at the moment that all resources have completed loading - that would be helpful to start playing any background music or to start rendering a scene only when all resources are available. If anyone comes up with a way to use Fields as with non-async images please do post it. |
| ||
And another post on this subject. The code below is also not perfect but seems to work well without the need for a StringMap. It has the OnLoadImageComplete() method in an AsyncImage Class. The sample code below loads a "normal" image and one image that's loaded asynchronously with LoadImage(), DrawImage() and LoadAsyncImage(), DrawAsyncImage() respectively. [monkeycode] Strict Import mojo Function Main:Int() New AsyncTestApp Return 0 End Class AsyncTestApp Extends App Field normalImage:Image Field asyncImage:AsyncImage Method OnCreate:Int() SetUpdateRate(60) normalImage = LoadImage("myimage.png") asyncImage = LoadAsyncImage("myasyncimage.png") Return 0 End Method OnUpdate:Int() UpdateAsyncEvents() Return 0 End Method OnRender:Int() Cls() DrawImage(normalImage, 0, 0) DrawAsyncImage(asyncImage, 100, 0) 'asyncImage.Draw(100, 0) ' alternative to the line above Return 0 End End Class AsyncImage Implements IOnLoadImageComplete Private Field image:Image Public Method Initialize:AsyncImage(path$, nframes%, iflags%) LoadImageAsync(path, nframes, iflags, Self) Return Self End Method OnLoadImageComplete:Void(_image:Image, path$, source:IAsyncEventSource) image = _image End Method Draw:Void(x#, y#, frame%=0) If image And image.Loaded() Then graphics.DrawImage(image, x, y, frame) End Method Draw:Void(x#, y#, rotation#, scaleX#, scaleY#, frame%=0) If image And image.Loaded() Then graphics.DrawImage(image, x, y, rotation, scaleX, scaleY, frame) End ' duplicating methods from Image class below (this needs to be updated if the Image class changes in a future release) Method Width:Int() Return image.Width() End Method Height:Int() Return image.Height() End Method Loaded:Int() Return image.Loaded() End Method Frames:Int() Return image.Frames() End Method Flags:Int() Return image.Flags() End Method HandleX:Float() Return image.HandleX() End Method HandleY:Float() Return image.HandleY() End Method GrabImage:Image(x%, y%, width%, height%, frames%=1, flags%=DefaultFlags) Return image.GrabImage(x, y, width, height, frames, flags) End Method SetHandle:Int(tx#, ty#) image.SetHandle(tx, ty) Return 0 End Method Discard:Void() image.Discard() End Method WritePixels:Void(pixels%[], x#, y#, width#, height#, offset#=0, pitch#=0) image.WritePixels(pixels, x, y, width, height, offset, pitch) End End Function LoadAsyncImage:AsyncImage(path$, frameCount%=1, flags%=Image.DefaultFlags) If path = "" Then Return null Return (New AsyncImage).Initialize(path, frameCount, flags) End Function DrawAsyncImage:Void(asyncImage:AsyncImage, x#, y#, frame%=0) If asyncImage Then asyncImage.Draw(x, y, frame) End Function DrawAsyncImage:Void(asyncImage:AsyncImage, x#, y#, rotation#, scaleX#, scaleY#, frame%=0) If asyncImage Then asyncImage.Draw(x, y, rotation, scaleX, scaleY, frame) End [/monkeycode] I would much prefer to have the AsyncImage class extend Image, [monkeycode] Class AsyncImage Extends Image Implements IOnLoadImageComplete [/monkeycode] which would probably allow to use the regular DrawImage() function with AsyncImage. However, I didn't find a way to do that because there are too many private Fields in Image class that I would need in AsyncImage.OnLoadImageComplete(), and I can't just do something like [monkeycode] Method OnLoadImageComplete:Void(_image:Image, path$, source:IAsyncEventSource) Self = (AsyncImage)_image ' bad idea End [/monkeycode] Anyway, any other input here would be great! Maybe I'm completely overlooking another and much easier solution. |
| ||
there are several ways to go about this. this is the easiest way: [monkeycode] Class AsyncImage Implements IOnLoadImageComplete Field image:Image Field loaded:Bool = false Method OnLoadImageComplete(_image:Image, path$, source:IAsyncEventSource) image = _image loaded = true End End '' ...etc... Field puppy:AsyncImage = New AsyncImage Method OnRender() '' you can check each image, or use a global ALL_IMAGES_LOADED If Not puppy.loaded Then Return DrawImage puppy.image,x,y End [/monkeycode] I know what you're going after, but ideally you'd Extend Image and assign surface to the new surface-- but since it's private we can't do that. Optimally, I'd create a resource manager and keep the Image Fields in that (ie. DrawImage MyImages.puppy) so when the file is loaded, it is assigned. Similar to what you have above. |
| ||
Thanks for the reply, Adam. Yes, the private surface Field is the main problem to extend the Image class. Another "convenience option" with the AsyncImage class as above is to add a few DrawImage methods to the App class, that would then be called instead of the DrawImage function: [monkeycode] Class MyApp Extends App 'Method OnCreate:Int(), etc. Method DrawImage:Void(image:Image, x#, y#, frame%=0) graphics.DrawImage(image, x, y, frame) End Method DrawImage:Void(image:Image, x#, y#, rotation#, scaleX#, scaleY#, frame%=0) graphics.DrawImage(image, x, y, rotation, scaleX, scaleY, frame) End Method DrawImage:Void(asyncImage:AsyncImage, x#, y#, frame%=0) If asyncImage.image And asyncImage.image.Loaded() Then graphics.DrawImage(asyncImage.image, x, y, frame) End Method DrawImage:Void(asyncImage:AsyncImage, x#, y#, rotation#, scaleX#, scaleY#, frame%=0) If asyncImage.image And asyncImage.image.Loaded() Then graphics.DrawImage(asyncImage.image, x, y, rotation, scaleX, scaleY, frame) End End [/monkeycode] But again, these are just workarounds for a "proper" extension of the Image class. I'm using a resources manager which is quite important for my project, I'm just trying to keep the forum posts focused on the problem. Cheers! (Ah! I just discovered the forum tag "monkeycode" in square brackets!) |