《跑酷》Starter Kit -- C++ 版本


原创: ZeroYang
首发于泰然,转载请著名出处。

业余学习cocos2d-x 游戏开发引擎,之前看到 泰然网的跑酷, 是jsb版本的,本着学习的心态,用c++重写了一遍。

游戏和jsb版本基本相同,你会看到runner向前奔跑,吃金币,在屏幕上向上滑动手势jump或向下滑动手势crouch 控制runner 躲避岩石。当游戏角色碰到了金币,金币将会消失但是石头不会消失,当他碰到石头,game over! 相比jsb版本 缺少 画圈手势 开启无敌状态。

游戏效果图:
overview

本文描述与jsb版本不同的地方,其他参考之前的文章。
文章重点讲解:

  1. 如何创建cocos2d-x c++ 语言的多工程。
  2. 物理世界的创建
  3. 游戏精灵的抽象
  4. 动画播放
  5. 碰撞检测的基本使用
  6. 简单的手势判断。

新建工程

获取 Cocos2d-x

C++版本基于泰然发布的JSB版本编写,所用引擎是Cocos2d-x-2.1.5。

创建多平台工程

  • 打开 Terminal 终端. 使用cd 命令跳转到你解压cocos2d-x所在的目录, 如下:
YTB$ cd /Users/YTB/Downloads/cocos2d-x-2.1.5/tools/project-creator 
  • 执行create_project.py
YTB$ python create_project.py
Usage: create_project.py -project PROJECT_NAME -package PACKAGE_NAME -language PROGRAMING_LANGUAGE
Options:
  -project   PROJECT_NAME          Project name, for example: MyGame
  -package   PACKAGE_NAME          Package name, for example: com.MyCompany.MyAwesomeGame
  -language  PROGRAMING_LANGUAGE   Major programing lanauge you want to used, should be [cpp | lua | javascript]

Sample 1: ./create_project.py -project MyGame -package com.MyCompany.AwesomeGame
Sample 2: ./create_project.py -project MyGame -package com.MyCompany.AwesomeGame -language javascript
  • 根据上面的命令提示,执行如下命令创建创建c++版本工程
YTBdeMacBook-Pro:project-creator YTB$ ./create_project.py -project paoku -package com.ytb.Paoku -language cpp
  • 看见下列信息创建成功
YTB$ ./create_project.py -project PaoKu -package com.MyCompany.PaoKu -language cpp
proj.ios        : Done!
proj.android        : Done!
proj.win32      : Done!
proj.mac        : Done!
proj.blackberry     : Done!
proj.linux      : Done!
proj.marmalade      : Done!
New project has been created in this path: /Users/YTB/Downloads/cocos2d-x-2.1.5/projects/PaoKu
Have Fun!

到此,你已经创建好支持多平台的工程。 文中开发使用的iOS的工程。

C++中chipmunk

在游戏中我们经常要加入物理碰撞等和物理有关的内容,在游戏中加入物理引擎可以使我们的游戏更加真实,为玩家展示一个更真实的世界,cocos2d-x支持两个物理引擎Box2d和Chipmunk,Paoku游戏使用的是Chipmunk。
PlayLayer是PlayScene最重要的层。这层处理玩家输入,碰撞检测,物体运动等等。和jsb版本一致在PlayLayer中进行处理。
整个游戏中只会用到一个物理空间cpSpace,它将被PlayScene中的其它layer使用到。

初始化

我们在PlayLayer::init()方法中创建物理世界,和跑酷所用的地面。

// initPhysics, must init first  
//面向对象的chipmunk API,用来创建一个物理世界   
this->space = cpSpaceNew();  //1
//设置物理世界的重力  
this->space->gravity = cpv(0, -350);  
//set up Walls  
//跑酷所用的地面,chipmunk中使用静态形状来描述。从物理空间新建一个静态SegmentShape,然后将它添加到物理空间   
wallBottom = cpSegmentShapeNew(this->space->staticBody,
                                   cpv(0, MapManager::getGroundHeight()),// start point
                                   cpv(4294967295, MapManager::getGroundHeight()),// MAX INT:4294967295
                                   0);// thickness of wall
    cpSpaceAddStaticShape(this->space, wallBottom);//2
    
