×
Namespaces

Variants
Actions

UI Framework for XNA and MonoGame with high DPI support

From Nokia Developer Wiki
Jump to: navigation, search
Featured Article
20 Apr
2014

This article explains how to create a useful class for XNA/MonoGame that will help with loading and drawing images independently on screen resolution and DPI. This class will automatically choose the correct size, position and resolution of the image. It is optimized for both 4-inch and 6-inch devices. We will show how to efficiently load images without the Content Pipeline.

Note.pngNote: This is an entry in the Nokia Imaging and Big UI Wiki Competition 2013Q4.

WP Metro Icon UI.png
WP Metro Icon XNA.png
WP Metro Icon WP8.png
Article Metadata
Code ExampleTested with
SDK: Windows Phone 8.0 SDK
Devices(s): Nokia Lumia 1020
Compatibility
Platform(s):
Windows Phone 8
Article
Created: Tomas Slavicek (15 Dec 2013)
Last edited: kiran10182 (21 Apr 2014)

Contents

Introduction

It was easy to create an app for Windows Phone 7. Every phone had the same screen resolution: WVGA (800x480). Windows Phone 8 brought a new 720p resolution (1280x720) and a WXGA one (1280x768). It was a little uncomfortable for developers that 720p displays had a different screen ratio (16:9 vs. 5:3) than previous ones.

New hi-resolution phones came with the WP8 GDR3 Update. Nokia Lumia 1520 has the Full HD display (1920x1080). Not only was the resolution increased, but also the size of those phones. Both Nokia Lumia 1320 and 1520 have a large 6-inch display that far exceeds in size all previous Windows phones.

It is good to optimize our apps for both high resolution (to provide better quality of graphics) and a different DPI (to show more items on a bigger screen). There are many articles on how to do this in standard C#/XAML. Optimising for large screen phones is a great start (on a Nokia Lumia Developer's Library), with the Dynamic Layout sample. There is also a nice summary on a Dev Center that covers this topic: Multi-resolution apps for Windows Phone 8.

Layout 6 4.png

Our solution for XNA and MonoGame

We will show in this article how to optimize an app (or game) written in XNA or MonoGame for hi-res phones. Standard solution can be slow, memory consuming and unnecessarily complicated. We will design a simple user interface (UI) framework that will help us with loading and drawing images. It will automatically choose the correct size, position and resolution of the image. It will be an efficient, easy to use method, optimized for both low-res and hi-res phones.

Note.pngNote: Images and fonts are not automatically scaled by DPI in XNA and MonoGame. We need to scale them manually. If we have multiple images prepared in multiple resolutions, we also need to select the most appropriate resolution by hand.

Why select XNA and MonoGame?

XNA is a graphics and game development framework for Windows Phone 7, Xbox 360 and PC. MonoGame is a new open source alternative, successor to the XNA. It is available on WP8, Windows 8 and many other platforms. You can use XNA or MonoGame for making games or graphically rich applications.

They are both suitable for creating user interfaces that are full of images, animations and transparency. We can easily work with textures. Shaders can be used to add effects. Typical smartphone can handle more than 7000 non-transparent textures drawn at once. In the article Real-time camera effects on Windows Phone 7 and 8 was shown how to draw an 8-bit-like picture composed of small images. The great advantage of MonoGame is also that it is multiplatform. With Xamarin framework, it is possible to make only one UI for Windows Phone, Android and iOS devices. It can be useful especially for games.

The main ideas

  • We want to draw images in a native resolution of the display, but position and measure them in one base resolution. They should be scaled to the correct size automatically. We want to simplify SpriteBatch syntax for this approach.
  • We want to display hi-resolution assets on hi-end phones. Cheap phones have usually less memory, slower CPU and GPU. Texture quality should be chosen automatically.
  • Large 6-inch displays should provide more "virtual space" for the content.
  • High resolution images converted by standard Content Pipeline take up a lot of space in storage and loading of them is slow. We want to find a better solution.
  • Only textures currently in use should be loaded, not the whole content once at startup
  • SpriteBatch fonts should also be automatically chosen and scaled according to display resolution and DPI.
  • We also want to simplify syntax of point vs. scaled images collisions.

