实例比較简单,如图所看到的,地图上有一个忍者精灵,玩家点击他周围的上、下、左、右,他能够向这个方向行走。

当他遇到障碍物后是无法穿越的,障碍物是除了草地以为部分,包含了:树、山、河流等。

忍者实例地图(TODO用这个精灵替换图中的)

设计地图
我们採用David Gervais提供开源免费瓦片集。下载的文件dg_grounds32.gif,gif文件格式会有一定的问题,我们须要转换为.jpg或.png文件。本实例中我是使用PhotoShop转换为dg_grounds32.jpg。
David Gervais提供的瓦片集中的瓦片是32 x 32像素,我们创建的地图大小是32 x 32瓦片。

我们先为地图加入普通层和对象层,普通层依照上图设计。对象层中加入几个矩形区域对象,这里不再赘述。这个阶段设计完毕的结果如图所看到的。

保存文件名称为MiddleMap.tmx,保存文件夹Resources\map。

设计地图

程序中载入地图
地图设计完毕我们就能够在程序中载入地图了。以下我们再看看详细的程序代码,首先看一下HelloWorldScene.h文件,它的代码例如以下: 

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__ #include "cocos2d.h" class HelloWorld : public cocos2d::Layer
{
cocos2d::TMXTiledMap* _tileMap; ①
cocos2d::Sprite *_player; ②
public:
static cocos2d::Scene* createScene(); virtual bool init(); CREATE_FUNC(HelloWorld);
}; #endif // __HELLOWORLD_SCENE_H__

上述代码第①行代码是定义成员变量地图成员_tileMap。。第②行代码是定义精灵成员变量_player。
HelloWorldScene的实现代码HelloWorldScene.ccp文件,它的HelloWorld::init()代码例如以下: 

bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
} Size visibleSize = Director::getInstance()->getVisibleSize();
Point origin = Director::getInstance()->getVisibleOrigin(); _tileMap = TMXTiledMap::create("map/MiddleMap.tmx"); ①
addChild(tileMap,0,100); ② TMXObjectGroup* group = _tileMap ->getObjectGroup("objects"); ③
ValueMap spawnPoint = group->getObject("ninja"); ④ float x = spawnPoint["x"].asFloat(); ⑤
float y = spawnPoint["y"].asFloat(); ⑥ _player = Sprite::create("ninja.png"); ⑦
_player ->setPosition(Point(x,y)); ⑧
addChild(_player, 2,200); return true;
}

上述第①代码是创建TMXTiledMap对象。地图文件是MiddleMap.tmx。map是资源文件夹Resources下的子文件夹。

TMXTiledMap对象也是Node对象,须要通过第②行代码加入到当前场景中。
第③行代码是通过对象层名objects获得层中对象组集合。第④行代码是从对象组中,通过对象名获得ninja对象信息,它的返回值类型是ValueMap,ValueMap是一种“键-值”对结构。第⑤行代码float x = spawnPoint["x"].asFloat()中的spawnPoint["x"]就是从依照x键取出它的值,即x轴坐标。spawnPoint["x"]的返回值是Value类型,还须要使用asFloat()函数转换为主要的int类型。

相似地,第⑥行代码是获得y轴坐标。
第⑦行代码是创建精灵_player,第⑧行代码是设置精灵位置。这个位置是从对象层中ninja对象信息获取的。

载入地图(TODO又一次截取,或者换精灵)

移动精灵
移动精灵是通过触摸事件实现移动的,须要在层中进行事件处理,我们须要在层中重写例如以下函数:
bool onTouchBegan(Touch * touch, Event* unused_event)
void onTouchEnded(Touch * touch,Event* unused_event)
void onTouchMoved(Touch * touch,Event* unused_event)

以下我们再看看详细的程序代码,首先看一下HelloWorldScene.h文件,它的代码例如以下: 

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__ #include "cocos2d.h" class HelloWorld : public cocos2d::Layer
{
cocos2d::TMXTiledMap* _tileMap;
cocos2d::Sprite *_player;
public:
static cocos2d::Scene* createScene(); virtual bool init(); virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event); ①
virtual void onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event);
virtual void onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event); ② CREATE_FUNC(HelloWorld);
}; #endif // __HELLOWORLD_SCENE_H__

