pyehouse
2Sep/110

cocos2d: Using Tilt and Calibration/Bias

If you have created a tilt-based game using the iOS accelerometer, one of the complaints you might have heard was about having to hold the device flat, face up, in order to keep the tilt centered. You hear people explain they would like to be able to at least hold it at a slight angle and still have it be playable.

Intuitively it seems obvious that there should be a way to allow for this, and as it turns out there is. What you need to do is calibrate the device to account for the level of tilt the player is comfortable with.

First, if you have used the accelerometer (whether in cocos2D or elsewhere) you know that you provide a hook for the following method:

-(void) accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration;

Then, when the accelerometer sends you a message, you will receive a UIAcceleration object which has four properties (assuming the device is lying flat on its back):

x - acceleration along the x-axis or side to side
y - acceleration along the y-axis or top to bottom
z - acceleration along the z-axis or up and down
timestamp - a precise time of when the acceleration event occurred

The wording might suggest that the acceleration implies movement, but instead think of it as how much tug there is on an invisible rubber ball at the center of the phone, based on gravity as well as movement. In that way, as you tilt your phone, the x, y and z values will change depending on which direction the ball would try to roll.

The values are bound to -1 to 1. So if the phone is tilted so that it is sitting vertically, the y value would be -1, and x and z would be 0 or neutral. Now tilt it to the left a bit and x starts going negative, with y increasing as you aren't tilting it straight down anymore. Eventually, once it is entirely horizontal, x is -1 and now y and z are 0.

Well, we want to recalibrate the tilt. Imagine that you've tilted your phone down a little so that y is at -0.5. Suppose that's the position your player wants to play in so that in that tilt their little avatar stands stock still, center screen. What we want is for the tilting to be remapped. Tilting back up to sitting flat on the back should now provide something akin to a tilt value of .5 and achieving tilt values of negative values should require tilting further down than the new neutral position our player picked.

You probably see where we're going with this. We're going to let our player tilt their phone and then tell us when it's at the new neutral position. We're then going to record the current amount of tilt (i.e. acceleration) and from then on subtract that tilt from acceleration values when calculating new positions.

One more thing though; we need to provide a calibration screen for our player. But once we have recorded the bias, it seems a bit silly to have to duplicate a bunch of code in order to build a new scene to utilize the tilt. What I'm going to show you are two (well, technically three) classes which you can customize for your own use but which might prove useful in putting a calibration screen in your own tilt based game.

Disclaimer: One of the classes, AcceleratableLayer, is based upon the GameScene class in the DoodleDrop example created by Steffen Itterheim and published in his book "Learn iPhone and iPad Cocos2D Game Development". Just as he allowed his code to be built upon with no strings attached, so do I for the purposes of this tutorial.

To begin with, let's look at the interface declaration for AcceleratableLayer:

@interface AcceleratableLayer : CCLayer {
    float biasX;
    float biasY;
    float lastAccelX;
    float lastAccelY;
    CGPoint playerVelocity;
    BOOL adjustForBias;
    float pctSlow;
}
@property (nonatomic) float biasX;
@property (nonatomic) float biasY;
@property (nonatomic) BOOL adjustForBias;
@property (nonatomic) float pctSlow;
-(CGPoint) adjustPositionByVelocity:(CGPoint)oldpos;
-(CGRect) allowableMovementArea;
+(float) biasX;
+(float) biasY;
+(void) setBiasX:(float)x;
+(void) setBiasY:(float)y;
@end

The biasX and biasY properties are what you expect. We could also do a Z bias easily enough but for cocos2D, we're only doing the first two D's 😉 These properties can be used to retrieve or set the bias.

The adjustForBias property is used to determine whether we want to turn off our tilt adjustment without actually zeroing out our stored bias.

The adjustPositionByVelocity function tells us the new position based on a combination of the old position, the amount of tilt, the recorded speed and bias adjustments.

The allowableMovementArea function will be used to fence in our movement.

The static bias methods are used to actually store and retrieve the bias values into and out of the NSUserDefaults.

Now let's take a look at the AcceleratableLayer implementation:

#import "AcceleratableLayer.h"
// You can alter this to prevent someone from calibrating for too much tilt
#define MAX_ACCEL_BIAS (0.5f)
#pragma mark AcceleratableLayer
@implementation AcceleratableLayer
@synthesize biasX, biasY, adjustForBias;
static NSString* NSD_BIASX = @"biasX";
static NSString* NSD_BIASY = @"biasY";
+(float) biasX
{
    return [[NSUserDefaults standardUserDefaults] floatForKey:NSD_BIASX];
}
+(float) biasY
{
    return [[NSUserDefaults standardUserDefaults] floatForKey:NSD_BIASY];
}
+(void) setBiasX:(float)x
{
    [[NSUserDefaults standardUserDefaults] setFloat:x forKey:NSD_BIASX];
    [[NSUserDefaults standardUserDefaults] synchronize];
}
+(void) setBiasY:(float)y
{
    [[NSUserDefaults standardUserDefaults] setFloat:y forKey:NSD_BIASY];
    [[NSUserDefaults standardUserDefaults] synchronize];
}
-(id) init
{
    if ((self = [super init]))
    {
        biasX = [AcceleratableLayer biasX];
        biasY = [AcceleratableLayer biasY];
        self.adjustForBias = YES;
    }
    return self;
}
// We will require a subclass
-(CGRect) allowableMovementArea
{
    [NSException exceptionWithName:@"MethodNotOverridden" reason:@"Must override this method" userInfo:nil];
    return CGRectZero;
}
-(void) accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
    // used for calibration
    lastAccelX = acceleration.x;
    lastAccelY = acceleration.y;
    lastAccelX = fmaxf(fminf(lastAccelX,MAX_ACCEL_BIAS),-MAX_ACCEL_BIAS);
    lastAccelY = fmaxf(fminf(lastAccelY,MAX_ACCEL_BIAS),-MAX_ACCEL_BIAS);
	// These three values control how the player is moved. I call such values "design parameters" as they
	// need to be tweaked a lot and are critical for the game to "feel right".
	// Sometimes, like in the case with deceleration and sensitivity, such values can affect one another.
	// For example if you increase deceleration, the velocity will reach maxSpeed faster while the effect
	// of sensitivity is reduced.
	// this controls how quickly the velocity decelerates (lower = quicker to change direction)
	float deceleration = 0.4f;
	// this determines how sensitive the accelerometer reacts (higher = more sensitive)
	float sensitivity = 6.0f;
	// how fast the velocity can be at most
	float maxVelocity = 10.0f;
	// adjust velocity based on current accelerometer acceleration (adjusting for bias)
    if (adjustForBias)
    {
        playerVelocity.x = playerVelocity.x * deceleration + (acceleration.x-biasX) * sensitivity;
        playerVelocity.y = playerVelocity.y * deceleration + (acceleration.y-biasY) * sensitivity;
    }
    else
    {
        playerVelocity.x = playerVelocity.x * deceleration + acceleration.x * sensitivity;
        playerVelocity.y = playerVelocity.y * deceleration + acceleration.y * sensitivity;
    }
    // we must limit the maximum velocity of the player sprite, in both directions (positive & negative values)
    playerVelocity.x = fmaxf(fminf(playerVelocity.x,maxVelocity),-maxVelocity);
    playerVelocity.y = fmaxf(fminf(playerVelocity.y,maxVelocity),-maxVelocity);
}
-(CGPoint) adjustPositionByVelocity:(CGPoint)oldpos
{
    CGPoint pos = oldpos;
	pos.x += playerVelocity.x;
    pos.y += playerVelocity.y;
	// Alternatively you could re-write the above 3 lines as follows. I find the above more readable however.
	// player.position = CGPointMake(player.position.x + playerVelocity.x, player.position.y);
	// The seemingly obvious alternative won't work in Objective-C! It'll give you the following error.
	// ERROR: lvalue required as left operand of assignment
	// player.position.x += playerVelocity.x;
	// The Player should also be stopped from going outside the allowed area
    CGRect allowedRect = [self allowableMovementArea];
	// the left/right border check is performed against half the player image's size so that the sides of the actual
	// sprite are blocked from going outside the screen because the player sprite's position is at the center of the image
	if (pos.x < allowedRect.origin.x) 	{ 		pos.x = allowedRect.origin.x;          		// also set velocity to zero because the player is still accelerating towards the border 		playerVelocity.x = 0; 	} 	else if (pos.x > (allowedRect.origin.x + allowedRect.size.width))
	{
		pos.x = allowedRect.origin.x + allowedRect.size.width;
		// also set velocity to zero because the player is still accelerating towards the border
		playerVelocity.x = 0;
	}
    if (pos.y < allowedRect.origin.y)     {         pos.y = allowedRect.origin.y;                  playerVelocity.y = 0;     }     else if (pos.y > (allowedRect.origin.y + allowedRect.size.height))
    {
        pos.y = allowedRect.origin.y + allowedRect.size.height;
        playerVelocity.y = 0;
    }
    return pos;
}
@end

That's a bit of meat with those potatoes. Let's chop it up a bit.

First, I want to point out the MAX_ACCEL_BIAS macro which is set to 0.5f. What this does for us is, as the name implies, lock the bias to a max of 0.5f in either direction. Consider for a moment what would happen if your user tilts their phone almost vertically. The y value will be near -1. Now you calibrate. They can't tilt their phone any further down in order to move the avatar downward on the screen. Meanwhile, tilting forward will result in moving upward with rocket like speed. Even letting the user go this far results in a little lagginess going downward. It becomes a matter of taste as to how much bias you want to let them introduce.

Next we have the static methods to set and retrieve bias via the NSUserDefaults system. If you feel like using a different method of stashing your player's bias settings, feel free to replace them here. The usage is pretty straightforward.

Next we have the -(id)init method where aside from the usual, we are grabbing any stored bias settings and stashing them in our local members as well as presuming we are adjusting for bias.

The next method is -(CGRect)allowableMovementArea and you will immediately notice it does nothing of any use whatsoever. In fact, it throws an NSException right off the bat and returns a CGPointZero just for good measure. What are we doing here? Unlike other languages, Objective C doesn't have a way to ensure that a class cannot be directly instantiated. You have to simply agree to do so. Just to be a little forceful about it, if you do instantiate an AcceleratableLayer object, it's going to blow up on you mighty quickly. This means that in order to make use of AcceleratableLayer, you must subclass it and specifically override this method with valid functionality.

So what is this method supposed to do? It is supposed to return a CGRect that describes the boundary outside of which the controlled object is not allowed to travel. This allows the later code to know when to stop increasing velocity and altering movement because you have reached a border location. There are some assumptions built in here. We use a CGRect, so with this code as is, you can't lock the movable object into another shaped area like a triangle or circle. Additionally, the code that adjusts movement and velocity does not take into account the dimensions of the CCSprite rectangle which represents the object being moved, so your CGRect should represent the area which the center of the moved object is bound within. That is, your CCSprite will likely overlap the edge of the returned CGRect so make sure it is small enough that the sprite is not clipped in a way you don't wish it to be. Once again, it must be stressed that you must subclass AcceleratableLayer and override this method with your own code to return your own CGRect.

This brings us to the -(void)accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration method. This is mostly identical to the original code Steffen Itterheim provided with a few alterations to make adjustments for bias. Adding code to accomodate the z value is as easy as it would appear. Note the use of the playerVelocity CGPoint value. AcceleratableLayer will maintain a constant update of the new velocity based on accelerometer callbacks without needing any further prompting.

Finally we reach -(CGPoint)adjustPositionByVelocity:(CGPoint)oldpos. This method gets called by subclasses in order to retrieve an updated position based on a combination of the previous position and the playerVelocity value being tracked. In my case, I perform this in the update:(ccTime) method which I schedule, but of course you can update this however you wish.

Okay, so that provides some core functionality, but how do we use it to actually do calibration? Glad you asked! First off, we're going to introduce a new CCScene subclass called CalibrationScene. For those keeping score at home, this is the second class I'm going to mention to you. Ready for the interface declaration?

@interface CalibrationScene : CCScene
{
}
+(id) scene;
@end

Exciting stuff, eh? The +(id)scene method, as you expect, creates a new CalibrationScene to be pushed onto the CCDirector. That's it.

Okay, so let's take a look at the implementation file for this guy:

#import "CalibrationScene.h"
#import "AcceleratableLayer.h"
#pragma mark CalibrationLayer
@interface CalibrationLayer : AcceleratableLayer {
    CCSprite* testsubject;
    CGPoint cenpt;
}
@end
@implementation CalibrationLayer
- (id) init
{
    if ((self = [super init]))
    {
        // We need the accelerometer
        self.isAccelerometerEnabled = YES;
        // You can create whatever CCSprite you want
        // For the purposes of this demo, I'm creating a simple red square on the fly
        GLubyte pixels[900][4]; // 60x60 square
		int i;
		for(i = 0; i < 900; i++) {
			pixels[i][0] = 0xFF; /* Red channel */
			pixels[i][1] = 0x00; /* Blue channel */
			pixels[i][2] = 0x00; /* Green channel */
			pixels[i][3] = 0xFF; /* Alpha channel */
		}
		CCTexture2D* myTexture = [[CCTexture2D alloc] initWithData: (void*) pixels
                                          pixelFormat: kTexture2DPixelFormat_RGBA8888
                                           pixelsWide: 30
                                           pixelsHigh: 30
                                          contentSize: CGSizeMake(30,30)];
        testsubject = [CCSprite spriteWithTexture:myTexture];
        // Let's start our test subject out in the center of the screen
        CGSize winSize = [[CCDirector sharedDirector] winSize];
        cenpt = ccp(winSize.width*0.5f,winSize.height*0.5f);
        testsubject.position = cenpt;
        [self addChild:testsubject];
        // And add a simple menu so we know whether we are Done, want to Calibrate to the current tilt, or Zero things
        // back out to normal
        CCMenuItemLabel* closeMenu = [CCMenuItemLabel
                                      itemWithLabel:[CCLabelTTF labelWithString:@"Done" fontName:@"Helvetica" fontSize:12.0f]
                                      target:self
                                      selector:@selector(closeScene)];
        CCMenuItemLabel* calibrateMenu = [CCMenuItemLabel
                                          itemWithLabel:[CCLabelTTF labelWithString:@"Calibrate" fontName:@"Helvetica" fontSize:12.0f]
                                          target:self
                                          selector:@selector(calibrate)];
        CCMenuItemLabel* zeroMenu = [CCMenuItemLabel
                                          itemWithLabel:[CCLabelTTF labelWithString:@"Zero" fontName:@"Helvetica" fontSize:12.0f]
                                          target:self
                                          selector:@selector(zero)];
        CCMenu* menu = [CCMenu menuWithItems:closeMenu, calibrateMenu, zeroMenu, nil];
        [menu alignItemsHorizontallyWithPadding:50];
        menu.position = ccp(winSize.width*0.5f, 25 /* arbitrary */);
        [self addChild:menu];
        // And finally, schedule an update callback
        [self scheduleUpdate];
    }
    return self;
}
// Remember, ANY class inheriting from AcceleratableLayer is going to have to
// override this method, which defines the allowable portion of the screen that
// a target is allowed to move into
-(CGRect) allowableMovementArea
{
	CGSize screenSize = [[CCDirector sharedDirector] winSize];
	float imageWidthHalved = [testsubject contentSize].width * 0.5f;
	float leftBorderLimit = imageWidthHalved;
	float rightBorderLimit = screenSize.width - imageWidthHalved;
    float imageHeightHalved = [testsubject contentSize].height * 0.5f;
    float topBorderLimit = screenSize.height - imageHeightHalved;
    float bottomBorderLimit = imageHeightHalved;
    return CGRectMake(leftBorderLimit, bottomBorderLimit, rightBorderLimit-leftBorderLimit, topBorderLimit-bottomBorderLimit);
}
// We just use the AcceleratableLayer method -(void)adjustPositionByVelocity: to
// set our test subject's new position
-(void) update:(ccTime)delta
{
    testsubject.position = [self adjustPositionByVelocity:testsubject.position];
}
// Only really useful if we're not the only scene in the app, which normally we won't be.
-(void) closeScene
{
    CCLOG(@"close the calibration scene");
    // Uncomment the following line ONLY if this scene is not the only remaining scene
    //[[CCDirector sharedDirector] popScene];
}
// Calibration is fairly straightforward... we adjust the bias based on how much
// we are currently tilted. Here we are saving it to NSUserDefaults via the
// AcceleratableLayer methods. We also push the test subject back to the center
// of the screen for further refinement if needed
-(void) calibrate
{
    float x = lastAccelX;
    float y = lastAccelY;
    self.biasX = x;
    self.biasY = y;
    // reposition test item to center
    testsubject.position = cenpt;
    // now save the prefs, which also sets the bias in the inputlayer if it needs it
    [AcceleratableLayer setBiasX:x];
    [AcceleratableLayer setBiasY:y];
}
// Zeroing means forcing the bias back to zero.
-(void) zero
{
    float x = 0;
    float y = 0;
    self.biasX = x;
    self.biasY = y;
    // reposition test item to center
    testsubject.position = cenpt;
    // now save the prefs, which also sets the bias in the inputlayer if it needs it
    [AcceleratableLayer setBiasX:x];
    [AcceleratableLayer setBiasY:y];
}
@end
#pragma mark CalibrationScene
@implementation CalibrationScene
+(id) scene
{
    return [[[self alloc] init] autorelease];
}
- (id)init
{
    self = [super init];
    if (self) {
        // Initialization code here.
        [self addChild:[CalibrationLayer node]];
    }
    return self;
}
- (void)dealloc
{
    [super dealloc];
}
@end

Whoa! That's a lot more than what you would expect from that tiny little interface right? Well, that's because there's actually an extra class defined and implemented in there, CalibrationLayer. That would be the third class I mentioned.

But let's start by pointing out that right there at the bottom of the implementation is the full implementation of CalibrationScene. And all it does is create and add as a child one CalibrationLayer. So in reality, the meat here is all in CalibrationLayer. Let's dig in!

To start with, CalibrationLayer has two members, a CCSprite we lovingly call testsubject, and CGPoint called cenpt. testsubject is the sprite that we will move around via tilt. cenpt is just a stashed copy of the center of the screen. No surprises here.

Opening up -(id)init we start off by enabling the accelerometer. Note that we actually didn't do that in the AcceleratableLayer init method. We probably could but it's fine either way. Just remember to enable it somewhere.

The next bit of code may look odd and it is the result of my wanting to not have to include an image in this tutorial or the project which will be available for download. I'm going to create a 30x30 red square as a sprite. I'll repeat the relevant code below:

        // You can create whatever CCSprite you want
        // For the purposes of this demo, I'm creating a simple red square on the fly
        GLubyte pixels[900][4]; // 60x60 square
		int i;
		for(i = 0; i < 900; i++) {
			pixels[i][0] = 0xFF; /* Red channel */
			pixels[i][1] = 0x00; /* Blue channel */
			pixels[i][2] = 0x00; /* Green channel */
			pixels[i][3] = 0xFF; /* Alpha channel */
		}
		CCTexture2D* myTexture = [[CCTexture2D alloc] initWithData: (void*) pixels
                                          pixelFormat: kTexture2DPixelFormat_RGBA8888
                                           pixelsWide: 30
                                           pixelsHigh: 30
                                          contentSize: CGSizeMake(30,30)];
        testsubject = [CCSprite spriteWithTexture:myTexture];

This isn't really the most pertinent code for this tutorial but I did want to make mention of it. Essentially, we construct the bytes to represent a red 30x30 bitmap with alpha channel. Then we pass those bytes into a texture object. Finally we pass that texture object in to create a new sprite on the fly.

Moving on, we calculate the center point of the screen, stash the value and put the testsubject there by setting its position attribute. We also add the testsubject to the layer.

Next we set up a menu at the bottom of the screen to allow us to either say we are done calibrating, we want to accept the current calibration and store it, or we want to zero out the calibration and start over from scratch.

Finally we call scheduleUpdate to we are getting our update method called each frame.

Next up is the implementation of our -(CGRect)allowableMovementArea method. Remember how I said that subclassing was required as was overriding of this method? Well, here's a sample implementation meant to lock you to .. anywhere on the screen. But note how I'm taking into account the size of the tracked sprite and using that to help define the CGRect.

The update method is pretty straight forward. We set the testsubject.position attribute to the result of calling adjustPositionByVelocity with the original testsubject.position. This calculates the new position based on how we've been tilting the device up til now.

The closeScene method is not terribly interesting. It gets invoked if the Done menu item is tapped. For now it doesn't do anything because popping the only scene tends to provide for a dull experience. You could uncomment that line if it's been pushed on top of another scene though and you would get the expected result.

The calibrate method is run when the user taps the Calibrate menu item. It grabs the previous acceleration (i.e. tilt) for x and y and pushes those values into the local bias members, sends the testsubject back to the center of the screen for possible further calibration, and then sets the user defaults with the new bias as well.

The zero method is identical to the calibrate method, but gets called when the Zero menu item is tapped and is hardcoded to storing 0 for the bias values. This represents restoring the bias back to the neutral state.

And that is it. Drop in the interface and implementation files for these classes and you could have your very own calibration scene along with a subclassable layer class that will provide this acceleration logic for you. Additionally by doing it this way, you guarantee that any tweaks you make in AcceleratableLayer to adjust your acceleration and deceleration curves, max velocities, max biases and other items will automatically be applied to any layer which subclasses it, making it easy to perform these adjustments in one place.

Have fun programming!

Also, if you're interested, you can download the full project here: CalibrationDemo