Handling the accelerometer input

Since the apparition of the first iPhone generation, mobile devices are equipped with a 3-axis accelerometer (x, y, and z) to detect orientation changes and it has modified the course of handheld games. It allows players to interact with games without the need to touch the screen, and it allows developers to create new subgenres of games too.

Throughout this chapter, you will mix this hardware feature with a classic arcades genre to develop the following game: the planet Earth has been attacked by an army of UFOs whose sole purpose is to wipe out all of mankind, but fortunately, a mad scientist equipped with some of his inventions is brave enough to defend us.

To control the movements of Dr. Nicholas Fringe, our mad scientist, you will take advantage of the accelerometer, but first of all you need a clean Xcode project. As you learned in the previous chapter how to create a new project and how to get it ready to start developing, we will skip this step, so open the code files of this chapter from the code bundle, where you'll find ExplosionsAndUFOs_init.zip, which contains the initial project.

Linking the CoreMotion framework

The first thing we will need to do to enable accelerometer management is link the CoreMotion framework:

  1. Open the ExplosionsAndUFOs project you just unzipped and go to the General properties screen where you will see the already linked frameworks at the bottom.
    Linking the CoreMotion framework
  2. Click on + and a dialog will appear where you will see the available frameworks to be linked.
    Linking the CoreMotion framework
  3. Look for CoreMotion.framework and click on Add.

This framework allows our game to receive gyroscope and accelerometer data, which we can process and work with. It includes a set of classes to get, manage, and measure motion data such as attitude, rotation rate, acceleration, or number of steps taken by the user. In our case, we are going to use CMMotionManager, the class in charge of the management of four types of motion: raw accelerometer data, raw gyroscope data, raw magnetometer data, and processed device-motion data; in other words, what Dr. Nicholas Fringe needs to fly over the clouds.

The next thing we need to do is include the CoreMotion classes; to achieve this, add the following line to GameScene.h after import "cocos2d.h":

#import <CoreMotion/CoreMotion.h>

Then you will need to declare a private instance variable of CMMotionManager, so go to GameScene.m and replace @implementation GameScene with the following block of code:

@implementation GameScene
{
    // Declaring a private CMMotionManager instance variable
    CMMotionManager *_accManager;
}

Now you just need to initialize this variable by adding the next line in the init method before return self;:

    // Initialize motion manager
    _accManager = [[CMMotionManager alloc] init];

This way, you've made your motion manager ready to start receiving data from the accelerometer and gyroscope.

Note

It's important to stress that you should create only one CMMotionManager instance as multiple instances can adversely impact the rate of the data received.

If you've been working with accelerometer events in Cocos2d 2.x, you will realize that we are initializing accelerometer handling in a different way. In fact, in the previous version, you didn't need to initialize CMMotionManager but to enable isAccelerometerEnabled and to implement the accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UiAcceleration *)acceleration method. This method in a class is derived from CCLayer, which is currently deprecated in Cocos2d 3.x.

Once you have initialized the CMMotionManager instance, there is one thing left to do to start and finish receiving motion data and that is manually starting and stopping it. An effective strategy to carry it out is to take advantage of the onEnter and onExit methods available in every class that inherits CCNode, and remember CCScene is one of them.

onEnter and onExit

These two methods are very useful to control what is happening when a node enters or leaves the main screen, as they are called as soon as a node comes into action or leaves. In total, there are four methods to cover the application behavior when a node enters or leaves with a transition:

  • onEnter: This is called when the transition starts
  • onEnterTransitionDidFinish: This is called when the transition finishes
  • onExit: This is called when the transition finishes
  • onExitTransitionDidStart: This is called when the transition starts

We need to activate accelerometer management as soon as the scene appears and deactivate it when the scene disappears; that's why we're going to focus just on the common versions of onEnter and onExit.

Go ahead and include the following method implementations in GameScene.m:

// Start receiving accelerometer events
- (void)onEnter {
    [super onEnter];
    [_accManager startAccelerometerUpdates];
}

// Stop receiving accelerometer events
- (void)onExit {
    [super onExit];
    [_accManager stopAccelerometerUpdates];
}

One thing to emphasize is the call to the super implementations [super onEnter] and [super onExit]. This means that we are not overriding these methods but extending their behavior because we want to keep their parent functionality.