上述代码第①~②行代码是声明触摸事件函数。HelloWorldScene的实现代码HelloWorldScene.ccp文件。它的HelloWorld::init()代码例如以下:

bool HelloWorld::init()
{
… …
setTouchEnabled(true);
//设置为单点触摸
setTouchMode(Touch::DispatchMode::ONE_BY_ONE); return true;
}

上述代码setTouchEnabled(true)是使层開始触摸事件支持。代码setTouchMode(Touch::DispatchMode::ONE_BY_ONE)是设置触摸模式为单点触摸。
HelloWorldScene.ccp文件的触摸事件函数代码例如以下:

bool HelloWorld::onTouchBegan(Touch* touch, Event* event)
{
log("onTouchBegan");
return true;
} void HelloWorld::onTouchMoved(Touch *touch, Event *event)
{
log("onTouchMoved");
} void HelloWorld::onTouchEnded(Touch *touch, Event *event)
{
log("onTouchEnded"); Point touchLocation = touch->getLocation(); ① Point playerPos = _player->getPosition(); ②
Point diff = touchLocation - playerPos; ③ if (abs(diff.x) > abs(diff.y)) { ④
if (diff.x > 0) { ⑤
playerPos.x += _tileMap->getTileSize().width;
_player->runAction(FlipX::create(false)); ⑥
} else {
playerPos.x -= _tileMap->getTileSize().width;
_player->runAction(FlipX::create(true)); ⑦
}
} else {
if (diff.y > 0) { ⑧
playerPos.y += _tileMap->getTileSize().height;
} else {
playerPos.y -= _tileMap->getTileSize().height;
}
}
_player->setPosition(playerPos); ⑨
}

上述第①代码touch->getLocation()是获得在Open GL坐标。Open GL坐标的坐标原点是左下角,touch对象封装了触摸点对象。第②行代码_player->getPosition()是获得精灵的位置。
第③行代码是获得触摸点与精灵位置之差。第④行代码是比較一下触摸点与精灵位置之差,是y轴之差大还是x轴之差大,那个轴之差大就沿着那个轴移动,(abs(diff.x) > abs(diff.y))情况是x轴之差大。否则是y轴之差大。

第⑤行代码,diff.x > 0情况是沿着x轴正方向移动。否则情况是沿着x轴负方向移动。第⑥行代码_player->runAction(FlipX::create(false))是把精灵翻转回原始状态。

第⑦行代码_player->runAction(FlipX::create(true))是把精灵是沿着y轴水平翻转。
第⑧行代码是沿着y轴移动。diff.y > 0是沿着y轴正方向移动。否则是沿着y轴负方向移动。

第⑨行代码是又一次设置精灵坐标。

检測碰撞
到眼下为止我们游戏中的精灵,能够穿越不论什么障碍物。为了能够检測到精灵是否碰撞到障碍物,我们须要再加入一个普通层(collidable),它的目的不是现实地图,而是检測碰撞。

我们在检測碰撞层中使用瓦片覆盖background层中的障碍物之上,如图所看到的。

检測碰撞层

检測碰撞层中的瓦片集能够是不论什么的满足格式要求的图片文件。

在本例中我们使用一个32 x 32像素单色jpg图片文件collidable_tiles. jpg,它的大小与瓦片大小一样,也就是说这个瓦片集中仅仅有一个瓦片。导入这个瓦片集到地图后。我们须要为瓦片加入一个自己定义属性,瓦片本身也有一些属性。比如:坐标属性x和y。
我们要加入的属性名为“Collidable”,属性值为“true”。

加入过程如图所看到的。首先,选择collidable_tiles瓦片集中的要设置属性的瓦片。然后,点击属性视图中左下角“+”button。加入自己定义属性,这时候会弹出一个对话框,我们在对话框中输入自己定义属性名“Collidable”。点击确定button。这时候回到属性视图,Collidable在属性后面是能够输入内容的,这里我们输入“true”。

加入检測碰撞属性

地图改动完毕后,我们还要改动代码。