scheduleUpdate();//3
  1. 创建物理世界
  2. 创建Body并关联shape.
    body对象可以任意移动,当然也可以加入静态body,用来表示游戏中的台阶或者墙壁等不可以移动的物体。
  3. 运作物理引擎
    周期性地调用world对象的step函数。一般会通过scheduleUpdate()方法,在游戏每一帧发生的时候都调用一次update函数,然后再update函数里面处理精灵的位置更新等。

Coin和Rock的抽象

跑酷游戏中的障碍物Coin和Rock,他们将参与Runner角色的碰撞检测。他们都可以抽象成CCPhysicsSprite的子类。

CCPhysicsSprite 定义一个物理类绑定的精灵,它继承自CCSprite精灵类。CCPhysicsSprite 只需要按照CCSprite的方法定义,然后将之前按照Chipmunk方式定义的body设置给精灵就可以了,代码如下:

Coin的初始化

完整初始化代码 参见Coin.cpp中Coin::Coin(CCSpriteBatchNode spriteSheet, cpSpace space, CCPoint position)方法

    this->pSpace = space;
    this->initWithSpriteFrameName("coin0.png");//1

    // init physics
    float radius = 0.95 * this->getContentSize().width / 2;
    this->pBody = cpBodyNewStatic();
    cpBodySetPos(this->pBody, cpv(position.x, position.y));
    this->setCPBody(this->pBody);//2

    this->pShape = cpCircleShapeNew(this->pBody, radius, cpvzero);
    this->pShape->collision_type = SpriteTagcoin;
    this->pShape->sensor = true;
    cpSpaceAddStaticShape(this->pSpace, this->pShape);//3

    // for collision
    cpShapeSetUserData(this->pShape, this);//4
    
    spriteSheet->addChild(this);
  1. 使用Coin图片初始化精灵对象
  2. 设置Coin的body,位置
  3. 设置Coin的shape,并加入到物理世界
  4. cpShapeSetUserData(this->pShape, this);如果你设置将其指向形状关联的游戏对象,那么你可以从Chipmunk回调中访问你的的游戏对象。

Coin动画的播放

精灵帧类CCSpriteFrame,表示精灵动画中的一帧,通常由贴图定义
精灵缓存类CCSpriteFrameCache,用于缓存精灵帧以提高效率,是单实例模式,所有精灵共享同一个缓存类实例。通过CCSpriteFrameCache::sharedSpriteFrameCache()获得
动画类CCAnimation,储存一个动画的所有帧,可以通过帧数组定义。并且定义了帧间隔时间。

  • 下面代码展示了如何创建Coin的动画并播放:
    CCArray *animFrames = CCArray::create();//1
    for (int i = 0; i < 8; i++)
    {
        CCString *name = CCString::createWithFormat("coin%d.png",i);
        CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(name->getCString());//2
        animFrames->addObject(frame);
    }
    CCAnimation *animation = CCAnimation::createWithSpriteFrames(animFrames, 0.1);//3
    CCAction *action =CCRepeatForever::create(CCAnimate::create(animation));
    this->runAction(action);//4

创建一个动画的过程。

(1) 定义精灵帧。

(2) 使用精灵帧定义动画帧。

(3) 通过动画帧数组定义动画(也可以直接通过精灵帧数组定义动画)。

(4) 通过动画定义动画动作,通过精灵执行动画动作播放相应动画。

Rock的初始化

游戏中有两种Rock. 贴放地面的Jump Rock.和悬在空中的Crouch RocK. Rock没有动画。Rock的初始化和Coin基本相同。代码如下:

