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
#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;
- (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;
- (void) update:(float)deltaTime;
- (bool) isButtonPressed:(CGRect)rect;
- (bool) isButtonHeld:(CGRect)rect;
- (void) convertCoordinatesToLandscape;
+ (bool) doesRectangle:(CGRect)rect ContainPoint:(CGPoint)point;
@end
And the .m file
#import "InputManager.h"
@implementation InputManager
@synthesize isLandscape;
@synthesize currentState;
@synthesize previousState;
- (id) init
{
self = [super init];
if (self != nil)
{
currentState = [[InputState alloc] init];
previousState = [[InputState alloc] init];
queryState = [[InputState alloc] init];
currentState.touchLocation = CGPointMake(0, 0);
previousState.touchLocation = CGPointMake(0, 0);
queryState.touchLocation = CGPointMake(0, 0);
}
return self;
}
- (void) dealloc
{
[currentState release];
[previousState release];
[queryState release];
[super dealloc];
}
- (void) update:(float)deltaTime
{
previousState.isBeingTouched = currentState.isBeingTouched;
previousState.touchLocation = currentState.touchLocation;
currentState.isBeingTouched = queryState.isBeingTouched;
currentState.touchLocation = queryState.touchLocation;
[self convertCoordinatesToLandscape];
}
- (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;
}
- (void) convertCoordinatesToLandscape
{
if ( !isLandscape )
return;
int x = currentState.touchLocation.y;
int y = (currentState.touchLocation.x - 320);
y = abs(y);
currentState.touchLocation = CGPointMake( x, y );
}
- (bool) isButtonPressed:(CGRect)rect
{
if ( !currentState.isBeingTouched &&
previousState.isBeingTouched )
{
return [InputManager doesRectangle:rect ContainPoint:currentState.touchLocation];
}
return NO;
}
- (bool) isButtonHeld:(CGRect)rect
{
if ( currentState.isBeingTouched &&
previousState.isBeingTouched )
{
return [InputManager doesRectangle:rect ContainPoint:currentState.touchLocation];
}
return NO;
}
+ (bool) doesRectangle:(CGRect)rect ContainPoint:(CGPoint)point
{
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:
#import <Foundation/Foundation.h>
@interface InputState : NSObject
{
bool isBeingTouched;
CGPoint touchLocation;
}
@property (nonatomic, readwrite) bool isBeingTouched;
@property (nonatomic, readwrite) CGPoint touchLocation;
@end
@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
#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.
#import "PausedScreen.h"
@implementation PausedScreen
@synthesize pausedText;
- (id) init
{
self = [super init];
if (self != nil)
{
self.isPopup = YES;
}
return self;
}
- (void) loadContent
{
self.transitionOnTime = 0;
self.transitionOffTime = 0;
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];
alphaValue = 0.75f;
}
- (void) unloadContent
{
[pausedText release];
[super unloadContent];
}
- (void) handleInput:(InputManager *)input
{
if ([input isButtonPressed:self.viewport])
{
[self exitScreen];
}
}
- (void) update:(float)deltaTime OtherScreenHasFocus:(
bool)otherFocus CoveredByOtherScreen:(bool)coveredByOtherScreen
{
[super update:deltaTime OtherScreenHasFocus:otherFocus CoveredByOtherScreen:coveredByOtherScreen];
}
- (void) draw:(float)deltaTime
{
[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.