tilemap-parserv5.2.0GITHUB

Animations: frame-based sprites

The animation system is two objects and one rule. SpriteAnimationSet holds the parsed clips and the loaded spritesheet; AnimationPlayer is a pure frame clock that advances clip time and hands you the right frame image. The rule:AnimationPlayer.update() takes milliseconds, not seconds. Feed it clock.tick(60) directly.

LOADING

SpriteAnimationSet.load() is the one-call entry point: it parses the animation JSON and loads the spritesheet image in one step. The JSON's spritesheet_path is resolved relative to the JSON file; pass spritesheet_path= to override it.

Pass render_scale= to scale the sheet and its atlas grid (tile_size, grid_offset) in one step — handy for hi-res art rendered at a lower resolution or vice versa. The grid is pinned from the original sheet, so fractional scales with a nonzero grid_offset still address cells correctly. Scales that are not finite, not > 0, or that produce zero-sized or non-fitting cells are rejected at load.

loading.pyPYTHON
from tilemap_parser import SpriteAnimationSet, AnimationPlayer

anim_set = SpriteAnimationSet.load("data/animations/player.json", render_scale=2.0)
# anim_set.warnings collects non-fatal issues from the JSON

player = AnimationPlayer(anim_set, "idle")   # animation_name is REQUIRED

If you already have the JSON loaded separately, parse_animation_file(path) (or parse_animation_dict / parse_animation_json) returns the AnimationLibrary: the parsed clips, spritesheet_path, tile_size and grid_offset. Bad files raise AnimationParseError.

PLAYBACK: ONE CALL PER FRAME

Advance the clock, grab the frame image, draw it. The player owns no position. You decide where to blit.

game loopPYTHON
dt_ms = clock.tick(60)      # pygame returns MILLISECONDS
player.update(dt_ms)

image = player.get_current_image()   # Surface | None
if image is not None:
    screen.blit(image, (player_x, player_y))
  • frame_index: current frame position (int).
  • finished: True when a non-looping clip has played out;update() then no-ops until reset().
  • clip: the current AnimationClip (or None if the name isn't in the library).
  • reset(): back to frame 0 of the current clip.

SWITCHING ANIMATIONS

There is no play() method. The player's animation_name attribute is the switching API. Set it, then reset the clock to start the new clip from frame 0:

state.pyPYTHON
target = "run" if moving else "idle"
if player.animation_name != target:
    player.animation_name = target
    player.reset()     # restart the new clip at frame 0

Each AnimationClip declares its frames, per-frame duration_ms, loop, fps and metadata. Clips repeat by looping; for an attack you restart the same clip with reset().

ANCHORING AND PIXEL-PERFECT DRAW

Frames are cut from the spritesheet on a grid, honoring the library's tile_size and grid_offset. If the JSON enables trim_transparent, each frame is trimmed to its content. Use get_content_bounds() to ask where the visible pixels are, and anchor your blit against it:

anchor.pyPYTHON
bounds = anim_set.get_content_bounds(player.animation_name)
if bounds is not None:
    image = player.get_current_image()
    if image is not None:
        screen.blit(image, (x - bounds.x, y - bounds.y))   # keep feet planted

MARKERS ARE DATA, NOT CALLBACKS

Clips can carry named AnimationMarkers (e.g. "hit", "footstep") at frame indexes. The player exposes them on clip.markers. There is no built-in callback; you do the frame-crossing check yourself:

markers.pyPYTHON
prev_frame = 0  # keep this across frames

clip = player.clip
if clip is not None:
    f = player.frame_index
    if f >= prev_frame:
        crossed = set(range(prev_frame + 1, f + 1))
    else:
        # looped: finish the old run, then frames 0..f of the new one
        crossed = set(range(prev_frame + 1, clip.frame_count())) | set(range(0, f + 1))
    for m in clip.markers:
        if m.name == "hit" and m.frame_index in crossed:
            apply_damage()
    prev_frame = f

OBJECT ANIMATIONS

Objects on an object layer can carry a typed ObjectAnimation (internal dataclass) — frames cut from the object's tileset in a row-major grid (left-to-right, top-to-bottom). Required fields fail early at parse time; optional fields have defaults. Access obj.animation directly for the raw parsed data, or use TilemapData.get_object_animation(obj) which returns normalized AnimData (a dict with frames as surfaces, frame_w/h, frame_duration_ms, loop, animation_mode, and properties).

object animation jsonPYTHON
// inside data.layers[].objects["1"]
{
  "area": {"x": 32, "y": 64, "w": 16, "h": 16},
  "ttype": 0,
  "tileset_type": "object",
  "variant": 0,
  "animation": {
    "frame_count": 4,          // Required
    "frame_duration_ms": 120,  // Required
    "speed": 1.0,
    "loop": true,
    "animation_mode": "default", // or "random_start_times"
    "random_phase": false,
    "frames": [0, 1, 2, 3]       // optional explicit order
  }
}
object animation — pythonPYTHON
from tilemap_parser import load_map

data = load_map("data/map.json")
obj = data.get_layer("Objects").objects[1]

# Access raw parsed animation data from the object
raw_anim = obj.animation             # ObjectAnimation | None (internal dataclass)
if raw_anim is not None:
    print(raw_anim.frame_count, raw_anim.frame_duration_ms)

# get_object_animation returns normalized AnimData dict with frames + metadata
anim_data = data.get_object_animation(obj)  # AnimData | None
if anim_data is not None:
    frames = anim_data["frames"]             # list[Surface]
    print(anim_data["frame_duration_ms"], anim_data["loop"])
    print(anim_data["frame_w"], anim_data["frame_h"])
    # Draw current frame
    screen.blit(frames[frame_index], (obj.area.x, obj.area.y))
  • frame_count and frame_duration_ms are required — missing or non-positive values raise MapParseError.
  • frames overrides the default 0..frame_count-1 order when present.
  • Frame slicing uses obj.area.w × obj.area.h (one frame) as the cell size across the object's tileset sheet.
  • Access via obj.animation.properties is not needed — the dataclass is the typed view. Raw JSON stays on data.parsed.raw.