When building games you may have assets you want to load from an external file, like sound, art, etc.
Resources must be loaded before they can be used with a Loader
When calling Engine.start, you can optionally pass an asset Loader. This loader will contain a reference to any "loadables" you want to load.
const loader = new ex.Loader([
/* add Loadables here */
])
Loadables are different kinds of assets such as image, sounds, and generic resources.
Generally we recommend creating a file in your project dedicated to your resources. Something like this
// resources.ts
import * as ex from 'excalibur'
const Images = {
// Characters
heroImage: new ex.ImageSource('/path/to/hero.png'),
enemyImage: new ex.ImageSource('/path/to/enemy.png'),
// Weapons
swordImage: new ex.ImageSource('/path/to/sword.png'),
arrowImage: new ex.ImageSource('/path/to/arrow.png'),
// Maps
overWorldImage: new ex.ImageSource('/path/to/overWorld.png'),
}
const Sounds = {
jump: new ex.Sound('/path/to/jump.mp3', '/path/to/jump.wav'),
hit: new ex.Sound('/path/to/hit.mp3', '/path/to/hit.wav'),
}
const loader = new ex.Loader()
const allResources = { ...Images, ...Sounds }
for (const res in allResources) {
loader.addResource(allResources[res])
}
export { loader, Images, Sounds }
Then in your main game you can import your loader, images, and sounds for use!
// main.ts
import * as ex from "excalibur";
import { loader, Images, Sounds } from "./resources"
const engine = new ex.Engine({...});
const hero = new ex.Actor({...});
const sprite = Images.heroImage.toSprite();
hero.graphics.use(sprite);
hero.on('postcollision', () => {
Sounds.hit.play()
});
engine.start(loader);
Sometimes you may have some other type of file you'd like to load, perhaps some data stored in a text file, json, or perhaps some binary data.
Excalibur supports a generic Resource to load arbitrary data.
const game = new Engine({...});
const text = new Resource<string>('./path/to/my/data.txt', 'text');
const json = new Resource<MyJsonShapeType>('./path/to/my/json.json', 'json');
const loader = new Loader([text, json]);
await game.start(loader);
console.log(text.data);
console.log(json.data);
The asset loader only works with a web server since it loads assets with XHR. That means you cannot use the loader when running an HTML file locally from the file-system (e.g. a file://
protocol URL will not work). The browser throws errors that will prevent you from loading assets.
The fastest way to serve a folder of files is by using the serve NPM package.
# Serve the current directory
npx serve .
# Serve a folder
npx serve ./dist
If you are developing a game using Excalibur with Webpack, Parcel, or another bundler, these typically already come with dev servers for running your game. See Excalibur project templates for templates you can start from that use these tools.
Given this directory structure:
/root
src/
game.js
assets/
textures/
map.png
index.html
And you serve from the root
directory like this:
> cd root
> npx serve .
Now serving on http://localhost:3000/
The path to your assets doesn't matter as much because both absolute and relative paths will work:
/assets/textures/map.png => HTTP 200 OK
assets/textures/map.png => HTTP 200 OK
But if you are serving under a sub-directory, like http://localhost:3000/root/index.html
then the format of your paths matter:
/assets/textures/map.png => HTTP 404 Not Found
assets/textures/map.png => HTTP 200 OK
The first path will fail to load as the absolute asset path would now be /root/assets
and not /assets
. Use a relative path to load assets relative to the HTML file serving your game.
In your HTML file(s), to set the base for any absolute paths like the example above, you can use the base tag:
<!DOCTYPE html>
<html>
<head>
<!-- Set the base for all absolute URLs -->
<base href="/root" />
</head>
<body>
<!-- The browser will now properly resolve /root/game.js -->
<script src="/game.js"></script>
</body>
</html>
This can be accessed programmatically using document.baseUri to resolve absolute paths in JavaScript.
This is a good approach to use when hosting your game at a sub-directory, such as publishing to GitHub Pages.
In your game, you may not want the default Excalibur loading screen. It is possible to customize the screen to show anything.
The default Loader comes with some builtin customization features to tweak the default behaviors. If you create a custom play button, it's important to give it the id excalibur-play
.The play button is necessary to unlock the browser's audio context for your game. Here is an example:
const loader = new ex.Loader([...]);
loader.loadingBarColor = ex.Color.Black;
loader.backgroundColor = ex.Color.White;
loader.loadingBarPosition = ex.vec(40, 270);
loader.startButtonFactory = () => {
let buttonElement: HTMLButtonElement = document.getElementById('excalibur-play') as HTMLButtonElement;
if (!buttonElement) {
buttonElement = document.createElement('button');
}
buttonElement.id = 'excalibur-play';
buttonElement.textContent = 'some different text'
buttonElement.style.backgroundColor = 'blue';
// Initially hide the button
buttonElement.style.display = 'none';
return buttonElement;
};
loader.playButtonPosition = ex.vec(100, 100);
loader.playButtonText = "Let's mosey";
loader.logoPosition = ex.vec(140, 180);
// base64 encoding of your custom logo
loader.logo = '"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQA...';
Play around with the code here
By extending the built in Loader you can draw anything you like. If you create a custom play button, it's important to give it the id excalibur-play
.The play button is necessary to unlock the browser's audio context for your game.
class CustomLoader extends ex.Loader {
// Return a custom play button html element
public startButtonFactory = () => {
let buttonElement: HTMLButtonElement = document.getElementById(
'excalibur-play'
) as HTMLButtonElement
if (!buttonElement) {
buttonElement = document.createElement('button')
}
buttonElement.id = 'excalibur-play'
buttonElement.textContent = this.playButtonText
buttonElement.style.display = 'none'
return buttonElement
}
draw(ctx: CanvasRenderingContext2D) {
// custom drawing code
c.fillStyle = 'red'
c.fillRect(0, 0, c.canvas.width, c.canvas.height)
console.log(this.progress) // progress is between [0, 1]
}
}