首选在头文件HelloWorldScene.h中加入一个成员变量和两个函数的声明。

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__ #include "cocos2d.h"
#include "SimpleAudioEngine.h" class HelloWorld : public cocos2d::Layer
{
cocos2d::TMXTiledMap* _tileMap;
cocos2d::TMXLayer* _collidable; ①
cocos2d::Sprite *_player;
public:
static cocos2d::Scene* createScene();
virtual bool init(); virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);
virtual void onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event);
virtual void onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event); void setPlayerPosition(cocos2d::Point position); ②
cocos2d::Point tileCoordFromPosition(cocos2d::Point position); ③ CREATE_FUNC(HelloWorld);
}; #endif // __HELLOWORLD_SCENE_H__

上述代码第①行是声明一个TMXLayer类型的成员变量,它是用来保存地图碰撞层对象。第②行代码setPlayerPosition函数是又一次设置精灵的位置,在这个函数中能够检測精灵是否与障碍物碰撞。第③行代码tileCoordFromPosition函数是把像素坐标点转换为地图瓦片坐标点。
改动HelloWorldScene.cpp中的HelloWorld::init()代码例如以下:

bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
} Size visibleSize = Director::getInstance()->getVisibleSize();
Point origin = Director::getInstance()->getVisibleOrigin(); _tileMap = TMXTiledMap::create("map/MiddleMap.tmx");
addChild(_tileMap,0,100); TMXObjectGroup* group = _tileMap->getObjectGroup("objects");
ValueMap spawnPoint = group->getObject("ninja"); float x = spawnPoint["x"].asFloat();
float y = spawnPoint["y"].asFloat(); _player = Sprite::create("ninja.png");
_player->setPosition(Point(x,y));
addChild(_player, 2, 200); _collidable = _tileMap->getLayer("collidable"); ①
_collidable->setVisible(false); ② setTouchEnabled(true);
//设置为单点触摸
setTouchMode(Touch::DispatchMode::ONE_BY_ONE); return true;
}

我们须要在HelloWorld::init()函数中创建并初始化碰撞层。第①行代码是_collidable = _tileMap->getLayer("collidable")是通过层名字collidable创建层,第②行代码_collidable->setVisible(false)是设置层隐藏,我们要么在这里隐藏的,要么在地图编辑的时候,将该层透明,如图所看到的,在层视图中选择层。然后通过滑动上面的透明度滑块来改变层的透明度,在本例中是须要将透明度设置为0,那么_collidable->setVisible(false)语句就不再须要了。
注意  在地图编辑器中。设置层的透明度为0与设置层隐藏。在地图上看起来一样,可是有着本质的差别。设置层隐藏是无法通过_collidable = _tileMap->getLayer("collidable")语句訪问的。

设置层透明度

我们在前面也介绍过。collidable层不是用来显示地图内容的,而是用来检測碰撞的。改动HelloWorldScene.cpp中的

HelloWorld::onTouchEnded代码例如以下:
void HelloWorld::onTouchEnded(Touch *touch, Event *event)
{
log("onTouchEnded"); //获得在OpenGL坐标
Point touchLocation = touch->getLocation(); Point playerPos = _player->getPosition();
Point diff = touchLocation - playerPos; if (abs(diff.x) > abs(diff.y)) {
if (diff.x > 0) {
playerPos.x += _tileMap->getTileSize().width;
_player->runAction(FlipX::create(false));
} else {
playerPos.x -= _tileMap->getTileSize().width;
_player->runAction(FlipX::create(true));
}
} else {
if (diff.y > 0) {
playerPos.y += _tileMap->getTileSize().height;
} else {
playerPos.y -= _tileMap->getTileSize().height;
}
}
this->setPlayerPosition(playerPos); ①
}

HelloWorld::onTouchEnded有一些变化。第①行代码this->setPlayerPosition(playerPos)替换了_player->setPosition(playerPos)。setPlayerPosition是我们自定的函数。这个函数的作用是移动精灵和检測碰撞。
setPlayerPosition代码例如以下: 

