还记得超级马里奥的青青草地蓝天白云吗?还记得曾让人爱恨交加又不屈不挠让人不忍放弃的洛克人ZERO吗,我们燃起小宇宙一招龙炎刃击败最终Boss的场面是曾多么热血澎湃!这些感动一代人的游戏陪伴了我们80后儿时整整一个时代。横版平台动作游戏,作为FC时代最早的游戏类型,以美丽精致的游戏画面,曲折有趣的关卡设计,丰富流畅的动作设定深深地俘获住了众玩家的芳心,让玩家过足了在游戏世界里冒险探索的瘾。这个悬崖怎么跳过去?这个机关怎么破解?这个洞里是不是还有隐藏宝物?下一关是啥样的?最终Boss到底是谁该怎么打?无数的问题让玩家欲罢不能。现在IOS时代这种游戏的光环已经渐渐褪散了,代之以没剧情没关卡只有背景无限滚动的无脑跑酷模式。这样的快餐游戏我已经不想再说什么了,一句话,任何所谓的创新类型都无法超越经典,大家是不是已经燃起想学习的Cosmos了?那么请看下面吧!
本教程所需要的美术资源:resources 源码见文章未尾
创建一个简单易用的2D物理引擎:
并非所有的2D游戏都需要像小鸟那样复杂的物理引擎,如超级马里奥,洛克人索尼克这种其实只需要着重模拟重力环境和平台碰撞,以及行走时的动力和摩擦力即可,关键一点就是能模拟运动,即在重力环境下的走,跑,跳和下落,模拟的好的话动作会非常流畅和自然,否则就非常生硬,此外就是碰撞检测。
我们的主角是一只可爱的考拉(树袋熊),在你下面要实现的简单物理引擎里,你的考拉有这几个物理变量:当前速度(velocity),运动加速度(acceleration)和当前位置(position)以及其他。你将利用这些变量实现下面的算法:
1.玩家启用了跳跃和和移动操作了吗?
2.如果是就给考拉一个跳跃或移动的力
3.同时时刻不停地给考拉加以重力
4.计算出各种力作用下的考拉速度
5.根据计算出的速度计算出考拉当前位置,并更新
6.检查考拉与墙和地等其他物体的碰撞
7.如果发生了碰撞,就通过将考拉移回碰撞发生前的位置来解决,直至碰撞不再发生,如果与怪物发生了碰撞,则可怜的考拉要承受伤害
你在每帧都要做这些操作,在游戏世界里,重力是一直都稳定存在的把考拉向下拉向地面,但是在碰撞解决步骤里又会把考拉向上推至地面顶。
class GameLevelLayer : public cocos2d::CCLayer
{
public:
GameLevelLayer(void);
~GameLevelLayer(void);
CREATE_FUNC(GameLevelLayer);
bool init();
static cocos2d::CCScene* scene();
protected:
cocos2d::CCTMXTiledMap *map;
};
下面我们在init里加入地图,如下:
//加载一个蓝色背景当装饰
CCLayerColor *blueSky = CCLayerColor::create(ccc4(100, 100, 250, 255));
this->addChild(blueSky);
//加载地图
_map = CCTMXTiledMap::create("level1.tmx");
this->addChild(_map);
我们先加了一个带有蓝色背景的CCLayerColor作为蓝天,下面两行就是大家熟知的加载地图了。
还有scene()的方法:
CCScene* GameLevelLayer::scene()
{
CCScene *scene = CCScene::create();
if(!scene)
return NULL;
GameLevelLayer *layer = GameLevelLayer::create();
scene->addChild(layer);
return scene;
}
好了现在运行游戏可以看到地图了,接着再加我们的主角考拉,在GameLevelLayer.h里弄一个前向声明class Player;然后加一个成员变量Player* _player;//地图上加载主角考拉熊
_player = Player::create("koalio_stand.png");
_player->setPosition(ccp(100, 50));
_map->addChild(_player, 15);
类Player的头文件Player.h
class Player : public cocos2d::CCSprite
{
public:
Player(void);
~Player(void);
//以图片初始化
virtual bool initWithFile(const char *pszFilename);
static Player* create(const char *pszFileName);
void update(float delta);
}
其实就是用一张纹理贴图初始化,给了考拉一个zorder,使它能显示在地图之上,player.cpp的关键代码如下:bool Player::initWithFile(const char *pszFilename)
{
CCAssert(pszFilename != NULL, "Invalid filename for Player");
//作些自己的初始化
bool bRet = CCSprite::initWithFile(pszFilename);
_velocity = ccp(0.f, 0.f); //速度初始化
return bRet;
}
Player* Player::create(const char *pszFileName)
{
Player *pobPlayer = new Player();
if (pobPlayer && pobPlayer->initWithFile(pszFileName))
{
pobPlayer->autorelease();
return pobPlayer;
}
CC_SAFE_DELETE(pobPlayer);
return NULL;
}
运行游戏,可以看到考拉已经出现在我们眼前:
考拉看上去悬在空中,下一步就是要给它添加重力支持了!
考拉的重力环境模拟
前面说了,考拉目前所处的世界非常简单,就是重力把考拉向下拉,而地面将考拉向上托,在经典牛顿力学里,决定一个物体运动的主要是速度,加速度和施加在物体上的力这几个量,我们一一分析:
void GameLevelLayer::update(float delta)
{
_player->update(delta);
}
好了,看下Player类的update方法:void Player::update(float dt)
{//2
CCPoint gravity = ccp(0.f, -450.f);
//3
CCPoint gravityStep = ccpMult(gravity, dt);
//4
this->_velocity = ccpAdd(this->_velocity, gravityStep);
//5
this->setPosition(ccpAdd(this->getPosition(), stepVelocity));
}
我们来仔细解释下:
1.在init方法里我们将Player的_velocity初始化为(0,0)
2.我们声明了一个代表重力的向量gravity (0,-450.f)y 值为负表示方向是垂直向下指向地的,这个力使考拉每帧加速度移动450像素,假设一帧时间是1秒,则第一帧考拉由0下落了450个像素,第二帧内会下落900个像素,这里不是物理学上的9.8因为计算机上像素和物理学的米单位差别是很大的
3. 我们用ccpMult来计算重力加速度,因为每帧时间是update参数里的dt,所以重力加速度值就是gravity*此帧时间dt
4. 计算下一帧速度velocity, 计算加速后的速度值物理学上的计算方法就是当前速度+加速度,如代码所示
5. 知道了速度我们就可以计算出物体位置了,先要计算出物体在本帧内的位移,就是速度*时间间隔,然后下一帧位置当前就是当前位置+位移了,如第5步所示
看来只有重力模拟是远远不能代表游戏里的物理世界,在任何物理引擎里,碰撞检测与处理都是最核心和基本的部分。检测碰撞首要解决的问题是要计算出游戏角色的包围盒,不过可喜的是cocos2d-x已经提供了这样的函数boundingBox,是根据提供的资源纹理来计算包围盒的,纹理多大包围盒就多大,实际使用中还需要修正。因为美术给的纹理图片不可避免的周围会留下空白透明部分,所以需要适当放缩。
在Player.h里,加入这一个方法:
cocos2d::CCRect collisionBoundingBox(); //返回考拉的包围盒
Player.cpp实现如下:
CCRect Player::collisionBoundingBox()
{
//这里要将包围盒宽度-2个单位,但中心点不变
CCRect collisionBox = Tools::CCRectInset(this->boundingBox(), 3, 0);
return returnBoundingBox;
}
在IOS版本的cocos2d-iphone里有CGRectInset方法,能使一个矩形在x轴和y轴上放缩指定像素大小,正值为缩小负值为放大,但可惜cocos2d-x版本没有提供此方法,需要自己实现,我写在了一个Tools类的一个静态方法,代码如下:
CCRect Tools::CCRectInset(CCRect &rect, float dx, float dy)
{
rect.origin.x += dx;
rect.size.width -= dx * 2;
rect.origin.y -= dy; //缩小时y轴应该向下,IOS的坐标系与cocos2d-x不一样,y原点是在左下角而非左上角
rect.size.height -= dy * 2;
return rect;
}
此代码完全是按照原MAC版CGRectInset方法来的,效果跟它一样,可以放心使用
你还需要在GameLevelLayer类里创建两个工具函数便于解决问题:
CCPoint GameLevelLayer::tileCoordForPosition(cocos2d::CCPoint position)
{
float x = floor(position.x / _map->getTileSize().width); //位置x值/地图一块tile的宽度即可得到x坐标
float levelHeightInPixels = _map->getMapSize().height * _map->getTileSize().height; //地图的实际高度
float y = floor((levelHeightInPixels - position.y)/_map->getTileSize().height); //地图的原点在左上角,与cocos2d-x是不同的(2dx原点在左下角)
return ccp(x, y);
}
CCRect GameLevelLayer::tileRectFromTileCoords(cocos2d::CCPoint tileCoords)
{
float levelHeightInPixels = _map->getMapSize().height * _map->getTileSize().height; //地图的实际高度
//把地图坐标tileCoords转化为实际游戏中的坐标
CCPoint origin = ccp(tileCoords.x * _map->getTileSize().width, levelHeightInPixels - ((tileCoords.y+1)*_map->getTileSize().height));
return CCRectMake(origin.x, origin.y, _map->getTileSize().width, _map->getTileSize().height);
}
注释写的很清楚,大家很容易理解.方法1里求y时是用地图高度-坐标高度,这个是因为游戏里采用OpenGL坐标系左下角为原点,而tileMap的坐标系的原点在左上角,所以要减一下。第二个方法返回指定tile坐标处tile的Rect,因为每个tile都是有大小的,而这个tile包围盒后面要用到,所以要计算一下,计算方法与第一个大同小异。
我被Tiles包围啦!
现在我们要真正实现第一个方法,计算求出考拉周围的8个tile,因为只有求出包围在主角身边的包围tile,这些tile可能是墙,可能是地,才好利用这些tile来和主角碰撞检测。在这个方法里我们要构建一个数组来返回这8个tile的GID,还要有这个tile的顶点origin,还有包围盒CCRect信息。
还有这个数组包含的tiles必须要有优先级排好序以利于我们解决碰撞。举个例子,我们总是希望先检查考拉的左,右,下,上这4个tile,然后再考虑相对来说不是很重要的对角线,下面是这个方法,也是放在GameLevelLayer里
CCArray* GameLevelLayer::getSurroundingTilesAtPosition(cocos2d::CCPoint position, cocos2d::CCTMXLayer* layer)
{
CCPoint plPos = this->tileCoordForPosition(position); //1 返回此处的tile坐标
//存gid的数组
CCArray* gids = CCArray::create();//2
gids->retain();
//3 我们的目的是想取出环绕在精灵四周的8个tile,这里就从上至下每行三个取9个tile(中间一个不算)仔细画画图就知代码的意义
for (int i=0; i<9; i++)
{
int c = i % 3; //相当于当前i所处的列
int r = (int)(i/3); //相当于当前i所处的行
CCPoint tilePos = ccp(plPos.x + (c-1), plPos.y + (r-1));
//4 取出包围tile的gid
int tgid = layer->tileGIDAt(tilePos);
//5
CCRect tileRect = this->tileRectFromTileCoords(tilePos); //包围盒
float x = tileRect.origin.x; //位置
float y = tileRect.origin.y;
//取出这个tile的各个属性,放到CCDictionary里
CCDictionary *tileDict = CCDictionary::create();
CCString* str_tgid = CCString::createWithFormat("%d",tgid);
CCString* str_x = CCString::createWithFormat("%f", x);
CCString* str_y = CCString::createWithFormat("%f", y);
tileDict->setObject(str_tgid, "gid");
tileDict->setObject(str_x, "x");
tileDict->setObject(str_y, "y");
tileDict->setObject((CCObject *)&tilePos, "tilePos");
//6
gids->addObject(tileDict);
}
//去掉中间(即自身结点tile)
gids->removeObjectAtIndex(4);
gids->insertObject(gids->objectAtIndex(2), 6);
gids->removeObjectAtIndex(2);
gids->exchangeObjectAtIndex(4, 6);
gids->exchangeObjectAtIndex(0, 4);//7
CCDictionary* d = NULL;
CCObject *obj = NULL;
CCARRAY_FOREACH(gids, obj)
{
d = (CCDictionary*)obj;
CCLog("%d", d);//8
}
return gids;
}
代码很长也很多,不用担心,我们会一点一点给它解释清楚
2. 接着,我们create了一个数组用来返回8个tiles所需要的信息
3.然后开始循环9次 包括8个包围考拉的tile还有考拉自己所站的位置。一一计算出这9个tile的地图tile坐标放在tilePos变量里。
4.第4步是调用tileGIDAt方法返回tilePos位置的tile的GID,如果当前位置没有tile,则GID返回0,我们可以据此判断当前位置有没有tile
5.接着用我们定义好的方法计算出tilePos处的tile的包围盒CCRect,以及顶点位置属性,我们会把存入一个CCDictionary里
6.然后在第7步,我们把考拉所在的tile从数组中移除出去,并把这些数组元素按优先级重新排序。我们想先解决考拉身边下,上,左,右这四个是最优先的,也是游戏中最容易发生碰撞的,其他才是对角线的tile
下面这张图先显示了这些tiles原先在数组中的次序,接着排序过后的位置,你会发现排过序后位于下,上,左,右的四个tiles最先被处理,了解这些次序有助于你以后设置什么时候考拉接触到地的标志位。
方法写好了,只等调用了,在这之前,先要做些准备工作,就是先要获取walls层,才能调用这个方法,在GameLevelLayer类的init方法里,在addChild(_player)之后,加入下面代码:
_walls = _map->layerNamed("walls"); //_walls就定义为GameLevelLayer类里的一个CCTMXLayer*变量吧
然后在update方法里,加入下面代码:
this->getSurroundingTilesAtPosition(_player->getPosition(), _walls);
现在编译执行仍会报错,因为我们的考拉还是会掉出地图外,后面我们会讲到如何修正这个问题,但现在要做的是如何让我们的主角考拉站在地上! 输出信息如下:
剥夺下考拉的特权!
what?我只是一只被人随意摆布的小熊,哪有什么Privileges?!错,目前为止这只小熊有一项了不起的技能就是可以随意设置自己的position,不受任何伟大的牛顿经典力学控制,就跟超人一般,想飞到哪就飞到哪,你说牛不牛?不过牛归牛看上去太假了可不行,这里稍稍做一下更正。
如果考拉更新它的位置时GameLevelLayers检测出来了碰撞(虽然目前我们还没实现这个功能),我们想要考来回退到碰撞发生前的位置而不是像一只不撞死南墙不回头的笨猫。
这时我们就需要考拉类定义一个新的变量 CCPoint _desiredPosition (期望想去的位置)
为什么好好的自己的Position不用非要多此一举?因为我们在后面的计算考拉行走,跳跃等状态时用一系列方法计算出了考拉的位置,但是碰撞检测系统还要检测这个位置是否发生了碰撞,是否需要修正,所以这个计算出的这个位置不一定有效,何况还要update过一帧才是真正的位置,所以我们需要增加这个期望值来方便计算,等所有的碰撞检测处理完毕后再用这个_desiredPosition设置考拉真正的位置.
好了,我们先在Player.h里增加这个成员变量 CCPoint _desiredPosition;
并且修改下Player.cpp里的计算碰撞盒方法:
CCRect Player::collisionBoundingBox()
{
//这里要将包围盒宽度-2个单位,但中心点不变
CCRect collisionBox = Tools::CCRectInset(this->boundingBox(), 3, 0);
CCPoint diff = ccpSub(this->_desiredPosition, this->getPosition()); //玩家当前距离与目的地的差距
CCRect returnBoundingBox = Tools::CCRectOffset(collisionBox, diff.x, diff.y); //计算调整后的碰撞盒,即包围盒x,y轴方向上移动diff.x, diff.y个单位
return returnBoundingBox;
}
跟以前相比碰撞盒是基于_desiredPosition的,游戏层会用它来做碰撞检测.
让我们解决一些碰撞!
是时候解决碰撞了,在GameLevelLayer类里加入下面代码
void GameLevelLayer::checkForAndResolveCollisions(Player* player)
{
CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _walls); //1
CCObject* obj = NULL;
CCDictionary* dic = NULL;
CCARRAY_FOREACH(tiles, obj)
{
dic = (CCDictionary*)obj;
CCRect playerRect = player->collisionBoundingBox(); //2 玩家的包围盒
int gid = dic->valueForKey("gid")->intValue(); //3 从CCDictionary中取得玩家附近tile的gid值
if (gid)
{
float rect_x = dic->valueForKey("x")->floatValue();
float rect_y = dic->valueForKey("y")->floatValue();
float width = _map->getTileSize().width;
float height = _map->getTileSize().height;
//4 取得这个tile的Rect
CCRect tileRect = CCRectMake(rect_x, rect_y, width, height);
if (tileRect.intersectsRect(playerRect)) //如果玩家包围盒与tile包围盒相撞
{
//5 取得相撞部分
CCRect intersection = Tools::intersectsRect(playerRect, tileRect);
int tileIndx = tiles->indexOfObject(dic); //6 取得dic的下标索引
if (tileIndx == 0)
{
//tile在koala正下方 考拉落到了tile上
player->_desiredPosition = ccp(player->_desiredPosition.x, player->_desiredPosition.y + intersection.size.height);
}
else if (tileIndx == 1) //考拉头顶到tile
{
//在koala上面的tile,要让主角向上移移
player->_desiredPosition = ccp(player->_desiredPosition.x, player->_desiredPosition.y - intersection.size.height);
}
else if (tileIndx == 2)
{
//左边的tile
player->_desiredPosition = ccp(player->_desiredPosition.x+intersection.size.width, player->_desiredPosition.y);
}
else if (tileIndx == 3)
{
//右边的tile
player->_desiredPosition = ccp(player->_desiredPosition.x-intersection.size.width, player->_desiredPosition.y);
}
else
{
//7 如果碰撞的水平面大于竖直面,说明角色是上下碰撞
if (intersection.size.width > intersection.size.height)
{
//tile is diagonal, but resolving collision vertically
float intersectionHeight;
if (tileIndx>5) //说明是踩到斜下的砖块,角色应该向上去
{
intersectionHeight = intersection.size.height;
}
else //说明是顶到斜上的砖块,角色应该向下托
{
intersectionHeight = -intersection.size.height;
}
player->_desiredPosition = ccp(player->_desiredPosition.x, player->_desiredPosition.y + intersectionHeight);
}
else //如果碰撞的水平面小于竖直面,说明角色是左右撞到
{
float resolutionWidth;
if (tileIndx == 6 || tileIndx == 4) //角色碰到斜左边的tile 角色应该向右去
{
resolutionWidth = intersection.size.width;
}
else //角色碰到斜右边的tile, 角色应该向左去
{
resolutionWidth = -intersection.size.width;
}
player->_desiredPosition = ccp(player->_desiredPosition.x + resolutionWidth, player->_desiredPosition.y );
}
}
}
}
}
player->setPosition(player->_desiredPosition); //7 把主角位置设定到它期望去的地方
}
又是洋洋洒洒一大段,让我们好好看看刚才写下的是什么
停下来考虑一个比较棘手的问题...
碰撞是能检测出来,接下来就是如何解决它们,刚才我们提到好多遍,将考拉移回碰撞发生前的位置就可以了,但事实果真是如此简单吗?看下图就一目了然了。
如图,这是非常见的情况,考拉前跳后落在地上与地面发生碰撞,我们期望的当然是它能站在地上,但碰撞前是在地面的斜上方,如果照我们之前的说法解决碰撞的办法是移回前一步所在的位置, 那么我们看到的是考拉不但向上而且后退了,这显然不是我们想要的。
同理还有上图这种跳下撞墙的情况,结果也是大同小异,下跳碰撞到墙后我们希望它是靠在墙边,而不能还往斜上方移动。
那我们该怎么决定考拉是上移还是左移呢?其实上面两种类似的情况隐含了一个决定性的不一样的地方,就是碰撞发生时的碰撞盒,一个是水平碰撞,一个是垂直碰撞,看下图:
原文在这里讲的很啰嗦,这里我说的简单一点。从图可以一目了然,决定性的不一样之处就是红色部分,即考拉与墙壁tile的碰撞矩形。左边是红框宽度比高度大,就是考拉落在地上,我们需要的是上移或下移,右图红色框是宽度小于高度,就是考拉左右撞墙了,我们需要将考拉左移或右移,就这么简单。
回到代码里...
回头我们刚才讨论的checkForAndResolveCollisions方法
6.在第6步 int tileIndx = tiles->indexOfObject(dic) 里我们取到了当前tile在数组里的索引号,这个索引号就告诉我们当前tile是在考拉上下左右哪个位置,这就好办了,根据位置的不同和碰撞盒的宽度和高度的大小比较我们就将考拉前移后移上移下移碰撞盒宽或高的位置,当碰撞是发生在对角线部分时,同样方法我们可以根据碰撞部分是宽度大还是高度大还决定是前后移还是上下移,这个过程可以看代码注释,可能会更好理解一些
7. 最用我们可以调用setPosition来设置考拉真正的位置啦!
让我们用一下这个方法,在GameLevelLayer的update方法里,在player->update()一句后面加上这一句:
this->checkForAndResolveCollisions(_player);
好了,编译执行,看看是不是真的有效果了?
?!开始的时候考拉是落在地上了,但不过1秒又沉到了地面下去!
你能猜到这是为什么吗?我们到底遗漏了什么?
回忆一下我们在player的update里做的事,我们不停地施加重力在考拉身上,那个重力就让考拉不停地向下加速,这样即使它碰到地地也把它向上抬了,但考拉加速度最终仍会增大到地能承受的地步最终沉下去。
所以当我们解决完碰撞冲突时,不要忘了把考拉速度重置为0,不论它是上碰还是下落还是左右撞墙,速度一律重设为0.
首先在player.cpp的update方法里,在最后一句设置_desiredPosition前面,要限定一下下落的最大速度,防止考拉下落速度过大掉到墙里。
if(_velocity.y<-kVelocityYMax)
_velocity = ccp(this->_velocity.x, -kVelocityYMax);
这个kVelocityYMax你可以在player.h里#define kVelocityYMax 500 //Y方向的最大速度
好了大功告成,这样每次我们在考拉下落到地面或上顶到tile时都会把y轴速度重置为0,并且设是否在地面标志,这下编译运行下游戏看看吧!
看到了吧?考拉稳稳地站在了地上!
好了第一部分有关物理引擎的部分讲完了,我们写了好多内容,大家先消化消化,在下一节里我会接着讲考拉的移动,跳跃,陷阱还有过关判定给内容!并把完整源码奉上!
此外,欢迎大家光临我的淘宝小店,里面有很多价廉物美的精品源码献给大家!
http://shop66085899.taobao.com