《精彩iPhone炫酷开发》试读:1.3.2 编写游戏对象

先来看游戏对象,因为这是游戏的核心部分。它将与视图控制器通信,保证游戏的状态可见,所以它在其 init 方法中取视图控制器的一个指针,并初始化游戏结构。见代码清单 1-1。 代码清单 1-1 初始化控制器 - (id)initWithViewController:(FormicViewController *)controller { // 初始化超类 self = [super init]; if (!self) return nil; // 基本初始化工作 mController = [controller retain]; mLives = 5; mTime = 0; mPoints = 0; mState = GAME_INIT; mBlocked = NO; mCenter[GAME_COLOR] = mCenter[GAME_SHAPE] = 0; for (int i = 0; i < GAME_CIRCLES; i++) mCircle[i][GAME_COLOR] = mCircle[i][GAME_SHAPE] = 0; return self; } 游戏变量将维护中间棋子和周围圆盘的形状和颜色、放置中间棋子所剩的时间、分数和剩余几条命,以及游戏所处的状态。对于所有类变量我都加了前缀 m。这样一来,就能很容易地在源代码中识别出这些类变量(见代码清单 1-2)。 代码清单 1-2 游戏变量 int mCenter[2]; //中间棋子的颜色和形状 int mCircle[GAME_CIRCLES][2]; //周围棋子的 //颜色和形状 int mTime; //剩余的时间 int mLives; //还剩几条命 int mPoints; //得分 BOOL mState; //游戏状态(运行、结束,等等) BOOL mBlocked; //是否阻塞等待动画结束 这里有一个变量需要特别注意,即 mBlocked,Formic 通过这个变量使用了阻塞概念。动画进行时,所涉及的棋子将处于一种中间状态。例如,当中间的棋子要移出到一个圆盘时,相应的外围圆盘棋子仍在原位并随着中间棋子的接近而开始逐渐淡出。不过游戏本身并没有中间状态。当中间棋子与轻击的圆盘中的形状相同时,这两个棋子都将更换。因此,在动画期间视图和游戏逻辑是不一致的。如果在这个时间帧中点击所涉及的圆盘会得到怪的效果。 这是一个一般性问题,而非特定于 Formic,可以采用多种方法来解决。第一种方法很彻底:进入动画的棋子要从正常的视图存储中清除,并置于一个特殊的动画队列中。另外,视图控制器不能依赖于它自己的视图存储,而必须在每次访问棋子时向游戏对象询问有关棋子的情况。当然,这种方法需要编写大量代码,而且会增加控制器和游戏对象之间的消息传递。 处理这个问题的第二种方法是阻塞游戏,直至动画完成(见图 1-9)。这种方法要简单得多,而且代码也更为简短。不过,如果阻塞会导致游戏出现不必要的间断,显然就要避免使用这种方法。 Formic 使用了第二种方法,即简单的阻塞方法。在这里,阻塞实际是好事:棋子移出时,唯一要做的是暂停定时器(见之前有关“定时器”的讨论),因为你还没有看到新棋子。
图 1-9 阻塞状态
图 1-9 阻塞状态
初始化之后,游戏对象处于一种等待状态。一旦轻击中间的圆盘,游戏将由 startGame方法启动。见代码清单 1-3。 代码清单 1-3 startGame 方法 - (void)startGame { // 不重新开始 if (mState == GAME_RUNNING) return; mState = GAME_RUNNING; // 告诉控制器 [mController startGame]; // 填充外圈 for (int i = 0; i < GAME_CIRCLES; i++) [self performSelector:@selector(newPieceForCircle:) withObject:[NSNumber numberWithInteger:i] afterDelay:((float)i*0.2)]; // 填充内圈 [self performSelector:@selector(newCenterPiece) withObject:nil afterDelay:1.4]; // 游戏开始 [self performSelector:@selector(startTimer) withObject:nil afterDelay:1.6]; [mController updateLives:mLives]; } startGame 方法在外围圆盘中填入不同形状,并在中间给出第一个棋子。之后,它启动游戏定时器使游戏开始。 以下是这个代码中最有意思的部分: (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay; 这个方法属于 NSObject 的功能,利用它可以调度一个方法在以后某个时间执行。这个方法使用极其方便、灵活,只需告诉对象本身要调用哪个方法、何时调用以及需要什么参数。 startGame 方法用来创建引导动画,即周围圆盘上的棋子相继移入,最后放入中间棋子(见图 1-10)。定时器延迟启动,以避免干扰这个引导动画。定时器由以下方法启动: - (void)startTimer { [NSTimer scheduledTimerWithTimeInterval:[self timerInterval] target:self selector:@selector(timerAdvanced:) userInfo:nil repeats:YES]; } 注意,游戏中推进定时器所用的延迟要由 timerInterval 方法计算得出。你的得分越高,间隔就越短。游戏进行时,每赢得一分定时器就会重启以使游戏运行速度更快。
图 1-10 引导动画的时间线
图 1-10 引导动画的时间线
定时器将重复调用代码清单 1-4 中的方法。 代码清单 1-4 定时器调用的方法 - (void)timerAdvanced:(NSTimer *)timer { // 阻塞时不推进 if (mBlocked) return; // 新棋子,重新计时 if (mTime == 0) { [timer invalidate]; [self startTimer]; } // 计时器推进 [mController updateTimer:mTime]; mTime++; if (mTime >= GAME_TIMERSTEPS) { // 丢一条命 mLives--; [mController updateLives:mLives]; if (mLives <= 0) { // 游戏结束 mState = GAME_OVER; [timer invalidate]; [mController gameOver]; } else { // 下一个棋子 [self newCenterPiece]; mTime = 0; } } } 首先,这里再次涉及之前提到的游戏阻塞。如果游戏被阻塞,这个方法什么也不做。 其次,调整定时器的间隔。用户的得分越高,游戏运行得就越快。由于定时器间隔不能改变,所以必须删除老的定时器,并创建一个新定时器。 最后,必须更新时间显示,并递增时间计数器。如果玩家耗尽所分配的时间,就会丢一条命,此时更换中间棋子。一旦 5 条命全部丢掉,游戏结束。这些信息都必须在这个方法中检查,这就像是游戏的“中枢”。所有修改都必须传送到视图控制器。 用户轻击一个圆盘将中间棋子移入时,会调用游戏对象的另一个重要方法。每次轻击一个圆盘时,背景视图都会调用这个方法。它返回一个 BOOL 值指示中间棋子是否可以移动。见代码清单 1-5。 代码清单 1-5 轻击一个圆盘时调用的方法 - (BOOL)moveCenterToCircle:(int)circle { // 阻塞或游戏结束时不放置 if (mBlocked || (mState == GAME_OVER)) return NO; if (mCenter[GAME_SHAPE] == mCircle[circle][GAME_SHAPE]) { // 查看是否同色 if (mCenter[GAME_COLOR] == mCircle[circle][GAME_COLOR]) { mPoints++; [mController updateScore:mPoints]; } // 开始移动,并创建新中间棋子 [mController moveCenterToCircle:circle]; mCenter[GAME_COLOR] = mCenter[GAME_SHAPE] = 0; [self newCenterPiece]; mBlocked = YES; // 成功了! return YES; } else // 不能放置 return NO; } 这里再一次必须考虑游戏阻塞,如果游戏被阻塞或者已经结束,则什么也不做。 如果形状匹配,会由一个视图控制器调用启动一个动画。此时,游戏会被阻塞。动画结束时,它将调用代码清单 1-6 中的方法,再解除游戏的阻塞。这样一来,实际上游戏会在动画期间暂停,从而保证游戏和图形保持同步。 代码清单 1-6 为给定的外围圆盘查找一个合适的新棋子 - (void)newPieceForCircle:(NSNumber *)circle { int num = [circle intValue]; BOOL centerFound = NO; // 寻找新棋子,确信可以放置中间棋子 for (int i = 0; i < GAME_CIRCLES; i++) if ((mCenter[GAME_SHAPE] == mCircle[i][GAME_SHAPE]) && (i != num)) centerFound = YES; mCircle[num][GAME_COLOR] = rand () % GAME_MAXCOLORS; if (centerFound) mCircle[num][GAME_SHAPE] = rand () % GAME_MAXSHAPES; else mCircle[num][GAME_SHAPE] = mCenter[GAME_SHAPE]; // 显示 [mController zoomInCircle:num withColor:mCircle[num][GAME_COLOR] andShape:mCircle[num][GAME_SHAPE]]; mBlocked = NO; } 这个方法只是要为这个圆盘创建一个新形状。处理这一事务的第一种方法就是简单地创建一个随机的棋子。如果采用这种做法,有可能出现这样一种窘境:用户无法用中间棋子替换圆盘中填充的形状,以至于丢一条命。 为了避免这种尴尬,一定要保证至少有一个圆盘可以放置中间棋子。这正是代码清单 1-6中所做的工作。 在 Frenzic 中,代码清单 1-6 中的方法耗费了很长时间才调整好。我们的根本目标是让游戏尽可能有趣,而这种无法放置的桔块则与这个目标相悖。另一方面,如果只给出可以顺利放置的桔块,这又会降低 Frenzic 的难度,使它变成一个极其简单的游戏,只需要尽可能快地轻击,而没有任何策略。Frenzic 的做法是,除了在后台玩一个理想游戏,它还使用了很多其他规则来提供桔块。由此我们可以获得一个经验:开发游戏期间要随时做出调整,一点极其重要。 提供一个新的中间棋子的方法见代码清单 1-7。 代码清单 1-7 为中间圆盘寻找一个合适的新棋子 - (void)newCenterPiece { // 将现有的一个棋子淡出 [mController zoomOutCenter]; // 找到一个新棋子 mCenter[GAME_COLOR] = rand () % GAME_MAXCOLORS; mCenter[GAME_SHAPE] = mCircle[rand () % GAME_CIRCLES][GAME_SHAPE]; // 显示 [mController zoomInCenterwithColor:mCenter[GAME_COLOR] andShape:mCenter[GAME_SHAPE]]; // 重置计时器 mTime = 0; [mController updateTimer:mTime]; } 在这里必须确保用户不会得到一个无法放置的棋子。大多数情况下,在外围的某个圆盘中增加新棋子的方法就可以完成这个工作。不过,如果你因超时而丢掉一个棋子,这个方法可以确保替换后的棋子确实可以放置。 以上已经完整地定义了游戏逻辑。余下的只是一些常规功能,比如保存和恢复游戏(本章最后介绍)以及显示和动画(在视图控制器中处理)。

>精彩iPhone炫酷开发

精彩iPhone炫酷开发
作者: [美] Gary Bennett, Wolfgang Ante, Mike Ash, Benjamin Jackson, Neil Mix, Steven Peterson, Matthew "Canis" Rosenfeld
副标题: 七位一线高手的编程和设计范例
原作名: iPhone Cool Projects: Learn the Coding Secrets of Master iPhone Designers and Developers
isbn: 7115236518
书名: 精彩iPhone炫酷开发
页数: 204
译者: 苏金国, 王小振 等
定价: 59.00元
出版社: 人民邮电出版社
装帧: 平装
出版年: 2010-10