[Snowman] A practical way of preloading images

My current project has a lot of images and it would be pretty ugly if an image takes a few seconds to load after the passage appears (the “delayed display problem”), which will happen if the story is played online.

My solution was loading all the images and hiding them with CSS in the initial passages. It helps, but the player may go to the next passage before they finish loading and some image will be missing, creating the delayed display problem in some later passage.

The ideal solution, I thought, would be using JS to detect when all the images in a passage have been downloaded, and not allowing the player to exit the passage until that. I found two JS scripts in Github (this and this) that do that, but both failed and my JS is way too limited to understand why.

Would you have any suggestion for this? Thanks!

I don’t know anything about Snowman or Twine, but here are some things you could try.

A lot of web games load everything before the game even starts. You could probably find some image loader library that loads everything and then fires some callback, and you could launch your game from that.

If you want to preload things on the fly, try only preloading the things that will be on whatever screens the user can be on next (maybe this is what you’re already doing). The browser will only load so many different things at once, so you could get stuck loading things the player won’t see until later, while they’re waiting for something that’s supposed to be on the screen already.

You could also save images as interlaced, so there won’t be ugly blank space when you first run into them. If that’s good enough you might not need preloading at all, unless your visitors are on satellite internet or dialup modems or something.

Also, make sure your images are as lightweight as possible. Sounds obvious, but it’s easy to forget. If they’re photographic images (JPEG), crank the quality down as much as you can to where it still looks good. If they’re illustrations (PNG), try running pngcrush on them, or pngquant/pngnq if they don’t have a lot of color variation. Don’t use JPEG for illustrations or PNG for photographs, and make sure they’re not being scaled down in browser.

The total disk cache for browsers is usually around 50 MB, and files larger than about 5 MB won’t get sent to disk cache. Those numbers are for Firefox on desktop, not sure about others. If you can crunch the images enough, you can take advantage of that for frequent visitors. Memory cache is much larger, obviously.

@cpb is absolutely correct. Start with preloading images and act after everything has been loaded.

For Snowman specifically, here is some code using the Preload-It code you linked to, @pseudavid.

I’ll include it all in Twee code and then discuss what is going on in each step.

:: StoryTitle
Snowman: Preloading Images

:: UserScript[script]

// Use jQuery's getScript() function to get and load a remote JS file
$.getScript( "https://unpkg.com/preload-it", function( data, textStatus) {
  
	// Prepare the loaded Preload library
	const preload = Preload();
	
	// Define what should happen when complete
	preload.oncomplete = function(items) {
		
		// Create an empty 'images' object
		window.images = {};
		
		// Add a property to window.images
		window.images = items;
		
		// Show() the correct starting passage
		window.story.show("True Beginning");
	};
  
	// Finally, fetch the files
	preload.fetch([
    'https://images.pexels.com/photos/248797/pexels-photo-248797.jpeg'
	]);
	
});

:: Start
Loading...

:: True Beginning
Images have loaded and now we can work!

<img src="<%= window.images[0].url %>" >

Using getScript()

Using jQuery’s getScript() function. The jQuery library is available in Snowman. It can be used anywhere and, because these images need to be loaded before anything else, putting this code in the Story JavaScript section makes the most sense.

The function can take multiple arguments, but we care about two of them: what to load and what should happen after it loads. In this example, the source “https://unpkg.com/preload-it” is loaded. jQuery retrieves this source and then executes it, loading and creating anything the script tells it to do.

In the “what should happen after loading argument,” there is a function. This helps us define that callback @cpb was talking about. The callback here waits for the script to be loaded and then runs its own code.

Using Preload-It

Based on the exact instructions from Preload-It, the code looks roughly like the following:

const preload = Preload();

preload.oncomplete = function(items) {
  ..
};

preload.fetch([
   ...
])

(For those not as knowledgeable about JavaScript ES6, Preload-It uses arrow functions for things. I’ve converted them back into “normal” ES5 functions.)

Using this library, we prepare it, define what should happen after it fetches, and then, finally, actually fetch a file.

Working with Snowman

You didn’t include which version of Snowman you are using, @pseudavid, so I wrote code that will work across 1.3, 1.4, and 2.0.2.

It starts by defining a new global property, window.images. Defining this allows us to load all the images and then access them in future passages.

Next, it calls window.story.show(). (I’ll link to the 2.X branch documentation here.) By supplying the name of a passage, the function immediately “goes” to that passage, replacing the current passage’s contents.

In my example, the first, starting passage (called “Start”) has text that reads Loading.... It will show this until the call to the function window.story.show() when its contents are replaced with the contents of the passage “True Beginning”.

Splitting it up like this allows for adding in instructions, details, or just a loading message in one passage and then having its contents be replaces when the game finally loads everything.

Displaying Loaded Images

Finally, images are displayed using their url property of each entry in the now window.images array.

Using value interpolation, the Underscore template system can write a variable’s value into a passage. In this example, the code is the following:

<img src="<%= window.images[0].url %>" >

It write the url property of the first (0) entry in the window.images array to the passage and then, when the browser sees the HTML code, it would see it as the following:

<img src="https://images.pexels.com/photos/248797/pexels-photo-248797.jpeg">

Moving Beyond the Example

To extend this example, list all of the images to fetch in the preload.fetch() function call and then, to use them in passages, write their values through where they are in the array. That is, window.images[X].url depending on which number they are in the array sequence.

Depending on the number and size of the images, as cpb mentioned, this could be consuming at first. However, once loaded, displaying them will be very fast.

As I mentioned, and many, many games do, including something for the player to read or do as the loading is going on can “hide” the experience. You could just as easily add a link to the starting passage that allows them to continue only after all the loading is finished. That’s a very common game design solution.

For what it’s worth, my Universal Inventory System (UInv) for Twine/SugarCube includes an image caching system. You can take a look at the UInv cacheImages() function documentation if you want to get an idea of how to use it.

The UInv image cache loader defaults to loading only 5 images at a time from the queue, so you won’t flood the bandwidth trying to download too many files at the same time, and it also lets you set a maximum number of images cached. You can tweak those settings by modifying the setup.UInvImageCache object.

If you wanted to pull the image cache code out of UInv and put it into your code, feel free. (Any of the UInv “utility functions”, which all start with a lowercase letter, can be safely extracted from UInv.)

Wow, this is amazing, thank you for all this! Based on your responses I finally got one of the scripts working.

I can’t stress how amazingly helpful the Twine community has been for years.