iPhone 游戏框架:第二阶段教程






4.29/5 (3投票s)
一个输入管理器,可以帮助您进行输入管理
注意:我仍在寻找一种将此转换为模板并发布给任何希望将其用作游戏基础的人的方法。如果您知道任何好的资源,请告诉我,因为到目前为止,Google 提供的帮助非常有限。
大家,欢迎回来!教程的第一阶段都完成了吗?如果还没有,请先完成,因为我们将在下一篇教程中在前一阶段的基础上进行开发。第一阶段可以在 此处 找到。
好的,现在第一阶段已经完成,您已经有了一个可用的屏幕管理系统,但您还没有一个输入管理器来帮助您处理输入!请记住我之前教程中的提示,解决一个问题有很多方法,而这只是其中一种。我们的输入类工作原理如下:如果检测到触摸,GLViewController
将向输入管理器发送一条消息,该消息将存储在一个名为“queryState”的输入“状态”中。在视图控制器更新任何游戏屏幕之前,它将向输入管理器发送一条消息来更新输入,此时输入将从“queryState
”更改为“currentState
”,然后当前状态将设置为之前的状态。
那么,我们来看看代码,对吧?首先是 InputManager.h 文件
// 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
然后是 .m 文件
// 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 只包含 2 个值,我选择将它们设置为如下:
#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
值得一提的是,输入管理器会根据您的游戏是横屏模式还是普通模式来更改实际触摸的坐标。在我意识到发生了什么之前,我遇到的一个问题是触摸屏坐标和游戏世界坐标不一致。例如,在横屏模式下,我的触摸屏坐标 (0,0) 在左下角,而在世界坐标中,(0,0) 在左上角。可以想象,这给我带来了麻烦。
为了解决这个问题,我编写了一个方法,可以在检测到屏幕处于横屏模式时转换坐标系统。需要注意的是,如果您的世界坐标的原点不同(例如,左下角而不是左上角),您的转换也需要不同。
输入管理器类中还有一个方法是 [self doesRectangle:rect ContainPoint:touchLocation];
在这个方法中,您会看到当前状态和前一状态都被用来检测触摸是“被按下”还是“被保持”。这可以以不同的方式实现,但我选择的实现方式如下:
如果按钮刚刚被释放(当前状态未按下,但前一状态已按下),那么按钮刚刚被“按下”。
这一点很重要,因为它意味着如果用户触摸屏幕并按住手指,直到他们释放手指才会被注册为“按钮按下”。有些人更喜欢反过来操作,也就是说,如果当前状态刚刚被按下,而前一状态未被按下。请使用您觉得舒适的方法。
那么现在我们只需要添加一两个游戏屏幕,对吧?好吧,我告诉过您我会帮助您创建一个暂停屏幕。这将帮助您学习如何启动任何类型的游戏屏幕。首先,让我们看看 PausedScreen.h 文件
// 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
如果您注意到,其中一些方法在“GameScreen.h”中,对吧?您还会注意到缺少触摸事件类。所有输入都由输入管理器处理,并将从视图控制器调用([self handleInput:input]
方法)。啊,现在您应该明白为什么我选择触摸管理器路线,而不是重写每个游戏屏幕中的每个 touchEvent
方法了!
每个游戏屏幕都需要几个方法才能正常工作。它需要 loadContent
/ unloadContent
方法、handleInput 方法以及 update / draw 方法。还要记住,每个游戏屏幕都需要在其 draw 方法中调用 [super update:deltaTime...],
并且每个屏幕(除了暂停屏幕,它是一个弹出屏幕)都需要在其 draw 方法中调用 [super draw:deltaTime];
。
现在您已经了解了游戏屏幕的基础知识,让我向您展示 .m 文件。
// 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
需要注意的一点是,由于 PausedScreen
是一个弹出屏幕,它覆盖了视图控制器发送给它的过渡“开启”和“关闭”值。任何游戏屏幕都可以这样做,例如,如果您正在过渡到一个自定义过渡的屏幕,请关闭视图控制器的过渡,让屏幕自己完成过渡。(例如,战斗屏幕)。
就是这样了!我不记得在此坐标系统中纹理是否会上下颠倒绘制,但如果它们是,您所要做的就是编辑 Texture2D.m 文件来修复它。如果您不知道如何做,请给我发电子邮件,我会向您展示我如何做的,但有几种方法可以做到。
我希望您喜欢我为屏幕管理器、输入管理器和游戏屏幕类提供的注释丰富的代码。这应该为您提供入门级 iPhone OpenGL 应用程序所需的基础知识。如果您觉得这些有用,请告诉我,我会在有时间时撰写更多内容。我自己还在学习所有这些,但分享我的知识将为所有人带来更有趣的游戏!请记住,这些都是非常基础的系统,富有创意的程序员可以对其进行大量扩展。迫不及待地想看到你们会创造出什么!
祝大家编码愉快!
在此 阅读原始博客文章。