Quantcast
Channel: catalinzima.com
Viewing all articles
Browse latest Browse all 31

Handling Orientation in a Windows Phone 8 game

$
0
0

So you want to make a landscape game, or a game that supports both portrait and landscape mode in your Direct3D game for Windows Phone 8… I had quite a few requests for such a sample, and documentation out there is really, really scarce for this subject. I spend a few days looking at what was available for Windows 8 apps and managed to convert and adapt that to Windows Phone 8 too.

In Windows Phone 7, with XNA, this was automatically handled for you. You just needed to specify what orientation you want and create a backbuffer of correct size, and everything else (rendering, input) just worked. In Windows 8 apps, you have two options: you can let the system automatically rotate the image for you, or use the more optimal method of doing the rotation yourself and using the IDXGISwapChain1::SetRotation method to tell the OS that you did this manually. But for performance reasons, the option of automatic rotation is not available on Windows Phone 8, so we’re stuck with having to manually take care of everything: rotating stuff before we draw them, and processing input values to fit them to the new orientation. There are several things that we need to do in order to support other orientations besides the default Portrait one.

General

The first things to do will be to specify what orientations you want to support, and then respond whenever the orientation changes.

Go to your IFrameworkView implementation (normally ProjectName.cpp), and navigate to the SetWindow method. This will be called when the window for the app is ready, and the app can subscribe to windows events, and do whatever other configurations it might want.

To specify what orientations your app supports, you can set the values of the DisplayProperties::AutoRotationPreferences property. By default, this is set to only support Portrait. To be able to react when the orientation changes, you’ll need to also subscribe to the OrientationChanged event.

void OrientationSampleWP::SetWindow(CoreWindow^ window)
{
  [...] //other event subscriptions here

  DisplayProperties::AutoRotationPreferences =
    Windows::Graphics::Display::DisplayOrientations::Landscape |
    Windows::Graphics::Display::DisplayOrientations::LandscapeFlipped |
    Windows::Graphics::Display::DisplayOrientations::Portrait;

  DisplayProperties::OrientationChanged +=
    ref new DisplayPropertiesEventHandler(this, &OrientationSampleWP::OnOrientationChanged);

  m_renderer->Initialize(CoreWindow::GetForCurrentThread());
}

void OrientationSampleWP::OnOrientationChanged(Platform::Object^ sender)
{
  // TODO: add code that reacts to change in orientation
}

The above code allows the application to run in both landscape and portrait orientations. Though I use both for easier exemplification in the sample, it’s likely you’ll just stick to only one of them in your games.To have a Landscape-only game, you could write:

DisplayProperties::AutoRotationPreferences = Windows::Graphics::Display::DisplayOrientations::Landscape;

The OnOrientationChanged event is called each time the physical device is rotated and a new orientation is set. We’ll come back and add code in here a bit later.

Rendering

Now that the application can support other orientations, we need to make sure the rendering code produces proper output. The following code/instructions were adapted from the DXGI swap chain rotation sample, and the MSDN article on Supporting screen orientation (DirectX and C++) (Windows). Both of these were written for Windows 8, so they need a few changes to work on Windows Phone 8.

The basic idea is:

  • we always leave the swap chain untouched. Since Windows Phone 8 doesn’t support automatic rotation, we won’t need to resize the swap buffers. But instead, we’ll need to…
  • compute a transformation matrix that will be used for all elements we draw on the screen. We’ll create two transformation matrices: for 2D and 3D rendering.
  • For 3D objects, this will be a rotation matrix that matches to rotation of the actual device. When drawing 3D objects, we’ll multiply this matrix with the projection matrix, so the rotation occurs when projecting the object on the screen, and doesn’t affect anything else in our process. We’ll store this in a member called m_orientationTransform3D.
  • For 2D objects, this matrix will be a combination of a rotation and a translation, because in 2D the origin is in the top-left corner of the screen, so we’ll need to properly move that too. If using DirectXTK, we can pass this matrix to the SpriteBatch->Begin() call, as the last parameter. We’ll store this in a member called m_orientationTransform2D.
  • We will make these changes in the Direct3DBase class, so the rest of the game code remains mostly isolated from orientation code (except for the need to use the above two matrices whenever we draw something)

As explained in my previous post, I’ll be using DirectXMath, so these matrices will be of type XMMATRIX.

We’ll start by adding a few new members to Direct3DBase: the current orientation of the device, the size of the oriented screen size and the two matrices.

    // Store the current Orientation of the device
    Windows::Graphics::Display::DisplayOrientations m_orientation;

    // Size of the virtual, oriented screen
    Windows::Foundation::Size m_orientedScreenSize;

    // Transforms used for rendering 2D and 3D elements in proper orientation
    DirectX::XMMATRIX m_orientationTransform3D;
    DirectX::XMMATRIX m_orientationTransform2D;