void HelloWorld::setPlayerPosition(Point position)
{
//从像素点坐标转化为瓦片坐标
Point tileCoord = this->tileCoordFromPosition(position); ①
//获得瓦片的GID
int tileGid = _collidable->getTileGIDAt(tileCoord); ② if (tileGid > 0) { ③
Value prop = _tileMap->getPropertiesForGID(tileGid); ④
ValueMap propValueMap = prop.asValueMap(); ⑤ std::string collision = propValueMap["Collidable"].asString(); ⑥ if (collision == "true") { //碰撞检測成功 ⑦
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("empty.wav"); ⑧
return ;
}
}
_player->setPosition(position);
}

上述代码第①行this->tileCoordFromPosition(position)是调用函数,实现从像素点坐标转化为瓦片坐标。

第②行代码_collidable->getTileGIDAt(tileCoord)是通过瓦片坐标获得GID值。
第③行代码tileGid > 0能够推断瓦片是否存在,tileGid == 0是瓦片不存在情况。第④行代码_tileMap->getPropertiesForGID(tileGid)是通过地图对象的getPropertiesForGID返回,它的返回值是Value类型。

因为Value类型能够代表非常多类型。因此第⑤行代码prop.asValueMap()是将Value类型转换成为ValueMap。ValueMap类是“键-值”对。第⑥行代码propValueMap["Collidable"].asString()是将propValueMap变量中的Collidable属性取出来。asString()函数能够将Value类型转换成为std::string类型。

第⑦行代码collision == "true"是碰撞检測成功情况。第⑧行代码是碰撞检測成功情况下处理。在本例中我们是播放一下音效。

tileCoordFromPosition代码例如以下: 

Point HelloWorld::tileCoordFromPosition(Point pos)
{
int x = pos.x / _tileMap->getTileSize().width; ①
int y = ((_tileMap->getMapSize().height * _tileMap->getTileSize().height) - pos.y) /
_tileMap->getTileSize().height; ②
return Point(x,y);
}

在该函数中第①行代码pos.x / _tileMap->getTileSize().width是获得x轴瓦片坐标(单位是瓦片数)。pos.x是触摸点x轴坐标(单位是像素),_tileMap->getTileSize().width是每一个瓦片的宽度,单位是像素。

代码第②行是获得y轴瓦片坐标(单位是瓦片数)。这个计算有点麻烦。瓦片坐标的原点在左上角,而触摸点使用的坐标是Open GL坐标。坐标原点在左下角。表达式(_tileMap->getMapSize().height * _tileMap->getTileSize().height) - pos.y)是反转坐标轴,结果除以每一个瓦片的高度_tileMap->getTileSize().height,就得到y轴瓦片坐标了。

滚动地图
因为地图比屏幕要大,当我们移动精灵到屏幕的边缘时候,那些处于屏幕之外的地图部分,应该滚动到屏幕之内。

这些须要我们又一次设置视点(屏幕的中心点)。使得精灵一直处于屏幕的中心。

可是精灵太靠近地图的边界时候。他有可能不在屏幕的中心。

精灵与地图的边界距离的规定是,左右边界距离不小于屏幕宽度的一半,否则会出现图所看到的的左右黑边问题。

上下边界距离不小于屏幕高度的一半。否则也会在上下黑边问题。
又一次设置视点实现的方式非常多,本章中採用移动地图位置实现这样的效果。
我们在HelloWorldScene.cpp中再加入一个函数setViewpointCenter,加入后代码例如以下: 

void HelloWorld::setViewpointCenter(Point position)
{
Size visibleSize = Director::getInstance()->getVisibleSize();
int x = MAX(position.x, visibleSize.width / 2); ①
int y = MAX(position.y, visibleSize.height / 2); ②
x = MIN(x, (_tileMap->getMapSize().width * _tileMap->getTileSize().width)
- visibleSize.width / 2); ③
y = MIN(y, (_tileMap->getMapSize().height * _tileMap->getTileSize().height)
- visibleSize.height/2); ④ //屏幕中心点
Point pointA = Point(visibleSize.width/2, visibleSize.height/2); ⑤
//使精灵处于屏幕中心。移动地图目标位置
Point pointB = Point(x, y); ⑥
log("目标位置 (%f ,%f) ",pointB.x,pointB.y); //地图移动偏移量
Point offset =pointA - pointB; ⑦ log("offset (%f ,%f) ",offset.x, offset.y);
this->setPosition(offset); ⑧
}

