5. All About Control

In this chapter, you will learn three main ways to control the player’s sprite: touching the screen, using an onscreen joystick, and tilting the iOS device.

To this point, Raiders has had only one scene. Now you will learn how to switch scenes and examine two types of sprites: an Action-Item that responds to user taps and the main player sprite.

Changing Scenes

In Chapter 4, you explored the concept of scenes; but the code in that chapter included only a main, or menu, screen. For this chapter, you need a mechanism to switch to a new scene. In the Chapter 5 code, you’ll notice several new files and classes that include the switching scene code and the new sprites.

At this point, the menu screen consists of a background sprite and a play sprite that starts the game. To make the play sprite usable, you could change the Sprite class to accept user input—screen touches—from the main view. However, this is not the best approach, mainly because not all sprites need to accept user touches. It is good programming practice to create a base class with the properties that are common to all child classes, but some functionality is required only in special cases. In those situations, it is better to have a new child class.

This is where you can leverage the power of inheritance. The current code for the main Sprite class can be enhanced to accept and respond to touches.

The Chapter 5 project includes a class called ActionItem, which contains two methods. An ActionItem is a special sprite that can process user touches and call an action or method, as required. The play sprite is now an ActionItem that calls a method to switch views.

The first of the two methods in the ActionItem class is:

- (BOOL)hasBeenTapped:(CGPoint)touchPoint;

This first method uses the bounds of the sprite (more information to follow) to check when a touch has occurred within the bounds of that sprite. This uses the parameter touchpoint, which is the x-y coordinate in View coordinates, to determine where a touch has taken place on the parent view.

To achieve this, the Sprite class was changed from Chapter 4. A new read-only property called bounds is a CGRect. This property is calculated using the starting position of the sprite and the size of the sprite’s image.

The second method in the ActionItem class is:

- (void)tapAction:(SEL)method target:(id)target;

This method allows a method to be called when the ActionItem sprite is tapped. What you may not have have seen previously is the first parameter, (SEL)method, which indicates that this parameter expects you to pass in the name of another method (defined elsewhere) that will be called by implemented code in tapAction:method:target. This is called a selector.

Target is the target object to which the selector message or method will be sent. If you look at MenuSceneController in the code, you’ll notice that the touchesEnded method is now filled in.

touchesEnded is available to any standard iOS UIViewController. The controller classes do not inherit from UIViewController, so this needs to be passed from the parent ViewController or, in this case, ViewController.m.

In ViewController.m, you will see that the standard touchesEnded method just calls the touchesEnded method for the current scene:

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
    // Pass touch events onto the current scene
    [[sharedGameController currentScene] touchesEnded:touches withEvent:event view:self.view];
}

In the MenuSceneController touchesEnded method, the actual touch is first assigned to a variable, then that UITouch object is used to store the UIView coordinates in which the touch occurred. The coordinates are specific to the UIView that is passed in to:

CGPoint touchPoint = [touch locationInView:aView];

For Raiders, the UIView most frequently passed in will be the parent view. That is, the touchPoint will be in the equivalent of screen coordinates. The touchPoint is always in the relative coordinates of the view you are inspecting. So, if you are checking the location for a subview, it will be checking from the bounds of that view, which will probably differ from absolute screen coordinates. For example, the top-left of the view will be 0,0, regardless of where it appears in screen coordinates.

Once the touchPoint is obtained, you need to determine if this point is within the bounds of any sprites in the scene. The only type of sprite that needs to be checked for is ActionItem, so a loop scans all the sprites and looks to see if it is an ActionItem. When a sprite meets this criteria, the tapAction method is called and passes in the MenuSceneController as the target for any messages along with the method signature of the message to send. In this case, it is passing in the method gotoLevel1Scene which simply changes the scene to level1Scene.

Changing Scenes

The ability to change scenes is also new to the code for this chapter. The code needed to achieve this is in GameController, which has a new method called -(void)changeScene:(NSString *)scene and an NSDictionary variable that contains the dictionary of available scenes.

The initScene method has been changed to initialize the available scenes and add them to the dictionary. The keys values are defined in a new file called Common.h. To change a scene you simply call:

[[GameController sharedGameController] changeScene:SCENE_KEY]

where SCENE_KEY is the unique string in Common.h. You call this from the scene you want to switch from.

Also new is Level1SceneController, a class that contains the code for level1 of the game, and is switched to from MenuSceneController.

Creating the Player Sprite

The player sprite is a special sprite. It can do things that other sprites can’t, so it needs its own class, PlayerSprite. The first and most obvious thing that a PlayerSprite can do—and enemy sprites can’t do—is move under the direction of the player. You have three main ways to control the player.

Touch-based control