Technology

We will use a Visual Studio 2012 with Windows Phone 8 SDK installed. We will also need to install MonoGame templates. You can download the installer of the latest stable version from MonoGame CodePlex website. Alternatively, you can compile MonoGame from the source code (from the GitHub). Do not forget to download also the ThirdParty/Libs folder (it contains necessary libraries). Or you can download the source code included in this article, it contains everything you need, including the actual compiled MonoGame dll. You can find more info about MonoGame in the MonoGame category on this wiki.

We will use a MonoGame Windows Phone 8 project template.

MonoGame VS.png

Main principles

As we said, we will position all our objects in one base resolution, regardless of the phone type. Our base resolution will be derived from the standard 800x480 resolution. Let's talk about all Windows Phones on the market and compare them by their width:

4 or 5 inch devices

  • 800x480 = 100% (standard cheap phones)
  • 1280x720 = 150% (for example HTC 8X)
  • 1280x768 = 160% (Lumia 1020 and others...)
  • 1920x1080 = 225% (maybe the rumored 5-inch Lumia 929?)

6 inch devices are bigger. They should provide more space for items, so the base resolution should be also bigger. Typical 6-inch display's width is approximately 1.43x bigger than width of the 4-inch display. For the simplicity we can choose the 1.5x multiplier.

  • 1280x720 = 150% / 1.5 = 100%
  • 1920x1080 = 225% / 1.5 = 150%

So we have only 4 possible resolutions for our graphics, if we want to cover all Windows Phones on the market. Only three, if we skip the rumored Lumia 929. If we want crisp non-blurred texture patterns on every device, this is the solution :)

The base resolution on small devices will be 800x480 (on 15:9 displays) or aproximately 854x480. The base resolution on 6-inch devices will be 1280x720.

Icons all.png

How to use SpriteHelper class

We will put our pictures into folders named 100, 150, 160 and 225. Their Build Action will be set to None, Copy to output as Copy if newer.

Monogame content.png

Now we will use a helper class SpriteHelper. Let's declare it in the Game1.cs class. iconTexture will be our image to draw.

SpriteHelper spriteHelper = new SpriteHelper();
Texture2D iconTexture;

In the LoadContent method we can determine if we are on a 6-inch device and initialize the SpriteHelper. We will talk about those methods in detail at the second part of the article.

// Load DPI of the phone, initialize sprite helper class
float dispSizeCoef = ScreenSizeHelper.GetScreenSize() / 4f;
spriteHelper.Initialize(dispSizeCoef, Content, GraphicsDevice);
iconTexture = spriteHelper.LoadTexture("icon");

In the Draw method we can simply draw the images using our helper class:

GraphicsDevice.Clear(Color.CornflowerBlue);
 
spriteBatch.Begin();
spriteHelper.Draw(spriteBatch, ref iconTexture, Vector2.Zero);
spriteHelper.Draw(spriteBatch, ref iconTexture, new Vector2(120, 0));
spriteHelper.Draw(spriteBatch, ref iconTexture, new Vector2(240, 0));
 
// Draw image aligned to right
spriteHelper.Draw(spriteBatch, ref iconTexture, new Vector2(spriteHelper.BaseSize.X - 120, 0));
 
// Draw only some part of the image (rectangle in base coordinates)
spriteHelper.Draw(spriteBatch, ref iconTexture, new Vector2(0, 119),
new Rectangle(0, 0, 80, 80));
spriteBatch.End();

The first three SpriteHelper lines simply draw images. Positions are set in the base coordinates, images are scaled to the correct size automatically. On the fourth line the image is aligned to the right side of the display. The last SpriteHelper line draws only one selected part of the image (by the rectangle).

We have used a couple of helper methods: SpriteHelper, ContentHelper, ScreenSizeHelper and Rect. Now we will talk about them in more detail.

SpriteHelper implementation

SpriteHelper class has a couple of private declarations and two public items:

public Vector2 BaseSize { get; private set; }
public bool IsInitialized = false;
 
int[] availableRes = { 100, 150, 160, 225 };
int zoom = 100;
 
float displaySizeCoef = 1f; // 1f = normal screen, 1.5f = 6 inch screen (items on the display will be smaller)
 