在上述代码①~④是保障精灵移动到地图边界时候不会再移动,防止屏幕超出地图之外,这一点非常重要。当中第①行代码是防止屏幕左边超出地图之外。MAX(position.x, visibleSize.width / 2)语句表示当position.x < visibleSize.width / 2情况下。x轴坐标始终是visibleSize.width / 2,即精灵不再向左移动。第②行代码与第①行代码相似,不再解释。第③行代码是防止屏幕右边超出地图之外,MIN(x, (_tileMap->getMapSize().width * _tileMap->getTileSize().width) - visibleSize.width / 2)语句表示当x > (_tileMap->getMapSize().width * _tileMap->getTileSize().width) - visibleSize.width / 2时候。x轴坐标始终是(_tileMap->getMapSize().width * _tileMap->getTileSize().width) - visibleSize.width / 2表达式计算的结果。

提示visibleSize 是表示屏幕的宽度,_tileMap->getMapSize().width * _tileMap->getTileSize().width) - visibleSize.width / 2表达式计算的是地图的宽度减去屏幕宽度的一半。
第④行代码与第③行代码相似,不再解释。

屏幕左边超出地图

屏幕右边超出地图

代码⑤~⑧行实现了移动地图效果。使得精灵一直处于屏幕的中心。

A点是眼下屏幕的中心点,也是精灵的位置。

玩家触摸B点。精灵会向B点移动。为了让精灵保持在屏幕中心,地图一定要向相反的方向移动。

第⑤行代码Point pointA = Point(visibleSize.width/2, visibleSize.height/2)是获取屏幕中心点(A点)。第⑥行代码是获取移动地图目标位置(B点)。第⑦行代码是计算A点与B点两者之差。这个差值就是地图要移动的距离。

因为精灵的世界坐标就是地图层的模型坐标,即精灵的坐标原点是地图的左下角,因此第⑧行代码this->setPosition(offset)是将地图坐标原点移动offset位置。

移动地图

很多其它内容请关注最新Cocos图书《Cocos2d-x实战 C++卷》‍

本书交流讨论站点:http://www.cocoagame.net

很多其它精彩视频课程请关注智捷课堂Cocos课程:http://v.51work6.com
欢迎加入Cocos2d-x技术讨论群:257760386

《Cocos2d-x实战 C++卷》现已上线,各大商店均已开售:‍

京东:http://item.jd.com/11584534.html

亚马逊:http://www.amazon.cn/Cocos2d-x%E5%AE%9E%E6%88%98-C-%E5%8D%B7-%E5%85%B3%E4%B8%9C%E5%8D%87/dp/B00PTYWTLU

当当:http://product.dangdang.com/23606265.html

互动出版网:http://product.china-pub.com/3770734

《Cocos2d-x实战 C++卷》源代码及样章下载地址:

源代码下载地址:http://51work6.com/forum.php?

mod=viewthread&tid=1155&extra=page%3D1 

样章下载地址:http://51work6.com/forum.php?

mod=viewthread&tid=1157&extra=page%3D1

欢迎关注智捷iOS课堂微信公共平台

