Fall Fury: Part 7 - Animations

Animations & Item Transitions

FallFury relies on a lot of dynamic content. As you already aware of how SpriteBatch is invoked inside the FallFury rendering stack, this article focuses on how dynamic activities are handled on existing textures and entities. If you need a quick look at what was already covered, refer to Part 3 of the series.

Check out the video for this article at http://channel9.msdn.com/Series/FallFury/Part-7-Animations . For a complete, offline version of this series, you may download a nicely formatted PDF of all the articles.

Menu Screen

The first animated element shown in the menu screen is the bear, which is positioned in the top half of the viewport. Notice that the bear moves its paws as well as moving across the screen:

image

The bear is implemented in the GameBear class and is the normal playable entity that is invincible when animated outside the scope of the gameplay screen. Its full body image is composed of four textures:

Microsoft::WRL::ComPtr<ID3D11Texture2D>        m_head;
Microsoft::WRL::ComPtr<ID3D11Texture2D>        m_leftPaw;
Microsoft::WRL::ComPtr<ID3D11Texture2D>        m_rightPaw;

Microsoft::WRL::ComPtr<ID3D11Texture2D>        m_body // Exposed through LivingEntity;

Each of these textures is internally dependent on the coupled float rotation and position values passed to the sprite batch in the host container (the parent screen):

HostContainer->CurrentSpriteBatch->Draw(
    m_rightPaw.Get(),
    m_rightPawPosition,
    PositionUnits::DIPs,
    float2(522.0f, 141.0f) * Scale,
    SizeUnits::Pixels,
    m_shading,
    m_rightPawRotation - 0.5f);

HostContainer->CurrentSpriteBatch->Draw(
    m_body.Get(),
    Position,
    PositionUnits::DIPs,
    float2(400.0f, 400.0f) * Scale,
    SizeUnits::Pixels,
    m_shading,
    Rotation);

HostContainer->CurrentSpriteBatch->Draw(
    m_leftPaw.Get(),
    m_leftPawPosition,
    PositionUnits::DIPs,
    float2(522.0f, 141.0f) * Scale,
    SizeUnits::Pixels,
    m_shading,
    m_leftPawRotation + 0.5f);

Let’s take a look at how the position and rotation values are modified. All item animations are performed with the help of timers set to brief intervals in which the values are cycled through two value exchange operations. Here is a snippet taken from inside the Update method in the GameBear class that is responsible for bear arm movement:

if (m_armRotationTimer < 0.6f)
{
    m_rightPawRotation -= 0.005f;
    m_leftPawRotation += 0.005f;
}
else if (m_armRotationTimer > 0.6f && m_armRotationTimer < 1.2f)
{
    m_rightPawRotation += 0.005f;
    m_leftPawRotation -= 0.005f;
}
else
{
    m_rightPawRotation = 0.0f;
    m_leftPawRotation = 0.0f;
    m_armRotationTimer = 0.0f;
}

m_armRotationTimer is a float value that is initially set to 0.0f. In the Update loop and increments each iteration by the value of timeDelta, which represents the time difference between two loop cycles. As this value is less than 600 milliseconds (0.6 seconds), the right paw rotation is incremented and the left paw rotation decremented. The difference in the value adjustment direction is caused by the fact that one paw is rotated clockwise and the other is rotated counterclockwise. Once the timer tracks a value above 600 milliseconds but below than 1200 milliseconds, the process reverses and the paws rotate in the opposite direction. After 1.2 seconds, the timer resets, as does the rotation.

Since this rotation is automated, GameBear->Update is called in the menu screen:

m_showBear->Update(timeTotal, timeDelta, float2(Manager->m_windowBounds.Width / 2, Manager->m_windowBounds.Height / 2));

Outside the context of the bear update cycle, there is a similar timed mechanism used to randomly move the bear across the screen:

if (m_positionYAdj == 0.0f)
{
    m_positionYAdj = RandFloat(0.1f, 1.2f);
    m_positionXAdj = RandFloat(-1.0f, 1.0f);
}

if (m_positionTimer < 4.0f)
{
    m_showBear->Position.x += m_positionXAdj;
    m_showBear->Position.y += m_positionYAdj;
    m_showBear->Rotation += 0.001f;
    m_showBear->Scale += 0.0008f;
    
    m_showMonster->Scale += 0.002f;
}
else if (m_positionTimer > 4.0f && m_positionTimer < 8.0f)
{
    m_showBear->Position.x -= m_positionXAdj;
    m_showBear->Position.y -= m_positionYAdj;
    m_showBear->Rotation -= 0.001f;
    m_showBear->Scale -= 0.0008f;
}
else
{
    m_positionYAdj = 0.0f;
    m_positionTimer = 0.0f;
}