In the Windows 8 implementation, the back-buffer was always resized to the new size of the screen, so everywhere you needed the screen size, you could simply use m_renderTargetSize. But on Windows Phone, since we leave the backbuffer untouched (the resolution of an app running on the phone never changes), we’ll need to keep track of the ‘virtual’ screen size, according to the orientation of the screen.

Now let’s add a method that computes the matrices and updates all members to the proper values.

// Method to compute the matrices that will need to be used for rotating whatever we draw.
void Direct3DBase::ComputeOrientationMatrices()
{
    // Store the current device orientation
    m_orientation = DisplayProperties::CurrentOrientation;

    // Generate the matrix transformations for rendering to the new orientation.
    switch (m_orientation)
    {
    case DisplayOrientations::Portrait:
        // Portrait is default for WP8, so no changes need to be made here
        // Just use Identity Matrices
        m_orientationTransform2D = XMMatrixIdentity();
        m_orientationTransform3D = XMMatrixIdentity();
        m_orientedScreenSize = m_renderTargetSize;
        break;
    case DisplayOrientations::Landscape:
        //2D: 90-degree Rotation + translation of origin
        m_orientationTransform2D = XMMatrixMultiply(
            XMMatrixRotationZ(XM_PIDIV2),
            XMMatrixTranslation(m_renderTargetSize.Width,0 ,0));
        //3D: 90-degree Z-rotation
        m_orientationTransform3D = XMMATRIX(
            0.0f, -1.0f, 0.0f, 0.0f,
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f );
        m_orientedScreenSize = Size(m_renderTargetSize.Height, m_renderTargetSize.Width);
        break;
    case DisplayOrientations::PortraitFlipped:
        //This is not supported on the phone, but we leave the math here, just in case...
        //2D: 180-degree Rotation + translation of origin
        m_orientationTransform2D = XMMatrixMultiply(
            XMMatrixRotationZ(XM_PI),
            XMMatrixTranslation(m_renderTargetSize.Width,m_renderTargetSize.Height,0));
        //3D: // 180-degree Z-rotation
        m_orientationTransform3D = XMMATRIX(
            -1.0f,  0.0f, 0.0f, 0.0f,
            0.0f, -1.0f, 0.0f, 0.0f,
            0.0f,  0.0f, 1.0f, 0.0f,
            0.0f,  0.0f, 0.0f, 1.0f
            );
        m_orientedScreenSize = m_renderTargetSize;
        break;
    case DisplayOrientations::LandscapeFlipped:
        //2D: 270-degree Rotation + translation of origin
        m_orientationTransform2D = XMMatrixMultiply(
            XMMatrixRotationZ(3 * XM_PIDIV2),
            XMMatrixTranslation(0,m_renderTargetSize.Height,0));
        //3D: 270-degree Z-rotation
        m_orientationTransform3D = XMMATRIX(
            0.0f, 1.0f, 0.0f, 0.0f,
            -1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f );
        m_orientedScreenSize = Size(m_renderTargetSize.Height, m_renderTargetSize.Width);
        break;
    default:
        throw ref new Platform::FailureException();
        break;
    }
}

The code is pretty self-explanatory. For each possible orientation, we compute the two matrices, and update the ‘oriented’ screen size. Portrait, being the default orientation requires no change, while all the others are 90, 180 and 270 degree rotations. The 3D matrices were manually specified for increased precision. The PortraitFlipped orientation is not supported on phones, but I leave the code here anyway, for completeness.

If you took the time to look at the Windows 8 sample, you’ll notice a couple of differences:

  • The code doesn’t use BasicMath, so the values in the 3D matrix are transposed from what you see in the Win8 sample, to match the DirectXMath format
  • The code doesn’t call DXGI::SetRotation() because this is not supported on WP8
  • For 2D, we still use a DirectXMath’s XMMATRIX, since we plan on using it with DirectXTK’s SpriteBatch. On Windows 8, Direct2D matrices are computed, but Driect2D is not supported on Windows Phone 8

Now let’s call this from appropriate places. Just add a call to this method at the end of CreateWindowSizeDependentResources(), and then we need to make sure that CreateWindowSizeDependentResources() is called if the orientation has changed, in UpdateForWindowSizeChange().

// This method is called in the event handler for the SizeChanged event.
void Direct3DBase::UpdateForWindowSizeChange()
{
    if (m_window == nullptr)
        return;
    if (m_window->Bounds.Width  != m_windowBounds.Width ||
        m_window->Bounds.Height != m_windowBounds.Height ||
        m_orientation != DisplayProperties::CurrentOrientation)
    {
        ID3D11RenderTargetView* nullViews[] = {nullptr};
        m_d3dContext->OMSetRenderTargets(ARRAYSIZE(nullViews), nullViews, nullptr);
        m_renderTargetView = nullptr;
        m_depthStencilView = nullptr;
        m_d3dContext->Flush();
        CreateWindowSizeDependentResources();
    }
}