Rock::Rock(CCSpriteBatchNode *spriteSheet, cpSpace *space, CCPoint position)
{
    this->pSpace = space;
    
    if ( position.y >= (g_groundHight + Runner::getCrouchContentSize()->height) ) {
        this->initWithSpriteFrameName("hathpace.png");
    } else {
        this->initWithSpriteFrameName("rock.png");
    }

    // init physics
    this->pBody = cpBodyNewStatic();
    cpBodySetPos(this->pBody, cpv(position.x, position.y));
    this->setCPBody(this->pBody);

    this->pShape = cpBoxShapeNew(this->pBody, this->getContentSize().width, this->getContentSize().height);

    this->pShape->collision_type = SpriteTagrock;
    this->pShape->sensor = true;
    cpSpaceAddStaticShape(this->pSpace, this->pShape);

    // for collision
    cpShapeSetUserData(this->pShape, this);
    
    spriteSheet->addChild(this);
}

Runner的抽象

游戏中的Runner角色同样可以看做精灵,并参与碰撞检测。故按照类似的方式将Runner抽象成CCPhysicsSprite的子类。

Runner 物理引擎的加入

  • Runner body的初始化
void Runner::initBody()
{
    // create chipmunk body
    this->body = cpBodyNew(1, cpMomentForBox(1,this->runningSize.width, this->runningSize.height));
    this->body->p = cpv(m_offsetPx, MapManager::getGroundHeight() + this->runningSize.height / 2);
    this->body->v = cpv(150, 0);//run speed
    cpSpaceAddBody(this->space, this->body);
}
  • Runner shape的初始化
    Runner有running/jumpUp/jumpDown/crouch几个动作,需要根据不同的动作初始化不同的shape。碰撞检测时根据shape的形状大小进行判断。
void Runner::initShape(const char* type)
{
    if (this->shape) {
        cpSpaceRemoveShape(this->space, this->shape);
    }
    
    if (0 == strcmp(type, RUNING_MODE)) {
        this->shape = cpBoxShapeNew(this->body, this->runningSize.width-14, this->runningSize.height);
    } else {
        // crouch
        this->shape = cpBoxShapeNew(this->body, this->crouchSize.width, this->crouchSize.height);
    }
    cpSpaceAddShape(this->space, this->shape);
    cpShapeSetCollisionType(this->shape, SpriteTagrunner);
}

  • Runner 动画
    Runner有running/jumpUp/jumpDown/crouch几个动画.默认是向前奔跑的runner。后面根据操作进行jump/crouch动画。动画的初始化代码如下:
void Runner::initAction()
{
    // init runningAction
    CCArray *animFrames = CCArray::create();
    for (int i = 0; i < 8; i++)
    {
        CCString *name = CCString::createWithFormat("runner%d.png",i);
        CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(name->getCString());
        animFrames->addObject(frame);
    }

    CCAnimation *animation = CCAnimation::createWithSpriteFrames(animFrames, 0.1);
    this->runningAction =CCRepeatForever::create(CCAnimate::create(animation));
    this->runningAction->retain();

    // init jumpUpAction
    animFrames = CCArray::create();
    for (int i=0; i<4; i++) {
        CCString *name = CCString::createWithFormat("runnerJumpUp%d.png",i);
        CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(name->getCString());
        animFrames->addObject(frame);
    }

    animation = CCAnimation::createWithSpriteFrames(animFrames, 0.2);
    this->jumpUpAction = CCAnimate::create(animation);
    this->jumpUpAction->retain();

    // init jumpDownAction
    animFrames->removeAllObjects();
    for (int i=0; i<2; i++) {
        CCString *name = CCString::createWithFormat("runnerJumpDown%d.png",i);
        CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(name->getCString());
        animFrames->addObject(frame);
    }
    animation = CCAnimation::createWithSpriteFrames(animFrames, 0.3);
    this->jumpDownAction = CCAnimate::create(animation);
    this->jumpDownAction->retain();

    // init crouchAction
    animFrames->removeAllObjects();
    for (int i=0; i<1; i++) {
        CCString *name = CCString::createWithFormat("runnerCrouch%d.png",i);
        CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(name->getCString());
        animFrames->addObject(frame);
    }
    animation = CCAnimation::createWithSpriteFrames(animFrames, 0.3);
    this->crouchAction = CCAnimate::create(animation);
    this->crouchAction->retain();
}

动画的实现 和前面讲到的 Coin的动画实现过程基本相同。此不在详细讲述。

ObjectManager