The X and Y adjustments are randomly generated and for the duration of a single timer cycle (in this case, 4 seconds) the bear’s horizontal and vertical positions are adjusted using those values. The rotation and scale are minimally adjusted to create a 3D motion effect. Because the displacement is minimal, the bear is always visible and does not move much outside the viewport.

There is also a flying monster animation in the main menu screen. Monster creation happens on top of the same instance that is replaced when an initial monster flies out of bounds:

m_showMonster->Position.y -= m_showMonster->Velocity.y;
if (m_showMonster->Position.y < -m_showMonster->Size.y)
{
    CreateNewMonster();
}

 

When a new monster is created, a random monster type is selected and its position on the X axis is randomized. The Y position is set to be right under the bottom screen boundary:

void MenuScreen::CreateNewMonster()
{
    m_showMonster = ref new Monster(this, (MonsterType)(rand() % (int)MonsterType::MT_CANDYLAND_E), true);
    m_showMonster->Scale = 0.3f;
    m_showMonster->Velocity.y = 1.0f;
    m_showMonster->Load();
    m_showMonster->Position = float2(RandFloat(LoBoundX, HiBoundX), Manager->m_windowBounds.Height + m_showMonster->Size.y * m_showMonster->Scale);
}

Monster scaling is done on the same timed loop as the bear. The results look like this:

image

Gameplay Screen

Bear

The gameplay screen carries the most animations in FallFury, including animations related to ammo displacement, monster movement paths, power-up animations, and secondary game character animations that are triggered in special cases such as character death.

Let’s start with character entrance. When the game begins, the teddy bear falls into the screen and stops at a specific point close to the top boundary of the gameplay screen. Once that transition is complete, the bear is no longer vertically displaced:

if ((GameBear->Position.y / HiBoundY) < 0.19f)
{
    GameBear->Position.y += GameBear->Velocity.y * (3.2f);
}

 

If the limit is not hit, the original bear velocity is multiplied by a fixed value, after which the condition is ignored for the rest of the gameplay.

Let’s now take a look at what happens when an enemy shell kills the bear. Generally, when the main character is killed, there is not much sense in maintaining other in-game activities. In FallFury, a killed bear results in a time freeze—the fall stops and the monsters are no longer shooting or moving. Not only that, but the bear is also flipped on its back:

image

In the Update loop, the position for the paws and the head are adjusted for the new body texture layout:

if (IsDead)
{
    if (m_armRotationTimer < 1.0f)
    {
        m_rightPawRotation += 0.001f;
        m_leftPawRotation -= 0.001f;
    }
    else if (m_armRotationTimer > 1.0f && m_armRotationTimer < 2.0f)
    {
        m_rightPawRotation -= 0.001f;
        m_leftPawRotation += 0.001f;
    }
    else
    {
        m_rightPawRotation = 0.0f;
        m_leftPawRotation = 0.0f;
        m_armRotationTimer = 0.0f;
    }

    m_rightPawPosition = Position + float2(40.0f, -50.0f) * Scale;
    m_leftPawPosition = Position + float2(10.0f, 150.0f) * Scale;
    m_headPosition = (Position - float2(-160.0f, 0.0f) * Scale);
}

The proper textures are assigned via the Kill method:

void Bear::Kill()
{
    IsDead = true;
    Rotation = -1.0f;
    m_head = m_deadHead;
    m_body = m_deadBody;
    m_leftPaw = m_deadLeftArm;
    m_rightPaw = m_deadRightArm;
}

The bear will continue falling as long as StopBackground in GamePlayScreen is called:

void GamePlayScreen::StopBackground()
{
    m_isBackgroundMoving = false;
}

This will cause the internal state check to fail in the Update loop, sending the bear off screen limits. Once the bear completes the fall, the state is set to GS_GAME_OVER:

if (!m_isBackgroundMoving)
{
    if (GameBear->Position.y > m_screenSize.y)
    {
        Manager->CurrentGameState = GameState::GS_GAME_OVER;
    }
    else
    {
        GameBear->Position.y += GameBear->Velocity.y * 1.5f;
    }
}

Monsters

Monsters, on the other hand, are being constantly moved during the duration of the game. Their basic displacement is performed by MoveMonsters:

void GamePlayScreen::MoveMonsters(float timeTotal ,float timeDelta)
{
    for (auto monster = m_monsters.begin(); monster != m_monsters.end();)
    {
        Monster^ currentMonster = (*monster);
        if ((currentMonster->Position.y - GameBear->Position.y) < -Manager->m_renderTargetSize.Height / 2)
        {
            monster = m_monsters.erase(monster);
        }
        else
        {
            currentMonster->Velocity.y = GameBear->Velocity.y;
            currentMonster->Update(timeTotal, timeDelta, GameBear->Position, GetScreenBounds());
            CheckForCollisionWithAmmo(currentMonster);
            ++monster;
        }
    }
}

This method adjusts the monster position relative to the bear:

image

The zig-zag like motion is defined in the Monster class in the Update method:

float adjustment = 0.0f;
adjustment = m_goingRight ? Position.x + Velocity.x : Position.x - Velocity.x;
if (adjustment >= (HostContainer->LoBoundX + (Size.x * Scale) / 2.0f) 
&& adjustment <= (HostContainer->HiBoundX - (Size.x * Scale) / 2.0f))
{
    Position.x = adjustment;
}
else
{
    m_goingRight = !m_goingRight;
}

 

The snippet above determines the direction in which the enemy has to move depending on which screen boundary is hit first. While the monster is active and is visible, its vertical adjustment is performed in a timed loop, like this:

m_jumpingTimer += timeDelta;
if (m_jumpingTimer > 0.0f && m_jumpingTimer < 1.0f)
{
    Position.y -= 1.0f;
}
else if (m_jumpingTimer >= 1.0f && m_jumpingTimer < 2.0f)
{
    Position.y += 1.0f;
}
else
{
    m_jumpingTimer = 0.0f;
}

Here you can either use the Y velocity or introduce a static value. The only condition has to be a number low enough that the monster does not leave the screen, which would prevent the bear from being able to kill it. When the monster is killed, it flies out by following an arch-like path:

image

The “death arc” is implemented via another timer:

Position.x += Velocity.y * 1.3f;
if (m_deathArcTimer > 0.4f)
{
    Position.y += Velocity.y;
    if (Scale > 0.1f)
    Scale -= 0.01f;
}
else
{
    Position.y -= Velocity.y;
    Scale += 0.01f;
    m_deathArcTimer += timeDelta;
}

 

Regardless of the monster position, the horizontal displacement is positive and the entity moves to the right. For 400 milliseconds its vertical position is decreased, moving the texture up, and the scale is also adjusted to create the proximity effect. After this time interval, the monster drops out of the screen boundaries.

As previously mentioned, each monster is composed of three cycled textures, which are loaded and stored in three ID3D11Texture2D containers:

Microsoft::WRL::ComPtr<ID3D11Texture2D> m_spriteA;
Microsoft::WRL::ComPtr<ID3D11Texture2D> m_spriteB;
Microsoft::WRL::ComPtr<ID3D11Texture2D> m_spriteC;

 

Each of these is assigned to the main body texture container every 300 milliseconds, replacing the previously assigned asset:

m_stateChangeTimer += timeDelta;
if (m_stateChangeTimer < 0.3f)
{
    m_body = m_spriteA;
}
else if (m_stateChangeTimer > 0.3f && m_stateChangeTimer < 0.6f)
{
    m_body = m_spriteB;
}
else if (m_stateChangeTimer > 0.6f && m_stateChangeTimer < 0.9f)
{
    m_body = m_spriteC;
}
else if (m_stateChangeTimer > 0.9f)
{
    m_stateChangeTimer = 0.0f;
}

 

This cycle can be disabled when the monster is not active.

Ammo

There are different types of ammo used in the game, and each behaves in its own way. The stock ammo type is a plasma ball.

The default shooting behavior releases a shell when the user taps anywhere on the screen (other than the pause button area). Given the texture of the shell, when it is released the tail must be oriented towards the bear at an angle equal to the one created by the bear and the target position. To get a better idea, take a look at the images below:

image image

In the image on the left, the player tapped at the bottom of the screen. In the one on the right, the player tapped at the right edge of the screen. The shell accordingly rotates depending on the tapped position. To get a better understanding of how the rotation is performed, imagine the XY coordinate grid with the bear located at the intersection of the axes:

image

Thinking back to trigonometry, notice the triangle formed by the shell and the bear origin point:

image

Remember also the trig concept of quadrants:

image