float realZoomCoef; // BaseSize * realZoomCoef = real screen size in pixels
float textureZoomCoef; // = selectedResolution / 100f (
float shrinkCoef; // How should textures shrink down (for example: from 400% graphics to realZoomCoef 3.33f)
 
ContentManager content;
ContentHelper contentHelper;
GraphicsDevice device;

BaseSize is a calculated base resolution (800x480, 854x480 or 1280x720). In the availableRes we have a collection of sizes of our graphics (the numbers must match our folder names). displaySizeCoef is set to 1f on 4-inch displays and to 1.5f to 6-inch displays. We can control the "virtual resolution" of the display by this parameter.

The most interesting is a method Initialize. The correct texture quality is selected here. Also, some parameters for drawing are pre-calculated here:

public void Initialize(float dispSizeCoef, ContentManager content, GraphicsDevice device)
{
this.content = content;
this.device = device;
contentHelper = new ContentHelper(device);
int baseWidth = 480;
 
// Calculates the correct base size and zoom params
displaySizeCoef = dispSizeCoef;
realZoomCoef = (device.Viewport.Width / (float)baseWidth) / dispSizeCoef;
BaseSize = new Vector2(baseWidth, device.Viewport.Height / realZoomCoef) * dispSizeCoef;
 
if (availableRes == null || availableRes.Length == 0)
{
zoom = 100;
textureZoomCoef = 1f;
shrinkCoef = 1f;
return;
}
 
zoom = availableRes[0];
for (int i = 1; i < availableRes.Length; i++)
if (availableRes[i] >= (int)(realZoomCoef * 100f))
{
zoom = availableRes[i];
textureZoomCoef = ((int)zoom) / 100f;
shrinkCoef = realZoomCoef / textureZoomCoef; // = for example: 0.83f for 3.33/4
return;
}
 
// Selects the biggest zoom available:
zoom = availableRes[availableRes.Length - 1];
textureZoomCoef = ((int)zoom) / 100f;
shrinkCoef = realZoomCoef / textureZoomCoef; // >= 1, but it's OK
}

Drawing is done in methods SpriteHelper.Draw. We can pass the texture and the base position. We also can set a specific scale or alpha:

public void Draw(SpriteBatch spriteBatch, ref Texture2D texture, Vector2 basePosition)
{
Draw(spriteBatch, ref texture, basePosition, 1f);
}
public void Draw(SpriteBatch spriteBatch, ref Texture2D texture, Vector2 basePosition, float scale)
{
Draw(spriteBatch, ref texture, basePosition, scale, 1f);
}
public void Draw(SpriteBatch spriteBatch, ref Texture2D texture, Vector2 basePosition, float scale, float alpha)
{
// Lazy-load the texture --> and draw it!
if (texture == null)
return;
LoadCachedTexture(ref texture);
 
// Draw texture to correct position
Vector2 realPos = basePosition * realZoomCoef;
spriteBatch.Draw(texture, realPos, null, Color.White * alpha, 0f, Vector2.Zero,
shrinkCoef * scale, SpriteEffects.None, 0f);
}

There are eight different overloads of the method SpriteHelper.Draw. Download the sample project to look for more info.

For example, the selected parts of the image (by the rectangle) are drawn like this:

Vector2 realPos = basePosition * realZoomCoef;
Rectangle sourceRect = Rect.MultiplyBy(baseSourceRect, textureZoomCoef);
spriteBatch.Draw(texture, realPos, sourceRect, Color.White * alpha, 0f, Vector2.Zero,
shrinkCoef * scale, SpriteEffects.None, 0f);

Drawing by the image center is also very similar:

Vector2 realCenterPos = baseCenterPosition * realZoomCoef;
Vector2 anchor = new Vector2(texture.Width, texture.Height) / 2f;
spriteBatch.Draw(texture, realCenterPos, null, Color.White * alpha, 0f, anchor,
shrinkCoef * scale, SpriteEffects.None, 0);

Note.pngNote: We can make a similar class for drawing SpriteFont objects. Fonts in the XNA and MonoGame are converted into textures. We also need to prepare a collection of fonts in different resolutions. Usually we do not need to have the biggest FullHD resolution for the font (because they are very space consuming), but otherwise the concept is the same.

