Fall Fury: Part 3 - Basic Rendering and Movement

Continuing the FallFury series, in this article I talk about basic game element rendering and character movement. As you already know, the FallFury user experience stack is split across two layers—native DirectX and XAML. Here, I will only be talking about the native DirectX component.  Check out the video for this article at http://channel9.msdn.com/Series/FallFury/Part-3-Basic-Rendering-and-Movement  For a complete, offline version of this series, you may download a nicely formatted PDF of all the articles.

The Background

Each game screen in FallFury has a moving background that creates the illusion of a fall. The way the screen is designed, it simulates vertical parallax scrolling, as the background moves faster than the overlaid objects. There is a simple way to make background movement possible without actually having to replicate parts of the texture and render them all over again. Take a look at this image, showcasing the process:

image

First of all, two textures are loaded that, connected at the bottom, create the illusion of a single texture. In the beginning, texture A takes the entire screen and texture B is positioned directly underneath it, with a non-existing gap between them. To initiate the scrolling, texture A is being displaced vertically by an arbitrary number of pixels, and texture B follows it at the same pace. As texture B reaches the zero point (top of the viewport), texture A is no longer visible, therefore it is displaced vertically to be below texture B. This cycle can be repeated as many times as necessary during gameplay as well as while the user is in the menu.

Let’s take a look at the code that makes it possible, starting with the main menu screen:

image

First of all, you need to be aware that every game screen is automatically inheriting the properties and capabilities of base class GameScreenBase. This is the class the offers the foundation both for basic texture loading and movement:

protected private:
    // Core background textures
    Microsoft::WRL::ComPtr<ID3D11Texture2D>                    m_backgroundBlockA;
    Microsoft::WRL::ComPtr<ID3D11Texture2D>                    m_backgroundBlockB;

void MoveBackground(float velocity);

It also offers the core texture containers for the overlaid elements that are present in most screens, such as clouds:

// Overlayed clouds
Microsoft::WRL::ComPtr<ID3D11Texture2D> m_overlayA;
Microsoft::WRL::ComPtr<ID3D11Texture2D> m_overlayB;

The overlay movement is inherently dependent on the base background displacement and can be adjusted relative to the initial velocity. Let’s take a look at GameScreenBase.cpp, specifically at MoveBackground:

void GameScreenBase::MoveBackground(float velocity)
{
    if (m_backgroundPositionA <= -BACKGROUND_MIDPOINT)
        m_backgroundPositionA = m_backgroundPositionB + (BACKGROUND_MIDPOINT * 2);

    if (m_backgroundPositionB <= -BACKGROUND_MIDPOINT)
        m_backgroundPositionB = m_backgroundPositionA + (BACKGROUND_MIDPOINT * 2);

    m_backgroundPositionA -= velocity;
    m_backgroundPositionB -= velocity;
}

The BACKGROUND_MIDPOINT value is relative to the height of the background texture. As we are working with a variable screen size, given that tablets and desktops do not have the same resolution, the movement has to be adjusted accordingly. One way, however, to ensure the proper texture positioning would be to place it in relation to its previous instance. Hence, this snippet in UpdateWindowSize:

BACKGROUND_MIDPOINT = 1366.0f / 2.0f;
m_backgroundPositionA = BACKGROUND_MIDPOINT;
m_backgroundPositionB = m_backgroundPositionA * 3;

Because overlays are not necessarily a part of every screen, I am not including the MoveOverlay method in the base class. Let’s now take a look at MenuScreen.cpp. Take a look at the Load method and you will see several lines that prepare the background and overlays:

m_loader = ref new BasicLoader(Manager->m_d3dDevice.Get(), Manager->m_wicFactory.Get());

m_loader->LoadTexture("Assets\\Backgrounds\\generic_blue_a.png", &m_backgroundBlockA, nullptr);
m_loader->LoadTexture("Assets\\Backgrounds\\generic_blue_b.png", &m_backgroundBlockB, nullptr);
m_loader->LoadTexture("Assets\\Backgrounds\\cloud_overlay_a.png", &m_overlayA, nullptr);
m_loader->LoadTexture("Assets\\Backgrounds\\cloud_overlay_b.png", &m_overlayB, nullptr);

