Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / iPhone

iPhone Gaming Framework: Stage 2 Tutorial

4.29/5 (3 votes)
2 Jun 2009CPOL5 min read 16.5K  
An input manager to help you with input management

NOTE: I am still looking for a good way to turn this into a template and release it for anyone wishing to install this for the base of their game. If you know of a good resource for doing so, please let me know, as Google has been fairly unhelpful as of this point.

Welcome back everyone! Did you all finish Stage 1 of the tutorial? If not, please do that, as we are going to be building off stage 1 in our next tutorial. Stage 1 can be found HERE.

Okay, now that stage 1 is done, you have a working screen management system except you don’t have an input manager to help you with input management! Remember my note from previous tutorials, there are MANY solutions to a problem, and this is just one of many. Our input class works as follows: If a touch is detected, the GLViewController will send a message to the input manager, and that message will be stored in an input “state” called “queryState.” Before the view controller updates any of the game screens, it will send a message to the input manager to update the input, and this is when the input is changed from the “queryState” to “currentState” and the current state is then set to the previous state.

So lets look at the code, yeah? First the InputManager.h file

C++
//	The input manager is a helper class that houses any input
//	that can be obtained by the game engine. As a touch is detected
//	on screen, it will send a message to the input helper. The input
//	helper will then hold onto that message and filter it into the
//	current state on the next game loop. The current state is moved
//	to the previous state, and any incoming touches are then put
//	back into the query state, waiting to be updated.
//
//	This method of updating lets the game filter updates to the top-most
//	game screen and not have to worry about un-focused screens handling
//	input that was not intended for them.
//
//  Created by Craig Giles on 1/3/09.
//

#import <Foundation/Foundation.h>
#import "InputState.h"

@interface InputManager : NSObject
{
@private
	bool isLandscape;
	InputState *currentState;
	InputState *previousState;
	InputState *queryState;
}

@property (nonatomic, readwrite) bool isLandscape;
@property (nonatomic, readonly) InputState *currentState;
@property (nonatomic, readonly) InputState *previousState;

- (void) update:(float)deltaTime;

//
//	Touch events
//
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)view  WithTimer:(NSTimer *)deltaTimer;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)view  WithTimer:(NSTimer *)deltaTimer;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)view  WithTimer:(NSTimer *)deltaTimer;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)view WithTimer:(NSTimer *)deltaTimer;

//
//	Helper Methods
//
- (void) update:(float)deltaTime;
- (bool) isButtonPressed:(CGRect)rect;
- (bool) isButtonHeld:(CGRect)rect;
- (void) convertCoordinatesToLandscape;

//
//	Class methods
//
+ (bool) doesRectangle:(CGRect)rect ContainPoint:(CGPoint)point;

@end

And the .m file

C++
//	The input manager is a helper class that houses any input
//	that can be obtained by the game engine. As a touch is detected
//	on screen, it will send a message to the input helper. The input
//	helper will then hold onto that message and filter it into the
//	current state on the next game loop. The current state is moved
//	to the previous state, and any incoming touches are then put
//	back into the query state, waiting to be updated.
//
//	This method of updating lets the game filter updates to the top-most
//	game screen and not have to worry about un-focused screens handling
//	input that was not intended for them.
//
//  Created by Craig Giles on 1/3/09.
//

#import "InputManager.h"

@implementation InputManager

//
//	Getters / Setters
//
@synthesize isLandscape;
@synthesize currentState;
@synthesize previousState;

//
//	Initialization
//
- (id) init
{
	self = [super init];
	if (self != nil)
	{
		//
		//	Allocate memory for all of the possible states
		//
		currentState = [[InputState alloc] init];
		previousState = [[InputState alloc] init];
		queryState = [[InputState alloc] init];

		//
		//	Set the initial coords for the touch locations.
		//
		currentState.touchLocation = CGPointMake(0, 0);
		previousState.touchLocation = CGPointMake(0, 0);
		queryState.touchLocation = CGPointMake(0, 0);
	}
	return self;
}

//
//	Deallocation
//
- (void) dealloc
{
	[currentState release];
	[previousState release];
	[queryState release];
	[super dealloc];
}

//
//	Update the input manager. This method will take the
//	values in updateState and apply them to the current state.
//	in essence, this method will "query" the current state
//
- (void) update:(float)deltaTime
{
	//	Sets previous state to current state
	previousState.isBeingTouched = currentState.isBeingTouched;
	previousState.touchLocation = currentState.touchLocation;

	//	Sets the current state to the query state
	currentState.isBeingTouched = queryState.isBeingTouched;
	currentState.touchLocation = queryState.touchLocation;

	//	converts the coordinate system if the game is in landscape mode
	[self convertCoordinatesToLandscape];
}

