如何用Sprite Kit制作《太空入侵者》(2)
作者:Joel Shapiro
注:这是作为iOS 7 Feast的组成部分而发布的全新Sprite Kit教程。
在第1部分中,你创造了游戏的基础。到目前为止你已经在游戏中添加了侵略者,舰船和平视显示器(HUD)。你还编写了逻辑代码让侵略者可以自动移动并在倾斜设备时移动舰船。
在第2部分也是最后一部分中,你将为舰船和外星人添加能力让它们相互开火!你同样也将添加音效和真实图像去替代彩色的矩形(游戏邦注:即用于代表侵略者和舰船)而优化游戏
现在让我们真正开始吧。
让你的舰船发射激光炮
首先,你需要思考自己想要的发射舰船激光炮的场景。你决定使用单发快射去发射大炮。但是你该如何检测这些单发快射?
你拥有两个选择:
1.UITapGestureRecognizer—-创造一个并将其附加到场景视图中。
2.touchesEnded:withEvent:—-你的场景是UIResponder的子类;因此你可以使用它直接检测碰触。
在这种情况下(touchesEnded:withEvent:),第二种方法是最佳选择。当你需要基于游戏中不同场景节点检测并处理碰触时,第一种方法会较为棘手,因为你只能够在场景视图中为UITapGestureRecognizer制定一个回调函数选择器。而其它方法则不适合放在这一例子中。
因为所有的SKNode节点(包括SKScene)都能通过touchesEnded:withEvent:直接处理碰触,第二种选择能够更自然地处理针对于节点的碰触—-当你开发的游戏带有更复杂发射处理时它将更有效。
既然你将在场景的touchesEnded:withEvent:方法中检测用户的发射,你该在这方法中做些什么?
发射可以在游戏过程中的任何一个点发生。与之相反的是你的场景将发生改变—-源自update:方法的独立间隔。所以在touchesEnded:withEvent:中任何时候你将如何储存快射检测,并在之后,也就是当被Sprite Kit游戏循环援用时对其进行加工。
答案是一个队列!你将使用一个简单的NSMutableArray在FIFO(先进先出)队列中储存你的快射。
在GameScene.m添加如下属性到类扩展中:
@property (strong) NSMutableArray* tapQueue;
现在将如下内容添加到didMoveToView:中,也就是在[self.motionManager startAccelerometerUpdates];后面:
self.tapQueue = [NSMutableArray array];
self.userInteractionEnabled = YES;
上述代码将快射队列初始化为一个空白数组,并确保用户互动适用于场景中,从而让它能够接收快射事件。
现在在#pragma mark – User Tap Helpers之后添加如下代码:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Intentional no-op
}-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// Intentional no-op
}-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
// Intentional no-op
}-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
if (touch.tapCount == 1) [self.tapQueue addObject:@1];
}
前三个问题只是无意义的存根;之所以会添加它们是因为当你没有调用super而推翻touchesEnded:withEvent:时,苹果会建议你这么做。
touchesEnded:withEvent:方法本身非常简单。它只添加了一个条目到队列中。你不需要定制类在队列中储存快射,因为你所需要做的便是明确快射的出现。因此,你可以使用任何早旧的对象。在此,你可以将整数1作为当一快射的记忆(@1是全新的对象-文字语言,即将把文字1转变为一个NSNumber对象)。
因为你知道侵略者最终将向你的舰船发射子弹,所以在文件最上方的Custom Type Definitions上添加如下代码:
typedef enum BulletType {
ShipFiredBulletType,
InvaderFiredBulletType
} BulletType;
你将使用BulletType面向入侵者和你的舰船分享同样的子弹代码。这会于你和入侵者在同一个弹药商店中消费时出现!
接下来在#define kHealthHudName @”healthHud”下方添加如下代码:
#define kShipFiredBulletName @”shipFiredBullet”
#define kInvaderFiredBulletName @”invaderFiredBullet”
#define kBulletSize CGSizeMake(4, 8)
现在添加如下方法到Scene Setup和Content Creation中:
-(SKNode*)makeBulletOfType:(BulletType)bulletType {
SKNode* bullet;switch (bulletType) {
case ShipFiredBulletType:
bullet = [SKSpriteNode spriteNodeWithColor:[SKColor greenColor] size:kBulletSize];
bullet.name = kShipFiredBulletName;
break;
case InvaderFiredBulletType:
bullet = [SKSpriteNode spriteNodeWithColor:[SKColor magentaColor] size:kBulletSize];
bullet.name = kInvaderFiredBulletName;
break;
default:
bullet = nil;
break;
}return bullet;
}
该方法相对直接:它只是创造一个矩形且带有颜色的精灵去代表子弹,并设置了子弹的名字从而让你之后能在场景中找到它。
现在,添加如下方法到#pragma mark – Bullet Helpers中:
-(void)fireBullet:(SKNode*)bullet toDestination:(CGPoint)destination withDuration:(NSTimeInterval)duration soundFileName:(NSString*)soundFileName {
//1
SKAction* bulletAction = [SKAction sequence:@[[SKAction moveTo:destination duration:duration],
[SKAction waitForDuration:3.0/60.0],
[SKAction removeFromParent]]];
//2
SKAction* soundAction = [SKAction playSoundFileNamed:soundFileName waitForCompletion:YES];
//3
[bullet runAction:[SKAction group:@[bulletAction, soundAction]]];
//4
[self addChild:bullet];
}-(void)fireShipBullets {
SKNode* existingBullet = [self childNodeWithName:kShipFiredBulletName];
//1
if (!existingBullet) {
SKNode* ship = [self childNodeWithName:kShipName];
SKNode* bullet = [self makeBulletOfType:ShipFiredBulletType];
//2
bullet.position = CGPointMake(ship.position.x, ship.position.y + ship.frame.size.height – bullet.frame.size.height / 2);
//3
CGPoint bulletDestination = CGPointMake(ship.position.x, self.frame.size.height + bullet.frame.size.height / 2);
//4
[self fireBullet:bullet toDestination:bulletDestination withDuration:1.0 soundFileName:@"ShipBullet.wav"];
}
}
一步一步检查fireBullet:toDestination:withDuration:soundFileName:中的代码,你做了如下任务:
1.创造一个SKAction能够将子弹移动到目的地,然后将其从场景中删除。这一结果连续执行了个人行动—-下一步行动只会在之前行动完成后发生。因此子弹只会在被移走时才被删除。
2.播放设定好的音效去传达子弹的发射。所有音效都包含在开始项目中,iOS知道如何找到并加载它们。
3.通过将子弹和音效放置在同一个群组中而同时移动子弹并播放音效。群组将平行运行它的行动,而非按照顺序。
4.通过添加子弹到场景中而进行发射。这让它能够出现在屏幕上并开始行动。
以下是你在fireShipBullets中所做的:
1.如果当前屏幕上并没有任何子弹,你便只能发射一枚子弹。这是一个激光炮,而不是激光机枪—-它需要花时间重新加载!
2.设置子弹的位置让它可以出现在舰船上方。
3.设置子弹的目的地,即在屏幕顶部以外。因为x坐标与子弹的位置是一样的,所以子弹将竖直飞射。
4.发射子弹!
在fireShipBullets中的第1点只允许同时发射一枚子弹是一种游戏决策,从技术上来看并非绝对需要。如果舰船可以每分钟发射数千枚子弹,《太空入侵者》未免就太过简单了。你的游戏的部分乐趣在于广泛地选择设计,并找准时机与入侵者相碰撞。
你的激光炮已经准备好发射了!
添加如下代码到Scene Update Helpers中:
-(void)processUserTapsForUpdate:(NSTimeInterval)currentTime {
//1
for (NSNumber* tapCount in [self.tapQueue copy]) {
if ([tapCount unsignedIntegerValue] == 1) {
//2
[self fireShipBullets];
}
//3
[self.tapQueue removeObject:tapCount];
}
}
让我们审视上述代码:
1.在你的tapQueue副本中循环;它必须是副本是因为在代码运行时你可能会修改最初的tapQueue,并在循环时修改数组。
2.如果队列条目是单发快射,处理它。作为开发者,你清楚地知道自己现在只能处理单发快射,而在之后防御双打快射(或其它行动)的可能性才是最佳行动。
3.从队列中删除快射。
注:processUserTapsForUpdate:在每次调用时完全耗尽了快射的队列。结合事实,如果屏幕上已经存在子弹,fireShipBullets将不会发射另外一枚子弹,空白的队列意味着额外或迅速的快射将会被忽视。只有第一次的快射才真正重要。
最后,在update:中添加如下代码作为第一行:
[self processUserTapsForUpdate:currentTime];
这在更新循环中调用了processUserTapsForUpdate: 并处理了任何用户快射。
创建你的游戏并运行!
创造入侵者的攻击
酷,你的舰船最终能够向任何邪恶的入侵者发射子弹了!你将尽快运行它们。
但是你可能已经注意到自己的子弹是直接穿过入侵者而不是炸毁它们。这是因为你的子弹还不足以检测到何时撞击入侵者。现在你需要修改这一点。
首先,你将通过添加如下代码到Scene Update Helpers中而让入侵者做出还击:
-(void)fireInvaderBulletsForUpdate:(NSTimeInterval)currentTime {
SKNode* existingBullet = [self childNodeWithName:kInvaderFiredBulletName];
//1
if (!existingBullet) {
//2
NSMutableArray* allInvaders = [NSMutableArray array];
[self enumerateChildNodesWithName:kInvaderName usingBlock:^(SKNode *node, BOOL *stop) {
[allInvaders addObject:node];
}];if ([allInvaders count] > 0) {
//3
NSUInteger allInvadersIndex = arc4random_uniform([allInvaders count]);
SKNode* invader = [allInvaders objectAtIndex:allInvadersIndex];
//4
SKNode* bullet = [self makeBulletOfType:InvaderFiredBulletType];
bullet.position = CGPointMake(invader.position.x, invader.position.y – invader.frame.size.height/2 + bullet.frame.size.height / 2);
//5
CGPoint bulletDestination = CGPointMake(invader.position.x, – bullet.frame.size.height / 2);
//6
[self fireBullet:bullet toDestination:bulletDestination withDuration:2.0 soundFileName:@"InvaderBullet.wav"];
}
}
}
上述方法的核心逻辑如下:
1.如果屏幕上没有子弹的话只能发射一枚子弹。
2.在屏幕上收集所有的入侵者。
3.随机选择一个入侵者。
4.创造一枚子弹,并从选定的入侵者下方进行发射。
5.子弹应该直下飞行,并从屏幕下方离开。
6.发射入侵者的子弹。
在update:最后添加如下内容:
[self fireInvaderBulletsForUpdate:currentTime];
关于fireInvaderBulletsForUpdate:的调用开始让入侵者向你展开反击。
创建并运行游戏,你可以看到入侵者朝你的舰船发射紫色子弹,就如下图所示:
在游戏设计中你会注意到入侵者的子弹是紫色的,而你的舰船的子弹则是绿色的。这一强烈的颜色反差让我们能在激烈的战斗中更清楚地进行辨明。同样的,你也会在入侵者与自己的舰船相互攻击时听到不同的声音。不同音效的使用在某种程度上是基于不同风格,将赋予游戏更丰富的音频并让它更具有吸引力。并且这同时也涉及到了可及性的问题,即有7%至10%的男性以及0.5%至1%的女性是色盲。所以不同的音效将帮助这些人更轻松地玩游戏。
检测子弹何时撞击它们的目标
虽然现在子弹能够在屏幕上飞射着,但是却没有任何对象会因此被炸毁!这是因为你的游戏还没有撞击检测。它需要在舰船的子弹撞击入侵者,以及入侵者的子弹撞击到舰船时进行检测。
你可以手动完成这一设置,在每个update:调用中比较子弹/入侵者/舰船位置,并检查撞击。但是为什么不让Sprite Kit帮你做这些事?
因为你已经使用了物理主体,而Sprite Kit的物理引擎只能在一个主体撞击另一个主体时才能进行检测。对此,你将使用碰触检测—-而不是碰撞检测。你并不是在使用物理元素去移动子弹或入侵者,所以你不会对他们之间的物理碰撞感兴趣。碰触检测只会在一个物理主体覆盖了另一个主体(从空间上)时进行检测,否则它便不会移动或影响碰触到的实体。
有些游戏拥有许多不同的物理主体类型,并且不会对所有物理主体类型之间的碰触感兴趣。Sprite Kit只会检测你所命令的物理主体类别之间的碰触。
这既是一种速度优化也是一种正确性的约束,即某种碰触类型也许并不符合人们的期望。你可以通过定义类别位掩码控制那些物理主体是用于检测碰触开始。
添加如下代码到Custom Type Definitions中:
static const u_int32_t kInvaderCategory = 0×1 << 0;
static const u_int32_t kShipFiredBulletCategory = 0×1 << 1;
static const u_int32_t kShipCategory = 0×1 << 2;
static const u_int32_t kSceneEdgeCategory = 0×1 << 3;
static const u_int32_t kInvaderFiredBulletCategory = 0×1 << 4;
这些看似奇怪的常量是位掩码。位掩码是填充变量到一个单独的32位体无符号整数的方法。当作为u_int32_t进行储存时,位掩码可以拥有32个不同值。在这5个类别中每个类别会定义一种物理实体的类型。注意在每个例子中<< operator 右边的数字是如何不同的,这将保证每个位掩码是独特的且区分于其它代码。
添加如下代码到[self setupInvaders];之前:
self.physicsBody.categoryBitMask = kSceneEdgeCategory;
这一新代码为你的场景的物理实体设置了类别。
添加如下代码到makeShip(也就是在return ship;前面)为你的舰船设置类别:
//1
ship.physicsBody.categoryBitMask = kShipCategory;
//2
ship.physicsBody.contactTestBitMask = 0×0;
//3
ship.physicsBody.collisionBitMask = kSceneEdgeCategory;
以下是对于上述代码的分析:
1.设置舰船的类别
2.不检测舰船和其它物理实体间的碰触
3.检测舰船和场景外缘间的碰撞
注:你不需要设置舰船的collisionBitMask,因为只有你的舰船和场景拥有物理实体。在这种情况下默认的“所有”collisionBitMask已经足够了。因为你将添加物理实体到入侵者中,所以准确设置舰船的collisionBitMask将保证你的舰船只会与场景的边缘发生碰撞而不会与入侵者相撞。
当你做到这些时,你应该为入侵者设置类别,因为这将帮助你检测舰船的子弹与入侵者之间的碰撞。
添加如下代码到makeInvaderOfType:之后,也就是return invader;之前:
invader.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:invader.frame.size];
invader.physicsBody.dynamic = NO;
invader.physicsBody.categoryBitMask = kInvaderCategory;
invader.physicsBody.contactTestBitMask = 0×0;
invader.physicsBody.collisionBitMask = 0×0;
上述代码也明确了舰船发射的子弹,并让Sprite Kit检测舰船发射的子弹与入侵者间的碰触,但是碰撞却应该被忽视。
关照了舰船的子弹后,现在需要转向入侵者的子弹了!
在makeBulletOfType:中添加如下代码到第二个case声明最后面,即break前面:
bullet.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:bullet.frame.size];
bullet.physicsBody.dynamic = YES;
bullet.physicsBody.affectedByGravity = NO;
bullet.physicsBody.categoryBitMask = kInvaderFiredBulletCategory;
bullet.physicsBody.contactTestBitMask = kShipCategory;
bullet.physicsBody.collisionBitMask = 0×0;
该代码与之前的组块相似:它同样也定义了入侵者发射的子弹,并让Sprite Kit去检查入侵者发射的子弹与你的舰船之间的碰触,并且再一次忽视了碰撞元素。
注:为了让碰触检测能够运行,你必须通过设置bullet.physicsBody.dynamic = YES将舰船发射的子弹定义为动态。如果不这么做,Sprite Kit便不会检测这些子弹间的碰触,并且静态入侵者的定义变为invader.physicsBody.dynamic = NO。入侵者之所以是静态的是因为它们并不受物理引擎的影响而移动。Sprite Kit不会检测两个静态实体间的碰触,所以如果你需要检测两个物理实体类别间的碰触,至少一个类别必须具有动态物理实体。
你也许想要知道为什么contactTestBitMask值是对称的。举个例子来说吧,为什么你设置了一个入侵者的contactTestBitMask=0×0但是一个舰船发射的子弹为contactTestBitMask = kInvaderCategory?
原因在于当Sprite Kit检测任何两个物理实体A和B间的碰触时,只有其中一个实体需要宣称它应该测试与其它实体间的碰触,而不是双方。只要A宣称它可以与B进行碰触,或B宣称它能与A相碰触,那么这种碰触便是可检测得到的。我们不需要两个实体同时宣称,它们应该测试与彼此间的碰触。
基于一种实体类型设置contactTestBitMask。你可能更想基于两种实体类型设置contactTestBitMask值,这也没关系,只要你能选择其中一种方法去完成便可。
基于这些改变,你的游戏物理引擎将检测到舰船发射的子弹与入侵者间的碰触,以及入侵者发射的子弹与你的舰船间的碰触。但是物理引擎将如何通知你的游戏这些碰触?
答案便是使用SKPhysicsContactDelegate。
执行物理碰触委托方法
打开GameScene.h并基于如下代码修改@interface:
@interface GameScene : SKScene <SKPhysicsContactDelegate>
这宣称了你的场景将作为物理引擎的委托代表。SKPhysicsContactDelegate的didBeginContact:方法会在每次两个物理实体发生碰触时执行,即基于你是如何设置物理实体的categoryBitMask和contactTestBitMask。一会后你将执行didBeginContact:。
就像快射那样,碰触可以随时发生。所以didBeginContact:可以随时执行。但是按照你的离散时间信号,你只能在update:被调用的时候处理碰触。所以,就像快射那样,你将创造队列去储存碰触,直到它们可以通过update:获得处理。
回到GameScene.m并添加如下新属性到类扩展中:
@property (strong) NSMutableArray* contactQueue;
现在添加如下代码到didMoveToView:的最后,也就是在self.userInteractionEnabled = YES;行后面:
self.contactQueue = [NSMutableArray array];
self.physicsWorld.contactDelegate = self;
这只是初始化了一个空白的碰触队列,并将场景设置为物理引擎的碰触委托。
接下来添加如下方法到#pragma mark – Physics Contact Helpers中:
-(void)didBeginContact:(SKPhysicsContact *)contact {
[self.contactQueue addObject:contact];
}
该方法在你的碰触队列中简单记录了碰触,让你能在之后update:执行时处理。
在同样的位置上添加如下方法:
-(void)handleContact:(SKPhysicsContact*)contact {
//1
// Ensure you haven’t already handled this contact and removed its nodes
if (!contact.bodyA.node.parent || !contact.bodyB.node.parent) return;NSArray* nodeNames = @[contact.bodyA.node.name, contact.bodyB.node.name];
if ([nodeNames containsObject:kShipName] && [nodeNames containsObject:kInvaderFiredBulletName]) {
//2
// Invader bullet hit a ship
[self runAction:[SKAction playSoundFileNamed:@"ShipHit.wav" waitForCompletion:NO]];
[contact.bodyA.node removeFromParent];
[contact.bodyB.node removeFromParent];
} else if ([nodeNames containsObject:kInvaderName] && [nodeNames containsObject:kShipFiredBulletName]) {
//3
// Ship bullet hit an invader
[self runAction:[SKAction playSoundFileNamed:@"InvaderHit.wav" waitForCompletion:NO]];
[contact.bodyA.node removeFromParent];
[contact.bodyB.node removeFromParent];
}
}
该代码相对直接,以下是相关解释:
1.不允许同样的碰触出现两次。
2.如果一个入侵者子弹撞击了你的舰船,将你的舰船和子弹从场景中删除,并播放一个音效。
3.如果舰船撞击了一个入侵者,将入侵者和子弹从场景中删除并播放一个不同的音效。
添加如下代码到#pragma mark – Scene Update Helpers:
-(void)processContactsForUpdate:(NSTimeInterval)currentTime {
for (SKPhysicsContact* contact in [self.contactQueue copy]) {
[self handleContact:contact];
[self.contactQueue removeObject:contact];
}
}
上处代码只是关于碰触队列,面向队列中的每个碰触调用handleContact: 。
添加如下代码到update:最上方去调用你的队列处理器:
[self processContactsForUpdate:currentTime];
创建并运行你的应用,并开始向入侵者发射子弹!
现在,当你的舰船子弹撞击了一个入侵者时,入侵者会从场景中小时,并出现爆炸声。相反地,当入侵者子弹撞击了你的舰船,代码便会将你的舰船从场景中删除,并且也会出现不同的爆炸声。
基于你的相关技能,你可能需要运行几次才能看到入侵者和舰船被摧毁。击中Command R并再次运行。
更新你的视图显示器(HUD)
你的游戏看起来不错,但是它还缺少一些东西。即游戏中没有足够的戏剧性张力。如果你未得到奖励,那么使用子弹撞击入侵者的优势是什么?如果没有惩罚,那么被入侵者撞击到的劣势又是什么?
你可以给予使用舰船子弹撞击入侵者的行为分数点作为奖励,并在玩家被入侵者的子弹击中时减少他们舰船的生命值。
添加如下属性到类扩展中:
@property NSUInteger score;
@property CGFloat shipHealth;
你的舰船的生命值将从100%开始,但是你将按照0到1的范围去储存它。
添加如下代码到setupShip:
self.shipHealth = 1.0f;
上述代码设置了你的舰船初始生命值。
现在你可以在setupHud将如下行:
healthLabel.text = [NSString stringWithFormat:@"Health: %.1f%%", 100.0f];
换成:
healthLabel.text = [NSString stringWithFormat:@"Health: %.1f%%", self.shipHealth * 100.0f];
新一行代码基于你的舰船的实际生命值(而不是100的静态值)设置了初始HUD文本。
接下来添加如下两个方法到#pragma mark – HUD Helpers:
-(void)adjustScoreBy:(NSUInteger)points {
self.score += points;
SKLabelNode* score = (SKLabelNode*)[self childNodeWithName:kScoreHudName];
score.text = [NSString stringWithFormat:@"Score: %04u", self.score];
}-(void)adjustShipHealthBy:(CGFloat)healthAdjustment {
//1
self.shipHealth = MAX(self.shipHealth + healthAdjustment, 0);SKLabelNode* health = (SKLabelNode*)[self childNodeWithName:kHealthHudName];
health.text = [NSString stringWithFormat:@"Health: %.1f%%", self.shipHealth * 100];
}
这些方法非常直接:更新分数和分数标签,更新舰船的生命值和生命值标签。第1点仅仅保证了舰船的生命值不会趋于负数。
最后一步是在游戏过程中的适当时间调用这些方法。用如下更新方案替换handleContact::
-(void)handleContact:(SKPhysicsContact*)contact {
// Ensure you haven’t already handled this contact and removed its nodes
if (!contact.bodyA.node.parent || !contact.bodyB.node.parent) return;NSArray* nodeNames = @[contact.bodyA.node.name, contact.bodyB.node.name];
if ([nodeNames containsObject:kShipName] && [nodeNames containsObject:kInvaderFiredBulletName]) {
// Invader bullet hit a ship
[self runAction:[SKAction playSoundFileNamed:@"ShipHit.wav" waitForCompletion:NO]];
//1
[self adjustShipHealthBy:-0.334f];
if (self.shipHealth <= 0.0f) {
//2
[contact.bodyA.node removeFromParent];
[contact.bodyB.node removeFromParent];
} else {
//3
SKNode* ship = [self childNodeWithName:kShipName];
ship.alpha = self.shipHealth;
if (contact.bodyA.node == ship) [contact.bodyB.node removeFromParent];
else [contact.bodyA.node removeFromParent];
}
} else if ([nodeNames containsObject:kInvaderName] && [nodeNames containsObject:kShipFiredBulletName]) {
// Ship bullet hit an invader
[self runAction:[SKAction playSoundFileNamed:@"InvaderHit.wav" waitForCompletion:NO]];
[contact.bodyA.node removeFromParent];
[contact.bodyB.node removeFromParent];
//4
[self adjustScoreBy:100];
}
}
以下是方法中所发生的改变:
1.当舰船被入侵者的子弹击中时调整它的生命值。
2.如果舰船的生命值为0,将舰船和入侵者的子弹从场景中删掉。
3.如果舰船的生命值大于0,只删除入侵者的子弹。稍微模糊舰船的精灵以暗示破坏的发生。
4.当入侵者遭到撞击时,在分数上添加100个点。
上述内容同样也解释了为什么你将舰船的生命值储存为0至1的数值,尽管你的生命值是从100开始。因为阿尔法值的范围是0至1,你可以使用舰船的生命值作为舰船的阿尔法值去代表累进破坏。这很方便!
再次创建并运行你的游戏;你将看到当子弹撞击一个入侵者时分数会发生改变;同时你也应该看到当舰船遭遇撞击时,它的生命值也会改变,如下图所示:
优化你的入侵者和舰船图像
你已经耐着性子去面对这些红,绿,蓝和洋红色的矩形。保持视觉效果的简单化非常有效,因为它让你能够专注于获得正确的逻辑。
现在你将添加一些实际的图像精灵去创造更加现实化的游戏—-并让它变得更有趣!
用如下代码换掉makeInvaderOfType::
-(NSArray*)loadInvaderTexturesOfType:(InvaderType)invaderType {
NSString* prefix;
switch (invaderType) {
case InvaderTypeA:
prefix = @”InvaderA”;
break;
case InvaderTypeB:
prefix = @”InvaderB”;
break;
case InvaderTypeC:
default:
prefix = @”InvaderC”;
break;
}
//1
return @[[SKTexture textureWithImageNamed:[NSString stringWithFormat:@"%@_00.png", prefix]],
[SKTexture textureWithImageNamed:[NSString stringWithFormat:@"%@_01.png", prefix]]];
}-(SKNode*)makeInvaderOfType:(InvaderType)invaderType {
NSArray* invaderTextures = [self loadInvaderTexturesOfType:invaderType];
//2
SKSpriteNode* invader = [SKSpriteNode spriteNodeWithTexture:[invaderTextures firstObject]];
invader.name = kInvaderName;
//3
[invader runAction:[SKAction repeatActionForever:[SKAction animateWithTextures:invaderTextures timePerFrame:self.timePerMove]]];invader.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:invader.frame.size];
invader.physicsBody.dynamic = NO;
invader.physicsBody.categoryBitMask = kInvaderCategory;
invader.physicsBody.contactTestBitMask = 0×0;
invader.physicsBody.collisionBitMask = 0×0;return invader;
}
以下是新代码的作用:
1.面向每个入侵者类型加载一对精灵图像—InvaderA_00.png和InvaderA_01.png ,并通过它们创造SKTexture对象。
2.使用像纹理等作为精灵的基础图像。
3.在连续的动画循环中赋予这两个图像活力。
所有的图像都包含于开始的项目中,iOS知道如何找到并加载它们,所以在这里你不需要做其它事了。
创建并运行你的应用;你应该看到一些类似如下截图的画面:
看起来很酷吧!接下来你需要将块状的绿色舰船换成较现代的版本。
用如下代码替换makeShip:
-(SKNode*)makeShip {
//1
SKSpriteNode* ship = [SKSpriteNode spriteNodeWithImageNamed:@"Ship.png"];
ship.name = kShipName;
//2
ship.color = [UIColor greenColor];
ship.colorBlendFactor = 1.0f;
ship.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:ship.frame.size];
ship.physicsBody.dynamic = YES;
ship.physicsBody.affectedByGravity = NO;
ship.physicsBody.mass = 0.02;
ship.physicsBody.categoryBitMask = kShipCategory;
ship.physicsBody.contactTestBitMask = 0×0;
ship.physicsBody.collisionBitMask = kSceneEdgeCategory;return ship;
}
这一代码看起来有点不同。
1.你的舰船精灵是根据图像所构思的。
2.最初舰船图像是白色的,就像入侵者的图像。但是代码设置了精灵颜色让图像变为绿色。这有效地将绿色与精灵图像混合在一起。
创建并运行你的游戏;你将看到证实的绿色舰船如下图那样出现:
玩一会你的游戏——你注意到什么?尽管你可以炸毁入侵者,但是却不存在明确的胜利或失败。这并不像太空之战,对吧?
执行最后的游戏
思考你的游戏该如何结束。怎样的情况将引出游戏的结局?
你的舰船的生命值降至0。
你破坏了所有入侵者。
入侵者离地球太近。
现在你将为上述的每个情境添加检测。
首先添加如下代码到#pragma mark – Custom Type Definitions中,在kShipSize:的定义下方:
#define kMinInvaderBottomHeight 2 * kShipSize.height
上述代码定义了入侵者入侵地球的最高点。
接下来添加如下输入内容到#import,即在文件最上方:
#import “GameOverScene.h”
上述代码所输入的名为GameOverScene的场景开头已经呈现在开始项目中。
接下来添加如下新属性到类扩展中:
@property BOOL gameEnding;
这设置了各种游戏结束场景。
现在添加如下两个方法到#pragma mark – Game End Helpers:
-(BOOL)isGameOver {
//1
SKNode* invader = [self childNodeWithName:kInvaderName];//2
__block BOOL invaderTooLow = NO;
[self enumerateChildNodesWithName:kInvaderName usingBlock:^(SKNode *node, BOOL *stop) {
if (CGRectGetMinY(node.frame) <= kMinInvaderBottomHeight) {
invaderTooLow = YES;
*stop = YES;
}
}];//3
SKNode* ship = [self childNodeWithName:kShipName];//4
return !invader || invaderTooLow || !ship;
}-(void)endGame {
//1
if (!self.gameEnding) {
self.gameEnding = YES;
//2
[self.motionManager stopAccelerometerUpdates];
//3
GameOverScene* gameOverScene = [[GameOverScene alloc] initWithSize:self.size];
[self.view presentScene:gameOverScene transition:[SKTransition doorsOpenHorizontalWithDuration:1.0]];
}
}
以下是发生在第一个方法里的情况,即检测游戏是否结束了:
1.获得所有留在场景中的入侵者。
2.循环访问入侵者以检测是否有任何入侵者的高度太低了。
3.为你的舰船获得一个指示器:如果舰船的生命值降至0,玩家便被当成死亡,玩家的舰船将被从场景中删除。在这种情况下,你将获得一个nil值指代场景中没有任何玩家舰船。
4.不管你的游戏是否结束,如果不再有入侵者会出现,或者一名入侵者太低了,或者你的舰船被摧毁了,那么游戏便算是结束了。
第二个方法才真正结束了游戏,并呈现了游戏结束的场景。
1.一次结束游戏。否则你将多次尝试在场景中呈现游戏,而这将是绝对的漏洞。
2.停止加速计的更新。
3.呈现GameOverScene。你可以检测GameOverScene.m的细节,但这是一个带有简单的“游戏结束”信息的基本场景。如果你轻敲它的画场景将开始另一次游戏。
在update中添加如下代码作为代码的第一行内容:
if ([self isGameOver]) [self endGame];
上述内容是为了在每次场景更新时检查游戏是否结束。如果游戏结束了,它便会呈现出游戏结束的场景。
创建并运行,炸毁入侵者直至游戏结束。希望你能在入侵者摧毁你时抢占先机摧毁对方。一旦你的游戏结束,你就会看到类似下图的场景:
轻敲游戏结束场景,你便能够再次游戏!
最后:优化和精确
游戏开发的真实性在于,剩下20%的工作将花费与之前80%的工作同样的时间。当你正致力于自己的下一款游戏时,你最好能够开始快速迭代低保真度的图像资产(游戏邦注:如彩色矩形),如此你便能够快速明确游戏是否足够有趣。
如果基于彩色矩形的游戏并不有趣,那么即使拥有花俏的图像也不可能有趣!你应该先明确游戏玩法和游戏逻辑,然后带着花俏的图像资产与吸引人的音效开始创造。
话虽这么说,在面向App Store发行前优化游戏真的非常必要。App Store是一个拥挤的市场,只有真正的优化才能让你的应用突显于竞争中。尝试着添加一些小动画,故事情节和少许的可爱元素,这将更有效地吸引用户。同样地,如果你所创造的是经典游戏,那就考虑忠实于游戏。
如果你是《太空入侵者》的粉丝,你便会知道自己的游戏再制错过了一个重要元素。在最初游戏中,当入侵者前进速度越快,他们便会越逼近屏幕底部。
这是早前CPU用于运行最初《太空入侵者》游戏时所遇到的情况—-游戏循环速度越快,出现的入侵者数量便会越少,因为每个循环过程将没有多少事做。最终游戏程序员Tomohiro Nishikado决定将这一行为留在游戏中作为一个挑战机制。
你将更新游戏去整合这一游戏机制而迎合怀旧游戏纯粹主义者的需求。
添加如下方法到#pragma mark – Invader Movement Helpers中:
-(void)adjustInvaderMovementToTimePerMove:(NSTimeInterval)newTimePerMove {
//1
if (newTimePerMove <= 0) return;//2
double ratio = self.timePerMove / newTimePerMove;
self.timePerMove = newTimePerMove;[self enumerateChildNodesWithName:kInvaderName usingBlock:^(SKNode *node, BOOL *stop) {
//3
node.speed = node.speed * ratio;
}];
}
让我们检查这一代码:
1.忽视假的数值——少于或等于0的值意味着极快或极慢的移动,这并没有任何意义。
2.设置场景的timePerMove为给定值。这将在moveInvadersForUpdate:中加速入侵者的移动。记录改变的比率从而让你能够相应调整节点的速度。
3.加速入侵者的动画,从而让动画能够更快地在两帧间循环。比率将确保如果每次移动的新时间为旧时间的1/3,那么新动画的速度便是旧动画速度的3倍。设置节点速度以确保所有行动能够快速运行,包括精灵帧之间的动画行动。
现在你需要想办法唤醒这一新方法。
如下修改determineInvaderMovementDirection:
…
case InvaderMovementDirectionDownThenLeft:
proposedMovementDirection = InvaderMovementDirectionLeft;
// Add the following line
[self adjustInvaderMovementToTimePerMove:self.timePerMove * 0.8];
*stop = YES;
break;
case InvaderMovementDirectionDownThenRight:
proposedMovementDirection = InvaderMovementDirectionRight;
// Add the following line
[self adjustInvaderMovementToTimePerMove:self.timePerMove * 0.8];
…
新节点将减少20%入侵者每次移动时间。这将帮助他们提高25%的速度(游戏邦注:4/5的移动时间意味着5/4的移动速度)。
创建并运行你的游戏,并观看入侵者的移动;你将注意到这些入侵者在越靠近屏幕下方时移动速度便会越快:
这是一个快捷的代码改变,即将使你的游戏变得更具挑战性且更加有趣。如果你将从入侵者手中拯救地球,你还是好好表现吧!花些时间去做出这样的细微调整是区别一款好游戏与出色游戏的关键。
接下来要怎么做?
以下是来自Sprite Kit教程的最后示例项目。
我鼓励你去实验自己的《SKInvaders》。玩游戏,做出调整并明确自己可以做什么!分析你的代码,做出修改,如果最终能够看到一个有趣的新功能的诞生,这便会成为许多游戏开发惊喜的一部分。
以下是关于如何调整游戏的相关理念:
在GameOverScene中添加胜利或失败信息
提示:添加属性到能够储存信息的GameOverScene。思考如何在场景中设置属性以及如何将其呈现在屏幕上。
在游戏中添加一个标题场景
提示:添加另一个SKScene子集(称为TitleScene),最初是由你的GameViewController呈现出来。轻敲这一场景将实现在你现有GameScene的转变。
当你的舰船连续使用3枚子弹射击入侵者时添加连胜奖励
提示:添加属性到你的GameScene去追踪射击vs错失:击中时增加,错失时重置为0。当玩家进行连续3次的射击时,呈现一个特殊的“STREAK!”动画并播放一个特别的音效。
当入侵者被子弹击中时为其设置动画
提示:着眼于handleContact:并思考你将如何使用SKAction行动序列去赋予被击中的入侵者动画。这可能会让你的游戏状态变得更复杂?你是否需要将这些入侵者标记为“死亡”如此在未来的碰触或记分时将不再考虑他们,并同时给予他们“死亡动画”,但是不会将其从场景中删除?
添加“boss”入侵者到游戏中,即只能够在屏幕上方呈现水平移动
提示:为这一入侵者的名字和类别添加新的常量。使用一个像素图像工具,如Pixen去绘制你自己的“Boss入侵者”。确保他足够邪恶!着眼于现有的代码并思考你需要在哪里添加代码去管理新的入侵者。
在场景中摆脱节点和FPS调试信息
提示:思考GameScene是何时并且在哪里被创造出来以及初始化的。
添加“玩家生命”功能去替代生命值
提示:现在,你的游戏呈现出了舰船生命值。相反地,让玩家在每次舰船遭遇袭击时失去一个“生命”。当你不再剩下任何舰船时游戏便算结束。你将如何在屏幕上呈现玩家生命?
添加计算机生成的画外音到游戏中
提示:在iOS7中使用一个新功能让你的iPhone传达出你所给予的任何NSString。
添加一个高分列表
提示:追踪玩家分数。在每次游戏后呈现给玩家高分列表。如果你想要进一步推动游戏的个性化,那就让它们能够基于每个分数输入首字母。只在某些区域执行这一方法,并避开Game Center。
在你的舰船和入侵者之间添加防卫盾牌
提示:在最初的游戏中,玩家的舰船上方存在“防卫盾牌”,即能够吸收入侵者的子弹。每个撞击盾牌的子弹将破坏部分盾牌。当太多子弹在同一个领域撞击盾牌时,它将塑造一个通道让子弹能够不受阻碍地穿越过去。这些盾牌是均匀分布的,之间都带有一些缝隙。玩家可以将自己的舰船隐藏在盾牌之下。
基于这一教程系列,你将通过创造一款非常酷的经典游戏而学习到有关Sprite Kit的一些新技巧。
现在你便可以享受新游戏的乐趣了。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦)
How To Make a Game Like Space Invaders with Sprite Kit Tutorial: Part 2
By Joel Shapiro
Note from Ray: This is a brand new Sprite Kit tutorial released as part of the iOS 7 Feast. Enjoy!
Welcome back to our 2-part Sprite Kit tutorial that teaches you how to make a game like Space Invaders!
In the first part, you created the foundation of the game. So far, you’ve added the invaders, your ship, and a Heads Up Display (HUD) to your game. You also coded the logic to make the invaders move automatically and to make your ship move as you tilted your device.
In this second and final part, you’ll add the ability for your ship and the aliens to fire on each other and blow each other up! You’ll also polish your game by adding sound effects and realistic images to replace the colored rectangles that currently serve as place holders for the invaders and your ship.
This tutorial picks up where the first part left off. If you don’t have the project already, you can download the example project where we left things off.
All right, it’s time to blow up some invaders!
Making Your Ship Fire its Laser Cannon
First, think about how you want your scene to fire the ship’s laser cannon. You decided to use single-taps to fire the cannon. But how should you detect these single taps?
You have two obvious choices:
1.UITapGestureRecognizer – Create one and attach it to your scene’s view.
2.touchesEnded:withEvent: – Your scene is a subclass of UIResponder; therefore, you could use this to detect touches directly.
The second approach is the best choice in this situation: touchesEnded:withEvent:. The first approach gets a bit tricky when you need to detect and handle touches differently in the various scene nodes of your game, since you can only specify a single callback selector for the UITapGestureRecognizer on the scene’s view. The extra work to get this working properly just isn’t worth it in this simple case.
Since all SKNode nodes (including SKScene) can handle touches directly via touchesEnded:withEvent:, the second choice is a much more natural approach to handling node-specific touches — and will pay off when you develop games with more complex tap handling.
Now that you’re going to detect user taps in your scene’s touchesEnded:withEvent: method, what should you do inside that method?
Taps can happen at any point during the gameplay. Contrast that with the way your scene changes: — at discrete intervals from within the update: method. So how can you save up taps detected at any time in touchesEnded:withEvent: and process them later in update: when it’s invoked by the Sprite Kit game loop?
The answer is a queue! You’re going to use a simple NSMutableArray to store your taps in a FIFO (First In First Out) queue.
Add the following property to the class extension in GameScene.m:
Now add the following lines to didMoveToView: right after the [self.motionManager startAccelerometerUpdates]; line:
The above code initializes the tap queue to an empty array and ensures that user interactions are enabled for the scene so it can receive tap events.
Now, add the following code right after #pragma mark – User Tap Helpers:
The first three methods are just empty stubs; they’re added because Apple suggests doing so when you override touchesEnded:withEvent: without calling super.
The touchesEnded:withEvent: method itself is fairly simple. It just adds an entry to the queue. You don’t need a custom class to store the tap in the queue since all you need to know is that a tap occurred. Therefore, you can use any old object. Here, you use the integer 1 as a mnemonic for single tap (@1 is the new object-literal syntax that converts the literal 1 into an NSNumber object).
Since you know that the invaders will also eventually fire bullets at your ship, add the following code to the Custom Type Definitions section at the top of the file:
You’re going to use BulletType to share the same bullet code for both invaders and your ship. It appears that you and the invaders shop at the same ammunition stores! :]
Next, add the following code just below #define kHealthHudName @”healthHud”:
Now, add the following method to the Scene Setup and Content Creation section:
This method is relatively straightforward: it simply creates a rectangular colored sprite to represent a bullet and sets the name of the bullet so you can find it later in your scene.
Now, add the following methods to the #pragma mark – Bullet Helpers section:
Going through the code in fireBullet:toDestination:withDuration:soundFileName: step-by-step, you do the following:
1.Create an SKAction that moves the bullet to the desired destination and then removes it from the scene. This sequence executes the individual actions consecutively — the next action only takes place after the previous action has completed. Hence the bullet is removed from the scene only after it has been moved.
2.Play the desired sound to signal that the bullet was fired. All sounds are included in the starter project and iOS knows how to find and load them.
3.Move the bullet and play the sound at the same time by putting them in the same group. A group runs its actions in parallel, not sequentially.
4.Fire the bullet by adding it to the scene. This makes it appear onscreen and starts the actions.
Here’s what you do in fireShipBullets:
1.Only fire a bullet if there isn’t one currently on-screen. It’s a laser cannon, not a laser machine gun — it takes time to reload!
2.Set the bullet’s position so that it comes out of the top of the ship.
3.Set the bullet’s destination to be just off the top of the screen. Since the x coordinate is the same as that of the bullet’s position, the bullet will fly straight up.
4.Fire the bullet!
The decision in //1 to only allow one ship bullet on-screen at the same time is a gameplay decision, not a technical necessity. If your ship can fire thousands of bullets per minute, Space Invaders would be too easy. Part of the fun of your game is choosing your shots wisely and timing them to collide with invaders.
Your laser cannon is almost ready to fire!
Add the following to the Scene Update Helpers section:
Let’s review the above code:
1.Loop over a copy of your tapQueue; it must be a copy because it’s possible that you’ll modify the original tapQueue while this code is running, and modifying an array while looping over it is a big no-no.
2.If the queue entry is a single-tap, handle it. As the developer, you clearly know that you only handle single taps for now, but it’s best to be defensive against the possibility of double-taps (or other actions) later.
3.Remove the tap from the queue.
Note: processUserTapsForUpdate: completely consumes the queue of taps at each invocation. Combined with the fact that fireShipBullets will not fire another bullet if one is already onscreen, this emptying of the queue means that extra or rapid-fire taps will be ignored. Only the first tap needed to fire a bullet will matter.
Finally, add the following code as the first line in update::
This invokes processUserTapsForUpdate: during the update loop and processes any user taps.
Build your game, run, and tap away!
Making Invaders Attack
Awesome, your ship can finally fire on those evil invaders! You’ll have them on the run soon enough.
But you’ve probably noticed that your bullets pass straight through invaders instead of blowing them up. That’s because your bullets aren’t yet smart enough to detect when they’ve hit an invader. You’re going to fix that in a moment.
First, you’ll make the invaders return fire by adding the code below to the Scene Update Helpers section:
The central logic for the above method is as follows:
1.Only fire a bullet if one’s not already on-screen.
2.Collect all the invaders currently on-screen.
3.Select an invader at random.
4.Create a bullet and fire it from just below the selected invader.
5.The bullet should travel straight down and move just off the bottom of the screen.
6.Fire off the invader’s bullet.
Add the following line to the end of update::
This invocation of fireInvaderBulletsForUpdate: starts the invaders firing back at you.
Build, run, and you should see the invaders firing their purple bullets at your ship, as shown in the screenshot below:
As a matter of game design, notice that the invaders’ bullets are purple while your ship’s bullets are green. This strong color contrast makes it easy to see the difference between bullets in the heat of battle. Also, you should hear a different sound when your ship fires versus when an invader fires. The use of different sounds is partly stylistic, to give your game rich audio and make it more immersive. But it’s also partly an accessibility issue since 7 – 10% of men and 0.5% – 1% women are color blind. The differentiation in sound effects will make your game more playable by those who are color blind.
Detecting When Bullets Hit Their Target
With all those bullets flying around on the screen it’s amazing that nothing blows up! That’s because your game has no hit detection. It needs to detect when your ship’s bullets hit an invader — and when an invader’s bullet hits your ship.
You could do this manually, comparing bullet/invader/ship positions at each update: invocation and checking for hits. But why not let Sprite Kit do the work for you?
Since you’re already using physics bodies, Sprite Kit’s physics engine can detect when one body hits another. For this, you’ll use contact detection — not collision detection. You’re not using physics to move bullets or invaders, so you’re not interested in the physical collisions between them. Contact detection merely detects when one physics body overlaps another in space; it doesn’t otherwise move or affect the bodies in contact.
Some games have many distinct types of physics bodies and are not interested in contact between all types of physics bodies. Sprite Kit will only check for contact between those categories of physics bodies that you tell it to check.
This is both a speed optimization and a correctness constraint, as some types of contact may not be desired. Controlling which physics bodies are checked for contact begins by defining category bitmasks.
Add the following code to the Custom Type Definitions section:
These strange-looking constants are bitmasks. A bitmask is basically a way of stuffing multiple on/off variables into a single 32-bit unsigned integer. A bitmask can have 32 distinct values when stored as a u_int32_t. Each of these five categories defines a type of physics body. Notice how the number to the right of the << operator is different in each case; that guarantees each bitmask is unique and distinguishable from the others.
Add the following code to createContent right before the [self setupInvaders]; line:
This new code sets the category for the physics body of your scene.
Add the following code to makeShip right before the return ship; line to set up the categories for your ship:
Here’s the breakdown of the above code:
1.Set the ship’s category.
2.Don’t detect contact between the ship and other physics bodies.
3.Do detect collisions between the ship and the scene’s outer edges.
Note:You didn’t need to set the ship’s collisionBitMask before because only your ship and the scene had physics bodies. The default collisionBitMask of “all” was sufficient in that case. Since you’ll be adding physics bodies to invaders next, setting your ship’s collisionBitMask precisely ensures that your ship will only collide with the sides of the scene and won’t also collide with invaders.
While you’re at it, you should set the category for the invaders since this will help detect collisions between your ship’s bullets and the invaders.
Add the following to the end of makeInvaderOfType: right before the return invader; line:
This code gives your invader a physics body and identifies it as an invader using kInvaderCategory. It also indicates that you don’t want invaders to contact or collide with other entities.
Your next step is to categorize bullets and set their contact and collision masks.
Add the following to the end of the first case statement in makeBulletOfType: right before the break:
The above code identifies ship-fired bullets as such and tells Sprite Kit to check for contact between ship-fired bullets and invaders, but that collisions should be ignored.
That takes care of the ship’s bullets — now on to the invaders’ bullets!
Add the following code block to the end of the second case statement in makeBulletOfType:, just before the break:
This code is similar to the previous block: it identifies invader-fired bullets as such and tells Sprite Kit to check for contact between invader-fired bullets and your ship, and again, ignores the collision aspect.
Note: In order for contact detection to work, the ship-fired bullets must be defined as dynamic by setting bullet.physicsBody.dynamic = YES. If not, Sprite Kit won’t check for contact between these bullets and the static invaders as their definition is invader.physicsBody.dynamic = NO. Invaders are static because they aren’t moved by the physics engine. Sprite Kit won’t check for contact between two static bodies, so if you need to check for contact between two categories of physics bodies, at least one of the categories must have a dynamic physics body.
You may be wondering why the contactTestBitMask values are not symmetrical. For example, why are you setting an invader’s contactTestBitMastk = 0×0 but a ship-fired bullet’s contactTestBitMask = kInvaderCategory?
The reason is that when Sprite Kit checks for contact between any two physics bodies A and B, only one of the bodies needs to declare that it should test for contact with the other, not both. As long as either A declares that it can contact with B, or B declares that it can contact with A, contact will be detected. It’s not necessary for both bodies to declare that they should test for contact with the other.
Setting the contactTestBitMask on only one type of body like you’ve done seems more manageable. You might prefer to set contactTestBitMask values on both types of bodies, and that’s fine, as long as you’re consistent in choosing one approach or the other.
With these changes, your game’s physics engine will detect contact between ship-fired bullets and the invaders, and between invader-fired bullets and your ship. But how does the physics engine inform your game of these contacts?
The answer is to use SKPhysicsContactDelegate.
Implementing the Physics Contact Delegate Methods
Open GameScene.h and modify the @interface line to look like the following:
This declares your scene as a delegate for the physics engine. The didBeginContact: method of SKPhysicsContactDelegate executes each time two physics bodies make contact, based on how you set your physics bodies’ categoryBitMask and contactTestBitMask. You’ll implement didBeginContact: in just a moment.
Much like taps, contact can happen at any time. Consequently, didBeginContact: can execute at any time. But in keeping with your discrete time ticks, you should only process contact during those ticks when update: is called. So, just like taps, you’ll create a queue to store contacts until they can be processed via update:.
Switch back to GameScene.m and add the following new property to the class extension at the top:
Now add the following code to the end of didMoveToView:, right after the self.userInteractionEnabled = YES; line:
This just initializes an empty contact queue and sets the scene as the contact delegate of the physics engine.
Next, add this method to the #pragma mark – Physics Contact Helpers section:
This method simply records the contact in your contact queue to handle later when update: executes.
Still in the same section, add the following method:
This code is relatively straightforward, and explained below:
1.Don’t allow the same contact twice.
2.If an invader bullet hits your ship, remove your ship and the bullet from the scene and play a sound.
3.If a ship bullet hits an invader, remove the invader and the bullet from the scene and play a different sound.
Add the following method to the #pragma mark – Scene Update Helpers section::
The above just drains the contact queue, calling handleContact: for each contact in the queue.
Add the following line to the very top of update: to call your queue handler:
Build and run you app, and start firing at those invaders!
Now, when your ship’s bullet hits an invader, the invader disappears from the scene and an explosion sound plays. In contrast, when an invader’s bullet hits your ship, the code removes your ship from the scene and a different explosion sound plays.
Depending on your playing skill (or lack thereof!), you may have to run a few times to see both invaders and your ship get destroyed. Just hit Command R to run again.
Updating Your Heads Up Display (HUD)
Your game looks good, but it’s lacking a certain something. There’s not much dramatic tension to your game. What’s the advantage of hitting an invader with your bullet if you don’t get credit? What’s the downside to being hit by an invader’s bullet if there’s no penalty?
You’ll rectify this by awarding score points for hitting invaders with your ship’s bullets, and by reducing your ship’s health when it gets hit by an invader’s bullet.
Add the following properties to the class extension:
Your ship’s health starts at 100% but you will store it as a number ranging from 0 to 1.
Add the following line to setupShip as the last line in the method:
The above sets your ship’s initial health.
Now, replace the following line in setupHud:
With this:
The new line sets the initial HUD text based on your ship’s actual health value instead of a static value of 100.
Next, add the following two methods to the #pragma mark – HUD Helpers section:
These methods are fairly straightforward: update the score and the score label, and update the ship’s health and the health label. //1 merely ensures that the ship’s health doesn’t go negative.
The final step is to call these methods at the right time during gameplay. Replace handleContact: with the following updated version:
Here’s what’s changed in the method:
1.Adjust the ship’s health when it gets hit by an invader’s bullet.
2.If the ship’s health is zero, remove the ship and the invader’s bullet from the scene.
3.If the ship’s health is greater than zero, only remove the invader’s bullet from the scene. Dim the ship’s sprite slightly to indicate damage.
4.When an invader is hit, add 100 points to the score.
The above also explains why you store the ship’s health as a value between 0 and 1, even though your health starts at 100. Since alpha values range from 0 to 1, you can use the ship’s health value as the alpha value for for your ship to indicate progressive damage. That’s pretty handy!
Build and run your game again; you should see the score change when your bullets hit an invader; as well, you should see your ship’s health change when your ship is hit, as below:
Polishing Your Invader and Ship Images
You’ve been incredibly patient working with these less-than-menacing red, green, blue and magenta rectangles. Keeping the visuals simple has worked well because it allowed you to focus ruthlessly on getting your game logic correct.
Now you’ll add some actual image sprites to make your game much more realistic — and more fun to play!
Replace makeInvaderOfType: with the following two methods:
Here’s what the new code does:
1.Loads a pair of sprite images — InvaderA_00.png and InvaderA_01.png — for each invader type and creates SKTexture objects from them.
2.Uses the first such texture as the sprite’s base image.
3.Animates these two images in a continuous animation loop.
All of the images were included in the starter project and iOS knows how to find and load them, so there’s nothing left to do here.
Build and run your app; you should see something similar to the screenshot below:
Looks pretty cool doesn’t it? Next, you’ll replace your blocky green ship with a much more retro and stylish looking version.
Replace makeShip with the following:
This code looks a bit different. Here’s what’s going on:
1.Your ship sprite is now constructed from an image.
2.Originally, the ship image is white, just like the invader images. But the code sets the sprite color to make the image green. Effectively this blends the green color with the sprite image.
Build and run your game; you should see your official-looking green ship appear as below:
Play your game for a while — what do you notice? Although you can blast happily away at the invaders, there’s no clear victory or defeat. It’s not much of a space war, is it?
Implementing the End Game
Think about how your game should end. What are the conditions that will lead to a game being over?
Your ship’s health drops to zero.
You destroy all the invaders.
The invaders get too close to Earth.
You’ll now add checks for each of the above conditions.
First, add the following constant to the #pragma mark – Custom Type Definitions section, underneath the definition for kShipSize::
The above defines the height at which the invaders are considered to have invaded Earth.
Next, add the following import to the #import section at the top of the file:
The above imports the header for a scene named GameOverScene which is already present in the starter project.
Next, add the following new property to the class extension:
That sets everything up for the various game over scenarios.
Now, add the following two methods to the #pragma mark – Game End Helpers section:
Here’s what’s happening in the first method, which checks to see if the game is over:
1.Get all invaders that remain in the scene.
2.Iterate through the invaders to check if any invaders are too low.
3.Get a pointer to your ship: if the ship’s health drops to zero, then the player is considered dead and the player ship will be removed from the scene. In this case, you’d get a nil value indicating that there is no player ship.
4.Return whether your game is over or not. If there are no more invaders, or an invader is too low, or your ship is destroyed, then the game is over.
The second method actually ends the game and displays the game over scene. Here’s what the code does:
1.End your game only once. Otherwise, you’ll try to display the game over scene multiple times and this would be a definite bug.
2.Stop accelerometer updates.
3.Show the GameOverScene. You can inspect GameOverScene.m for the details, but it’s a basic scene with a simple “Game Over” message. The scene will start another game if you tap on it.
Add the following line as the first line of code in update::
The above checks to see if the game is over every time the scene updates. If the game is over, then it displays the game over scene.
Build and run; blast away at the invaders until your game ends. Hopefully, you’ll destroy all of the invaders before they destroy you! Once your game ends, you should see a screen similar to the following:
Tap the game over scene and you should be able to play again!
One Last Thing: Polish and Fidelity
It’s a truism of game development that the last 20% of game development takes as long as the first 80%. When you’re working on your next game, it’s a good idea to start out iterating quickly with low-fidelity art assets (e.g. your colored squares) so you can quickly figure out if your game is fun to play.
If it’s not fun to play with colored squares, it’s not going to be fun to play with fancy art work, either! Nail down your gameplay and game logic first, then build out with fancy art assets and cool sound effects.
That being said, it’s essential that you polish your game before releasing it to the App Store. The App Store is a crowded market and spit and polish will distinguish your app from the competition. Try to add little animations, storylines and a dash of cute factor that will delight your users. Also, consider being true to the game if you’re remaking a classic.
If you’re a fan of Space Invaders, you’ll know that your remake is missing one important element. In the original game, the invaders march faster the closer they get to the bottom of the screen.
This was an artifact of the early CPU used to run the first Space Invaders game – the game loop ran faster and faster with fewer invaders because there was less work to do with each loop cycle. The game’s programmer, Tomohiro Nishikado, decided to leave this behavior in the game as a challenging game mechanic.
You’ll update your game to incorporate this game mechanic as well to please the retro gaming purists out there.
Add the following method to the #pragma mark – Invader Movement Helpers section:
Let’s examine this code:
1.Ignore bogus values — a value less than or equal to zero would mean infinitely fast or reverse movement, which doesn’t make sense.
2.Set the scene’s timePerMove to the given value. This will speed up the movement of invaders within moveInvadersForUpdate:. Record the ratio of the change so you can adjust the node’s speed accordingly.
3.Speed up the animation of invaders so that the animation cycles through its two frames more quickly. The ratio ensures that if the new time per move is 1/3 the old time per move, the new animation speed is 3 times the old animation speed. Setting the node’s speed ensures that all of the node’s actions run more quickly, including the action that animates between sprite frames.
Now, you need something to invoke this new method.
Modify determineInvaderMovementDirection as indicated by comments below:
The new code simply reduces the time per move by 20% each time the invaders move down. This increases their speed by 25% (4/5 the move time means 5/4 the move speed).
Build and run your game, and watch the movement of the invaders; you should notice that those invaders move faster and faster as they get closer to the bottom of the screen:
This was a quick and easy code change that made your game that much more challenging and fun to play. If you’re going to save the Earth from invading hordes, you might as well do it right! Spending time on seemingly minor tweaks like this is what makes the difference between a good game and a GREAT game.
Where to Go From Here?
Here is the final example project from this Sprite Kit tutorial.
I encourage you to experiment with your SKInvaders game. Play with it, tweak it and see what you can do! Breaking your code, fixing it, then finally seeing an awesome new feature come to life is one of the many thrills of game development.
If you want a few ideas for how to tweak your game, here are some:
Add a victory or defeat message to your GameOverScene
Hint: Add a property to GameOverScene that stores a message. Think about how to pass/set that property to the scene and how you would display it on-screen.
Add a title scene to your game
Hint: Add another SKScene subclass (call it TitleScene) that is displayed first by your GameViewController. Tapping this scene should transition in your existing GameScene.
Add a streak bonus when your ship hits invaders with three bullets in a row
Hint: Add a property to your GameScene to track hits vs misses: increment it for hits, reset to zero for misses. When the player gets three consecutive hits, show a special “STREAK!” animation and play a special sound.
Animate invaders when hit by your bullets
Hint: Take a look at handleContact: and think about how you could use a sequence of SKAction actions to animate the invader that was hit. How might this complicate your game state? Would you need to mark these invaders as “dead” so that they would no longer be considered for future contacts or scoring while they were doing their “death animation”, but not yet removed from the scene?
Add a “boss” invader that only moves horizontally across the top of the screen
Hint: Add new constants for this invader’s name and category. Use a pixel art tool such as Pixen to draw your own “Boss Invader”. Make him mean! Think about where you need to add code to manage this new invader by looking at existing code that adds existing invaders and go from there.
Get rid of the nodes and FPS debug info in the scene
Hint: Think about where and when the GameScene was created and initialized.
Add a “player lives” feature instead of health
Hint: Currently, your game shows ship health. Instead, make the player lose a “life” each time their ship gets hit. End your game when they have no ships remaining. How will you show player lives on the screen?
Add a computer-generated voiceover to your game
Hint: Use a new feature in iOS 7 that makes your iPhone utter any NSString that you give it.
Add a High Score List
Hint: Keep track of player scores. Show players the high score list after each game. If you want to personalize your game further, allow them to enter their initials with each score. Do this locally without Game Center.
Add defense shields between your ship and the invaders
Hint: In the original game, there were “defensive shields” above the player’s ship that could absorb invader bullets. Each bullet that hit the shield would destroy part of the shield. As enough bullet hit a shield in the same area, channels would be carved through it that allowed bullets to pass through unhindered. The shields were evenly-spaced with gaps between them. The player could hide his or her ship underneath these shields.
Through this tutorial series you learned some new tricks about Sprite Kit by building a very cool classic game along the way. If you want to learn how to build more fun games like this, check out our brand new book iOS Games by Tutorials, where you’ll learn how to make five complete games from scratch!
Enjoy playing your new game; I look forward to hearing from you with comments below or questions in the forums!(source:raywenderlich)
上一篇:中等硬核游戏的成功要素之留存率
下一篇:写给游戏行业新人的入门告诫