The quadrant in which the shell is located determines how the angle is calculated and the tail rotated. If the shell is in quadrant 2 or 3, calculate the rotation radians by following this algorithm:

1. Find the hypotenuse from the X and Y components of the velocity, which can be calculated by finding the difference between the bear and the position of the tap. To do this, us the well-known Pythagorean theorem:

image

The hypotenuse formula can be deduced from the formula above and is the square root of the sum of the velocity component squares:

image

2. Once calculated, it is necessary to find the sine of the target angle. The value can be obtained by dividing the Y component of the velocity by the hypotenuse.

3. Find the angle by calculating the arcsine from the resultant sine value. This will return the final angle in radians.

That said, for shells launched in quadrants 1 or 4, there is one extra step that needs to be performed. In addition to the rotation value obtained from the steps listed above, the rotation should be incremented by the radian value of a 180-degree angle minus twice the rotation value previously obtained. This ensures that the tail is correctly flipped relative to the axis origin.

In C++, the implementation is done inside the AmmoItem class. For flexibility purposes, it triggers inside the Update loop—that way, it is possible to modify the trajectory of the shell and not worry about turning it again manually after launch:

if ((Velocity.x > 0 && Velocity.y > 0) || (Velocity.x > 0 && Velocity.y <0))
{
    Rotation = CalculateRadians(Velocity);
    Rotation += CalculateRadiansFromAngle(180) - 2 * Rotation;
}
else
{
    Rotation = CalculateRadians(Velocity);
}

CalculateRadians is the method that transforms the velocity components in a rotation value. It is located in the BasicMath.h helper:

inline float CalculateRadians(float2 velocity)
{
    float hypothenuse = sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
    float sine = velocity.y / hypothenuse;
    float angle = asin(sine);
    return angle;
};

 

There is a potential problem with the implementation above. As the user taps on different parts of the screen, the X and Y components are different, each resulting in a different hypotenuse. As the target is set, the shell flies faster the further away from the bear the user taps. To avoid this, the triangle legs should be normalized to a near-constant value:

Take a look at the ShootAtTarget function in the Bear class:

void Bear::ShootAtTarget(float2 lastTargetTrace)
{
    OnPulledTrigger(Position.x, Position.y, 
    GetVelocityLegs(lastTargetTrace).x, GetVelocityLegs(lastTargetTrace).y, 
    CurrentDamage, IsFriendly, false, HostContainer->CurrentSpriteBatch);
}

 

Notice that the triangle legs are not passed as a raw value, but are proxied through GetVelocityLegs, which forces the resulting vector to be produced from a constant triangle with the velocity constant at 10 pixels per iteration:

float2 LivingEntity::GetVelocityLegs(float2 lastTargetTrace)
{
    float bottomLeg = 0.0f;
    float sideLeg = (Position.y - lastTargetTrace.y) / 100.0f;
    bottomLeg = (Position.x - lastTargetTrace.x) / 100.0f;
    float requiredVelocity = 10.0f;
    float hypothenuse = sqrt(bottomLeg * bottomLeg + sideLeg * sideLeg);
    float proportionalX = 0.0f;
    float proportionalY = 0.0f;
    proportionalX = (requiredVelocity * bottomLeg) / hypothenuse;
    proportionalY = (sideLeg < 0.0f ? -1 : 1) * sqrt(requiredVelocity * requiredVelocity - proportionalX * proportionalX);
    return float2(proportionalX,proportionalY);
}

 

For ammo that does not have a visual rotation dependency, such as the plasma ball, simply increment the rotation to make the thrown item continuously spin:

if (Type == PowerupType::PLASMA_BALL)
{
    if ((Velocity.x > 0 && Velocity.y > 0) || (Velocity.x > 0 && Velocity.y <0))
    {
        Rotation = CalculateRadians(Velocity);
        Rotation += CalculateRadiansFromAngle(180) - 2 * Rotation;
    }
    else
    {
        Rotation = CalculateRadians(Velocity);
    }
}
else
{
    Rotation += 0.2f;
}

 

Conclusions

FallFury mostly relies on sprite-based animations in which there are several textures cycled through in order to create the desired dynamic effect. These are not really hard to build, given that there is a possibility to integrate them in a timed loop. Be aware, however, that with slower machines the timing might be off and the time delta value between loops might be higher. In that case the sprite cycling will not be as smooth as it should be, which is why it’s important to perform animation testing on a variety of hardware with different OS loads.

Tags:

Follow the Discussion

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.