Advanced Animation Control in GameMaker Studio 2 – Method 1

Let’s say that you have a sprite with a complex animation (i.e. variable frame rate). As you can see from the following image, each frame will play at a specific time (I use a simple Photoshop script to export the frames, I’ll write an article about it later).

This is the folder’s content, exported from Photoshop. File naming scheme is important.

A simple string

If you look closely, each filename already encodes the information about its own animation timing. What if we could describe the animation with a simple string? Something like this:

var animation_string = "00001111223333444455666677778888"

Each number, represents the sub-image to draw. Each number’s position inside the string, represent the timing. As you can see this poses two problems:

  1. Currently, with my Photoshop export script, I don’t have any information about the duration of the last frame. It’s not encoded anywhere because there is no such information in the Photoshop exported files to begin with. I still don’t know how to solve this problem. It’s not a big deal though; I manually fill in the last frame information.Since I know the last frame’s duration, I manually edit the string from
    var animation_string = "00001111223333444455666677778888"
    to
    var animation_string = "000011112233334444556666777788889999"
  2. If we use only numbers from 0 to 9 to encode them, we can theoretically store only up to 10 different frames. This can easily be solved though.

Mapping more than 10 different frames

To go beyond 10 frames, we could theoretically use the hex code. We could indeed encode 16 different frames (from 0 to F). But who said we should limit ourselves to hexadecimal codes? Let’s extend the same concept to use all the alphabet… and then some…

I use the following GML script to map single characters to integers:

/// @desc       Return an int value from a pseudo-hex (custom extended-hex) char.
/// @func       scr_exhex_to_number(exhex_char)
/// @author     Nikles
/// @version    1.1
/// @arg        exhex_char  The "extended hex" character to translate

switch (argument[0])
{
    case "0":   return 0;
    case "1":   return 1;
    case "2":   return 2;
    case "3":   return 3;
    case "4":   return 4;
    case "5":   return 5;
    case "6":   return 6;
    case "7":   return 7;
    case "8":   return 8;
    case "9":   return 9;
    case "A":   return 10;
    case "B":   return 11;
    case "C":   return 12;
    case "D":   return 13;
    case "E":   return 14;
    case "F":   return 15; 
    case "G":   return 16; 
    case "H":   return 17;
    case "I":   return 18;
    case "J":   return 19;
    case "K":   return 20;
    case "L":   return 21;
    case "M":   return 22;
    case "N":   return 23;
    case "O":   return 24;
    case "P":   return 25;
    case "Q":   return 26;
    case "R":   return 27;
    case "S":   return 28;
    case "T":   return 29;
    case "U":   return 30;
    case "V":   return 31;
    case "W":   return 32;
    case "X":   return 33;
    case "Y":   return 34;
    case "Z":   return 35;
    case ",":   return 36;
    case ";":   return 37;
    case ".":   return 38;
    case ":":   return 39;
    case "-":   return 40;
    case "_":   return 41;
    case "|":   return 42;
    case "!":   return 43;
    case "£":   return 44;
    case "$":   return 45;
    case "%":   return 46;
    case "&":   return 47;
    case "/":   return 48;
    case "(":   return 49;
    case ")":   return 50;
    case "=":   return 51;
    case "?":   return 52;
    case "'":   return 53;
    case "^":   return 54;
    case "*":   return 55;
    case "]":   return 56;
    case "[":   return 57;
    case "{":   return 58;
    case "}":   return 59;
    default:    return -1;
}

With the above script I’m able to map 60 different frames to 60 different chars. That’s nice but how do I use it?

Usage

In the CREATE event of the animated object I can use this simple code:

// Set animation timeline and animation length
image_speed         = 0     // Stop the GMS2 standard animation playback
animation_string    = "00001111222233445566778899AABBCCDDEEFFFFGGGGHHHHIIIIJJJJ"
animation_len       = string_length(animation_string)
animation_index     = 1     // Starts at 1
  • Stop playing the standard GameMaker animation setting the image_speed = 0
  • Set the animation string, containing all the information about which frame should play at a specified time (time is the position inside the string, btw).
  • Calculate the duration of the animation (which is basically the length of the animation_string)
  • Set the starting animation_index = 1. It’s important to start at 1 and not 0 because of how GameMaker Studio 2 works with string indexes.

You can copy the code (and adapt it to your needs). Just make sure to use your own strings for your own sprites/animations 🙂

In the STEP event I place this code:

// Get the correct frame
animation_image     = scr_exhex_to_number(string_char_at(animation_string, animation_index))

// Increase Animation "Timer"
animation_index++

// Loop Animation
if animation_index > animation_len
    animation_index = 1

// Uncomment if you don't use a custom draw event
// image_index = animation_image
  • get the current animation frame using the scr_exhex_to_number script.
  • to do so, get the character of the string representing the current, encoded frame (string_char_at(animation_string, animation_index))
  • increase the animation_index (so the next step I get the next character)
  • if it reached the end of the animation, loop it.

I can then use a custom DRAW event with a draw_sprite_ext call or override the image_index with the animation_image.

GML code generation with Python 3

Let’s be serious: I can’t write those strings manually each time. I mean… ok, I might still need to manually fill the last frame duration info, but creating the whole strings from scratch, for many different sprites, looks like hard work.

Let’s automate. With Python 3.

import glob