//
//	Touch events
//
//	These events are filtered into the input manager as they occur.
//	Since we want to control how our input, we are going to use a
//	queryState as the "Live" state, while our current and previous
//	states are updated every loop. For this reason, we are always
//	modifying the queryState when these touch events are detected,
//	and the current state is only modified on [self update:deltaTime];
//
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)view  WithTimer:(NSTimer *)deltaTimer
{
	queryState.isBeingTouched = YES;
	queryState.touchLocation = [[touches anyObject] locationInView:view];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)view  WithTimer:(NSTimer *)deltaTimer
{
	queryState.isBeingTouched = YES;
	queryState.touchLocation = [[touches anyObject] locationInView:view];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)view  WithTimer:(NSTimer *)deltaTimer
{
	queryState.isBeingTouched = NO;
	queryState.touchLocation = [[touches anyObject] locationInView:view];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)view WithTimer:(NSTimer *)deltaTimer
{
	queryState.isBeingTouched = NO;
}

//
//	When in landscape mode, the touch screen still records input
//	as if it were in portrait mode. To get around this, if we
//	are writing a game in landscape we need to adjust the coordinate
//	system on the touchscreen to match that of our world.
//
- (void) convertCoordinatesToLandscape
{
	//	If we are not in landscape mode, don't convert anything
	if ( !isLandscape )
		return;

	//	Otherwise, we will need to take the current touch location
	//	swap the x and y values (since in landscape, the portrait
	//	coordinate system means x will point up / down instead of
	//	left / right) and move the y position to match our origin
	//	point of (0,0) in the upper left corner.
	int x = currentState.touchLocation.y;
	int y = (currentState.touchLocation.x - 320);

	//	make sure we take the absolute value of y, since if we didn't
	//	y would always be a negative number.
	y = abs(y);

	//	Since we were converting the current state, we need to update
	//	the current touch location
	currentState.touchLocation = CGPointMake( x, y );
}

//
//	Looks at the previous state, and the current state to determine
//	weather or not the button (rectangle) was just pressed and released.
//
- (bool) isButtonPressed:(CGRect)rect
{
	//
	//	If the current state is not being touched
	//	and the previous state is being touched, the
	//	user just released their touch. This is
	//	personal preference really, But i've found with
	//	my XNA programming, that its better to detect if
	//	a button was just released rather than if it was
	//	just pressed in when determining a button press.
	//
	if ( !currentState.isBeingTouched &&
		 previousState.isBeingTouched )
	{

		return 	[InputManager doesRectangle:rect ContainPoint:currentState.touchLocation];
	}

	return NO;
}

//
//	Looks at the previous state, and the current state to determine
//	weather or not the button (rectangle) is being held down.
//
- (bool) isButtonHeld:(CGRect)rect
{
	//
	//	If the current state and the previous states
	//	are being touched, the user is holding their
	//	touch on the screen.
	//
	if ( currentState.isBeingTouched &&
		 previousState.isBeingTouched )
	{
		return 	[InputManager doesRectangle:rect ContainPoint:currentState.touchLocation];
	}

	return NO;
}

//
//	Helper method for determining if a rectangle contains a point.
//	Unsure if this will stay in the input helper or be moved to some
//	sort of "RectangleHelper" class in the future, but for now this
//	is a good spot for it. Remember, this works with our current coord
//	system. If your engine uses a different coord system (IE: (0,0) is at
//	the bottom left of the screen) you'll need to modify this method.
//
+ (bool) doesRectangle:(CGRect)rect ContainPoint:(CGPoint)point
{
	//
	//	If the rectangle contains the given point, return YES
	//	Otherwise, the point is outside the rectangle, return NO
	//
	if (point.x > rect.origin.x &&
		point.x < rect.origin.x + rect.size.width &&
		point.y > rect.origin.y &&
		point.y < rect.origin.y + rect.size.height)
	{
		return YES;
	}

	return NO;
}

@end

InputState just contains 2 values, and I have chosen to set them as the following:

C++
#import <Foundation/Foundation.h>

@interface InputState : NSObject
{
	bool isBeingTouched;
	CGPoint touchLocation;
}

@property (nonatomic, readwrite) bool isBeingTouched;
@property (nonatomic, readwrite) CGPoint touchLocation;

@end

//
//  Implementation of InputState
//
@implementation InputState

@synthesize isBeingTouched;
@synthesize touchLocation;

- (id) init
{
	self = [super init];
	if (self != nil) {
		isBeingTouched = NO;
		touchLocation = CGPointMake(0, 0);
	}
	return self;
}

@end

It should be a good time to point out, that the input manager changes the coordinates of the actual touch, depending on if you’re playing your game in landscape mode or normal mode. One of the problems I was having, before I realized what was really happening, was my coordinates for the touch screen and coordinates for the game world were different. For example, my touch screen coordinates, if held landscape, (0,0) was in the lower left corner, while in the world coordinates, (0,0) was in the upper left corner. This gave me problems, as you can imagine.

In order to change this, I wrote a method that will convert the coordinates system if the screen is detected in landscape mode. It should be pointed out, that if you’re using a different origin point for your world (IE: Lower left corner instead of upper left) you’re conversions will have to be done differently.

Another method that is currently in the input manager class, is the [self doesRectangle:rect ContainPoint:touchLocation]; Inside this method, you’ll see that both current and previous states are being used in order to detect if a touch is being “touched” or “held.” This can be done differently, but the way I’ve chosen to implement it, is as follows;

if the button was just RELEASED (current state is not pressed, but previous state was) than the button was just “pressed.”

