Wednesday, September 12, 2012

Juicing with ease

It has been quite a while since my last post. I took a bit of a break from game programming in general (sometimes after spending all day coding at work, it can be the last thing I want to do when I get home). When I did come back to Shattered Throne, I found myself with a lot of enthusiasm to improve the visual style of the game.

One thing I have learned in developing games as a hobby, is that you have to follow where your current passion is. One of the main inspirations for this new obsession is the following most excellent video on adding juice to games.

The speakers in the video mention the use of easing functions to produce more natural animations as well as using particle effects among other great ideas.  So I wrote a list of simple animation ideas to enhance the game visuals and spent the next several weeks implementing each one.

  • Update the next turn swipe animation with easing
  • Changing the texture of the map selection highlights and add a fade in/out pulse animation
  • Increased the size of the damage numbers to make them easier to see and using an outlined font
  • Small screen shake and burst animation when a unit takes damage
  • Combo point stars flip up and sparkle when created
  • Stars and Gold resources fly up to the banner with a sparkles when these resources are gained
  • Coins flip up out of settlements when they generate gold
  • Star/combo icons on units have a periodic shimmer
  • Conditions applied to units use a fade in/out pulse, as well as cycle between multiple icons when a unit is suffering from multiple conditions at the same time
  • New Settlement capture animation and particle effects
  • Different sized stars on the player status banner show how many stars are needed to use both the leader's major and minor spells
  • The stars on the banner jump in a wave when the player has enough to cast a spell
  • A new simple animation when a unit heals
  • Arrows leave a trail behind as they fly towards their target
  • Stars explode from banner when used to cast a spell
  • New skull and bones image when a unit is killed
  • A simple light flash when a unit is built (I had trouble coming up with any good generic animation besides this one)
  • A rewind effect animation when the player chooses to Undo their last move

I am also considering adding some extra visual effects to the game dialog popups, but I do not yet have those in their final state yet, so this will have to wait.  The game is also missing a few art assets I am hoping to include such as Faction Emblems and Small Leader portraits by the status banner.

You can see the results in the following video:



As I mentioned above, a majority of the additional effects made use of both Easing functions and particle effects in addition to some simple sprite based animations.  There are a lot of different 3rd party particle effect engines that can be used, as well as good information on creating your own, so I will avoid discussing that in very much detail.  I did find it a bit more difficult to find actual good code samples for different easing functions, so I wanted to include those I used here.

Easing functions are all about providing more natural looking animations, by remapping a period of time to a curve.  As such, each of these functions take a float value between 0.0 and 1.0, representing the current time as a percentage of the whole.  The result is also a value between 0.0 and 1.0 that is used to scale some factor, such as distance, scale, or alpha values (the Bounce easing functions can generate values outside the normal 0.0 to 1.0 range to produce the actual bounce effect).

using System;

namespace ShatteredThrone
{
    class EasingFunctions
    {
        public enum EasingType
        {
            NONE = 0,
            LINEAR,
            PARABOLIC,
            PARABOLIC_QUAD,
            QUAD_IN,
            QUAD_OUT,
            QUAD_IN_OUT,
            QUAD_IN_WITH_START_BOUNCE,
            QUAD_OUT_WITH_END_BOUNCE
        }

        const float BOUNCE_TIME = .15f;
        const float NO_BOUNCE_TIME = 1f - BOUNCE_TIME;
        const float BOUNCE_SIZE = .05f;

        public static float LinearEase(float percent)
        {
            return percent;
        }

        public static float QuadEaseIn(float percent)
        {
            return (float)Math.Pow(percent, 2f);
        }

        public static float QuadEaseOut(float percent)
        {
            // p = 1 - (1-t)^2
            return 1f - (float)Math.Pow(1f - percent, 2f);
        }

        public static float QuadEaseInOut(float percent)
        {
            float ret = 0f;

            if (percent < .5)
            {
                ret = (float)(Math.Pow(percent * 2f, 2f)) / 2f;
            }
            else
            {
                ret = 1f - (float)(Math.Pow((1f - percent) * 2f, 2f)) / 2f;
            }

            return ret;
        }

        public static float ParabolicEase(float percent)
        {
            return (float)Math.Pow(percent - 0.5f, 2f) * (-4f) + 1f;
        }

        public static float QuadParabolicEase(float percent)
        {
            return (float)Math.Pow(Math.Pow(percent - 0.5f, 2f) * (-4f) + 1f, 2f);
        }

        public static float QuadOutWithEndBounce(float percent)
        {
            float ret = 0f;

            if (percent < NO_BOUNCE_TIME)
            {
                // ease out to dip
                ret = QuadEaseOut(percent / NO_BOUNCE_TIME) * (1f + BOUNCE_SIZE);
            }
            else
            {
                // dip down
                ret = (1f + BOUNCE_SIZE) - BOUNCE_SIZE * QuadEaseOut((percent - NO_BOUNCE_TIME) / BOUNCE_TIME);
            }

            return ret;
        }

        public static float QuadInWithStartBounce(float percent)
        {
            float ret = 0f;

            if (percent < BOUNCE_TIME)
            {
                // dip up
                ret = -BOUNCE_SIZE * QuadEaseIn(percent / BOUNCE_TIME);
            }
            else
            {
                // ease away from dip
                ret = QuadEaseIn((percent - BOUNCE_TIME) / NO_BOUNCE_TIME) * (1f + BOUNCE_SIZE) - BOUNCE_SIZE;
            }

            return ret;
        }

        public static float Compute(float percent, EasingType et)
        {
            float ret = 0f;

            switch (et)
            {
                case EasingType.LINEAR:
                    ret = LinearEase(percent);
                    break;
                case EasingType.PARABOLIC:
                    ret = ParabolicEase(percent);
                    break;
                case EasingType.PARABOLIC_QUAD:
                    ret = QuadParabolicEase(percent);
                    break;
                case EasingType.QUAD_IN:
                    ret = QuadEaseIn(percent);
                    break;
                case EasingType.QUAD_OUT:
                    ret = QuadEaseOut(percent);
                    break;
                case EasingType.QUAD_IN_OUT:
                    ret = QuadEaseInOut(percent);
                    break;
                case EasingType.QUAD_IN_WITH_START_BOUNCE:
                    ret = QuadInWithStartBounce(percent);
                    break;
                case EasingType.QUAD_OUT_WITH_END_BOUNCE:
                    ret = QuadOutWithEndBounce(percent);
                    break;
            }

            return ret;
        }

    }
}