金币属于哪个地图,需要在哪个地图放置哪种Rock,runner 碰到金币后,金币消失。这些都需要有个类专门来管理。故抽象ObjectManager 管理map/Rock/Coin的创建和清除。
以下方法实现在那个地图随机创建Coin/Rock。

  • 在地图上创建Coin/Rock
void ObjectManager::initObjectOfMap(int mapIndex, float mapWidth)
{
    int initCoinNum = 7;
	float jumpRockHeight = Runner::getCrouchContentSize()->height + g_groundHight;
	float coinHeight = Coin::getCoinContentSize()->height + g_groundHight;
    

	//random the center point of 7 coins.
	int randomCoinFactor = CCRANDOM_0_1()*(CCRANDOM_0_1()*2 + 1);

	int randomRockFactor = CCRANDOM_0_1()*(CCRANDOM_0_1()*2 + 1);
	float jumpRockFactor = 0;
    
	float coinPoint_x = mapWidth/4 * randomCoinFactor+mapWidth*mapIndex;
	float RockPoint_x = mapWidth/4 * randomRockFactor+mapWidth*mapIndex;
    
	float coinWidth = Coin::getCoinContentSize()->width;
	float rockWith = Rock::getRockContentSize()->width;
	float rockHeight =  Rock::getRockContentSize()->height;
    
	float startx = coinPoint_x - coinWidth/2*11;
	float xIncrement = coinWidth/2*3;
    CCLOG("xIncrement=%f",xIncrement);
	//add a rock
    Rock *rock = Rock::create(this->pSpriteSheet, this->pSpace, ccp(RockPoint_x, g_groundHight+rockHeight/2));
    rock->setTag(mapIndex);
    objects.push_back(rock);
	if(mapIndex == 0 && randomCoinFactor==1){
		randomCoinFactor = 2;
	}

	//add coins
	for(int i = 0; i < initCoinNum; i++)
	{
        Coin *coin = NULL;
		if((startx + i*xIncrement > RockPoint_x-rockWith/2)
           &&(startx + i*xIncrement < RockPoint_x+rockWith/2))
		{
            //coin位置和rock位置重,金币放在rock上
            coin = Coin::create(this->pSpriteSheet, this->pSpace, ccp(startx + i*xIncrement, coinHeight+rockHeight));
		}
		else
		{
            coin = Coin::create(this->pSpriteSheet, this->pSpace, ccp(startx + i*xIncrement, coinHeight));
		}
        coin->setTag(mapIndex);
        objects.push_back(coin);
	}
    
	for(int i=1;i<4;i++){
		if((i!=randomCoinFactor) && (i!=randomRockFactor))
		{
			jumpRockFactor = i;
		}
	}

	//add jump rock
	float JumpRockPoint_x = mapWidth/4 * jumpRockFactor + mapWidth*mapIndex;
    Rock *jumpRock = Rock::create(this->pSpriteSheet, this->pSpace, ccp(JumpRockPoint_x, jumpRockHeight+rockHeight/2));

    jumpRock->setTag(mapIndex);
    objects.push_back(jumpRock);

}
  • 将map/Coin/Rock加入playLayer

在PlayLayer::init()方法中加入ObjectManager的初始化. 在PlayLayer中加入地图,金币,岩石等元素。

    this->objectManager = new ObjectManager(this->spriteSheet, this->space);
    this->objectManager->initObjectOfMap(1, this->mapManager->getMapWidth());//默认从第二张地图初始化加入金币、岩石
  • Coin/Rock的清理

每一次地图重载,地图中的对象要回收。当游戏角色得到金币时,将这个金币从它的父类中和列表中移除。

//将地图上的object 全部清除
void ObjectManager::removeObjectOfMap(int mapIndex)
{
    // 正确的遍历删除方法
    std::list::iterator itList;
    for (itList = objects.begin(); itList != objects.end();/*Not ++ here*/) {
        CCSprite *sprite = (CCSprite *)*itList;
        if(mapIndex == sprite->getTag()) {
            sprite->removeFromParent();
            itList = objects.erase(itList);
        } else {
            itList++;
        }
    }
}
//清除指定的精灵obj
void ObjectManager::remove(CCSprite *obj)
{
    obj->removeFromParent();
    objects.remove(obj);
}