忍者无敌-实例解说Cocos2d-x瓦片地图的更多相关文章

  1. 忍者无敌-实例讲解Cocos2d-x瓦片地图

    实例比较简单,如图所示,地图上有一个忍者精灵,玩家点击他周围的上.下.左.右,他能够向这个方向行走.当他遇到障碍物后是无法穿越的,障碍物是除了草地以为部分,包括了:树.山.河流等. 忍者实例地图(TO ...

  2. Android 依赖注入: Dagger 2 实例解说(一)

    本文原创,转载请注明出处:http://blog.csdn.net/zjbpku [Duplicated]   link to  Dagger on Android - Dagger2具体解释 关于D ...

  3. Oracle之索引(Index)实例解说 - 基础

    Oracle之索引(Index)实例解说 - 基础 索引(Index)是关系数据库中用于存放表中每一条记录位置的一种对象.主要目的是加快数据的读取速度和数据的完整性检查.索引的建立是一项技术性要求很高 ...

  4. 【JEECG技术博文】JEECG 简单实例解说权限控制

    JEECG简单实例解说权限控制 请大家点击这里为我们投票.2015博客之星.很多其他分享敬请期待 博文地址:http://blog.itpub.net/30066956/viewspace-18687 ...

  5. OpenLayers加载谷歌地球离线瓦片地图

    本文使用OpenLayers最新版本V5.3.0演示:如何使用OpenLayer加载谷歌地球离线瓦片地图.OpenLayers 5.3.0下载地址为:https://github.com/openla ...

  6. OpenLayers加载高德地图离线瓦片地图

    本文使用OpenLayers最新版本V5.3.0演示:如何使用OpenLayer加载谷歌地球离线瓦片地图.OpenLayers 5.3.0下载地址为:https://github.com/openla ...

  7. OpenLayers加载百度离线瓦片地图(完美无偏移)

    本文使用OpenLayers最新版本V5.3.0演示:如何使用OpenLayer完美无偏移加载百度离线瓦片地图.OpenLayers 5.3.0下载地址为:https://github.com/ope ...

  8. Leaflet 调用百度瓦片地图服务

    在使用 leaflet 调用第三方瓦片地图服务的项目,主要谷歌地图.高德地图.百度地图和 OSM 地图,与其他三种地图对比,百度地图的瓦片组织方式是不同的.百度从中心点经纬度(0,0)度开始计算瓦片, ...

  9. scrollview 图片放大 捏合 瓦片地图 相关注意事项

    就职文博公司要为博物馆做APP 涉及到瓦片地图的编写 在这里总结一些开发中遇到的问题 (将会不断更新 也是学习阶段) 着急写项目的同学 可以直接看code4上现成的瓦片地图代码:http://www. ...

随机推荐

  1. MD markdown入门

    1.Headings: 2.Phrase emphasis *italic text* **Bold text** 3.Listing items (在文字之前添加 + , - 或者 * ) -ite ...

  2. 参考《机器学习实战》高清中文PDF+高清英文PDF+源代码

    机器学习是人工智能研究领域中一个极其重要的研究方向,在现今的大数据时代背景下,捕获数据并从中萃取有价值的信息或模式,成为各行业求生存.谋发展的决定性手段,这使得这一过去为分析师和数学家所专属的研究领域 ...

  3. azure云中 mount: wrong fs type, bad option, bad superblock on /dev/sdc1

    2016-01-30 mount失败问题解决 [root@mofficedb2 ~]# mount /dev/sdc /dta mount: you must specify the filesyst ...

  4. HH生病了(hpu1136)

    HH生病了 Time Limit: 1 Sec  Memory Limit: 128 MB Submit: 324  Solved: 90 [Submit][Status][Web Board] De ...

  5. android启动模式对于体验的影响

    说到Android的启动模式.懂Android的人肯定都懂. 通过设置启动模式我们不仅能够节省内存的使用.还能达到更好的体验,比方我们打开一个应用,点击home键回到主界面的时候程序是没有被kill掉 ...

  6. 浅析.Net数据操作机制

    举个栗子,获取新闻详情的案例. public ActionResult NewsView(int newsId) { var news = _newsService.GetNewsById(newsI ...

  7. OpenCV特征点检测——Surf(特征点篇)&flann

    学习OpenCV--Surf(特征点篇)&flann 分类: OpenCV特征篇计算机视觉 2012-04-20 21:55 19887人阅读评论(20)收藏举报 检测特征 Surf(Spee ...

  8. IOS - 获取UITextField的输入文本

    当UITextField文本改变时, 依据内容更新数据, 通过写监听事件就可以. 加入监听: [timesField addTarget:self action:@selector(textField ...

  9. CSS布局篇——固宽、变宽、固宽+变宽

    学了前端挺久了.近期写一个项目測试系统,布局时发现自己对变宽+固宽的布局还没有全然掌握,所以在这里总结一下,以后须要的时候回头看看. 1.最简单的当然是一列或多列固宽 比如两列固宽: <1> ...

  10. 细说 iOS 消息推送

    APNS的推送机制 与Android上我们自己实现的推送服务不一样,Apple对设备的控制很严格.消息推送的流程必需要经过APNs: 这里 Provider 是指某个应用的Developer,当然假设 ...