2014-02-10

Who stole my pixels?

Moving away from the overly detailed discussion on alpha layering from my last post, today I want to briefly discuss developing for iOS and taking advantage of the full resolution of retina devices. Although I knew some care was needed in order to use the full high-resolution retina display, for some reason I assumed this was already happening. Recently, however, I realized that that wasn't true. A little Googling found lot's of info on this and it all comes down to scaling between points and pixels.

Scaling the heights

There are many discussions on this topic, so I won't go into too much detail, but I did want to mention how I addressed the problem. Briefly, for those who don't know, when Apple released their first high-resolution retina devices they did it in a careful way that allowed all existing apps to still run and look good. Basically they added a new property of the screen that defines what it's scale is. The official Apple documentation for UIScreen.Scale says:

This value reflects the scale factor needed to convert from the default logical coordinate space into the device coordinate space of this screen. The default logical coordinate space is measured using points. For standard-resolution displays, the scale factor is 1.0 and one point equals one pixel. For Retina displays, the scale factor is 2.0 and one point is represented by four pixels.

To use this scale I needed to do three things:

  1. Set the ContentScaleFactor of my EAGLView, which is a subclass of iPhoneOSGameView to the same scale so that my rendering uses the entire high-resolution screen
  2. Tell my game the full high-resolution size of the screen
  3. Multiply all of my coordinates by the defined scale factor

For the first item I both get and remember the scale factor and set the ContentScaleFactor to it in the initialization of my EAGLView. Because I want my game to run on older devices with older versions of iOS I need to first check that the new scale property exists and then only reference it if it does. The purpose of ContentScaleFactor is defined by Apple as:

Hmm, reading over this now I just noticed that it says I shouldn't have to adjust the value of ContentScaleFactor. I'll need to go back and look at my code again.

The scale factor determines how content in the view is mapped from the logical coordinate space (measured in points) to the device coordinate space (measured in pixels). This value is typically either 1.0 or 2.0. Higher scale factors indicate that each point in the view is represented by more than one pixel in the underlying layer. For example, if the scale factor is 2.0 and the view frame size is 50 x 50 points, the size of the bitmap used to present that content is 100 x 100 pixels.

The default value for this property is the scale factor associated with the screen currently displaying the view. If your custom view implements a custom drawRect: method and is associated with a window, or if you use the GLKView class to draw OpenGL ES content, your view draws at the full resolution of the screen. For system views, the value of this property may be 1.0 even on high resolution screens.

In general, you should not need to modify the value in this property. However, if your application draws using OpenGL ES, you may want to change the scale factor to trade image quality for rendering performance. For more information on how to adjust your OpenGL ES rendering environment, see “Supporting High-Resolution Displays” in OpenGL ES Programming Guide for iOS.

Here's the code that does that:

_screenScale = 1;
if(UIScreen.MainScreen.RespondsToSelector(new Selector("scale"))
   && RespondsToSelector(new Selector("contentScaleFactor")))
{
    _screenScale = ContentScaleFactor = UIScreen.MainScreen.Scale;
}

For the second item I simply multiply the screen size by the scale I determined above via this code snippet: new Size(Frame.Size.Width, Frame.Size.Height) * _screenScale.

Finally, for the third item, I perform another multiplication of the scale when I handle touch events.

/// <summary>Called when touch events end.</summary>
/// <param name="touches">Touches.</param>
/// <param name="e">The touch event.</param>
public override void TouchesEnded(NSSet touches, UIEvent e)
{
    base.TouchesEnded(touches, e);
    UITouch touch = (UITouch)touches.AnyObject;
    _lastTouch = touch.LocationInView(this);
    _screenManager.Screen.HandleTouch(new Point(_firstTouch.X, _firstTouch.Y) * _screenScale, new Point(_lastTouch.X, _lastTouch.Y) * _screenScale, true);
}

/// <summary>Called when a touch event moves.</summary>
/// <param name="touches">Touches.</param>
/// <param name="e">The touch event.</param>
public override void TouchesMoved(NSSet touches, UIEvent e)
{
    UITouch touch = (UITouch)touches.AnyObject;
    _lastTouch = touch.LocationInView(this);
    _screenManager.Screen.HandleTouch(new Point(_firstTouch.X, _firstTouch.Y) * _screenScale, new Point(_lastTouch.X, _lastTouch.Y) * _screenScale, false);
}

All of the above code happens in my iOS specific EAGLView class and the rest of my code needed no adjustments.

Next: coming soon...

No comments:

Post a Comment