当Coin和Rock从地图上清理了,Coin和Rock也不再需要了。需进行销毁。Coin和Rock的removeFromParent实现如下

    cpSpaceRemoveStaticShape(this->pSpace, this->pShape);
    cpShapeFree(this->pShape);
    cpBodyFree(this->pBody);//清理相关的shape和body.注意清理顺序。
    
    CCPhysicsSprite::removeFromParent();//从node节点移除

地图循环

目前为止,游戏角色还很孤独的跑在一个黑色的世界,现在要做的是给游戏添加背景图片。 背景由上下两部分组成,当游戏角色在两张图片中间的夹缝上跑动的时候,第一张背景图片会慢 慢被第二张背景图片替换,第一张图片将重新加载。

runner的新位置回由物理世界在每帧计算,相机需要跟随runner的移动步伐。
通过移动摄像机,让它跟随runner移动,并用map图片替换背景实现,然后让map图片无限循环起来。

关键代码如下:
参见PlayLayer::update(float dt)方法

    // check and reload map
    if (true == this->mapManager->checkAndReload(this->lastEyeX)) {
        this->objectManager->removeObjectOfMap(this->mapManager->getCurMap() - 1);
        this->objectManager->initObjectOfMap(this->mapManager->getCurMap() + 1, this->mapManager->getMapWidth());
        //level up
        //this->runner->levelUp();
    }//1
    
    this->runner->step(dt);
    // move Camera
    lastEyeX = this->runner->getPositionX() - this->runner->getoffsetPx();
    CCCamera *camera = this->getCamera();
    float eyeZ = camera->getZEye();
    camera->setEyeXYZ(lastEyeX, 0, eyeZ);
    camera->setCenterXYZ(lastEyeX, 0, 0);//2

(1)检查runner 移动的点,是否到达下一个地图,清理上一个地图,并初始化下一个地图上的元素。
(2)移动摄像机,让它跟随runner移动

碰撞检测用法

  • chipmunk中的碰撞回调函数

在chipmunk中有两种设定碰撞回调函数的方法,一种是cpSpaceSetDefaultCollisionHandler,另一种是cpSpaceAddCollisionHandler。

cpSpaceSetDefaultCollisionHandler是为物理空间设定默认的碰撞回调函数,它的设置是针对全局的,也就是物理空间里任何对象的碰撞都会执行设定的回调函数:

void cpSpaceSetDefaultCollisionHandler(
	cpSpace *space,
	cpCollisionBeginFunc begin,
	cpCollisionPreSolveFunc preSolve,
	cpCollisionPostSolveFunc postSolve,
	cpCollisionSeparateFunc separate,
	void *data
);

cpSpaceAddCollisionHandler用来设定自定义的碰撞回调规则,比上面多出了两个参数,用来指定物理空间中哪类对象碰撞时执行回调函数:

void cpSpaceAddCollisionHandler(
	cpSpace *space,
	cpCollisionType a, cpCollisionType b,
	cpCollisionBeginFunc begin,//碰撞开始时的回调
	cpCollisionPreSolveFunc preSolve,//单次碰撞准备结束时的回调
	cpCollisionPostSolveFunc postSolve,//单次碰撞结束时的回调
	cpCollisionSeparateFunc separate,//碰撞对象分离后的回调
	void *data
);
  • 跑酷游戏的碰撞检测设置

详细实现查看PlayScene.cpp

(1) 加入cpCollisionBeginFunc 的C方法。

// c function for chipmunk
static void postStepRemove(cpSpace *space, cpShape *shape, void *param)
{
    ObjectManager *objectManager = (ObjectManager *)param;
    switch (shape->collision_type) {
        case SpriteTagcoin:
            CCNotificationCenter::sharedNotificationCenter()->postNotification(NOTIFI_MEET_COIN);
            objectManager->remove((CCSprite *)shape->data);
            break;
        case SpriteTagrock:
            CCNotificationCenter::sharedNotificationCenter()->postNotification(NOTIFI_MEET_ROCK);
            break;
        default:
            break;
    }
}

