本文翻译自国外著名IOS源码教学商业网站raywenderlich 的IOS Game Start Kits三件套之一的Platformer Game/平台动作游戏的前奏曲,另一个是Beat'Em up Game/横版格斗游戏,作者是国外著名游戏开发专家Jake Gundersen,曾参与开发过SFC时代的洛克人X系列。
原文网址:
http://www.raywenderlich.com/15230/how-to-make-a-platform-game-like-super-mario-brothers-part-1
开篇之前先怀旧一番吧!
还记得超级马里奥的青青草地蓝天白云吗?还记得曾让人爱恨交加又不屈不挠让人不忍放弃的洛克人ZERO吗,我们燃起小宇宙一招龙炎刃击败最终Boss的场面是曾多么热血澎湃!这些感动一代人的游戏陪伴了我们80后儿时整整一个时代。横版平台动作游戏,作为FC时代最早的游戏类型,以美丽精致的游戏画面,曲折有趣的关卡设计,丰富流畅的动作设定深深地俘获住了众玩家的芳心,让玩家过足了在游戏世界里冒险探索的瘾。这个悬崖怎么跳过去?这个机关怎么破解?这个洞里是不是还有隐藏宝物?下一关是啥样的?最终Boss到底是谁该怎么打?无数的问题让玩家欲罢不能。现在IOS时代这种游戏的光环已经渐渐褪散了,代之以没剧情没关卡只有背景无限滚动的无脑跑酷模式。这样的快餐游戏我已经不想再说什么了,一句话,任何所谓的创新类型都无法超越经典,大家是不是已经燃起想学习的Cosmos了?那么请看下面吧!
本教程所需要的美术资源:resources 源码见文章未尾
创建一个简单易用的2D物理引擎:
并非所有的2D游戏都需要像小鸟那样复杂的物理引擎,如超级马里奥,洛克人索尼克这种其实只需要着重模拟重力环境和平台碰撞,以及行走时的动力和摩擦力即可,关键一点就是能模拟运动,即在重力环境下的走,跑,跳和下落,模拟的好的话动作会非常流畅和自然,否则就非常生硬,此外就是碰撞检测。
我们的主角是一只可爱的考拉(树袋熊),在你下面要实现的简单物理引擎里,你的考拉有这几个物理变量:当前速度(velocity),运动加速度(acceleration)和当前位置(position)以及其他。你将利用这些变量实现下面的算法:
1.玩家启用了跳跃和和移动操作了吗?
2.如果是就给考拉一个跳跃或移动的力
3.同时时刻不停地给考拉加以重力
4.计算出各种力作用下的考拉速度
5.根据计算出的速度计算出考拉当前位置,并更新
6.检查考拉与墙和地等其他物体的碰撞
7.如果发生了碰撞,就通过将考拉移回碰撞发生前的位置来解决,直至碰撞不再发生,如果与怪物发生了碰撞,则可怜的考拉要承受伤害
你在每帧都要做这些操作,在游戏世界里,重力是一直都稳定存在的把考拉向下拉向地面,但是在碰撞解决步骤里又会把考拉向上推至地面顶。
加载TMXTiledMap:
假设大家对Tiled map editor工具已经非常熟悉,让我们打开下载的资源目录里的level1.tmx,就会看到:
你会发现地图有三个层,分别是:
hazards: 这个层包含了一些objects我们的小考拉碰到了就会受伤-_-
walls:本层包含的tiles是考拉不能穿过的,也就是墙壁,地面这些物体
background: 本层是背景图,只是些装饰用的,如山呀云呀树之类,不会起到碰撞反应
终于可以coding了,让我们先建立一个GameLevelLayer的类,就是游戏层啦,具体代码我就不全贴了,可以参考源码,头文件类似这个样子:
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;
在GameLevelLayer.cpp里加上主角考拉:
//地图上加载主角考拉熊
_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;
}
运行游戏,可以看到考拉已经出现在我们眼前:
考拉看上去悬在空中,下一步就是要给它添加重力支持了!
考拉的重力环境模拟
前面说了,考拉目前所处的世界非常简单,就是重力把考拉向下拉,而地面将考拉向上托,在经典牛顿力学里,决定一个物体运动的主要是速度,加速度和施加在物体上的力这几个量,我们一一分析:
Velocity: 速度,决定了物体在给定的方向上移动的有多快
Acceleration:加速度,一定时间内物体的速度变化量
force:力,使物体改变运动速度和方向的决定因素
当一个力施加在物体上时(即重力),物体就不会匀速运动(当没有任何力影响时物体要么静止要么匀速运动),而是以一定的加速度的方式不断加速度运动,直到有其他的力改变。因为我们做的是2D游戏,cocos2dx很好的支持这种运动模拟,不论位置position和速度我们都可以用CCPoint代替,加速度可以考虑用ccpAdd来计算加速度,这些会使编码更容易。
首先我们在GameLevelLayer的init方法里启用update:
this->scheduleUpdate();
GameLevelLayer类的update方法目前就只是调用Player的update,如下:
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步所示
好了,大功告成,重力模拟就靠这几句简单的代码就已经模拟了,现在编译运行,看看效果吧!
不过....可悲的是,考拉的确能做自由落体下落运动了,不过也不出意外的掉出了屏幕,程序Down掉!
不要在黑夜里瞎摸 --- 碰撞检测
看来只有重力模拟是远远不能代表游戏里的物理世界,在任何物理引擎里,碰撞检测与处理都是最核心和基本的部分。检测碰撞首要解决的问题是要计算出游戏角色的包围盒,不过可喜的是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方法来的,效果跟它一样,可以放心使用
举重开始!
我们的考拉学会了下落,下一步就要教它举重了(什么?你想说我很胖?)这里的举重不是考拉举,当然是地了
我们需要写一些方法来实现地面与考拉的碰撞检测,如下:
实现找到考拉周围8个tile砖块的方法,这8个tiles分别是左右上下和四个对角的tile,正好包围住我们的主角
实现决定哪些tile是能产生碰撞的tile,有些tile是没有物理属性的,没云,树等背景,这些是不需要检测的
一个来解决碰撞的方法
你还需要在GameLevelLayer类里创建两个工具函数便于解决问题:
计算考拉在地图中的tile坐标的方法
根据地图上某一tile坐标返回它的tile的Rect包围盒的方法
这两个方法很好实现,代码如下:
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;
}
代码很长也很多,不用担心,我们会一点一点给它解释清楚
在开始之前,注意到这个方法有一个CCTMXLayer*参数,之前提到我们检测冲突时有些是不希望检测到的,如背景,所以在tilemap地图里分好了三个层,一个hazards是放敌人和陷阱的,当考拉碰到会受伤害,一个是walls层,就是现在我们主要讨论的地和墙,考拉碰上了一般是退回原位。还有一个就是backgrounds背景层,只是起个装饰作用,就不需要考虑怎么碰撞了。
开始分析代码:
1. 第一件要做的事就是将传入的position(考拉在游戏层中的位置)转换为地图中的tile坐标
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的,游戏层会用它来做碰撞检测.
还有一点要修改的是在Player.cpp里的update方法里最后一句不能直接setPosition了,要修改的是desiredPosition
this->_desiredPosition = ccpAdd(this->getPosition(), stepVelocity); //当前期望要去的位置=当前位置+当前速度
让我们解决一些碰撞!
是时候解决碰撞了,在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 把主角位置设定到它期望去的地方
}
又是洋洋洒洒一大段,让我们好好看看刚才写下的是什么
1.首先我们调用getSurroundingTilesAtPosition方法返回在walls层考拉四周的tiles数组。接着你就针对数组里每个tile进行循环,检测主角包围是否与这些tile相撞,如果发生了碰撞则我们通过改变desiredPosition的办法来解决
2.在每次循环,我们首先计算出考拉的包围盒,就像刚刚我们提到的,这个desiredPosition是计算包围盒的基础,每当碰撞发生时,我们就要改变desiredPosition值直到不再和tile发生碰撞。
3.下一步是我们要从数组元素中取出GID,这是我们之前存在dictionary里的。很可能当前tile坐标下没有任何tile存在,这时GID就为0,如果是这样,当前循环就可以直接转到下一次循环。
4.如果当前位置有tile存在,你就需要计算出那个tile的CCRect(就是tile的包围盒),存入tileRect变量里,现在你有了主角考拉和tile的包围盒,就可以检测它们是否发生碰撞了
5. 为了检测碰撞,我们调用了intersectsRect方法检测是否发生了碰撞,如果有碰撞还要再计算出这个碰撞部分的真实大小CCRect,后面会有用。可惜cocos2d-x版没有提供计算两个矩形相交的方法,而cocos2d-iphone版里却有CGRectIntersection方法能计算出,汗一个!没办法只有自己实现了,我自己实现了一个Tools::intersectsRect(rectA, rectB)可以计算出两矩形相交部分,目测用到现在好几年了没出过问题。
停下来考虑一个比较棘手的问题...
碰撞是能检测出来,接下来就是如何解决它们,刚才我们提到好多遍,将考拉移回碰撞发生前的位置就可以了,但事实果真是如此简单吗?看下图就一目了然了。
如图,这是非常见的情况,考拉前跳后落在地上与地面发生碰撞,我们期望的当然是它能站在地上,但碰撞前是在地面的斜上方,如果照我们之前的说法解决碰撞的办法是移回前一步所在的位置, 那么我们看到的是考拉不但向上而且后退了,这显然不是我们想要的。
同理还有上图这种跳下撞墙的情况,结果也是大同小异,下跳碰撞到墙后我们希望它是靠在墙边,而不能还往斜上方移动。
那我们该怎么决定考拉是上移还是左移呢?其实上面两种类似的情况隐含了一个决定性的不一样的地方,就是碰撞发生时的碰撞盒,一个是水平碰撞,一个是垂直碰撞,看下图:
原文在这里讲的很啰嗦,这里我说的简单一点。从图可以一目了然,决定性的不一样之处就是红色部分,即考拉与墙壁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方向的最大速度
再回到那个checkForAndResolveCollisions方法,加以改动:
在开头处:
CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _walls);
player->_onGround = false; ///这里加上这一句
在if(tileIndx==0)里,设完desiredPosition后,加上下面两句
player->_velocity = ccp(player->_velocity.x, 0.f);
player->_onGround = true;
注意你要在player类里加个bool _onGround这个成员变量,表示考拉是否在地上,在player的init方法或构造函数里设初始值为false;
接着在if(tileIndx==1)里,设完desiredPosition后,加上
player->_velocity = ccp(player->_velocity.x, 0.f); //此时考拉是向上跳顶到砖块,不是在地上
接着在else分支里,第一个判断 if (intersection.size.width > intersection.size.height)里面第一行加上
player->_velocity = ccp(player->_velocity.x, 0.f);
紧接着下面:
if (tileIndx>5)
{
intersectionHeight = intersection.size.height;
player->_onGround = true; ///注意加上这一句
}
好了大功告成,这样每次我们在考拉下落到地面或上顶到tile时都会把y轴速度重置为0,并且设是否在地面标志,这下编译运行下游戏看看吧!
看到了吧?考拉稳稳地站在了地上!
好了第一部分有关物理引擎的部分讲完了,我们写了好多内容,大家先消化消化,在下一节里我会接着讲考拉的移动,跳跃,陷阱还有过关判定给内容!并把完整源码奉上!
此外,欢迎大家光临我的淘宝小店,里面有很多价廉物美的精品源码献给大家!
http://shop66085899.taobao.com
转载于:https://blog.51cto.com/qiou2719/1601727