Converting accelerometer input into movements

Now that our game is waiting for moving data, we need something to realize what is happening when you move your device.

Note

Remember that this time you will need a physical iOS device as iOS Simulator isn't capable of simulating this data.

Let's create a sprite for our crazy scientist so we can move him along the screen. In Xcode, follow these steps:

  1. Right-click on the Resources group and select Add Files to "ExplosionsAndUFOs"….
  2. Select the scientist.png image you will find in the Resources folder.
  3. Be sure that Copy items into destination group's folder (if needed) is selected and click on Add.

Note

In this chapter, we are not covering Universal apps, so when specifying an image name, you will find its –hd.png version too, which will cover all iPhone devices but you won't find the versions related to iPads.

First, we will declare private instance variables for both accelerometer data and acceleration by adding the next lines after CMMotionManager *_accManager;:

    // Declare accelerometer data variable
    CMAccelerometerData *_accData;
    
    // Declare acceleration variable
    CMAcceleration _acceleration;

Then declare the CCSprite private instance variable for our scientist by adding the following line:

    // Declaring a private CCSprite instance variable
    CCSprite *_scientist;

Initialize it by adding the following lines to the init method just before return self;:

    // Initialize the scientist
    _scientist = [CCSprite spriteWithImageNamed:@"scientist.png"];
    
    CGSize screenSize = [CCDirector sharedDirector].viewSize;
    
    _scientist.position = CGPointMake(screenSize.width/2, _scientist.contentSize.height);
    [self addChild:_scientist];

This block of code is pretty simple: you're initializing a sprite using the scientist's image you just added to the project and then you're placing it in a relative position before adding it to the scene.

Nothing new so far. Now it's time to make the scientist move on the palm of our hand. First add the following block of code to GameScene.m:

-(void) update:(CCTime)delta{
    
    // Getting accelerometer data
    _accData = _accManager.accelerometerData;
    
    // Getting acceleration
    _acceleration = _accData.acceleration;
    
    // Calculating next position on 'x' axis
    CGFloat nextXPosition = _scientist.position.x + _acceleration.x * 1500 * delta;

    // Calculating next position on 'y' axis
    CGFloat nextYPosition = _scientist.position.y + _acceleration.y * 1500 * delta;

    // Avoiding positions out of bounds
    nextXPosition = clampf(nextXPosition, _scientist.contentSize.width / 2, self.contentSize.width - _scientist.contentSize.width / 2);    
    nextYPosition = clampf(nextYPosition, _scientist.contentSize.height / 2, self.contentSize.height - _scientist.contentSize.height / 2);
    
    _scientist.position = CGPointMake(nextXPosition, nextYPosition);
}

We are using the update method to retrieve data from the accelerometer, from which we take the acceleration information. Then we calculate the next position on the x and y axes as the sum of the current scientist position plus the multiplication of the acceleration on the corresponding axis, delta, and a constant that I decided to be 1500.

By default, the delta variable's value is 1/60 as the update method is called every frame and the default frame rate is 60 fps. The act of multiplying the velocity of a node by delta is what developers call framerate independent movement and it's a very sensitive issue. It means that if the framerate drops below 60 fps, the movement of the node won't be affected. However, it will affect a framerate-dependent node (the node's velocity isn't multiplied by delta), decreasing its performance by slowing it down and being overloaded.

But what causes a framerate drop? System events, loading textures, or large sprites may cause your game's framerate to drop, so you will need to keep in mind the debug stats while developing the game. Applying the delta solution can have side effects too, such as bad collision detection on low framerates. But for now and for convenience, we will keep our node's framerate independent, that is, we will multiply its velocity by delta.

Going back to the preceding code, once we've calculated the next positions, we seek to ensure that these positions are inside the screen. Do you remember how we solved this issue in the previous chapter? The clampf function is a fancier way of dealing with nodes going out of bounds, you just need to provide the position you want to check, and the minimum and maximum positions available and it will do the rest for you.

Okay, time to run the game for the very first time!

Converting accelerometer input into movements

As you can see, there are two unwanted things: the movement of our sprite is opposite to what we expected and the display orientation is landscape, but we are developing a shoot 'em up so it should be portrait.