static int collisionBegin(cpArbiter *arb, cpSpace *space, void *param)
{
    // we get shapes here, so postStepRemove's second param is cpShape
    CP_ARBITER_GET_SHAPES(arb, a, b);
    cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, b, param);
    return 0;
}

(2) 设置监听碰撞

游戏中,有两种碰撞,runner 碰到Coin或Rock.需分别监听两种碰撞。

    cpSpaceAddCollisionHandler(this->space, SpriteTagrunner, SpriteTagcoin, collisionBegin, NULL
                               , NULL, NULL, this->objectManager);
    cpSpaceAddCollisionHandler(this->space, SpriteTagrunner, SpriteTagrock, collisionBegin, NULL
                               , NULL, NULL, this->objectManager);
                               
    CCNotificationCenter::sharedNotificationCenter()->addObserver(this, callfuncO_selector(PlayLayer::notifiCoin),
                                                                  NOTIFI_MEET_COIN, NULL);
    CCNotificationCenter::sharedNotificationCenter()->addObserver(this, callfuncO_selector(PlayLayer::notifiRock),
                                                                  NOTIFI_MEET_ROCK, NULL);

(3) 处理监听回调

监听runner 碰到Coin或者Rock进行相应的处理。碰到Coin,更新Coin数和播放音效。碰到Rock,game over!

void PlayLayer::notifiCoin(CCObject *unuse)
{
    CocosDenshion::SimpleAudioEngine *audioEngine = CocosDenshion::SimpleAudioEngine::sharedEngine();
    audioEngine->playEffect("pickup_coin.mp3");
    StatusLayer *statusLayer = (StatusLayer *)getParent()->getChildByTag(TAG_STATUSLAYER);
    statusLayer->addCoin(1);
}

void PlayLayer::notifiRock(CCObject *unuse)
{
    GameOverLayer *gameoverLayer = GameOverLayer::create(ccc4(0, 0, 0, 180));
    gameoverLayer->setTag(TAG_GAMEOVER);
    getParent()->addChild(gameoverLayer);
    
    CCDirector::sharedDirector()->pause();
}

手势

前面游戏的基本角色,场景已经添加完毕。
还需加入手势,控制runner的奔跑。
参照jsb版,加入SimpleRecognizer类。进行简单的上滑,下滑手势判断。
PlayScene作为游戏运行的主layer,应在其中处理游戏的手势。实现Touch相关的接口
在PlayScene.h中加入

    virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent);
    virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent);
    virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent);

设置TouchEnabled,在PlayLayer::init()中加入如下代码

    setTouchEnabled(true);
    setTouchMode(kCCTouchesOneByOne);
    recognizer = new SimpleRecognizer();

监听Tocuh回调,判断手势。在PlayScene.cpp中加入

bool PlayLayer::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) {
    CCLOG("PlayScene::ccTouchBegan");
    CCPoint pos = pTouch->getLocation();//获取touch点在坐标系的位置
    recognizer->beginPoint(pos.x, pos.y);
    return true;
}

void PlayLayer::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent)
{
    CCPoint pos = pTouch->getLocation();
    recognizer->movePoint(pos.x, pos.y);
}

void PlayLayer::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent)
{

    CCLOG("PlayScene::ccTouchEnded");
    
    SimpleGestures rtn = recognizer->endPoint();
    
    switch (rtn) {
        case SimpleGesturesUp:
        CCLOG("Runner::jump");
        this->runner->jump();
        break;
        
        case SimpleGesturesDown:
        CCLOG("Runner::crouch");
        this->runner->crouch();
        break;
        
        case SimpleGesturesNotSupport:
        case SimpleGesturesError:
        // try dollar Recognizer
        // 0:Use Golden Section Search (original)
        // 1:Use Protractor (faster)
        CCLOG("not support or error touch,use geometricRecognizer!!");
        break;
        
        default:
        break;
    }
}

根据手势,上滑 播放 runner jump动画,下滑播放 runner crouch1动画。

后记

到此,C++版本的跑酷游戏基本完成。难免有不完美之处。欢迎指出。欲获取完整资源,请到这里下载

标签: none

?>