2014/09/04

Asynchronous Animations



The more animation and movement you have in your games, the more flashy and appealing they can be to players. In this tutorial I'll be showing you how to build animations that run in their own self-contained functions.

This allows them to be interrupted at any time, allows for simultaneous user input and anything else you wish to occur in your game loop without having to wait for the animation script to finish. In our most recent game, Sudoku In Space, we make extensive use of this for background animations, main menu animations, and particle animations when you make a completion move. Since we use a timer in Sudoku In Space, it is important to not prevent the user from interacting with the game while an animation is happening for a row, column, or block completion.

To keep things simple this tutorial will only show the background animations for the larger stars that wink in and out. If you're interested in seeing how I did the particle animations just let me know and I'll do it in a future continuation of this article.

First, we'll need a user defined type and a variable of that type. In Sudoku In Space I actually made this variable global so that all of our screens could have the background animation, however, it is not necessary if you are using the animation in only one area of your game since the function to handle the animation has a feedback loop. A feedback loop is when a function accepts input of a specific variable and then outputs to that same variable.

The following setup will make a star appear in a random location on the screen, color it one of three different colors at random, and make it grow and then shrink.


Our type:
type star_type 
    max_W# as float
    dir as integer
    spriteID as integer
    abs_max_W# as float
    abs_min_W# as float
    time as integer
endtype
  • max_W# will contain a random size that we want the star to grow to. 
  • dir is the direction of the sprite's growth. 
  • spriteID is the sprite ID of the star. 
  • abs_max_W# is the maximum width the star should ever grow to so that the sprite's image doesn't become fuzzy or larger than we want it to appear. 
  • abs_min_W# is the minimum width the star should ever shrink to and serves as a starting and stopping point for the shrink/grow animation. 
  • time will allow us to control when the star reappears. 