Touch-based input is synonymous with the iPhone and iOS devices, so the first control type you will use is touch. The player swipes left and right to move the player.

Implementing touch control is very similar to using the ActionItem. In fact, it’s almost exactly the same. Instead of touchesEnded, the main ViewController receives a touchesMoved delegate method that is then sent to the SceneController, which tells the PlayerSprite to reposition itself based on the touchPoint.

The PlayerSprite has a property called currentPosition that will store the CGPoint location of itself at a specific point in time. The PlayerSprite also uses a slightly different method for rendering itself. Instead of using the standard draw or drawAtPoint, a new method, drawPlayer, is used. It’s basically just a proxy for the standard drawAtPoint, but it uses the sprite’s currentPosition as the position to draw:

- (void)drawPlayer {
    [self drawAtPosition:currentPosition];
}

It is called from the touchesMoved method in Level1SceneController:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event view:(UIView *)aView {
    UITouch *touch = [[event touchesForView:aView] anyObject];
    CGPoint touchPoint = [touch locationInView:aView];
    playerSprite.currentPosition = CGPointMake(touchPoint.x, playerSprite.currentPosition.y);
}

Because the sprite is redrawn every sixtieth of a second, there is no need to explicitly redraw the sprite after its position has changed.

Tilt-based Control

Along with the touch screen, another important control feature of the iPhone is its accelerometer, which consists of one or more sensors that monitor acceleration in each of the three movement axes (x, y, z), or left to right, up and down, and in and out. In layman’s terms, the accelerometer can determine if and how the device is being moved or tilted.

In code, reading the accelerometer is surprisingly easy. You need to reference only a shared singleton UIAccelerometer class, and implement one delegate method.

In Level1SceneController.h, you will notice the interface declaration:

@interface Level1SceneController : AbstractSceneController<UIAccelerometerDelegate>

You may or may not have seen protocols previously. This one tells the compiler that Level1SceneController will be implementing the UIAccelerometerDelegate method:

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

Because it does so before the accelerometer delegate starts firing, you must obtain a reference to the UIAccelerometer singleton and set two of its properties. First, set the update frequency, or sample times per second, to poll the accelerometer for changes. Also, the delegate needs to be set to self. This code is in the init of Level1SceneController:

accelerometer = [UIAccelerometer sharedAccelerometer];
accelerometer.updateInterval = 0.1;
accelerometer.delegate = self;

In this code, the updateInterval is set to one-tenth (0.1) of a second. Although you can theoretically set the updateInterval to any valid number, the hardware automatically sets limits, as indicated by this quote from Apple’s developer documentation. “To ensure that it can deliver device orientation events in a timely fashion, the system determines the appropriate minimum value based on its needs.”

That is, the system uses the accelerometer to detect if the device has switched orientation and will not allow third party apps to bombard the accelerometer in a way that could slow it down or cause it to miss changes in orientation.

The code to move the player sprite based on tilt is:

#define kThreshold 0.1
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
    if (acceleration.x > kThreshold)
        [playerSprite movePlayer:5];
    if (acceleration.x < kThreshold * -1)
        [playerSprite movePlayer:-5];
}

The delegate includes a variable called acceleration that has the raw acceleration readings for all three axes. Because the player sprite in Raiders moves only in the x axis, the code reads only the acceleration.x property. Any positive values of x indicate acceleration from left to right, and negative values indicate right to left acceleration.

Eagle-eyed readers will spot that the acceleration is checked along with kThreshold. Because the accelerometer is so sensitive, and the player doesn’t hold the device dead level, a threshold level must be set so any acceleration value less than the threshold level is ignored as too small to be considered an actual tilt. The appropriate threshold level will be different for different types of games, and you may need to fine tune this level during your play testing.

Virtual Joystick Control

The last control technique is a virtual joystick. You can implement a virtual joystick to simulate analog or digital movement.

In the old-school sense, a digital joystick has four discrete directions (up, down, left, and right) and a combination of two directions—up and left, for example—produces diagonal movement.

With an analog joystick, the level of input increases continuously the farther from the center position the joystick moves. Analog sticks usually have a full 360-degree movement which allows for greater precision when required.

The gameplay in Raiders requires only a single-direction digital joystick, as the player moves only left and right on a horizontal line. You’ll implement this using two separate ActionItem objects, one for each direction (Figure 5.1).

Figure 5.1 The player sprite with the virtual joystick

image

At first, it would seem easy to do the code for each button. You could use code similar to the touchesEnded code for the playerButton that you created in Chapter 5, but move the player sprite rather than change the scene. This would work, but to move the player sprite, the player would have to continually tap the button, which would get annoying very quickly.