Collisions

It can be useful to test if the user clicked with his finger on the image. Real coordinates must be re-calculated to the base coordinates. To the method Collides we can pass a screen position of a touch (obtained from standard TouchCollection), the position of the texture and the base position the texture. This method will return if the point collides with the scaled image:

public bool Collides(Vector2 touchPosition, Texture2D texture, Vector2 basePosition)
{
if (texture == null)
return false;
LoadCachedTexture(ref texture);
 
Rectangle scrRect = Rect.Get(
basePosition.X * realZoomCoef,
basePosition.Y * realZoomCoef,
texture.Width * shrinkCoef,
texture.Height * shrinkCoef);
 
return scrRect.Contains((int)touchPosition.X, (int)touchPosition.Y);
}

Cache mechanism for images

We need to load all images through the SpriteHelper class (not directly via ContentManager). SpriteHelper will choose automatically the best resolution of the image. It is also using a very interesting mechanism for loading and caching images:

public Texture2D LoadTexture(string assetName)
{
// Save the path to the texture (it will be loaded lazily)
Texture2D tx = new Texture2D(device, 1, 1);
tx.Tag = Path.Combine("Images", ((int)zoom).ToString(), assetName) + ".png";
return tx;
}

In the LoadTexture method it selects the correct folder for images. However the texture is not loaded yet. This path is set as a Tag property of Texture2D object. The image is loaded asynchronously and not until we need to really draw it.

We are NOT using a ContentPipeline here, we are loading images directly from the .png files.

Method LoadCachedTexture is called from the Draw:

public void LoadCachedTexture(ref Texture2D texture)
{
if (texture.Tag != null)
texture = contentHelper.Load((string)texture.Tag, ref texture);
}

For loading images we are using another helper object: ContentHelper.

Content helper

ContentHelper class is used for efficient loading images. It is not using a ContentPipeline, it is loading images directly from .png files, because it is faster and more memory efficient. Standard ContentPipeline converts images into uncompressed format, so the 100 kB Full HD background can grow up to 6 MB. Loading of those large images takes a lot of time. Application package can also grow up to very big size.

Images are loaded asynchronously, so the loading is not slowing down the main UI thread. As soon as the image is loaded (by the selected path in Texture2D.Tag property), it is saved into the dictionary object: textures. It is used as a cache for images. If we want to load more images with the same asset name, this dictionary will return the already loaded image.

The main ContentHelper.Load method looks like this:

// List of cached items: path to texture / loaded Texture2D object
Dictionary<string, Texture2D> textures = new Dictionary<string, Texture2D>();
 
// Paths that are loaded now...
List<string> loadingPaths = new List<string>();
 
/// <summary>
/// Load selected image (with cache mechanism)
/// - path must be set with ".png"
/// </summary>
public Texture2D Load(string path, ref Texture2D origTexture)
{
if (textures.ContainsKey(path))
return textures[path];
else
{
// Texture is empty at the start
// - it will be saved to textures, as soon as it is loaded asynchronously
// - this method must be called before every Draw call.
if (!loadingPaths.Contains(path))
{
loadingPaths.Add(path);
LoadTextureFromStream(device, path);
}
return origTexture;
}
}

We can't call the method Texture2D.FromStream on Windows Phone 8, because it is not implemented yet in the last build of MonoGame. We need to use this implementation:

private void LoadTextureFromStream(GraphicsDevice device, string path)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
// Run on UI thread
BitmapImage image = new BitmapImage();
image.SetSource(Application.GetResourceStream(new Uri(@"Content\" + path, UriKind.Relative)).Stream);
WriteableBitmap bitmap = new WriteableBitmap(image);
var pixels = SetColorsFromPixelData(bitmap.Pixels);
Texture2D tx2D = new Texture2D(device, bitmap.PixelWidth, bitmap.PixelHeight);
tx2D.SetData(pixels);
 
// Save to "cached storage"
loadingPaths.Add(path);
if (textures.ContainsKey(path))
textures[path] = tx2D;
else
textures.Add(path, tx2D);
});
}

