SFTtech / openage

Free (as in freedom) open source clone of the Age of Empires II engine 🚀
http://openage.dev
Other
12.73k stars 1.12k forks source link

Sprite definition format #965

Closed TheJJ closed 4 years ago

TheJJ commented 6 years ago

We have to get rid of the crappy csv files that describe where subtextures are. Instead, we need to create far more sophisticated™ key-value files that describe animations, frames and angles.

Why not nyan? The frame and animation definitions are directly linked to the image files. They don't change at runtime. And the renderer can directly organize the sprites internally without nyan queries.

TheJJ commented 6 years ago

Draft: https://pad.stusta.de/p/openage-sprite-format

phrohdoh commented 6 years ago

Have you considered an existing textual format such as toml?

zuntrax commented 6 years ago

We had a discussion in IRC, a C++ toml does not seem to be packaged on all the common distros. We generally would like to avoid pulling another dependency for that. Parsing whatever format we choose in python should be easy and allows using existing formats like json if we want. Nyan has enough fancyness, I'd like to use something that people are used to already.

TheJJ commented 5 years ago

propsed specification:

Sprite File Formats

.sprite files define a unit/building/... sprite and its animations.

.terrain files define a terrain sprite and its properties like blending and animation.

Both file types contains key-value pairs. They select properties like which .png has which frames, where the frames are and where the hotspots are, what animations are provided for which direction, and what the animation mode/speed is.

Why not nyan?

https://github.com/SFTtech/nyan has quite some overhead and is not as compact as this format. The sprite files can directly be loaded into the renderer and bypass nyan. The animations are requested by the game simulation, the renderer displays them independently by, for example, choosing the best matching angle.

Requirements

.sprite file

What the .sprite can define:

What the .sprite can NOT define (multiple .sprites are defined in nyan):

.terrain file

Format

.sprite file

# comments are ignored
version 0             # file version, so we can maybe extend the format

# image file reference, relative to this file (-> .sprites can reference other mods' png files)
imagefile <image_id> <filename>

# layer definitions, from bottom to top (later defined layers overdraw earlier layers)
# all layers will be drawn.
layer <id> mode=off  position=<default|roof|shadow>
layer <id> mode=once position=<default|roof|shadow> time_per_frame=<float>
layer <id> mode=loop position=<default|roof|shadow> time_per_frame=<float> replay_delay=<float>

# define an angle where frames can be assigned to or mirror from an existing angle
angle <degree> mirror-from=<existing_angle>

# assign frames to their layers and angles.
# angle is the direction in degrees, 90 = east, etc.
# *pos, *size and *hotspot is within the source image.
# all the hotspots of the frames will be drawn at the same pixel (requested by renderer)
# so that the alignment/movement of the frames is done solely by hotspots.
frame <layer_id> <angle> <image_id> <xpos> <ypos> <xsize> <ysize> <xhotspot> <yhotspot>

.terrain file

# comments are ignored
version 0

# priority for selecting this terrain's blending mask
blending_priority <int>

# selection of blendomatic borders
blending_mask <blend_id> <filename>

# how many dots in one frame are used for one tile
dots_per_tile <float>

# source image definitions
imagefile <image_id> <filename>

# layer and animation definitions
# layers defined first will be overdrawn by later definitions
layer <id> mode=off
layer <id> mode=loop time_per_frame=<float> replay_delay=<float>

# definition of a terrain frames
# these are iterated for an animation
frame <layer_id> <image_id> <blend_id> <xpos> <ypos> <xsize> <ysize>

Examples

villager_female_walking.sprite

One png file has the whole sprite sheet and we just pick the right places for all the walking directions. Walking to the left uses the same sprites as walking to the right, just mirrored. The shadow is already included in the same png.

version 0
imagefile 0 female_walking.png
layer 0 mode=loop position=default time_per_frame=0.1 replay_delay=0.0
angle 90  # 90-degree = walk right

frame 0 0 3 5 40 60 20 20
frame 0 0 33 35 70 90 50 50
...

angle 270 mirror_from=90
angle 180
frame 0 0 .....

Random house graphics

House with random graphic, needs multiple .sprites. Each sprite file defines one graphical variant out of the "houses" spritesheet texture.

variant0.sprite

version 0
imagefile 0 houses.png
imagefile 1 house_shadows.png
layer 0 mode=off position=default
layer 1 mode=off position=shadow

angle 0
frame 0 0 0 0 50 50 25 25
frame 1 1 0 0 30 50 TODOOOOOOOOO

variant1.sprite

TODOOOOOOOOOOOOO
version 0
imagefile 0 houses.png
layer 0 mode=off
frame 0 0 50 0 50 50 75 25

Wall gate:

4 .sprites:

In each, the animations and frames for each angle are stored. Handling must be done in the engine, their overlay etc is done with nyan.

brisvag commented 4 years ago

As I mentioned here, there are some things I noticed while working on the sprite formatter:

heinezen commented 4 years ago

In the explanation, mirror_from is on a different line than angle definitions, but in the example, it's on the same line. I would suggest leaving it on the same line.

That was an error in the definition. It is supposed to be on the same line. Fixed it in jj's post :)

values are sometimes represented with keywords (in the form mirror_from=...) and sometimes pure data (such as frame 0 0 3...). Is there a reason for not using the same system in all places?

It is very pythonic (like calling a method). Mandatory values have fixed positions and require no keyword, all other values have a keyword because they are only valid for specific modes or optional. Plain data will also be marginally faster to parse.

related to the previous point: in the example some lines have variable number of members (layer's position parameter can be omitted). Does this mean the data is actually missing, or simply that you would omit it when the value is equal to a "default"?

There is no default value in that case. Sometimes the value is simply not be necessary. replay_delay makes no sense for animations that do not loop for example.

in the sprite definition, angle information is carried by each frame. It seems redundant to have it there and also divide them under angle X categories. I would suggest using only categories (so mirror_from is easier to see/implement) and leaving out the angle from the frame data.

Hmm.. what would be the benefit of that? Internally, the frame must be attached to an angle anyway and we would have to remember the previously defined angle. We can do it of course, but I do not see a real benefit.

brisvag commented 4 years ago

There is no default value in that case. Sometimes the value is simply not be necessary. replay_delay makes no sense for animations that do not loop for example.

Gotcha.

Hmm.. what would be the benefit of that? Internally, the frame must be attached to an angle anyway and we would have to remember the previously defined angle. We can do it of course, but I do not see a real benefit.

It just felt unnecessary to have the redundancy, it's more stuff to parse. So this makes me think: does angle information (stuff like mirror_from) come from a different place altogether than frame information?

heinezen commented 4 years ago

Let's keep the redundancy for now as it might make debugging a tiny bit easier. We can always change it later on when everything works well.

does angle information (stuff like mirror_from) come from a different place altogether than frame information?

Yes, some attributes of a sprite come from the .dat file, some from the SLP/SMP graphics file. Frame attributes are from different places and graphics for example.

TheJJ commented 4 years ago

It just felt unnecessary to have the redundancy, it's more stuff to parse. So this makes me think: does angle information (stuff like mirror_from) come from a different place altogether than frame information?

It's true, we could implicitly assign an angle by assigning it to the last-defined angle. I made that explicit to allow rearranging the lines: you could with the explicit angle reference first define all angles, and then assign all sprites to their angles.