Instead, the joystick taps are monitored in both the touchesBegan and the touchesEnded methods. touchesEnded is called when the finger is lifted off the screen. touchesBegan instructs the PlayerSprite to start moving in the desired direction and continue to do so until further notice. The touchesEnded method tells the PlayerSprite to stop moving.

You need a new method in PlayerSprite to achieve this as the current code will handle the tap only once to cause one type of movement, which is not desirable. You also need a property that determines if the sprite is currently moving.

In PlayerSprite, the isMoving property is either YES or NO, and movePlayer sets a variable that tells PlayerSprite how many pixels to move and in which direction:

- (void)movePlayer:(int)pixelsToMove {
    if (isMoving)
        amountToMove = pixelsToMove;
    self.currentPosition = CGPointMake(currentPosition.x + pixelsToMove,
    currentPosition.y);
}

Then, in drawPlayer, if isMoving is YES, the PlayerSprite’s currentPosition is updated with the amount stored in the previous variable.

- (void)drawPlayer {
    if (isMoving)
        [self movePlayer:amountToMove];
    [self drawAtPosition:currentPosition];
}

In the touchesEnded method on Leve1Scene, isMoving is then set to NO, which stops the player sprite.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event view:(UIView *)aView {
    UITouch *touch = [[event touchesForView:aView] anyObject];
        CGPoint touchPoint = [touch locationInView:aView];
    if ([leftJoystick hasBeenTapped:touchPoint]) {
        [leftJoystick tapAction:@selector(moveLeft) target:self];
    }
    else if ([rightJoystick hasBeenTapped:touchPoint]) {
        [rightJoystick tapAction:@selector(moveRight) target:self];
    }
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event view:(UIView *)aView {
    UITouch *touch = [[event touchesForView:aView] anyObject];
    CGPoint touchPoint = [touch locationInView:aView];
    if ([leftJoystick hasBeenTapped:touchPoint]) {
        if (playerSprite.isMoving)
            [leftJoystick tapAction:@selector(stopMoving) target:self];
    }
    else if ([rightJoystick hasBeenTapped:touchPoint]) {
        if (playerSprite.isMoving)
            [rightJoystick tapAction:@selector(stopMoving) target:self];
    }

}
- (void)moveLeft {
    playerSprite.isMoving = YES;
    [playerSprite movePlayer:-5];
}
- (void)moveRight {
    playerSprite.isMoving = YES;
    [playerSprite movePlayer:5];
}
- (void)stopMoving {
    playerSprite.isMoving = NO;
}

As you have now seen, there is no perfect solution for player control. The best choice depends on the type of game you’re creating. Touch control is the most obvious for iOS devices, but it won’t be suitable for all games. A racing game, for example, is best controlled using a virtual joystick or the accelerometer. Sometimes a little trial and error is required to find the ideal control option. Or you can implement two or three types of controls and let the player decide which to use.


Clever developers sometimes devise novel approaches to player control. In the iPad version of Chopper 2 by Majic Jungle Software, for example, the player can control the game using the iPhone as a secondary controller. Onscreen buttons fire weapons, and the accelerometer in the iPhone flies the chopper. This solution gives an unobstructed view of the iPad, while using the iPhone controls.

Another novel approach is to use technology such as TV Out, which displays the game on your HD TV and enables a console-like living room experience. One of the best examples of this is Real Racing 2 which shows the game on your TV, but also displays additional information—such as speed, time, and a course map—on an iPad.


Checking Bounds

To make sure the PlayerSprite doesn’t go off the screen, you must implement bounds checking. Without it, the PlayerSprite will happily wander off the edge of the screen, never to be seen again.

Luckily, bounds checking is pretty easy. The PlayerSprite has a property called currentPosition that is used when rendering the sprite to the screen. To do the bounds checking, manual getters and setters are coded for the property. In the setter, if the sprite’s currentPosition value is less than zero or greater than the screen bounds, the sprite is restricted to either the left or right edge of the screen:

- (CGPoint)currentPosition {
    return currentPosition;
}
- (void)setCurrentPosition:(CGPoint)currentPos {
    currentPosition = currentPos;
    int screenWidth = [UIScreen mainScreen].bounds.size.width;
    if (currentPosition.x > screenWidth - self.width)
        currentPosition.x = screenWidth - self.width;
    else if (currentPosition.x <= 0)
        currentPosition.x = 0;
}

Wrapping Up

You now know a bit more about controlling the PlayerSprite and the three primary techniques of player input on iOS devices. You have seen how to respond to touch delegates, and understand how touch works. You’ve used the accelerometer and learned how it adds tilt control to a game. You’ve also coded a virtual joystick.

In the next chapter, you will create the enemy sprites and give them a little intelligence.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.120.6