It loads a texture directly from .png file, converts it to the XNA/MonoGame format on an UI thread and save it to the cache.

Here is the second part of this method:

private Color[] SetColorsFromPixelData(int[] pixelData)
{
var Colors = new Color[pixelData.Length];
for (var i = 0; i < pixelData.Length; i++)
{
var packedColor = pixelData[i];
Color unpackedColor = new Color();
unpackedColor.B = (byte)(packedColor);
unpackedColor.G = (byte)(packedColor >> 8);
unpackedColor.R = (byte)(packedColor >> 16);
unpackedColor.A = (byte)(packedColor >> 24);
Colors[i] = unpackedColor;
}
return Colors;
}

Reload after deactivation

Beware! If you load your images using this method and deactivate the app to background - when you return back, all textures will be gone (and the only blue screen will be displayed). You need to reload all textures after every deactivation! This is what the method Reset on both SpriteHelper and ContentHelper is for.

You can easily fix it by inserting a little code into Game1 class:

bool initializeInUpdate = false;
 
protected override void OnActivated(object sender, EventArgs args)
{
// Clears memory / reloads images after deactivation
// - called later, when GraphicsDevice is already initialized
initializeInUpdate = true;
base.OnActivated(sender, args);
}

When the app is activated, it is set that the content should be reloaded. But we can't reload it in the Activated event. The graphics device is not ready yet. We need to call it from the Update as soon as it is possible:

protected override void Update(GameTime gameTime)
{
if (initializeInUpdate)
ReloadImages();
base.Update(gameTime);
}
 
private void ReloadImages()
{
// Called after deactivation: clears memory / reloads images
spriteHelper.Reset();
LoadContent();
initializeInUpdate = false;
}

In the Update we are resetting our SpriteHelper object and loading the content again.

Determining screen size

Let's look at the last helper class: ScreenSizeHelper. It has a static GetScreenSize method that returns size of the diagonal of a display in inches. For classic older phones it returns 4f, for the new GDR3 devices it tries to detect and return a real physical screen size (for example 6f inches).

This method is similar to the method presented in the Dynamic Layout sample. However, it is returning a correct size even on non-6-inch devices with GDR3 update. The standard method presented in that article returned on Lumia 1020 both screenDpiX and screenDpiY == 0, so the diagonalSize was an infinity.

static private float diagonalSize = -1.0f;
static private double screenDpiX = 0.0f;
static private double screenDpiY = 0.0f;
static private Size resolution;
 
/// <summary>
/// Return size in inches of the diagonal of display
/// - returns 4f if not running on GDR3 update
/// </summary>
public static float GetScreenSize()
{
if (Microsoft.Devices.Environment.DeviceType == Microsoft.Devices.DeviceType.Emulator)
diagonalSize = (App.Current.Host.Content.ScaleFactor == 150) ? 6f : 4f;
 
if (diagonalSize == -1f)
{
try
{
screenDpiX = (double)DeviceExtendedProperties.GetValue("RawDpiX");
screenDpiY = (double)DeviceExtendedProperties.GetValue("RawDpiY");
resolution = (Size)DeviceExtendedProperties.GetValue("PhysicalScreenResolution");
 
// Beware! Standard method does not work on Lumia 1020 GDR3 (returns screenDpiX == 0)
if (screenDpiX == 0 || screenDpiY == 0)
diagonalSize = 4f;
else
{
// Calculate screen diagonal in inch
diagonalSize = (float)Math.Sqrt(Math.Pow(resolution.Width / screenDpiX, 2) + Math.Pow(resolution.Height / screenDpiY, 2));
}
}
catch (Exception e)
{
// We're on older software with lower screen size, carry on
Debug.WriteLine("IsBigScreen error: " + e.Message);
diagonalSize = 4f;
}
}
return diagonalSize;
}

Summary

We have learned how to efficiently load and draw images in XNA and MonoGame. Our useful class helped us with automatic choosing the correct size, position and resolution of the images. Images were positioned independently on the screen resolution; we took into consideration the DPI of the displays. We have also learned how to efficiently load images without the Content Pipeline. You can look for more info into the source code that is attached to this article.

This page was last modified on 21 April 2014, at 00:27.
328 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×