Lastly, in the event handler for OrientationChanged, we need to let the renderer update it’s state for the new orientation

void OrientationSampleWP::OnOrientationChanged(Platform::Object^ sender)
{
  m_renderer->UpdateForWindowSizeChange();
}

Using the 3D matrix

When the screen orientation changes, you’ll need to re-create the projection matrix to take into account the new screen orientation. In the case of the sample (based on the default project template), this happens in CubeRenderer::CreateWindowSizeDependentResources()

void CubeRenderer::CreateWindowSizeDependentResources()
{
    Direct3DBase::CreateWindowSizeDependentResources();

    // Update the 3D Matrices, using the oriented screen size
    float aspectRatio = m_orientedScreenSize.Width / m_orientedScreenSize.Height;
    float fovAngleY = 70.0f * XM_PI / 180.0f;
    if (aspectRatio < 1.0f)
    {
        fovAngleY /= aspectRatio;
    }

    XMMATRIX projection = XMMatrixPerspectiveFovRH(
        fovAngleY,
        aspectRatio,
        0.01f,
        100.0f
    );
    // Multiply with the orientation matrix
    XMMATRIX rotatedProjection = XMMatrixMultiply(projection, m_orientationTransform3D);
        XMStoreFloat4x4( &m_constantBufferData.projection, rotatedProjection);
}

We multiply the projection matrix by the 3D orientation matrix, which will cause all objects to be rotated correctly. Also notice that we use m_orientedScreenSize to compute the aspect ratio, instead of the m_windowBounds, which always has the same value, regardless of orientation.

Using the 2D matrix

For 2D, when using SpriteBatch, it’s must easier. Simply pass the 2D orientation matrix as the last parameter to the SpriteBatch->Begin() call.

m_spriteBatch->Begin(DirectX::SpriteSortMode_Deferred,m_commonStates->NonPremultiplied(),nullptr,nullptr,nullptr,nullptr,m_orientationTransform2D);

Now all sprites drawn with this batch will be properly rotated and translated for the new display.

Note:

You only need to use these two matrices when you draw directly to the backbuffer. If you draw offscreen, into a rendertarget, ignore these and draw normally.

Input

Another thing that you need to manually process is the input. Regardless of device orientation, the raw pointer input comes in unaltered, so we’ll need to manually rotate it too. For this, we can add a simple method to Direct3DBase, the transforms a Point into new coordinates to match the orientation of the device.

Point Direct3DBase::TransformToOrientation(Point point, bool dipsToPixels)
{
    Point returnValue;

    switch (m_orientation)
    {
    case DisplayOrientations::Portrait:
        returnValue = point;
        break;
    case DisplayOrientations::Landscape:
        returnValue = Point(point.Y, m_windowBounds.Width - point.X);
        break;
    case DisplayOrientations::PortraitFlipped:
        returnValue = Point(m_windowBounds.Width - point.X, m_windowBounds.Height - point.Y);
        break;
    case DisplayOrientations::LandscapeFlipped:
        returnValue = Point(m_windowBounds.Height -point.Y, point.X);
        break;
    default:
        throw ref new Platform::FailureException();
        break;
    }
    // Convert DIP to Pixels, or not?
    return dipsToPixels ? Point(ConvertDipsToPixels(returnValue.X),
                                ConvertDipsToPixels(returnValue.Y))
                        : returnValue;
}

To exemplify, you can get the pointer location in OnPointerMoved, transform it, and send it to your game’s code. To see the example in action, check the code available to download at the end of the article.

void OrientationSampleWP::OnPointerMoved(CoreWindow^ sender, PointerEventArgs^ args)
{
  Point orientedCursor = m_renderer->TransformToOrientation(args->CurrentPoint->Position, true);
	m_renderer->SetDotPosition(orientedCursor.X,orientedCursor.Y);
}

I haven’t yet tested anything related to the GestureRecognizer APIs, but I assume those will behave in the same way, and you’ll need to manually convert any values to the proper orientation that you desire

The code

The accompanying code contains all of the above, and uses DirectXTK, as in my previous samples.

It responds to orientation changes between Portrait, Landscape and LandscapeFlipped. It shows a spinning 3D cube and Shawn’s 2D cat moving from side to side, rotating them properly according to the orientation. It also draws a red dot at the position of a touch input, to exemplify how input can be processed.

Download the code here: OrientationSampleWP.zip

  • Supporting multiple orientations
  • Rotating

Viewing all articles
Browse latest Browse all 31

Trending Articles