index_to_exhex = {
    0:    "0"  ,
    1:    "1"  ,
    2:    "2"  ,
    3:    "3"  ,
    4:    "4"  ,
    5:    "5"  ,
    6:    "6"  ,
    7:    "7"  ,
    8:    "8"  ,
    9:    "9"  ,
    10:    "A" ,
    11:    "B" ,
    12:    "C" ,
    13:    "D" ,
    14:    "E" ,
    15:    "F" , 
    16:    "G" , 
    17:    "H" ,
    18:    "I" ,
    19:    "J" ,
    20:    "K" ,
    21:    "L" ,
    22:    "M" ,
    23:    "N" ,
    24:    "O" ,
    25:    "P" ,
    26:    "Q" ,
    27:    "R" ,
    28:    "S" ,
    29:    "T" ,
    30:    "U" ,
    31:    "V" ,
    32:    "W" ,
    33:    "X" ,
    34:    "Y" ,
    35:    "Z" ,
    36:    "," ,
    37:    ";" ,
    38:    "." ,
    39:    ":" ,
    40:    "-" ,
    41:    "_" ,
    42:    "|" ,
    43:    "!" ,
    44:    "£" ,
    45:    "$" ,
    46:    "%" ,
    47:    "&" ,
    48:    "/" ,
    49:    "(" ,
    50:    ")" ,
    51:    "=" ,
    52:    "?" ,
    53:    "'" ,
    54:    "^" ,
    55:    "*" ,
    56:    "]" ,
    57:    "[" ,
    58:    "{" ,
    59:    "}"
}

folder  = r'.'
folders = glob.glob(folder + "\*\\")

with(open(folder + '\\anim.txt', 'a')) as out:
    print("""if  !variable_global_exists("ds_animation_strings") || !ds_exists(global.ds_animation_strings, ds_type_map)
{
    variable_global_set("ds_animation_strings", ds_map_create());
}
""", file = out)

for _folder in folders:
    sprite_name = (_folder[:-1].rpartition("\\")[-1])
    frame_files = glob.glob(_folder + "\*.png")
    frame_list = [(filename.rpartition('_')[-1][:-4]) for filename in frame_files]
    frame_list.sort(key=int)
    
    with(open(folder + '\\anim.txt', 'a')) as anim:
        index      = 0
        prev_frame = 0
        anim_string = ""
        for frame in frame_list:
            if int(frame) == 0:
                prev_frame = 0
                index+=1
                continue
            else:
                frame_list = [(index_to_exhex[index - 1]) for i in range(prev_frame, int(frame))]
                anim_string += ''.join(frame_list)
                prev_frame = int(frame)
                index+=1
        print("ds_map_replace(global.ds_animation_strings, \"" + sprite_name + "\", \"" + anim_string + "\")", file = anim)

I have that Python script in a file called anim_string.py.

Now let’s say that I have multiple folders. Each of them named as the final sprite in the GMS2 project. What if I could generate GML code for each sprite, automatically, without having to write the animation strings by myself?

As you can see I have that python 3 script in the root folder where I keep all my Photoshop sprites exports.

I then type cmd in the address bar to open up a command prompt.

This will generate a text file with the correct GMS2 GML code for the system initialization. All my animation_strings will be automagically generated.

I execute the script and close the prompt. An interesting text file will appear.

I open it and just copy this GML code to use in GameMaker Studio 2:

if  !variable_global_exists("ds_animation_strings") || !ds_exists(global.ds_animation_strings, ds_type_map)
{
    variable_global_set("ds_animation_strings", ds_map_create());
}

ds_map_replace(global.ds_animation_strings, "spr_door_anim", "00001111222233445566778899AABBCCDDEEFFFFGGGGHHHHIIIIJJJJ")
ds_map_replace(global.ds_animation_strings, "spr_lamp_anim", "00001111223333444455666677778888")

I then add the final frame animation info by hand and I get this:

I added the KKKK and the 9999 at the end of the strings (click to enlarge)

I run this GML code only once at the beginning of the game (e.g. in the create event of my controller object). The following happen:

  • a global variable called ds_animation_strings gets created
  • an empty ds_map gets assigned to that variable
  • the ds_map gets filled with the animation strings of my sprites

I then use a slightly different CREATE event for my objects to make use of the global ds_map I created.

The STEP event remains the same:

Things to remember

There are a few things to bear in mind, though:

  • Unless your Photoshop exports use the same filename scheme (Frame_X.png) the Python script won’t work for you (if there’s request, I will publish an article about it as well).
  • This system works as long as the object’s sprite you’re importing has the same name as the subfolder. It’s easy to edit the scripts though, so… not a big deal.
  • I created two different GML scripts for the CREATE and STEP event, and just call them (instead of writing the same code over and over in each object that needs this system).
  • If I edit the GML mapping script, I need to edit the Python script as well. The mapping should obviously be the same.
  • REMEMBER that I still NEED to fill in the LAST FRAME duration info by hand. For each sprite (way better than writing the whole damn strings though).

What’s next?

I really don’t know if people are interested in such tricks. If so, let me know. I could write an article about how I export my Photoshop files (and get that precise naming scheme for my .png files).

Let me know in the comments if you think this is overkill or if you want more (also if you find bugs). And forgive me for the horrible (but working) Python script.

Liked it? Take a second to support Nikles on Patreon!

What are your thoughts on this?