In fact, the unexpected movement is due to the display orientation but wait and breathe, as we're going to solve both problems with just one line of code; if you don't believe me, keep reading.

Go to the didFinishLaunchingWithOptions method on AppDelegate.m and add the following setup option to setupCocos2dWithOptions:

CCSetupScreenOrientation: CCScreenOrientationPortrait,

Run the game again and this will verify that everything is under control now.

Converting accelerometer input into movements

There is another way of managing accelerometer input and that is making use of the startAccelerometerUpdatesToQueue:withHandler method available in CMMotionManager. It receives a queue of operations and invokes a CMAccelerometerHandler block to handle the accelerometer data. For now, we will keep it simple using the startAccelerometerUpdates approach.

Calibrating the accelerometer

As one last thing, you may have noticed that you must keep your device flat because if you position it in the usual way, the scientist will move to the bottom of the screen as soon as the game starts. This has a pretty easy solution; we just need to take into account the initial acceleration values so we can compensate for them and stand the device comfortably as we wish.

Declare the next variable just after CMAcceleration _acceleration;:

    // Declare the initial acceleration variable
    CMAcceleration _initialAcceleration;

Go back to the update method, and add these lines after _acceleration = _accData.acceleration;:

 // As soon as we get acceleration store it as the initial acceleration to compensate
    if (_initialAcceleration.x == 0 && _initialAcceleration.y == 0 && _acceleration.x != 0 && _acceleration.y != 0) {
        _initialAcceleration = _acceleration;
    }

This block detects when the device starts receiving acceleration data and we store the first piece of information in the initialAcceleration variable so we can compensate for the inclination.

Find these two lines:

CGFloat nextXPosition = _scientist.position.x + _acceleration.x * 1500 * delta;

CGFloat nextYPosition = _scientist.position.y + _acceleration.y * 1500 * delta;

Replace them with these two lines:

CGFloat nextXPosition = _scientist.position.x + (_acceleration.x - _initialAcceleration.x) * 1500 * delta;

CGFloat nextYPosition = _scientist.position.y + (_acceleration.y - _initialAcceleration.y) * 1500 * delta;

We are modifying the original movement strategy; now we subtract the initial acceleration on both axes from the current acceleration so the user is not required to keep the device flat.

If you run the game now, you will notice that you can stand your device as you want!

Before going ahead, let's clean the code a little. I decided to multiply the node's velocity by 1500 as it provided the desired results, but you can modify it at your convenience. Add the following line at the top of GameScene.m just after #import "GameScene.h":

// Acceleration constant multiplier
#define ACCELERATION_MULTIPLIER 1500.0

Modify these lines:

CGFloat nextXPosition = _scientist.position.x + (_acceleration.x - _initialAcceleration.x) * 1500 * delta;

CGFloat nextYPosition = _scientist.position.y + (_acceleration.y - _initialAcceleration.y) * 1500 * delta;

Replace them with the following ones:

CGFloat nextXPosition = _scientist.position.x + (_acceleration.x - _initialAcceleration.x) * ACCELERATION_MULTIPLIER * delta;
        
CGFloat nextYPosition = _scientist.position.y + (_acceleration.y - _initialAcceleration.y) * ACCELERATION_MULTIPLIER * delta;

We have created a constant and replaced the hardcoded values so we can quickly change the velocity of both axes by just updating a constant. This way, the code looks cleaner.

Note

Using constants instead of hardcoded values is one of the key points in clean code development.

On the other hand, let's make screenSize a private variable so we can use its value whenever we need. First, add the following line after the line declaring the _scientist sprite:

    // Declare global variable for screen size
    CGSize _screenSize;

Then go back to the init method and find the following line:

CGSize screenSize = [CCDirector sharedDirector].viewSize;

Replace it with the following one:

_screenSize = [CCDirector sharedDirector].viewSize;

Finally, find this line:

_scientist.position = CGPointMake(screenSize.width/2, _scientist.contentSize.height);

Replace it with this line:

_scientist.position = CGPointMake(_screenSize.width/2, _scientist.contentSize.height);

Again, this last block of code is a variable replacement so we can use screenSize globally in the class.

Now that we've put our scientist on the screen and we can move him, let's give him a sky to fly through!