Today we’ll be
- Animating in Procreate Dreams
- Exporting our animations
- Building SpriteSheets
- Using those animations to drive character animation
Procreate Dreams is an animation tool developed by the team behind the wildly successful drawing app Procreate.
It presents a novel way to build frame-by-frame animation using the Apple pencil, which as someone who studied art in college is very exciting to me.
So naturally I wanted to use it in the upcoming Bevy game jam to build sprite sheets and other animated graphics.
To do that I need to develop a workflow for getting images out of Procreate Dreams and into Bevy.
Inside Procreate Dreams
In Dreams, we can create a new canvas with a specific sizing. I’m using 256x256 squares, but that’s probably quite big. The default is 1080x1080 though and that’s way too big, so I brought it down a bit.
Once inside our timeline, we can zoom in as far as possible or go to the flipbook and start drawing. There’s a bunch of great tools here like onion skins and so on.
For us, the important part is that each frame is going to be used by us as a sprite index. In Bevy you can control how long these frames are displayed and anything else, but the most straightforward way to handle it right now is if you equate a frame to being some period of time in your head.
Something like 0.1s/frame would be 10 fps in the Dreams UI.
After some animation work you should end up with a bunch of frames for different states of your object. I’ve chosen to include “idle”, “healing”, and “moving” states by slightly modifying a brush pack and doing some light animation work.
Procreate handles creating each individual frame in between each keyframe, which is great for us.
Our final move in procreate is to export as frames. For my use I’m going to export and save to iCloud using Files because that will send it over to my laptop easily.
Once we have the files on our computer, we need to create a spritesheet and get that into Bevy.
Spritesheets
There’s a bunch of tools out there that will pack a spritesheet, and many of them have complicated algorithms for packing spritesheets using as little space as possible. We’re going for the simple route using a square grid sheet.
I wanted to build an automatable process for this, so I started with a tool by Amethyst (a Rust game engine before Bevy existed) called sheep.
CLI tools tend to order files so that 1 and 10 are next to each other and Procreate Dreams doesn’t pad its numeric frame exports, so I had to write a script for renaming frame_1.png
to frame_01.png
to satisfy ordering for CLI tools. Its not pretty, but I wrote it as a one-off in nushell in a couple minutes.
ls frame_*.png | get name | each {|name| parse 'frame_{version}.png' | get 0 | get version } | each {|version| mv $'frame_($version).png' $'frame_($version | fill -a right -c '0' -w 2).png' }
I used sheep
to pack a sprite sheet from the resulting images, but it seemed to drop some of the sprites. Some of the animation cycles would have n-1 frames in the resulting spritesheet and I’m not really sure why. Either way this tool hasn’t seen an update in years so I moved to using a piece of GUI software to pack the textures.
sheep pack *.png --options max_height=256 max_width=4864 --out shroom_sheet
If I continue this path in the future, I’ll build a tool to do this if I have to so that the process is as automatable as possible.
Once we have the spritesheet generated, we need to get it into Bevy to use as a TextureAtlas.
This is most easily done using bevy_asset_loader.
Bevy Asset Loader
bevy_asset_loader
with the 2d
feature allows us to specify the size of our texture atlas grid, as well as how many rows and columns there are.
#[derive(AssetCollection, Resource)]
struct MyAssets {
#[asset(texture_atlas(
tile_size_x = 256.,
tile_size_y = 256.,
columns = 4,
rows = 5
))]
#[asset(
path = "mushroom/frames/color-small/shroomy.png"
)]
dream: Handle<TextureAtlas>,
}
This will give us access to the TextureAtlas
to use in a SpriteSheetBundle
.
Animating the Sprite
That sets everything up and the last thing we need to do is actually animate the sprite’s texture index.
The most basic way to do this is to set a Timer
to repeat every 0.1 seconds.
Timer::from_seconds(
0.1,
TimerMode::Repeating,
)),
)
We can query for and tick the timer in our systems.
timer.0.tick(time.delta());
if timer.0.finished() {...
and control the sprite index based on any arbitrary logic we want
sprite.index = 4 + *charge_step;