- Navigate to ~library/Application Support/Developer/Shared/Xcode/ and create the following folders: Project Templates/iPhone Templates/ .. Your final directory structure should look like ~library/Application Support/Developer/Shared/Xcode/Project Templates/iPhone Templates/
- Unzip the OpenGL Screen Controller.zip into your iPhone Templates directory. This will add the OpenGL Screen Controller folder and project template. Your new directory structure will look like: ~library/Application Support/Developer/Shared/Xcode/Project Templates/iPhone Templates/OpenGL Screen Controller/(project files here)
- Open up XCode, create new project, and you’ll see on the left side “User Templates” with a sub-category “iPhone Templates,” clicking on that will bring up your Screen Controller template.
Look at the code, see how its done. Build and run it, and if you click “Level Play” it should transition to a new screen (the gameplay screen) .. click anywhere else in the frame while on the gameplay screen, and it will remove that screen from the screen controller and go back to the title screen. Trace the code, see how it does it, and start playing around.
Buckle in guys, this is going to be a rather large tutorial. The goal for this tutorial is to get a basic screen management system up and running, ready to start writing game code. I would like to start off by saying the following: This is not the only way to get a project up and running! There are many many many ways of solving problems in the programming world. Some people might prefer to use Cocos2D for the iPhone, while others might prefer to roll their own engine from scratch. I did this more as a learning process for myself, and now that I’ve learned what I could, I thought I should share the knowledge to everyone else who is struggling with the problems that I’ve solved.
Also, keep in mind that this screen management system is basically a port of the GameStateManagement demo from the XNA sample ( http://creators.xna.com/ ) from C# to Objective-C with some modifications. Anyone currently using XNA should have a fairly easy time porting this over, as a lot of the code should be recognizable to you.
So, now that I’ve got that out of the way, lets begin! Your first step is going to head over to Jeff’s blog iPhoneDevelopment and pick up the Updated OpenGL Xcode Project Template.
Next step, is to follow his directions and install it! Once you have it installed, load a new project using his template. You will find that a few things are different, and a whole lot is added. Open up the GLView.m in the classes subdirectory, and add the following four methods:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[controller touchesBegan:touches withEvent:event InView:self WithTimer:animationTimer];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[controller touchesMoved:touches withEvent:event InView:self WithTimer:animationTimer];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[controller touchesEnded:touches withEvent:event InView:self WithTimer:animationTimer];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[controller touchesCancelled:touches withEvent:event InView:self WithTimer:animationTimer];
}
What this is going to do, is when your iPod touch or iPhone is touched, a message is sent to the GLView. We are going to capture that message, and send it to the GLViewController
. Okay, got that done? GREAT! Now comes the fun stuff.
Open up your GLViewController.h file. You are going to be putting in quite a bit of code, and I’ll explain everything when we do the .m file, so for right now just adjust your .h file to look like the following. You’ll see the .m file is HEAVILY commented to show what everything is and what it does, and I’ll make some additional notes here as well. Here is the GLViewController.h file.
#import <UIKit/UIKit.h>
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES1/gl.h>
#import <OpenGLES/ES1/glext.h>
#import "Texture2D.h"
#import "InputManager.h"
#import "GameScreen.h"
#import "TitleScreen.h"
@class GLView;
@interface GLViewController : UIViewController
{
NSMutableArray *screens;
NSMutableArray *screensToUpdate;
InputManager *input;
Texture2D *blankTexture;
bool isInitialized;
bool traceEnabled;
UIView *gameView;
CGRect viewport;
}
@property (nonatomic, retain) NSMutableArray *screens;
@property (nonatomic, retain) NSMutableArray *screensToUpdate;
@property (nonatomic, retain) InputManager *input;
@property (nonatomic, retain) Texture2D *blankTexture;
@property (nonatomic, readwrite) bool isInitialized;
@property (nonatomic, readwrite) bool traceEnabled;
@property (nonatomic, retain) UIView *gameView;
@property (nonatomic, readwrite) CGRect viewport;
- (void) setupView:(GLView*)view;
- (void) loadContent;
- (void) addScreen:(GameScreen *)screen;
- (void) removeScreen:(GameScreen *)screen;
- (void) releaseScreen:(GameScreen *)screen;
- (void) updateView:(GLView *)view WithTime:(float)deltaTime;
- (void) drawView:(GLView *)view WithTime:(float)deltaTime;
- (void) traceScreens;
- (void) fadeBackBufferToBlack:(double)alpha;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)touchView WithTimer:(NSTimer *)timer;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)touchView WithTimer:(NSTimer *)timer;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)touchView WithTimer:(NSTimer *)timer;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)touchView WithTimer:(NSTimer *)timer;
@end
Its a good idea to look through the .h file to see what you can do with the screen manager. Obviously you can do the touch events (remember we connected those from the GLView?) but also, looking at the methods you will be able to add and remove screens from the screen manager, update and draw, fade back buffer to black, and a few other things. Let's see how it all works!
#import "GLViewController.h"
#import "GLView.h"
#import "OpenGLCommon.h"
#import "ConstantsAndMacros.h"
const bool LANDSCAPE_MODE = NO;
const float TRANSITION_ON_TIME = .70f;
const float TRANSITION_OFF_TIME = .20f;
@implementation GLViewController
@synthesize screens;
@synthesize screensToUpdate;
@synthesize input;
@synthesize blankTexture;
@synthesize isInitialized;
@synthesize traceEnabled;
@synthesize gameView;
@synthesize viewport;
-(void)setupView:(GLView*)view
{
gameView = view;
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if ( LANDSCAPE_MODE )
{
viewport = CGRectMake(0, 0, 480, 320);
glViewport(0, 0, viewport.size.height, viewport.size.width);
glRotatef(-90, 0, 0, 1);
glOrthof(0, viewport.size.width, viewport.size.height, 0, -10.0, 10.0);
}
else {
viewport = CGRectMake(0, 0, 320, 480);
glViewport(0, 0, viewport.size.width, viewport.size.height);
glOrthof(0.0, viewport.size.width, viewport.size.height, 0.0, -1.0, 1.0);
}
glMatrixMode(GL_MODELVIEW);
glDisable(GL_DEPTH_TEST);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnableClientState(GL_VERTEX_ARRAY);
glAlphaFunc(GL_GREATER, 0.1f);
glEnable(GL_ALPHA_TEST);
glLoadIdentity();
glClearColor(.39f, 0.58f, 0.920f, 1.0f);
[self loadContent];
}
- (void)loadContent
{
screens = [[NSMutableArray alloc] init];
screensToUpdate = [[NSMutableArray alloc] init];
input = [[InputManager alloc] init];
input.isLandscape = LANDSCAPE_MODE;
blankTexture = [[Texture2D alloc] initWithImage:[UIImage imageNamed:@"blankTexture.png"]];
for (GameScreen *screen in screens)
[screen loadContent];
isInitialized = YES;
traceEnabled = NO;
TitleScreen *newScreen = [[TitleScreen alloc] init];
[self addScreen:newScreen];
[newScreen release];
}
- (void)dealloc
{
NSMutableArray *deleteScreens = [[NSMutableArray alloc] initWithArray:screens];
for (GameScreen *screen in deleteScreens)
{
[self removeScreen:screen];
[self releaseScreen:screen];
}
[deleteScreens release];
[screens release];
[screensToUpdate release];
[input release];
[blankTexture release];
[super dealloc];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
- (void) addScreen:(GameScreen *)screen
{
screen.controller = self;
screen.viewport = self.viewport;
screen.transitionOnTime = TRANSITION_ON_TIME;
screen.transitionOffTime = TRANSITION_OFF_TIME;
screen.currentScreenState = TransitionOn;
screen.transitionPosition = 1;
[screen loadContent];
[screen retain];
[screens addObject:screen];
}
- (void) removeScreen:(GameScreen *)screen
{
[screen unloadContent];
}
- (void) releaseScreen:(GameScreen *)screen
{
[screens removeObject:screen];
[screensToUpdate removeObject:screen];
[screen release];
}
- (void) updateView:(GLView *)view WithTime:(float)deltaTime
{
[input update:deltaTime];
[screensToUpdate removeAllObjects];
for(GameScreen *screen in screens)
[screensToUpdate addObject:screen];
bool otherScreenHasFocus = NO;
bool coveredByOtherScreen = NO;
while ([screensToUpdate count] > 0)
{
GameScreen *screen = [screensToUpdate objectAtIndex:([screensToUpdate count] - 1)];
[screensToUpdate removeObjectAtIndex:[screensToUpdate count] - 1];
[screen update:deltaTime OtherScreenHasFocus:otherScreenHasFocus
CoveredByOtherScreen:coveredByOtherScreen];
if ([screen currentScreenState] == TransitionOn ||
[screen currentScreenState] == Active)
{
if (!otherScreenHasFocus)
{
[screen handleInput:input];
otherScreenHasFocus = YES;
}
if (![screen isPopup])
coveredByOtherScreen = YES;
}
}
if (traceEnabled)
[self traceScreens];
for (GameScreen *screen in screens)
{
if (screen.hasBeenUnloaded)
{
[self releaseScreen:screen];
}
}
}
- (void) drawView:(GLView *)view WithTime:(float)deltaTime
{
glClear(GL_COLOR_BUFFER_BIT);
for (GameScreen *screen in screens)
{
if (screen.hasBeenUnloaded)
continue;
[screen draw:deltaTime];
}
}
- (void) traceScreens
{
}
- (void) fadeBackBufferToBlack:(double)alpha
{
glColor4f(alpha,alpha,alpha,alpha);
[blankTexture drawInRect:self.viewport];
glColor4f(1, 1, 1, 1);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)touchView WithTimer:(NSTimer *)timer
{
[input touchesBegan:touches withEvent:event InView:touchView WithTimer:timer];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)touchView WithTimer:(NSTimer *)timer
{
[input touchesMoved:touches withEvent:event InView:touchView WithTimer:timer];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)touchView WithTimer:(NSTimer *)timer
{
[input touchesEnded:touches withEvent:event InView:touchView WithTimer:timer];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event InView:(UIView *)touchView WithTimer:(NSTimer *)timer
{
[input touchesCancelled:touches withEvent:event InView:touchView WithTimer:timer];
}
@end
As you can see, this is a pretty good sized class. What makes it bigger is I have a nasty habit of commenting everything to hell! I always believe it is best to comment heavily, because if you come back a year later and want to adjust your game, comments make it very helpful for remembering what does what.
The comments in the GLViewController
(screen manager) class explains what everything does, but if you have any questions feel free to post them here.
So now we need a game screen or two!! Remember, the following code should never be used to make a game screen directly. What I mean is, use this as a super class and inherit from it with your screens. For example, your TitleScreen should inherit from GameScreen. Make sense?
Here is the GameScreen.h file
#import <Foundation/Foundation.h>
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES1/gl.h>
#import <OpenGLES/ES1/glext.h>
#import "InputManager.h"
#import "Texture2D.h"
enum ScreenState {
TransitionOn = 0,
Active,
TransitionOff,
Hidden
};
@class GLViewController;
@interface GameScreen : NSObject
{
@private
GLViewController *controller;
CGRect viewport;
bool hasBeenUnloaded;
bool isPopup;
float transitionOnTime;
float transitionOffTime;
float transitionPosition;
float transitionAlpha;
enum ScreenState currentScreenState;
bool isExiting;
bool isActive;
bool otherScreenHasFocus;
}
@property (nonatomic, retain) GLViewController *controller;
@property (readwrite) CGRect viewport;
@property (readwrite) bool hasBeenUnloaded;
@property (readwrite) bool isPopup;
@property (readwrite) float transitionOnTime;
@property (readwrite) float transitionOffTime;
@property (readwrite) float transitionPosition;
@property (readwrite) float transitionAlpha;
@property (readwrite) enum ScreenState currentScreenState;
@property (readwrite) bool isExiting;
@property (readwrite) bool isActive;
@property (readwrite) bool otherScreenHasFocus;
- (void) loadContent;
- (void) unloadContent;
- (void) handleInput:(InputManager *)input;
- (void) update:(float)deltaTime OtherScreenHasFocus:(bool)otherFocus CoveredByOtherScreen:(bool)coveredByOtherScreen;
- (bool) updateTransition:(float)deltaTime TransitionTime:(float)transition Direction:(int)direction;
- (void) draw:(float)deltaTime;
- (void) exitScreen;
@end
and the GameScreen.m file
#import "GameScreen.h"
@implementation GameScreen
@synthesize controller;
@synthesize viewport;
@synthesize hasBeenUnloaded;
@synthesize isPopup;
@synthesize transitionOnTime;
@synthesize transitionOffTime;
@synthesize transitionPosition;
@synthesize transitionAlpha;
@synthesize currentScreenState;
@synthesize isExiting;
@synthesize otherScreenHasFocus;
@dynamic isActive;
- (bool) isActive
{
return !otherScreenHasFocus &&
(currentScreenState == TransitionOn ||
currentScreenState == Active);
}
- (id) init
{
self = [super init];
if (self != nil)
{
isExiting = NO;
}
return self;
}
- (void) dealloc
{
[super dealloc];
}
- (void) loadContent
{
}
- (void) unloadContent
{
[controller release];
hasBeenUnloaded = YES;
}
- (void) handleInput:(InputManager *)input
{
}
- (void) update:(float)deltaTime OtherScreenHasFocus:(bool)otherFocus CoveredByOtherScreen:(bool)coveredByOtherScreen
{
otherScreenHasFocus = otherFocus;
if (isExiting)
{
currentScreenState = TransitionOff;
if (![self updateTransition:deltaTime TransitionTime:transitionOffTime Direction: 1])
{
[controller removeScreen:self];
}
}
else if (coveredByOtherScreen)
{
if ([self updateTransition:deltaTime TransitionTime:transitionOffTime Direction: 1])
{
currentScreenState = TransitionOff;
}
else
{
currentScreenState = Hidden;
}
}
else
{
if ([self updateTransition:deltaTime TransitionTime:transitionOnTime Direction: -1])
{
currentScreenState = TransitionOn;
}
else
{
currentScreenState = Active;
}
}
}
- (bool) updateTransition:(float)deltaTime TransitionTime:(
float)time Direction:(int)direction
{
float transitionDelta;
if (time <= 0)
transitionDelta = 1;
else
transitionDelta = deltaTime / time;
transitionPosition += transitionDelta * direction;
if (direction < 0 && transitionPosition <= 0 ||
direction > 0 && transitionPosition >= 1)
{
if (transitionPosition >= 1)
transitionPosition = 1;
else if (transitionPosition <= 0)
transitionPosition = 0;
return NO;
}
return YES;
}
- (void) draw:(float)deltaTime
{
[self.controller fadeBackBufferToBlack:self.transitionPosition];
}
- (void) exitScreen
{
if (transitionOffTime == 0)
{
[controller removeScreen:self];
}
else
{
isExiting = YES;
}
}
@end
Well that does it for the screen manager and the game screen classes. But keep in mind — this will NOT build until the InputManager
is built. Why don’t you try to fiddle with that for a while, see what you can come up with? I’ll post Part 2 of this tutorial detailing how I did my input manager and two screens (TitleScreen and PausedScreen) in my next update.
Two thing before I go: I would really like to find a way to turn this into a template for anyone wishing to use it, but I simply do not know how. If you know how to do so, please leave a message on the forum below.
Secondly, if you have any questions, comments, sly remarks, please post them. I am very good at reading everyones comments, and getting back to you if needed. You can either post a comment or send me an email and I’ll get back to you.
EDIT:
I just wanted to let everyone know that there have been a few changes to this article since first posting. I just modified the code within the GLViewController in several places. The following changes were made:
- Added
[self releaseScreen:screen];
in the dealloc method inside the for loop. - added const bool
LANDSCAPE_MODE = NO;
at the top of the view controller, and an if / else statement in the setup view method to help with setting up a landscape view rather than a portrait view. (The first screen manager was written for only portrait view) - added
input.isLandscape = LANDSCAPE_MODE;
to the loadContent method.
Again, there are too many people to thank for helping me to get to where I am with my current knowledge. I hope this was informational to a few of you! Happy coding everyone!
Read the original blog post here.