在代码中怎样使用?
首先,为不同的物体类别创建常量。在 MyScene.m 中的常量声明语句后加入下列语句:
static const uint32_t ballCategory = 0x1 << 0; // 00000000000000000000000000000001 static const uint32_t bottomCategory = 0x1 << 1; // 00000000000000000000000000000010 static const uint32_t blockCategory = 0x1 << 2; // 00000000000000000000000000000100 static const uint32_t paddleCategory = 0x1 << 3; // 00000000000000000000000000001000 |
上述代码定义了 4 种物体类型。首先将字节(32位)的末位设置为1,其他位设置为0。然后通过左移操作符<< 将 1 不停往左移。这样,每个类型常量都只有一个位被置为1,对于每一个常量来说,它的 1 的位置都是不同的。
目前你只用到两个类别,地(屏幕底部)和小球。不久后你会用到其他类别,如果必要,你还可以扩展更多的类别常量。
定义好类别常量后,你可以创建一个物体,让它围在屏幕的底部。尝试独立完成这个任务,这和我们曾经创建过的围住屏幕四周的笼子是一样的。(将该物体的节点命名为 bottom,最终我们再来配置这个节点)
隐含内容:创建一个edge-based 状物体盖在屏幕底部 |
|
在 MyScene.m 的 initWithSize: 方法中加入:
CGRect bottomRect = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, 1); SKNode* bottom = [SKNode node]; bottom.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:bottomRect]; [self addChild:bottom]; |
万事俱备,只欠东风。让我们开始第一次亲密接触。首先,在 initWithSize 方法中设置地面、球和球拍的 categoryBitMasks。
bottom.physicsBody.categoryBitMask = bottomCategory; ball.physicsBody.categoryBitMask = ballCategory; paddle.physicsBody.categoryBitMask = paddleCategory; |
代码很简单。将你早先创建的常量赋给相应物体的categoryBitMask 掩码。
接着,用以下代码( initWithSize: 方法中)设置contactTestBitMask 掩码:
ball.physicsBody.contactTestBitMask = bottomCategory; |
这里,我们仅仅关心什么时候球和地面发生接触。因此将 contactTestBitMask设置为 bottomCategory。
然后创建一个 SKPhysicsContactDelegate。由于这只是一个简单游戏,可以用 MyScene 作为所有接触的委托。
在 MyScene.h 中找到:
@interface MyScene : SKScene |
一句,修改为:
@interface MyScene : SKScene |
采用比较正式的描述:MyScene 是一个SKPhysicsContactDelegate ,它将接受所有(配置好的)物体的碰撞通知!
现在将 physicsWorld 的 delegate 设置为 MyScene。在MyScene.m 的 initWithSize: 方法中加入:
self.physicsWorld.contactDelegate = self; |
设置SKPhysicsContactDelegate
最后,实现 didBeginContact: 方法处理碰撞。在 MyScene.m中增加方法:
-(void)didBeginContact:(SKPhysicsContact*)contact { // 1 创建两个物体的局部变量 SKPhysicsBody* firstBody; SKPhysicsBody* secondBody; // 2 始终把类别代码较小的物体赋给firstBody变量 if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) { firstBody = contact.bodyA; secondBody = contact.bodyB; } else { firstBody = contact.bodyB; secondBody = contact.bodyA; } // 3 对球和地接触的碰撞进行处理 if (firstBody.categoryBitMask == ballCategory && secondBody.categoryBitMask == bottomCategory) { //TODO: 将打印语句替换为游戏结束界面 NSLog(@"Hit bottom. First contact has been made."); } } |
这段代码可能有点绕,让我们来过一遍代码。
- 创建两个局部变量,用于指向碰撞发生时所涉及到的两个物体。
- 判断两个物体其中哪一个的 categoryBitmask 值更小,并将它们赋给两个局部变量,其中 categoryBitmask 较小的一个始终要赋给 firstBody 变量(实际上是对二者进行排序)。当两个指定物体间的碰撞进行处理时,这样做(排序)会节省一些工作。
- 由于前面排序过的缘故,现在你只需判断 firstBody 是否为球,secondBody 是否为地即可知道二者间是否发生碰撞。因为很显然,这种情况是不存在的: firstBody 是地而 secondBody 是球(我们在定义常量时,ballCategory>bottomCategory)。然后简单输出一个打印语句。
让我们来看看效果。编译运行程序,当球拍没有接住小球而让球落到了地上,则控制台中会输出消息:
第一次亲密接触:)
创建游戏结束界面
很不幸,当玩家输掉游戏时不可能看到控制台消息。因此,你需要用一个图形化的界面来告诉玩家。这样,我们需要专门为游戏结束创建一个场景。
点击菜单 File\New\File…,选择iOS\CocoaTouch\Objective-C class 模板,点击Next。类命名为GameOverScene, 继承自 SKScene,一次点击 Next, Create。
打开 GameOverScene. h 在@end 语句前加入:
-(id)initWithSize:(CGSize)size playerWon:(BOOL)isWon; |
这个初始化方法多了一个参数,用于表示玩家是输还是赢。这个场景可以既用于游戏胜利也用可用于游戏失败。非常省事 :)
将 GameOverScene.m 修改为如下代码:
#import "GameOverScene.h" #import "MyScene.h" @implementation GameOverScene -(id)initWithSize:(CGSize)size playerWon:(BOOL)isWon { self = [super initWithSize:size]; if (self) { SKSpriteNode* background = [SKSpriteNode spriteNodeWithImageNamed:@"bg.png"]; background.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2); [self addChild:background]; // 1 SKLabelNode* gameOverLabel = [SKLabelNode labelNodeWithFontNamed:@"Arial"]; gameOverLabel.fontSize = 42; gameOverLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); if (isWon) { gameOverLabel.text = @"Game Won"; } else { gameOverLabel.text = @"Game Over"; } [self addChild:gameOverLabel]; } return self; } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { MyScene* breakoutGameScene = [[MyScene alloc] initWithSize:self.size]; // 2 [self.view presentScene:breakoutGameScene]; } @end |
这些代码太常见了,所以我只对有注释的部分进行介绍:
- 创建一个 Label 用于显示胜利或失败信息。SKLabelNode 通常用于以设备字体显示文本信息。
注意:要想知道设备上安装了哪些字体,可以使用这段代码:
NSArray*familyNames = [UIFont familyNames];
for(NSString *familyName in familyNames ){
printf( "Family: %s \n",[familyName UTF8String] );
NSArray *fontNames =[UIFont fontNamesForFamilyName:familyName];
for( NSString *fontName infontNames ){
printf( "\tFont: %s \n", [fontName UTF8String] );
}
}
- 当用户触摸 game over 窗口,我们再次显示游戏开始界面,以便玩家可以重新开始游戏。
来试一下新场景的使用。在 MyScene.m 顶部加入导入语句:
#import "GameOverScene.h" |
然后在 didBeginContact:方法中将 NSLog 语句替换为:
GameOverScene* gameOverScene = [[GameOverScene alloc] initWithSize:self.frame.size playerWon:NO]; [self.view presentScene:gameOverScene]; |
编译运行程序,进行游戏,当球拍没接住球时:
不错,很有成就感吧!但搞笑的是,这个游戏怎样才能获胜?
加入砖块——并将它们击飞
Adding Some Blocks – and They Are Gone…
在 MyScene.m 的 initWithSize 方法中加入一些砖块:
// 1 局部变量声明 int numberOfBlocks = 3; int blockWidth = [SKSpriteNode spriteNodeWithImageNamed:@"block.png"].size.width; float padding = 20.0f; // 2 计算 xOffset float xOffset = (self.frame.size.width - (blockWidth * numberOfBlocks + padding * (numberOfBlocks-1))) / 2; // 3 创建砖块并加入游戏场景 for (int i = 1; i <= numberOfBlocks; i++) { SKSpriteNode* block = [SKSpriteNode spriteNodeWithImageNamed:@"block.png"]; block.position = CGPointMake((i-0.5f)*block.frame.size.width + (i-1)*padding + xOffset, self.frame.size.height * 0.8f); block.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:block.frame.size]; block.physicsBody.allowsRotation = NO; block.physicsBody.friction = 0.0f; block.name = blockCategoryName; block.physicsBody.categoryBitMask = blockCategory; [self addChild:block]; } |
这段代码创建了3 块砖块,以固定距离间隔并居于屏幕中轴。
- 局部变量声明,例如砖块数量以及砖块的宽度。
- 计算 x 偏移。即屏幕左边与第一块砖之间的间隔。用屏幕宽度减去三块砖的宽度和砖块间的空格,再除以2来得到。
- 创建砖块,用 blockWidth、padding 和 xOffset 设置每块砖的物理属性和位置。
加入砖块后的效果。编译运行程序。
游戏一开始,砖块井然有序。