这次我们利用Box2D物理引擎来制作一款类似于下楼梯的小游戏,关于Box2D物理引擎的介绍,可以参考我博客中的Box2D v2.3.0 用户指南进行学习。
我们先来看一下最终效果:
游戏的组成元素有:
滚动背景(左右两个宽度为15,与屏幕等高的部分),上升的台阶(或者说是楼梯),滚动的球,分数。
滚动背景的制作我们不做介绍了,制作方法可以参考FlaggyBird的滚动背景的制作方法(手把手教你制作那个风靡的flappy bird小游戏),背景大部分面积填充的是深蓝色(静止),用平铺的方法平铺两个小的纹理:
我们先来介绍使用Box2D来创建台阶:
首先创建StepBlock类,注意我们在创建类之后默认会创建出.h和.m文件,因为Box2D是基于C++的框架,所以需要我们手动将.m文件的扩展名更改为.mm。StepBlock类声明如下:
#import "CCNode.h"
#import "CCPhysicsSprite.h"
#import "Box2D.h"
#import "TagDefinitions.h"
@interface StepBlock : CCNode{
float screenWidth;
float screenHeight;
CCPhysicsSprite* shape;
b2Body* body;
int tagSave;
}
- (id)initWithY:(float) y;
- (id)initWithPos:(CGPoint) pos andType:(StepBlockTypes)type;
- (void)resetWithY:(float) yPos;
- (BOOL)moveUp:(float) length lowestStepY:(float) lowestStepY;
- (void)resetWithPos:(CGPoint) pos andType:(StepBlockTypes) type;
@end
类中我们定义了screenHeight和screenWidth用来存放屏幕长宽,避免反复获取影响效率。同时我们定义了shape,来存储StepBlock(后面简称Block)的图形(CCPhysicsSprite和CCSprite用法相同,只不过可以指定b2Body作为其成员,这里我们也可以使用CCSprite,不影响效果)。另外又定义了一个body对象,用来存储StepBlock的刚体对象。tagSave我们后面会用到,这里先忽略这个成员。
另外我们定义了一系列的方法,initWithY用来初始化StepBlock,所以这个初始化方法只需要指定y值即可。initWithPos:andType:方法用一个确定的初始位置和Block类型来初始化,这里Block类型我们接下来会介绍,resetWithY方法重置Block的类型和位置,moveUp用来将Block上移,resetWithPos:andType:用确定的位置和Block类型来重置Block。我们看到头文件中的“TagDefinitions.h”,这个头文件中包含了下面的枚举定义和预编译指令:
#ifndef Steps_TagDefinitions_h
#define Steps_TagDefinitions_h
#define PTM_RATIO 100
typedef enum{
StepBlockNormal = 0,
StepBlockConcave,
StepBlockConvex,
StepBlockHighLow,
StepBlockLowHigh,
StepBlockRough,
StepBlockSlideLeft,
StepBlockSlideRight
} StepBlockTypes;
typedef enum {
ForceNone,
ForceLeft,
ForceRight
} ForceType;
#endif
这里PTM_RATIO定义了Box2D中“米”和“像素”的转化比例,StepBlockTypes定义了Block类型的枚举,ForceType定义了对小球的施力方向枚举,我们后面会用到。
下面是用到的不同类型Block的贴图:
step_high_low.png:
step_low_high.png:
step_concave.png:
step_normal.png:
step_rough.png:
step_slide_left.png:
step_slide_right.png:
step_convex.png:
将这些图片导入到工程资源中。接着我们来看StepBlock.mm中的类定义。首先定义一个静态数组,用来存放Block的类型名:
static NSArray* stepBlockTypes = [NSArrayarrayWithObjects:@"step_normal", @"step_concave",@"step_convex",
@"step_high_low",@"step_low_high", @"step_rough",@"step_slide_left", @"step_slide_right", nil];
接着定义下面的方法,用来设置Block的精灵对象:
- (void)initShape:(NSString*) shapeName{
shape = [CCSprite spriteWithFile:shapeName];
[self addChild:shape];
}
下面的方法用来初始化screenHeight和screenWidth属性:
- (void)initProps{
//screen dimensions
CGSize screenSize = [[CCDirector sharedDirector]winSize];
screenHeight = screenSize.height;
screenWidth = screenSize.width;
}
接着我们定义resetWithPos:andType:方法:
- (void)resetWithPos:(CGPoint) pos andType:(StepBlockTypes) type{
float xPos = pos.x;
xPos = max(65,xPos); // 15(wall width) + 50(half the step block width)
xPos = min(xPos, screenWidth -65); // 65 = 100(step block width) + 15(wall width) - 50(halfthe step block width)
if (body) {
[[GameLayer gameLayer]sharedWorld]->DestroyBody(body);
}
b2BodyDef bodyDef;
bodyDef.type = b2_staticBody;
bodyDef.userData = [NSNumbernumberWithInteger:tagSave];
body = [[GameLayer gameLayer]sharedWorld]->CreateBody(&bodyDef);
[self generateShapeAndFixture:ccp(xPos, pos.y)withType:type];
}
在计算xPos的时候我们进行了一些判断,防止Block的位置超出左右的墙壁。并且由于我们在reset的时候会随机更换Block的类型,所以会先将body清除掉(destroy),但是第一次初始化的时候body并没有定义,所以添加了if语句进行了判断。这里用到的GameLayer类是游戏的主场景(CCScene)的层(CCLayer),我们下面会介绍,sharedWorld是我们这个游戏里的世界对象。在清除了body对象后,我们用b2BodyDef来定义body,定义的类型是静态物体(staticBody),因为我们不需要Block像小球一样受力运动,他们只需要匀速上升即可。接着我们定义了body的userData,将tagSave存储进去,这个值在我们后面记分(score)的时候会用到。接着使用CreateBody来创建刚体,然后调用generateShapeAndFixture:withType:方法。方法定义如下:
- (void)generateShapeAndFixture:(CGPoint) pos withType:(int) type{
NSString *shapeName = [stepBlockTypesobjectAtIndex:type];
shape.texture = [[CCTextureCachesharedTextureCache] addImage:[NSString stringWithFormat:@"%@.png",shapeName]];
shape.position = pos;
[[GB2ShapeCache sharedShapeCache]addFixturesToBody:body forShapeName:shapeName];
shape.anchorPoint = ccp(0.5f, 0.5f);
b2Vec2 newPos;
newPos.Set(pos.x/PTM_RATIO, pos.y/PTM_RATIO);
body->SetTransform(newPos, 0);
}
这个方法刷新了形状的纹理(texture),然后使用GB2ShapeCache类的addFixturesToBody:forShapeName:来初始化物体的fixture。GB2ShapeCache类可以在PhysicsEditor(简称PE)的dmg文件中的“\Loaders\generic-box2d-plist”路径下找到,关于PE的用法,请参考手把手教你使用PhysicsEditor来辅助制作Box2D刚体。将PE中创建导出的plist文件导入到Box2D工程中,使用下面的语句导入:
[[GB2ShapeCache sharedShapeCache]addShapesWithFile:@"steps-elements.plist"];
其中“steps-elements.plist”为PE生成的文件。
SetTransform方法用来重置物体的位置和角度,第一个参数是位置,第二个是角度,这里Block为水平放置,所以角度为0。
定义好之后,我们继续添加下面两个reset方法,只是参数上有所不同,复用上面的方法:
- (void)resetWithY:(float) yPos{
float xPos = CCRANDOM_0_1() * (screenWidth -100); // 100 is the width of the step block
[self resetWithPos:ccp(xPos, yPos)];
}
- (void)resetWithPos:(CGPoint) pos{
int type = rand() % 8;
[self resetWithPos:posandType:(StepBlockTypes)type];
}
最后,我们定义好Block的初始化方法:
- (id)initWithY:(float) y{
if (self = [super init]) {
[self initProps];
[selfinitShape:@"step_normal.png"];
[self resetWithY:y];
}
return self;
}
- (id)initWithPos:(CGPoint)pos andType:(StepBlockTypes)type{
if (self = [super init]) {
[self initProps];
NSString *shapeName =[NSString stringWithFormat:@"%@.png", [stepBlockTypesobjectAtIndex:(int)type]];
[selfinitShape:shapeName];
[self resetWithPos:posandType:type];
}
return self;
}
这里我们还差一个moveUp的方法,定义如下:
- (BOOL)moveUp:(float) length lowestStepY:(float) lowestStepY{
float y = shape.position.y + length;
float x = shape.position.x;
BOOL isReallyMovedUp = true;
if (y > screenHeight + 15) {
y = lowestStepY;
isReallyMovedUp = false;
[self resetWithY:y];
}
shape.position = ccp(x, y);
b2Vec2 newPos;
newPos.Set(x/PTM_RATIO, y/PTM_RATIO);
body->SetTransform(newPos, 0);
return isReallyMovedUp;
}
使用SetTransform方法来移动刚体的位置,注意的一点是,当Block移动到屏幕外时,并不清理掉这个Block,而是重置Block并将其移动到最下方的Block的下面去,lowestStepY即是最下方的Block的y坐标。返回值isReallyMovedUp用来标记Block是否被重置到了最下方。
到这里StepBlock类就完成了定义。