CurrentSpriteBatch->AddTexture(m_backgroundBlockA.Get());
CurrentSpriteBatch->AddTexture(m_backgroundBlockB.Get());
CurrentSpriteBatch->AddTexture(m_overlayA.Get());
CurrentSpriteBatch->AddTexture(m_overlayB.Get());

The BasicLoader class was imported from the Direct3D sprite sample. A call to LoadTexture will read the data from a DDS or PNG file and output the data in an ID3D11Texture2D object. Once loaded, the texture is added to the SpriteBatch instance associated with the screen, also declared as a part of GameScreenBase.

Depending on the screen, this procedure might have to be done for multiple textures, as you will see later in this article. Each page also has two timed loops—RenderScreen and Update. RenderScreen is responsible for taking everything from the SpriteBatch instance and showing it to the user. If you’ve used the XNA SpriteBatch before, you are aware that you need to start the drawing cycle by calling Begin and end it by calling End. The same applies here:

CurrentSpriteBatch->Begin();

CurrentSpriteBatch->Draw(
    m_backgroundBlockA.Get(),
    float2(Manager->m_windowBounds.Width / 2, m_backgroundPositionA),
    PositionUnits::DIPs,
    m_screenSize,
    SizeUnits::Pixels);

CurrentSpriteBatch->Draw(
    m_backgroundBlockB.Get(),
    float2(Manager->m_windowBounds.Width / 2 ,m_backgroundPositionB),
    PositionUnits::DIPs,
    m_screenSize,
    SizeUnits::Pixels);

if (m_showBear->IsLoaded)
    m_showBear->Render();

if (m_showMonster->IsLoaded)
    m_showMonster->Render();

CurrentSpriteBatch->Draw(
    m_overlayA.Get(),
    float2(Manager->m_windowBounds.Width/2, m_backgroundPositionA),
    PositionUnits::DIPs,
    m_screenSize,
    SizeUnits::Pixels);

CurrentSpriteBatch->Draw(
    m_overlayB.Get(),
    float2(Manager->m_windowBounds.Width / 2 ,m_backgroundPositionB),
    PositionUnits::DIPs,
    m_screenSize,
    SizeUnits::Pixels);

CurrentSpriteBatch->End();

The current overloaded Draw call gets the following items:

  • The texture object
  • The position where the object has to be drawn on the screen
  • The way the object is positioned (can also be a normalized value or a pixel value)
  • The size of the texture (stretching may occur)
  • The type of the size value (in this case, I am using pixels, but can also be normalized)

The order in which the Draw calls are arranged determines the order of objects drawn on the screen. The calls on top will place texture objects at the bottom of the rendering stack, and the calls at the end will place the objects at the top of the stack.

The Gameplay Screen Background & Overlays

Let’s move on to the arguably most critical screen in the project, GamePlayScreen.cpp. There are a few nuances that differentiate it from any other screen—in particular, the background loading routine.

For every game, there is a different level type that is being used. With each level type, there is a different combination of a background and the overlay. Currently, there are four supported level types:

  • Space
  • Nightmare
  • Magic Bean
  • Dream

When the proper level is detected and the metadata is loaded from the associated XML file (more about this process later on in the series), the level textures are loaded into memory:

switch (m_currentLevelType)
{
    case LevelType::SPACE:
    {
        m_loader->LoadTexture("Assets\\Backgrounds\\generic_dark_blue_a.png", &m_backgroundBlockA, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\generic_dark_blue_b.png", &m_backgroundBlockB, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\star_overlay_a.png", &m_overlayA, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\star_overlay_b.png", &m_overlayB, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\galaxy_overlay_a.png", &m_overlayGalaxyA, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\galaxy_overlay_b.png", &m_overlayGalaxyB, nullptr);
        
        CurrentSpriteBatch->AddTexture(m_overlayGalaxyA.Get());
        CurrentSpriteBatch->AddTexture(m_overlayGalaxyB.Get());
        break;
    }
    case LevelType::NIGHTMARE:
    {
        m_loader->LoadTexture("Assets\\Backgrounds\\generic_red_a.png", &m_backgroundBlockA, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\generic_red_b.png", &m_backgroundBlockB, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\cloud_overlay_a.png", &m_overlayA, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\cloud_overlay_b.png", &m_overlayB, nullptr);
        break;
    }
    case LevelType::MAGIC_BEANS:
    {
        m_loader->LoadTexture("Assets\\Backgrounds\\generic_blue_a.png", &m_backgroundBlockA, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\generic_blue_b.png", &m_backgroundBlockB, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\cloud_overlay_a.png", &m_overlayA, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\cloud_overlay_b.png", &m_overlayB, nullptr);
        
        break;
    }
    case LevelType::DREAM:
    {
        m_loader->LoadTexture("DDS\\Levels\\Dream\\TEST_backgroundDream_01.dds", &m_backgroundBlockA, nullptr);
        m_loader->LoadTexture("DDS\\Levels\\Dream\\TEST_backgroundDream_02.dds", &m_backgroundBlockB, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\cloud_overlay_a.png", &m_overlayA, nullptr);
        m_loader->LoadTexture("Assets\\Backgrounds\\cloud_overlay_b.png", &m_overlayB, nullptr);
        break;
    }
}

This way, no unnecessary textures are loaded. A screen-based level flag allows me to control the incoming assets. At this point, the level environment does not change dynamically, so it is safe to assume that the textures should be loaded on a per-level basis.

The RenderScreen method is called in the same manner as in the menu screen, with the moving background and overlays located at the bottom of the rendering stack:

CurrentSpriteBatch->Draw(
    m_backgroundBlockA.Get(),
    float2(Manager->m_windowBounds.Width / 2, m_backgroundPositionA),
    PositionUnits::DIPs,
    m_screenSize,
    SizeUnits::Pixels);

CurrentSpriteBatch->Draw(
    m_backgroundBlockB.Get(),
    float2(Manager->m_windowBounds.Width / 2, m_backgroundPositionB),
    PositionUnits::DIPs,
    m_screenSize,
    SizeUnits::Pixels);

CurrentSpriteBatch->Draw(
    m_overlayA.Get(),
    float2(Manager->m_windowBounds.Width / 2, m_overlayPositionA),
    PositionUnits::DIPs,
    float2(768.0f, 1366.0f),
    SizeUnits::Pixels);

CurrentSpriteBatch->Draw(
    m_overlayB.Get(),
    float2(Manager->m_windowBounds.Width / 2, m_overlayPositionB),
    PositionUnits::DIPs,
    float2(768.0f, 1366.0f),
    SizeUnits::Pixels);

Character Movement

As you are now aware of the basic screen structure and how the basic rendering process is built, let’s take a look at how the main game character moves on the screen. Falling down, the teddy bear also needs to move left and right to ensure that he is able to pick up power-ups and buttons as well as avoid obstacles and enemy ammo.

There are several important considerations here. The most important one is to not assume that the user will have a specific input device. Potential movement controllers include a keyboard, the mouse, the touch screen and the accelerometer. In the most common scenarios, the desktop machines will not have an accelerometer, and the tablet computers will not have a constantly attached keyboard and a mouse. In FallFury, I decided to leverage all potential input engines and let the user choose the best option for himself.

When the current GamePlayScreen instance loads, I am attempting to get access to the system accelerometer device:

m_systemAccelerometer = Windows::Devices::Sensors::Accelerometer::GetDefault();

m_systemAccelerometer is of type Windows::Devices::Sensors::Accelerometer and is declared in GamePlayScreen.h. If an accelerometer is detected, I need to bind it to a ReadingChanged event handler that will give me the current G-force transformed into X, Y, and Z displacement:

if (m_systemAccelerometer != nullptr)
{
    m_systemAccelerometer->ReadingChanged += ref new TypedEventHandler<Accelerometer^, AccelerometerReadingChangedEventArgs^>
            (this, &GamePlayScreen::AccelerometerReadingChanged);
}

ReadingChanged itself does not participate in updating the position for the character, but rather passes the current values to m_xAcceleration, which is later used in the Update method:

void GamePlayScreen::AccelerometerReadingChanged(_In_ Accelerometer^ accelerometer, _In_ AccelerometerReadingChangedEventArgs^ args)
{
    if (StaticDataHelper::IsAccelerometerEnabled)
    {
        auto currentOrientation = DisplayProperties::CurrentOrientation;
        float accelValue;
        
        if (currentOrientation == DisplayOrientations::Portrait)
                    accelValue = args->Reading->AccelerationY;
        else if (currentOrientation == DisplayOrientations::PortraitFlipped)
                    accelValue = -args->Reading->AccelerationY;
        else if (currentOrientation == DisplayOrientations::Landscape)
                    accelValue = args->Reading->AccelerationX;
        else if (currentOrientation == DisplayOrientations::LandscapeFlipped)
                    accelValue = -args->Reading->AccelerationX;
        else
                    accelValue = 0.0f;
        
        if (StaticDataHelper::IsAccelerometerInverted)
                    m_xAcceleration = -accelValue;
        else
                    m_xAcceleration = accelValue;
    }
}

The reason for this lies in the fact that ReadingChanged is triggered at a much lower rate than the Update loop. If the character position would be adjusted through the core accelerometer event handler, the result would be choppy (“step-by-step”) movement instead of a smooth transition.

Notice, also, that depending on the screen orientation, I need to get the acceleration either on the X- or Y-axes. Since an accelerometer is detected, I am assuming that the device that’s being used is a tablet. Therefore, it can have auto-rotate enabled, which means that the reference axis (horizontal) might change depending on how the user holds the device. DisplayProperties::CurrentOrientation can give me the current orientation, whether portrait (Y acceleration value) or landscape (X acceleration value):

image

As with many other titles, the user may invert the movement based on the accelerometer reading. For example, if the device is tilted to the right, the character will move to the left, and vice-versa. The effect is easily achieved by negating the current reading, regardless of the axis it is relying on.

Obviously, as the character moves, there should be boundaries that restrict the movement within the context of the current game screen. To get the correct screen bounds, GameScreenBase, the foundation class for every screen, sets four properties: LoBoundX, HiBoundX, LoBoundY and HiBoundY:

LoBoundX = (rWidth - 768.0f) / 2.0f;
HiBoundX = LoBoundX + 768.0f;

LoBoundY = 0;
HiBoundY = LoBoundY + rHeight;

LoBoundX is the leftmost limit for the horizontal playable area and HiBoundX is the rightmost limit for the same horizontal area. LoBoundY and HiBoundY are responsible for carrying the limits for the vertical space. All four boundary values are relative to the screen size:

image

To consistently get the correct X boundary no matter the screen size, the playable area is set to a fixed width of 768 pixels. The rest of the screen is accordingly cancelled out and the remaining area split in two. The original X point (zero) is added to the curtain size and that way there is the leftmost X boundary. The Y boundaries are simply obtained from the screen height, as there are no gameplay experience restrictions in that domain.

The bear position is updated in the Update loop in the game screen. There are several parameters that need to be adjusted, such as the vertical velocity, the bear rotation on tilt and the horizontal displacement.

When the level just starts, the bear falls faster until he reaches the standard static Y point. Because of screen size differences, this should not be done by comparing the pixel distance but rather by utilizing a ratio built from the current bear Y position and the high Y boundary:

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

Assuming that the game is not paused, and thus the background is moving, the bear Y position should be adjusted relative to the current velocity set for the background scrolling. Because of how parallax scrolling works, the bear velocity has to be higher than the initial screen movement. Therefore, it is multiplied by a fixed value independent of the level played:

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;
    }
}

In the statement above, there is also an extra condition that verifies whether the bear is below the low Y boundary. If it is, then the game is over. This is imposed by a game animation that takes the bear off the Y limits, meaning that the bear is dead.

Bear rotation is based on the current X acceleration, but the bear shouldn’t rotate 360 degrees. To limit the rotation, there is a rotation threshold set (value taken in radians) that is being checked before rotating the character:

float compositeRotation = GameBear->Rotation - (float)m_xAcceleration / 10.0f;
if (compositeRotation < m_rotationThreshold && compositeRotation > -m_rotationThreshold)
    GameBear->Rotation = compositeRotation;

The composite value lets me verify the perspective rotation without actually assigning it to the bear. If it is within the imposed threshold, only then will the bear rotation be adjusted.

Using the composite value approach is also efficient when performing the horizontal displacement adjustment. The initial value is composed of the current position and the X-based acceleration multiplied by a dynamic multiplier value to create the inertia effect:

float compositePosition = GameBear->Position.x + ((float)m_xAcceleration * m_accelerationMultiplier);

if (m_xAcceleration < 0)
    compositePosition -= 100.0f;
else
    compositePosition += 100.0f;

if (Manager->IsWithinScreenBoundaries(float2(compositePosition, GameBear->Position.y)))
{
    GameBear->Position.x += (float)m_xAcceleration * m_accelerationMultiplier;
}
else
{
    if (GameBear->Position.x > HiBoundX)
    {
        GameBear->Position.x = HiBoundX - 180.0f;
    }
    else if (GameBear->Position.x < LoBoundX)
    {
        GameBear->Position.x = LoBoundX + 180.0f;
    }
}

If the bear is attempting to hit a “wall” (screen limit), its position is set back to the one barely prior to the limit. By doing this, I am avoiding locking the character on the side of the screen.

There is something in this snippet above that you might not be aware of—a reference to Manager->IsWithinScreenBoundaries. This method belongs to the ScreenManager (ScreenManager.cpp) class—a utility class that ensures the proper screen is displayed depending on the current game mode, and also allows control of mouse actions and boundary checks:

bool ScreenManager::IsWithinScreenBoundaries(float2 position)
{
    if (position.x < CurrentGameScreen->LoBoundX || position.x > CurrentGameScreen->HiBoundX || position.y < CurrentGameScreen->LoBoundY || position.y > CurrentGameScreen->HiBoundY)
            return false;
    else
            return true;
}

You saw how the movement is accomplished with the help of the accelerometer. Let’s take a look at how a keyboard can be used to do the same thing. There is a HandleKeyInput method, exposed through the GamePlayScreen class. As a matter of fact, HandleKeyInput is wired into the GameScreenBase class, therefore if a specific combination needs to be handled outside the context of the game play screen, it can be:

void GamePlayScreen::HandleKeyInput(Windows::System::VirtualKey key)
{
    if (key == Windows::System::VirtualKey::Right)
    {
        if (GameBear->IsWithinScreenBoundaries(GameBear->Size.x, 0.0f, GetScreenBounds()))
        {
            GameBear->Direction = TurningState::RIGHT;
            
            m_xAcceleration = 0.8f;
            
            if (GameBear->Rotation >= -m_rotationThreshold)
                            GameBear->Rotation -= 0.02f;
        }
    }
    else if (key == Windows::System::VirtualKey::Left)
    {
        if (GameBear->IsWithinScreenBoundaries(-GameBear->Size.x, 0.0f, GetScreenBounds()))
        {
            GameBear->Direction = TurningState::LEFT;
            
            m_xAcceleration = -0.8f;
            
            if (GameBear->Rotation <= m_rotationThreshold)
                            GameBear->Rotation += 0.02f;
        }
    }
}

As the accelerometer is no longer influencing the rotation or displacement, both indicators have to be manipulated through key presses. There are still standard thresholds in place to limit the potential incorrect movement, but the idea remains the same. This method is being called from the XAML page, which is overlaid on top of the DirectX renders:

<SwapChainBackgroundPanel
    x:Class="Coding4Fun.FallFury.DirectXPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Coding4Fun.FallFury" x:Name="XAMLPage"
    xmlns:controls="using:Coding4Fun.FallFury.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" Loaded="OnLoaded" KeyDown="OnKeyDown" LayoutUpdated="XAMLPage_LayoutUpdated">
void DirectXPage::OnKeyDown(Platform::Object^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e)
{
    m_renderer->CurrentGameScreen->HandleKeyInput(e->Key);
}

When the keyboard is not available, movement can be controlled with the help of a mouse. Again, with standard event handlers, I am simply using OnPointerMoved:

void GamePlayScreen::OnPointerMoved(Windows::UI::Core::PointerEventArgs^ args)
{
    if (StaticDataHelper::IsMouseEnabled)
    {
        if (GameBear != nullptr)
        {
            m_touchCounter++;
            if (GameBear->IsWithinScreenBoundaries(GameBear->Size.x, 0.0f, GetScreenBounds()))
            {
                m_xAcceleration = (args->CurrentPoint->RawPosition.X - GameBear->Position.x) / m_screenSize.x;
            }
        }
    }
}

By using this method, the bear will accelerate to the point where the mouse cursor is located, having a higher velocity the further it is located from the cursor. Once again, this creates the inertia visualization.

Conclusion

Concluding Part 3 of the series, remember that because Windows 8 is not a tablet-only OS, some users might have different input devices. Try to accommodate as many of those as possible.

The next article in this series will focus on the XAML overlay used in FallFury.

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.