Now let's look at the self-contained feedback loop function:
function HandleStar(star as star_type , fFT# , tT#)    // fFT# is frame time as obtained from GetFrameTime()
 // in the main loop
    // tT# is the total time we want the growing or shrinking to take
 
    //the depth of the sprite
    depth = 2000 
 
    // If the direction is 0 then this is the first time we're 
    // running the function so we need to set direction to 1 (grow).
    if star.dir = 0
        star.dir = 1
    endif
 
    //Instantiate the sprite if it doesn't already exist.
    if GetSpriteExists(star.spriteID) = 0
        img = LoadImage(“star.png”)
        star.spriteID = CreateSprite(img) 
 
        // We'll allow the star to grow up to 5%
        // of the screen width.
        star.abs_max_W# = 5.0
 
        // The star image was designed for an 800px wide screen
        // We want to prevent it from being smaller
        // than 10px at 800px resolution
        star.abs_min_W# = 100.0 * 10.0 / 800.0
 
        // Initialize the size at the minimum size
        // so that it will grow
        SetSpriteSize(star.spriteID , star.abs_min_W# , -1)
 
        // Initialize the max_W# variable to a negative so that
        // we can set it to a random value
        star.max_W# = -1.0
    endif
 
 
    // Initialize the animation variables – this happens every time the 
    // star is shrunk to its minimum size as well
    // as the first run of the function
    //
    // If the max_W# variable is negative then we need to
    // determine a random size we'll allow the star to grow to
    // as well set it's initial position, color, and angle
 
    if star.max_W# < 0.001
        // Determine a random value for the max width to grow to.
        // Here I multiply the maximum and minimum by 100 so that I can 
        // make use of the random() function which only works on integers
        // and then divide by 100. This way the star can have a max width
        // with fractional percentages.
        r_low = floor(100.0 * (star.abs_max_W# - 0.4 * star.abs_max_W#))
        r_high = floor(100.0 * (star.abs_max_W#))
        star.max_W# = random(r_low , r_high) / 100.0
 
        // Similar is done to find the initial position
        r_x_low = 100
        r_x_high = floor(100.0 * (100.0 - star.max_W#))
        x# = random(r_x_low , r_x_high) / 100.0
 
        r_y_low = 100
        maxY# = 100.0 – GetSpriteHeight(star.spriteID)
        r_y_high = floor(100.0 * maxY#)
        
        y# = random(r_y_low , r_y_high) / 100.0
 
        // Now we can position the star
        SetSpritePosition(star.spriteID , x# , y#)
 
        // Give it a random angle for more randomness
        r_angle = random(1,360)
        SetSpriteAngle(star.spriteID , r_angle)
 
        // Set the time at which we initialized the star and make it visible.
        star.time = GetMilliseconds()
        SetSpriteVisible(star.spriteID , 1)
 
        // Set random color
        r_color_num = random(1,4)
        if r_color_num = 1
            color$ = "200,255,255" //blue-ish
        elseif r_color_num = 2
            color$ = "255,255,200" //purple-ish
        else
            color$ = “255,255,255” //white
        endif
 
        // This is a home brewed function I use that makes it a
        // Little easier to set sprite colors –
        // included after this function
        _SetSpriteColor(star.spriteID , color$)
    endif
 
 
 
    // Now we can actually animate the growth and shrinking of the star.
 
    // Determine the distance between the random width we grow to
    // and the minimum allowable width.
    dist# = star.max_W# - star.abs_min_W#
 
    // Get the current widht of the star so we can see if it should still 
    // grow or shrink
    current_W# = GetSpriteWidth(star.spriteID)
 
    // Initialize this to a negative so that we can skip the sizing of 
    // the star when we're pausing between appearances.
    new_W# = -1.0
 
    // If the direction is 1 then we are growing the star
    if star.dir = 1 
 
        // If the current width is less than the maximum 
        // width we want the star to grow to then grow it!
        // Otherwise stop growing and negate the direction
        // So that it will start shrinking.
        if current_W# < star.max_W#
            // Here we use a linear equation based on frame time (fFT#)
            // to increase the size of the sprite over time tT#
            new_W# = current_W# + dist# * fFT# / tT#
        else
            // We're done growing, so negate the direction
            new_W# = star.max_W#
            star.dir = star.dir * -1 
        endif
 
 
    elseif star.dir = -1  //Otherwise we're shrinking the star
 
        // If the current width of the star is still greater
        // than the allowable minimum width then grow it.
        // Otherwise stop.
        if current_W# > star.abs_min_W#
            // Use the same linear equation for growth based on frame time,
            // but negate the distance variable
            // so we're making the width smaller.
            new_W# = current_W# - dist# * fFT# / tT#
 
        else
 
            // We're done growing so set the new width to the minimum width
            // Doing this helps if a large number of frames were skipped.
            new_W# = star.abs_min_W#
 
            // Hide the star for now because we're going to wait to make
            // it reappear
            SetSpriteVisible(star.spriteID , 0)
 
            // Now when it is time, set max_W# to a negative to 
            // reinitialize the star's animation (random max w, color, position)
            // The calculation here is tT# in seconds which needed to be 
            // converted to milliseconds – so basically we're adding 3 seconds
            // before the star will restart the animation.
            if GetMilliseconds() > star.time + 3000.0 * tT#
                star.max_W# = -1.0
                star.dir = star.dir * -1
                
            endif
        endif
    endif
 
    // Only grow and reposition the star if new_W# if positive.
    // This allows us to skip over the sizing when the star is waiting 
    // to be reinitialized
    if new_W# > 0.0
        x# = GetSpriteXByOffset(star.spriteID)
        y# = GetSpriteYByOffset(star.spriteID)
        SetSpriteSize(star.spriteID , new_W# , -1)
        SetSpritePositionByOffset(star.spriteID , x# , y#)
    endif
endfunction star


This is used to color sprites with only a comma separated string for the R, G, and B values.
function _SetSpriteColor(iID , sColor$) 
     if GetSpriteExists(iID) = 1
          r = val(GetStringToken(sColor$ , "," , 1))
          g = val(GetStringToken(sColor$ , "," , 2))
          b = val(GetStringToken(sColor$ , "," , 3))
          SetSpriteColor(iID , r , g , b , 255)
     endif
endfunction


Now all that needs to be done is run this function in a loop like so:
myStar as star_typeshrink_grow_time# = 0.50
 
do
    ft# = GetFrameTime()
    myStar = HandleStar(myStar , ft# , shrink_grow_time#)
    Sync()
loop


Hopefully you can also see how this can be extended for more complex animations where there are various states for the animation. One example you can see is in Sudoku In Space on the home screen. Our game character, Allen the Alien, flies onto the screen, appears to hover with some up and down tweening, and continues to do so until the player presses the play button, at which time he flies away. I also built that animation so it can be interrupted and the player does not need to wait for Allen to be in the center of the screen before they can tap a button. Tapping a button will simply allow Allen to keep moving across the screen until he's gone.

In Sudoku In Space we take advantage of this method for many of the animations so that user input is not hampered. Please take a look at the game to see these animations in action. The most complex of them are the particle animations for when you complete a puzzle. If you'd like to see an example of that in a future tutorial, just let me know (naplandgames@gmail.com)! Sudoku In Space is free on Google Play and the App Store.

No comments:

Post a Comment