This is important to note, because it means if the user touches the screen, and holds their finger down, it won’t register as a “button press” until they release their finger. Some people prefer to do the opposite, meaning if the current state was just pressed and the previous state was un-pressed. Use what you feel comfortable with.

So now all we need to do is add a game screen or two, right? Well, I told you that I would help you get up a paused screen. This will help you learn how to get any sort of game screen up and running. First, lets take a look at the PausedScreen.h file

C++
//	The paused screen is just an overlay popup screen that is
//	displayed to the user when the game is paused.
//
//  Created by Craig Giles on 1/5/09.
//

#import <Foundation/Foundation.h>

#import "GameScreen.h"
#import "InputManager.h"
#import "Texture2D.h"

@interface PausedScreen : GameScreen
{
	Texture2D *pausedText;
	double alphaValue;
}

@property (nonatomic, retain) Texture2D *pausedText;

- (void) loadContent;
- (void) unloadContent;

- (void) handleInput:(InputManager *)input;
- (void) update:(float)deltaTime	OtherScreenHasFocus:(
    bool)otherFocus	CoveredByOtherScreen:(bool)coveredByOtherScreen;
- (void) draw:(float)deltaTime;

@end

If you notice, several of these methods are in “GameScreen.h” right? You’ll also notice the LACK of touch event classes. All input is handled by the input manager, and will be called from the view controller (the [self handleInput:input] method) Aah now you are getting why I went the touch manager route instead of re-writing every single touchEvent method in each game screen!

There are several methods that every game screen needs in order to function properly. It will need a loadContent / unloadContent methods, handleInput method, and the update / draw methods. Also keep in mind, every game screen will need to call the [super update:deltaTime...], and every screen (other than the paused screen, which is a popup screen) will need to call the [super draw:deltaTime]; within its draw method.

So now you have the basics of the game screen, let me show you the .m file.

C++
//	The paused screen is just an overlay popup screen that is
//	displayed to the user when the game is paused.
//
//  Created by Craig Giles on 1/5/09.
//

#import "PausedScreen.h"

@implementation PausedScreen

@synthesize pausedText;

//
//	Initialize the pause menu screen
//
- (id) init
{
	self = [super init];
	if (self != nil)
	{
		//	flag that there is no need for the game to transition
		//	off when the pause menu is on top of it
		self.isPopup = YES;

	}
	return self;
}

//
//	Load all content associated with the paused screen
//
- (void) loadContent
{
	//
	//	Since this is a popup screen, we will over-ride the
	//	view controllers transition time and set this to instantly
	//	transition on and off.
	//
	self.transitionOnTime = 0;
	self.transitionOffTime = 0;

	//	set the paused text
	pausedText = [[Texture2D alloc] initWithString:@"Paused\nTap screen to unpause"
		dimensions:CGSizeMake(self.viewport.size.width, self.viewport.size.height)
		alignment:UITextAlignmentCenter
		fontName:@"Zapfino"
		fontSize:32];

	//	The alpha value of the background. below
	//	the paused text.
	alphaValue = 0.75f;
}

- (void) unloadContent
{
	//TODO: all unload content goes here
	[pausedText release];
	[super unloadContent];
}

- (void) handleInput:(InputManager *)input
{
	//	If the 'tap here to resume' button was pressed
	//	resume game
	if ([input isButtonPressed:self.viewport])
	{
		[self exitScreen];
	}
}

- (void) update:(float)deltaTime	OtherScreenHasFocus:(
    bool)otherFocus	CoveredByOtherScreen:(bool)coveredByOtherScreen
{
	//TODO: Update logic goes here

	//	Update the base class
	[super update:deltaTime OtherScreenHasFocus:otherFocus CoveredByOtherScreen:coveredByOtherScreen];
}

- (void) draw:(float)deltaTime
{
	//	Darken the screen to alert the player that it has been paused
	[self.controller fadeBackBufferToBlack:alphaValue];

	[pausedText drawInRect:CGRectMake(self.viewport.size.width / 4,
					  self.viewport.size.height / 4,
					  self.viewport.size.width / 2,
					  self.viewport.size.height / 2)];
}
@end

One thing to take note, since the PausedScreen is a popup screen, it has overwritten the transition on and transition off values sent to it by the view controller. Any game screen can do the same, for example if you are transitioning to a screen that you’ve made your own custom transition, turn the view controllers transition off and let the screen transition itself. (IE: a battle screen.)

So there you have it! I don’t remember if textures are drawing upside down with this coord system, but if they are, all you have to do is edit the Texture2D.m file to fix that. If you don’t know how, send me an email and I’ll show you how I did it, but there are several ways of doing it.

I hope you enjoyed my heavily commented code for the screen manager, input manager, and game screen classes. This should give you the basic knowledge to get up and running with your own iPhone OpenGL applications. If you found these useful, let me know and I’ll write up some more when I have the time. I’m still learning all of this myself, but sharing my knowledge will lead to more interesting games for everyone! Keep in mind these are very basic systems and can be heavily expanded on by creative programmers. Can’t wait to see what you guys come up with!

Happy coding all!

Read original blog post here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)