Sprite Sheets
A major challenge of 2D (and UI) game development is solved by using sprite sheets. It means that we copy together many single images into one big sheet file and write down a config file (e.g. json) which contains the position of the source image on that sprite sheet.
How to create a Sprite Sheet?
TypeSpriteJS does that automatically using naming conventions. As soon as we name a folder and files in the right way the dev-server creates a sprite sheet for us.
A Sprite Sheet is composed of: a folder, a sheet.json and source files:
assets/sprites1.sheet.json
assets/sprites1/
assets/sprites1/company-logo.single.aseprite
assets/sprites1/game-logo.single.aseprite
assets/sprites1/ui-skin.slices.aseprite
assets/sprites1/tiny.fnt
assets/sprites1/tiny-page0.png
NOTE
This only works in a folder (or subfolder) of assets/
.
Sprite Sheet Sources: Overview
Naming/Convention | Source/Program | Description |
---|---|---|
*.slices.asprite | Aseprite (1) | Imports all slices, 9-patch-slices and fonts |
*.single.asprite | Aseprite | Imports whole file as a single image |
*.anim.aseprite | Aseprite | All frame animations in that Aseprite file |
*.font.aseprite | Aseprite | Uses correctly named slices to export single letters |
*.fnt + *.png | True Type Font + BMFont Generator | Imports Bitmap Fonts with their page files. |
Notes
(1) You need Aseprite for this. Also make sure to configure the asepritePath
in typesprite.config.mjs
.
(2) Support is very early
The configuration json my look like this:
{
"width": 2048, // [optional] max with of sheet
"height": 2048, // [optional] max height of sheet
"trim": true, // [optional] trim to rectangle
"alwaysCompose": false // [optional] forces composition on every request
}
Load and use a Sprite Sheet
import {Component, SpriteSheet} from 'typesprite';
export class MyComponent extends Component {
@res('sheet', 'assets/sprites1.sheet.json')
private sprites1:SpriteSheet;
onInit() {
const skin = this.sprites1.ninePatches["main-button"];
}
}
Like all resources they can be requested via @res()
in a component. The SpriteSheet
class gives you access to fonts, slices and everything is ready to be used.
Workflow
One time:
- You create a simple json for configuration
- You create your real world program assets in the sprite sheet folder (aseprite, etc.)
While working:
- You run your game
- You modify your big file (e.g. adjust an animation in Aseprite)
- You reload your game in browser and it's updated
Benifits:
- Allows modifications in the real program in one place
- Export is automatically (no extra step in Aseprite)
- Exports only happen when your game requests them
- No automatic background exporting when you just switch your app window
- To move a sprite into another sheet is just a file copy in explorer
- Adding Things is just a file copy
- Configuration changes trackable and human-readable in git
- The conventions help all involved to understand what happens - no side-workflows.
Sprite Sheet Sources: Aseprite
Aseprite is a first class citizen in TypeSpriteJS sheet export workflow. There are some configurations and conventions to consider and we need to explain them.
Aseprite: Animations (*.anim.ase)
Aseprite supports frame based animations. There are two major ways of doing things:
- One animation in the entire file
- Many animations grouped by
tags
All in One:
If there are no tags in the animation all frames will be exported as one animation called default
:
File | Name in Sprite Sheet |
---|---|
run.anim.ase | run:default |
walk.anim.ase | walk:default |
turn.anim.ase | turn:default |
Using Tags:
File | Tag | Name in Sprite Sheet |
---|---|---|
player.anim.ase | run | player:run |
" | walk | player:walk |
" | turn | player:turn |
Additional Settings (looping, pivot):
To set a pivot for a single animation use the following name convention on the tag:
File | Tag | Anim Loops | Custom Pivot | In Sheet |
---|---|---|---|---|
player.anim.ase | run | ❌ | ❌ | player:run |
" | run:L! | ✅ | ❌ | player:run |
" | run:12x14! | ❌ | ✅ x:12 y:14 | player:run |
" | run:L,12x14! | ✅ | ✅ x:12 y:14 | player:run |
Use in code:
import {Component, GraphicsEngine, AnimationElement, SpriteSheet} from 'typesprite';
export class MyComponent extends Component {
@res('sheet', 'assets/sprites1.sheet.json')
private sprites1:SpriteSheet;
@link('GraphicsEngine:typesprite')
private gfx:GraphicsEngine; // given that it's defined in the EDF somewhere
onInit() {
const spr = new AnimationElement();
spr.setAnimation("player:run");
this.gfx.gameLayer.addChild(spr);
}
}
Aseprite: Slices + Nine-Patches + Fonts (*.slices.ase, *.font.ase)
Slices are exported directly:
File | Slice Name | Name in Sprite Sheet |
---|---|---|
level.slices.ase | block | block |
" | grass | grass |
A slice is imported as a Nine Patches when you provide the slice values in Aseprite:
File | Slice Name | Name in Sprite Sheet |
---|---|---|
ui.slices.ase | grow-block | grow-block |
" | ice-block | ice-block |
To define a Font you create a slice for each letter you like to support:
File | Slice Name | Letter | Font in Sheet |
---|---|---|---|
ui.slices.ase | font:tinytim:a | a | tinytim |
" | font:tinytim:b | b | " |
" | font:tinytim:c | c | " |
" | ... | ... | (a lot of work) |
Use in code:
import {Component, GraphicsEngine, AnimationElement, SpriteSheet} from 'typesprite';
export class MyComponent extends Component {
@res('sheet', 'assets/sprites1.sheet.json')
private sprites1:SpriteSheet;
@link('GraphicsEngine:typesprite')
private gfx:GraphicsEngine; // given that it's defined in the EDF somewhere
onInit() {
// SLICES, SINGLES
const blockFrame = this.sprites1.slices["block"];
const fixedBlock = new SpriteElement(blockFrame);
this.gfx.gameLayer.addChild(fixedBlock);
// NINEPATCH
const growBlock9P = this.sprites1.setNinePatch["grow-block"];
const growBlock = new NinePatchElement().setNinePatch(growBlock9P);
growBlock.setSize(20, 150);
this.gfx.gameLayer.addChild(growBlock);
// FONT
const font = yy.fonts["tinytim"];
const text = new TextElement().setFont(font)
text.textbox.setText("My Text")
this.gfx.gameLayer.addChild(text);
}
}
NOTE
For creating UIs the same SpriteSheet class is used. But instead of this.gfx.gameLayer
you use $ui(this.gfx.gui)
or new LUILabelButton(...)
.