(译)使用cocos2d和box2d制作简单的platformer游戏
我收到许多网友的请求,问能不能写一个简单的超级玛利platfomer游戏教程。因为我是守承诺的人,所以这篇教程就此问世了。当然,本教程离一个完整的玛利奥游戏还相差甚远,但是,我希望它至少能给你一些启发。本教程建立在上一篇教程的基础之上,所以,在继续之前,建议你先看完上一篇教程。
内容大纲?
我将在本教程中覆盖下面的内容:
- 简单的面向对象游戏设计
- 用户输入和touch检测
- 给游戏对象施加力,使之移动和跳跃
- 碰撞检测
简单的面向对象设计
这个教程是个非常简单的平台游戏,只有一些简单的平台和一个圆形的主角。说明一下,我在这里使用的是上一个教程中的TileMap编辑器来制作游戏地图的,所以,你可以参照上一篇教程。本游戏示例中只有两种对象。一种是player对象,另一种是platform对象。这两种对象都继承至GameObject类。而GameObject类又继承至CCSprite类,同时它包含一个type属性,用来区分不同的游戏对象。把游戏对象都继承至同一基类有一个好处,就是在做碰撞检测的时候,可以直接强制转换过来。
下面是GameObject类的代码,它从CCSprite类继承过来的。同时把它的type初始化为kGameObjectNone。(这是一个枚举类型,它定义在Constants.h中,本教程不会显示它的实现,但是大家可以在后面下载的源码中找到其实现)
// GameObject.h
#import "cocos2d.h"
#import "Constants.h"
@interface GameObject : CCSprite {
GameObjectType type;
}
@property (nonatomic, readwrite) GameObjectType type;
@end
////////////////////////////////////////////////////////
// GameObject.m
@implementation GameObject
@synthesize type;
- (id)init
{
self = [super init];
if (self) {
type = kGameObjectNone;
}
return self;
}
- (void)dealloc
{
[super dealloc];
}
@end
接下来是我们的Player类。注意,这里Player类的后缀是.mm。因为我们将使用box2d,而box2d是c++写的,所以必须如此。如果你没有这样做的话,那么编译的时候可能会出现上百个莫名其妙的错误。确保文件后缀名没有错误之后,我们把player的type设置为kGameObjectPlayer。后面碰撞检测的时候你就会看到type的用途了。
Player主要负责创建它自己的box2d世界中的body,当你调用createBox2dObject的时候,就会创建相应的body,同时把它加到world中去,world是传参进来的。然后,我们把playerBodyDef.userData设置为self,这样的话,我们在box2d的contact listener中就可以强制转换了。同时,player类还包含使player往右移动和跳跃的方法,我们会在教程的后面讨论。
// Player.h
#import "cocos2d.h"
#import "Box2D.h"
#import "GameObject.h"
@interface Player : GameObject {
b2Body *body;
}
-(void) createBox2dObject:(b2World*)world;
-(void) jump;
-(void) moveRight;
@property (nonatomic, readwrite) b2Body *body;
@end
///////////////////////////////////////////////////////
// Player.mm
#import "Player.h"
#import "Constants.h"
@implementation Player
@synthesize body;
- (id) init {
if ((self = [super init])) {
type = kGameObjectPlayer;
}
return self;
}
-(void) createBox2dObject:(b2World*)world {
b2BodyDef playerBodyDef;
playerBodyDef.type = b2_dynamicBody;
playerBodyDef.position.Set(self.position.x/PTM_RATIO, self.position.y/PTM_RATIO);
playerBodyDef.userData = self;
playerBodyDef.fixedRotation =true;
body = world->CreateBody(&playerBodyDef);
b2CircleShape circleShape;
circleShape.m_radius =0.7;
b2FixtureDef fixtureDef;
fixtureDef.shape =&circleShape;
fixtureDef.density =1.0f;
fixtureDef.friction =1.0f;
fixtureDef.restitution =0.0f;
body->CreateFixture(&fixtureDef);
}
-(void) moveRight {
b2Vec2 impulse = b2Vec2(7.0f, 0.0f);
body->ApplyLinearImpulse(impulse, body->GetWorldCenter());
}
-(void) jump {
b2Vec2 impulse = b2Vec2(4.0f, 15.0f);
body->ApplyLinearImpulse(impulse, body->GetWorldCenter());
}
@end
至于platform对象,对于本例来说,其它没有必要单独创建一个类。为了方便起见,我们只是分配一个GameObject对象,然后把它的type设置为kGameObjectPlatform(后面你会在GameScene的makeBox2dObjectAt方法中找到)
用户输入 / Touch检测
下面两行代码定义在GameScene的init方法中,主要是用来激活touch输入的。swallowTouches的作用就是使当前层的touch事件不会传递给它下面的层。
// Inside GameScene's init method
self.isTouchEnabled = YES;
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
下面的方法是在你鼠标点击模拟器或者手触摸设备屏幕的时候被调用的,也就是touch事件啦。我们需要把touch坐标点转换成cocos2d的坐标点,通过调用convertToGL方法,一定要记得调用这个方法。否则,坐标会不对。因为[touch locationInView:[touch view]]返回的是UIKit里面的坐标系的点,左上角是原点。然后,我们判断,当单击的点在左半屏幕的时候,就让player往右移动。当单击右半边屏幕的时候,就让player跳起来。可以跳到platfrom上哦,呵呵。如果在下面起跳,会撞到头。这可能不是我们想要的结果。如果想让玩家从下面也可以跳上platform,可以参考box2d的testBed里面的one-side platform示例。
// GameScene.mm
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView:[touch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
if (location.x <= screenSize.width /2) {
[player moveRight];
} else {
[player jump];
}
return TRUE;
}
-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
CCLOG(@"Touch ended");
}
给游戏对象施加力,使之移动和跳跃
下面的代码其实很简单。想让玩家往右移,那么就提供一个向右的力。想让玩家往上跳,就提供一个往右上方的力。这里使用的是ApplyLinearImpulse,关于这个函数的信息,大家可以参考box2d手册。
-(void) moveRight {
b2Vec2 impulse = b2Vec2(7.0f, 0.0f);
body->ApplyLinearImpulse(impulse, body->GetWorldCenter());
}
-(void) jump {
b2Vec2 impulse = b2Vec2(4.0f, 15.0f);
body->ApplyLinearImpulse(impulse, body->GetWorldCenter());
}
碰撞检测
对于这个简单的例子来说,物理世界中只有两种类型的对象(player和platform)。当player碰撞到platform的时候,我们怎么样才能知道呢?box2d提供一个碰撞侦听器类,叫做b2ContactListener类。你只需要从这个类派生,然后覆盖其中的虚函数。然后把这个侦听器注册给box2d的world对象,那么当游戏里面的对象发生碰撞的时候,就会回调你自己覆盖的b2dContactListener类中的函数。主要是BeginContact和EndContact函数。然后,你就可以在这两个函数中做碰撞处理了。
BeginContact函数是两个对象发生碰撞的时候被回调的,而EndContact是停止碰撞时被回调的。这两个函数的参数里面都有一个b2Contact对象,它可以用来提取用户自定义的数据。就是之前我们赋值给bodydef的userData中的数据。只需要通过b2Contact->GetFixtureA或者b2Contact->GetFixtureB得到fixture,然后再通过fixture->GetBody->GetUserData()就ok了。由于我们所有的对象都继承至GameObject,所以,我们可以直接用GameObject来强制转换userData。但是,假如你的游戏中userData的类型不一致的话,那就有点麻烦了。最好是统一,不然的话,你想想也会知道,强制转换之后,由于类型不统一,最后肯定会出错。
为了使代码整洁,便于阅读,我在这里定义了两个宏IS_PLAYER(x,y) 和 IS_PLATFORM(x,y)。用这两个宏可以来判断哪个对象是player,哪个对象是platform了。在本例中,当发生碰撞以后,我们只是输出一条语句。但是,在实际的游戏开发过程中,你可以播放音效和动画。还有,如果你想根据碰撞删除某个body的话,切记一定不能在BeginContact和EndContact函数里面做。因为box2d不允许在一个回调中修改物理世界。那样会导致程序挂掉,或者未定义的行为。对于缓存要删除的body,然后在回调之外删除的具体做法,可以参考本博客的其它物理教程。
// ContactListener.h
#import "Box2D.h"
class ContactListener : public b2ContactListener {
public:
ContactListener();
~ContactListener();
virtualvoid BeginContact(b2Contact *contact);
virtualvoid EndContact(b2Contact *contact);
virtualvoid PreSolve(b2Contact *contact, const b2Manifold *oldManifold);
virtualvoid PostSolve(b2Contact *contact, const b2ContactImpulse *impulse);
};
/////////////////////////////
// ContactListener.mm
#import "ContactListener.h"
#import "Constants.h"
#import "GameObject.h"
#define IS_PLAYER(x, y) (x.type == kGameObjectPlayer || y.type == kGameObjectPlayer)
#define IS_PLATFORM(x, y) (x.type == kGameObjectPlatform || y.type == kGameObjectPlatform)
ContactListener::ContactListener() {
}
ContactListener::~ContactListener() {
}
void ContactListener::BeginContact(b2Contact *contact) {
GameObject *o1 = (GameObject*)contact->GetFixtureA()->GetBody()->GetUserData();
GameObject *o2 = (GameObject*)contact->GetFixtureB()->GetBody()->GetUserData();
if (IS_PLATFORM(o1, o2) && IS_PLAYER(o1, o2)) {
CCLOG(@"-----> Player made contact with platform!");
}
}
void ContactListener::EndContact(b2Contact *contact) {
GameObject *o1 = (GameObject*)contact->GetFixtureA()->GetBody()->GetUserData();
GameObject *o2 = (GameObject*)contact->GetFixtureB()->GetBody()->GetUserData();
if (IS_PLATFORM(o1, o2) && IS_PLAYER(o1, o2)) {
CCLOG(@"-----> Player lost contact with platform!");
}
}
void ContactListener::PreSolve(b2Contact *contact, const b2Manifold *oldManifold) {
}
void ContactListener::PostSolve(b2Contact *contact, const b2ContactImpulse *impulse) {
}
然后你需要在world里面注册contact listener。
contactListener =new ContactListener();
world->SetContactListener(contactListener);
Wrap-up and Loose Ends
下面是GameScene类的实现。这里面使用的技术大家应该都接触过了,如果不清楚,可以参考我翻译的其它教程,或者在下方留言。可能需要指出来的是碰撞检测的代码,大家可以花点时间看看。
// GameScene.h
#import "cocos2d.h"
#import "Box2D.h"
#import "GLES-Render.h"
#import "ContactListener.h"
@class Player;
@interface GameScene : CCLayer
{
CGSize screenSize;
b2World* world;
GLESDebugDraw *m_debugDraw;
CCTMXTiledMap *tileMapNode;
Player *player;
ContactListener *contactListener;
}
+(id) scene;
@end
///////////////////////////////////////////////
// GameScene.mm
#import "GameScene.h"
#import "Constants.h"
#import "Player.h"
#import "GameObject.h"
@interface GameScene(Private)
-(void) setupPhysicsWorld;
@end
@implementation GameScene
+(id) scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// 'layer' is an autorelease object.
GameScene *layer = [GameScene node];
// add layer as a child to scene
[scene addChild: layer];
// return the scene
return scene;
}
- (void) makeBox2dObjAt:(CGPoint)p
withSize:(CGPoint)size
dynamic:(BOOL)d
rotation:(long)r
friction:(long)f
density:(long)dens
restitution:(long)rest
boxId:(int)boxId {
// Define the dynamic body.
//Set up a 1m squared box in the physics world
b2BodyDef bodyDef;
// bodyDef.angle = r;
if(d)
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);
GameObject *platform = [[GameObject alloc] init];
[platform setType:kGameObjectPlatform];
bodyDef.userData = platform;
b2Body *body = world->CreateBody(&bodyDef);
// Define another box shape for our dynamic body.
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(size.x/2/PTM_RATIO, size.y/2/PTM_RATIO);
// Define the dynamic body fixture.
b2FixtureDef fixtureDef;
fixtureDef.shape =&dynamicBox;
fixtureDef.density = dens;
fixtureDef.friction = f;
fixtureDef.restitution = rest;
body->CreateFixture(&fixtureDef);
}
- (void) drawCollisionTiles {
CCTMXObjectGroup *objects = [tileMapNode objectGroupNamed:@"Collision"];
NSMutableDictionary * objPoint;
int x, y, w, h;
for (objPoint in [objects objects]) {
x = [[objPoint valueForKey:@"x"] intValue];
y = [[objPoint valueForKey:@"y"] intValue];
w = [[objPoint valueForKey:@"width"] intValue];
h = [[objPoint valueForKey:@"height"] intValue];
CGPoint _point=ccp(x+w/2,y+h/2);
CGPoint _size=ccp(w,h);
[self makeBox2dObjAt:_point
withSize:_size
dynamic:false
rotation:0
friction:1.5f
density:0.0f
restitution:0
boxId:-1];
}
}
- (void) addScrollingBackgroundWithTileMap {
tileMapNode = [CCTMXTiledMap tiledMapWithTMXFile:@"scroller.tmx"];
tileMapNode.anchorPoint = ccp(0, 0);
[self addChild:tileMapNode];
}
// initialize your instance here
-(id) init
{
if( (self=[super init])) {
// enable touches
self.isTouchEnabled = YES;
screenSize = [CCDirector sharedDirector].winSize;
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
[self setupPhysicsWorld];
[self addScrollingBackgroundWithTileMap];
[self drawCollisionTiles];
player = [Player spriteWithFile:@"Icon-Small.png"];
player.position = ccp(100.0f, 180.0f);
[player createBox2dObject:world];
[self addChild:player];
// Start main game loop
[self scheduleUpdate];
}
return self;
}
-(void) setupPhysicsWorld {
b2Vec2 gravity = b2Vec2(0.0f, -9.8f);
bool doSleep =true;
world =new b2World(gravity, doSleep);
m_debugDraw =new GLESDebugDraw(PTM_RATIO);
world->SetDebugDraw(m_debugDraw);
uint32 flags =0;
flags += b2DebugDraw::e_shapeBit;
m_debugDraw->SetFlags(flags);
contactListener =new ContactListener();
world->SetContactListener(contactListener);
}
-(void) draw {
glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
world->DrawDebugData();
// restore default GL states
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}
- (void) update:(ccTime)dt {
//It is recommended that a fixed time step is used with Box2D for stability
//of the simulation, however, we are using a variable time step here.
//You need to make an informed choice, the following URL is useful
//http://gafferongames.com/game-physics/fix-your-timestep/
int32 velocityIterations =8;
int32 positionIterations =1;
// Instruct the world to perform a single step of simulation. It is
// generally best to keep the time step and iterations fixed.
world->Step(dt, velocityIterations, positionIterations);
//Iterate over the bodies in the physics world
for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) {
if (b->GetUserData() != NULL) {
//Synchronize the AtlasSprites position and rotation with the corresponding body
CCSprite *myActor = (CCSprite*)b->GetUserData();
myActor.position = CGPointMake( b->GetPosition().x * PTM_RATIO,
b->GetPosition().y * PTM_RATIO);
myActor.rotation =-1* CC_RADIANS_TO_DEGREES(b->GetAngle());
}
}
b2Vec2 pos = [player body]->GetPosition();
CGPoint newPos = ccp(-1* pos.x * PTM_RATIO +50, self.position.y * PTM_RATIO);
[self setPosition:newPos];
}
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView:[touch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
if (location.x <= screenSize.width /2) {
[player moveRight];
} else {
[player jump];
}
return TRUE;
}
// on "dealloc" you need to release all your retained objects
- (void) dealloc {
// in case you have something to dealloc, do it in this method
delete contactListener;
delete world;
world = NULL;
delete m_debugDraw;
// don't forget to call "super dealloc"
[super dealloc];
}
@end
这里有本教程的完整源代码。
著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!