ES6之Promise

Promise是对异步操作做的封装,它解决的是回调嵌套的问题,它的实现仍然是注册和调用回调函数。

构造和使用

使用Promise很简单,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var p = new Promise(function(resolve, reject){
//do some asynchronous thing
if(xxx) {
console.log("resolve ...")
resolve();
}else{
console.log("reject....")
reject();
}
})
p.then(function(){
console.log("in resolve callback");
}, function(){
console.log("in reject callback");
}
)

在构造时,传入一个函数作为参数,在这个函数里做我们需要做的事情,比如某个异步操作,然后在恰当的时候,调用resolve或者reject。

一个Promise对象有三种状态,pending(进行中), resolved(已解决), rejected(已失败),初始是pending状态,当resolve或者reject函数被调用时进入resolved或者rejected状态,我们通过then方法,分别指定resolved和rejected的回调,当状态变化时,相应的回调就会被调用。

需要注意的是Promise对象在创建时传入的函数,会立即执行,但它的回调,则在当前脚本所有同步任务都执行完成后,才会开始执行。所以对于下面的代码,输出是132,而不是123

1
2
3
4
5
6
7
8
var p = new Promise((resolve, reject)=>{
console.log("1");
resolve()
})
p.then(()=>{
console.log("2");
})
console.log("3");

Promise.resolve, Promise.reject

这两个是Promise类的静态方法,通过它可以生成一个Promise对象,它的状态已经是resolved或者rejected,实际上下面这两种方式是等同的

1
2
3
4
var p = Promise.resolve(123);
var p = new Promise(function(resolve, reject){
resolve(123);
})

这两个方法还可以接受thenable对象,将其转换成Promise对象,一个thenable对象就是具有then方法的对象,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Test {
then(resolve, reject) {
if (resolve) {
resolve();
}
if (reject) {
reject();
}
}
}

let t = new Test();
t.then(() => {
console.log("resolved....");
})
let p = Promise.resolve(t);

promise chain

then函数返回的是一个新的Promise对象,而不是我们在resolve或者reject回调里return的值,所以我们可以在then函数后面继续接then函数,形成一个promise链,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var p = Promise.resolve(1);
p.then((v1)=>{
console.log("resolve 1...."+v1);
//throw new Error();
//return Promise.reject();
//return Promise.resolve();
/*
return new Promise(function(resolve, reject){
setTimeout(()=>{
resolve();
}, 5000)
})
*/
return 2*v1;
}).then((v2)=>{
console.log("resolve2...." + v2)
}, (reason)=>{
console.log("reject2...")
})

如果我们在then的回调函数里return了一个pending状态的Promise对象,则等待这个对象状态变化后进入下一个then的相应回调中。如果返回一个rejected状态的Promise对象,或者抛出了一个错误,那么会立刻进入下一个then的reject回调中,否则立刻进入下一个then的resolve回调中。如果return的不是Promise对象,则return的值会作为下个then里回调的参数值。

promise对象作为resolve或者reject参数

当我们调用一个Promise对象a的resolve函数时,如果参数是另一个promise对象b,那么只有当b的状态发生改变,a的状态才会改变。且a的状态取决于b的状态

当我们调用一个Promise对象a的reject函数时,如果参数是另一个promise对象b,则a的状态不需等待b的状态,且a的状态不受b影响,一定是rejected。以下是实验代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let p1 = new Promise(function(resolve, reject) {
setTimeout(() => {
// console.log("resolve p1....");
// resolve();
console.log("reject p1....");
reject();
}, 5000)
})

let p2 = new Promise(function(resolve, reject) {
setTimeout(() => {
// console.log("resolve p2...");
// resolve(p1);
console.log("reject p2....");
reject(p1);
}, 1000)
})

p2.then(function(value) {
console.log("in p2 resolve callback");
}, function(error) {
console.log("in p2 reject callback");
});

p1.then(function(value) {
console.log("in p1 resolve callback");
}, function(error) {
console.log("in p1 reject callback");
});

输出为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
resolve(p1), p1 resolve

resolve p2...
resolve p1... //4秒后
in p1 resolve callback
in p2 resolve callback
/**************************************/
resolve(p1), p1 reject

resolve p2...
reject p1... //4秒后
in p1 reject callback
in p2 reject callback
/**************************************/
reject(p1), p1 resolve

reject p2...
in p2 reject callback
resolve p1 // 4秒后
in p1 resolve callback
/**************************************/
reject(p1), p1 reject

reject p2...
in p2 reject callback
reject p1 //4秒后
in p1 reject callback

Promise.all Promise.race

它们都接收一个Promise数组作为参数,并返回一个新的Promise。

对于all来说,只有当数组里每个Promise对象状态都变成resolved,新对象的状态才变成resolved,它在then的resolve回调函数参数为一个数组,包含所有Promise的返回值。但只要有一个Promise对象状态变为rejected,那么新对象的状态马上变为rejected。此时reject的参数被传递给then的reject回调

对于race,只要有一个Promise对象状态发生了变化,新对象的状态就马上跟着改变。第一个改变状态的对象的返回值作为新对象回调的参数。

javaScript之this

js中的this,很容易让刚接触的人摸不着头脑,但其实主要明白了怎么回事,也不会很复杂。函数里的this指向什么,跟它的当前执行环境有关。

我们从简单到复杂,通过一些例子来说明

1
2
3
4
var A = function(){
console.log(this);
}
var a = new A();

这个很简单,因为A是作为一个构造函数来使用的,所以这时的this指向的是a

1
2
3
4
var A = function(){
console.log(this);
}
A();

这里A是作为一个普通函数被调用的,当函数被作为一个普通函数调用时this指向全局变量,即使在嵌套了很多层的复杂情况也是如此。

然后再看一个情况:

1
2
3
4
5
6
var a = {
func:function(){
console.log(this);
}
}
a.func();

这里func是作为一个对象a的成员方法被调用的,所以this指向的是对象a本身。

我们不考虑通过bind和apply设定this的情况,则不论代码多么复杂,最终都可以归结为上面的三种情况,然后分析属于哪种就行了。

比如写个复杂点的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var A = function(){
console.log(this); //1
var B = (function(){
console.log(this); //2
return function(){
console.log(this); //3
}
})();
this.cb = function(){
console.log(this); //4
B(); //43
}
B(); //53
}

var a = new A();
a.cb();

这里1很简单,this就是a,因为A作为一个构造函数。 2这里是在一个闭包里,这个立即执行函数是被直接调用的,所以这里的this也是global。3这里会被执行两次,分别为43和53,但这两种情况都是直接执行函数,所以this也都是global。至于4,它被调用时是a.cb()的形式,cb作为a的成员函数被调用,所以this时a本身。

至于bind和apply,使用它们给一个函数绑定了什么对象,这个函数被调用时,this就是什么对象。举个简单的例子

1
2
3
4
5
6
7
8
9
10
11
var a = {
func:function(){
console.log(this);
},
func1:function(){
console.log(this);
}.bind(this)
}

a.func(); //1
a.func1(); //2

1这里this就是a本身,2这里则是global。2这里往func1绑定的this,是在定义a时环境的this,而不是a本身,需要注意,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var A = function(){
var a = {
func:function(){
console.log(this);
},
func1:function(){
console.log(this);
}.bind(this)
}

a.func(); //1
a.func1(); //2
}
var tmp = new A();

这时1的this仍然是a, 而2的this就是就是tmp

cocos之定时器scheduler

游戏中经常需要用到定时器,定时循环执行某个任务n次,或者延迟一段时间后执行某个任务,此时需要用到的类是Scheduler。它的原理和使用并不复杂,本文记录一些细节问题。

使用

使用起来非常简单,首先通过Director获取Scheduler对象,然后调用它的schedule方法,参数依次为回调函数,回调函数对象,回调周期,回调次数,第一次回调延迟,是否暂停等待。

1
Scheduler::schedule(SEL_SCHEDULE selector, Ref *target, float interval, unsigned int repeat, float delay, bool paused)

此外还有一个接口

1
void scheduleUpdate(T *target, int priority, bool paused)

它可以设定优先级priority,优先级越低,越早被调用。注册的对象target的update方法会被调用。

在CCNode里预先封装了一个函数

1
void Node::scheduleUpdate()

它就是通过上面scheduleUpdate实现的,其priority为0。如果我们调用它,则Node内的update方法会每帧被调用一次。

实现原理

定时器scheduler的实现原理很简单,它的update方法每帧都会被调用,此时查看所有注册了的定时器,如果满足触发条件,则触发一次,然后触发次数加1,触发次数达到注册时的次数,则结束并销毁这个定时器。

注意事项

  1. schedule方法,定时器的回调次数比参数repeat多1,也就是说如果希望只回调一次,repeat应该设为0.以此内推
  2. 通过schedule方法,同一个类注册的定时器,先注册者先回调
  3. 通过schedule方法,不同类注册的定时器,每个类的定时器会按上一条规则全部执行完,然后再执行下一个类的全部回调。不同类之间按照注册时先后顺序来,先注册的类先回调
  4. 通过scheduleUpdate注册的定时器,根据priority排序,priority越小越先被调用
  5. 定时器的回调都有一个参数float t,它表示当前与上一次回调的时间间隔,引擎无法确保两次回调的间隔一定是我们在schedule时设定的间隔,或者我们预计的每帧间隔。
  6. 如果需要将定时器加速或者减速,可以使用Scheduler::setTimeScale方法。设一个小于1的值,回调频率将会变慢,设一个大于1的值,回调频率会变快,但需要注意回调时参数t不会改变

c++之函数对象

std::function是一个类模板,定义于头文件<functional>。我们主要关注下它的几种生成方式。

  • 直接指向函数。例如
1
2
3
4
5
6
bool func(int item) {
return item % 2 == 0;
}

std::function<bool(int)> f = func;

  • 使用lambda表达式。例如
1
2
3
4
int t = 3;
std::function<bool(int)> f1 = [t](int item) {
return item % t == 0;
}
  • 使用函数对象。例如
1
2
3
4
5
6
7
8
9
10
11
12
class Func(){
private:
int t;
public:
Func(int _t):t(_t){}
bool operator()(int item) {
return item % t == 0;
}
}

std::function<bool(int)> f2 = Func(3);

  • 使用std::bind绑定。例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Func1(){
public:
bool func(int item) {
return item % 2 == 0;
}
bool func1(int _t, int item) {
return item % _t == 0;
}
}
func1 f1;
std::function<bool(int)> f3 = std::bind(&Func1::func, f1, std::placeholders::_1);
std::function<bool(int)> f4 = std::bind(&Func1::func, f1, 3, std::placeholders::_1);

bool func(in item) {
return item % 2 == 0;
}
bool func1(int _t, int item) {
return item % _t == 0;
}
std::function<bool(int)> fp5 = std::bind(func, std::placeholders::_1);
std::function<bool(int)> fp6 = std::bind(func1, 3, std::placeholders::_1);

使用函数对象的地方有很多,一个很典型的场景就是STL里的算法,经常会用到Predicate,它就是一个函数对象。例如

1
_InputIterator find_if(_InputIterator __first, _InputIterator __last, _Predicate __pred)

所以我们就可以直接拿上面的例子来使用,例如

1
2
3
vector<int> arr = {1,3,5,7,9,2,4,6,8};
auto iter5 = std::find_if(arr.begin(), arr.end(),fp5);
cout << *iter5 << endl;

c++11之lambda简单使用

lambda是C++11新增的功能,因为不是很熟,碰到需要回调的时候,我都是使用std::bind来绑定函数指针,但lambda有它使用方便的地方,尤其是闭包可以调用函数内的局部变量的特性,非常灵活,所以用简单的笔记记录下怎么使用,详细可以参考官方文档

语法

1
2
3
4
[ capture-list ] ( params ) mutable(可选) constexpr(可选)(C++17) exception attribute -> ret { body }
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) { body }
[ capture-list ] { body }
  1. mutable的作用是允许body修改按复制捕获的参数,及调用其非const成员函数,但修改只在lambda内起作用,不会真正改变外面的值。
  2. 可以省略-> ret指定返回值类型,返回类型为void。但有一个例外,若body只由单条带表达式return语句组成,而不含其它内容,则返回类型是被返回表达式的类型(在左值到右值、数组到指针,或函数到指针隐式转换后)
  3. 省略参数列表,等同于参数列表为()
  4. 参数列表和普通函数的写法一样,但C++14之前不允许使用默认参数和auto类型。
  5. 若隐式或显式地以引用捕获一个实体,且在该实体的生存期结束后调用闭包对象的函数调用运算符,则发生未定义行为。C++闭包不会延长被捕获引用的生存期。同样的规则应用于被捕获的this指针所指向对象的生存期
  6. lambda表达式是一个纯右值表达式,它可以被赋值给std::function类型的变量

捕获列表规则

  • [a,&b] 其中a以值捕获而b以引用捕获。
  • [this] 以值捕获this指针
  • [&] 以引用捕获所有lambda体内的odr使用的自动变量,及以引用捕获当前对象(*this),若它存在
  • [=] 以值捕获所有lambda体内的odr使用的自动变量,及以引用捕获当前对象(*this),若它存在
  • [] 无捕获

随便写个简单的例子:

1
2
3
4
5
6
std::for_each(arr.begin(), arr.end(), [](int item){
cout << item << endl;
});
std::find_if(arr.begin(), arr.end(), [](int item){
return item %2 == 0;
});

leetcode之kmp算法

kmp算法用于查找子字符串,阮老师有一篇介绍的很细致的博客基本上一遍就能看懂

整个流程可以概括为两步

  1. 生成子字符串的部分匹配表,它是一个数组,对应子字符串上每个位置上的部分匹配值。 “部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。”前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
  2. 匹配子字符串,如果子字符串匹配完,则查找成功,否则需要将匹配位置后移,后移的步数就是当前已匹配字符数量,减去最后一个匹配成功位置的部分匹配值

阮老师的文章里只讲了概念,没有算法,生成匹配表的算法,如果完全按照前后缀的概念去写,虽然能正确生成,但性能比较低。比如我写了一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//算出一个字符串的匹配值
var getNext = function(str) {
var len = str.length;
if(len === 1) {
return 0;
}
var max = 0;
for(var i = 1; i < len; i++) {
if(str.slice(0, i) === str.slice(len-i)) {
max = i;
}
}
return max;
}
//算出needle的部分匹配表
var nexts = [];
for(var i = 0; i < nlen; i++) {
nexts[i] = getNext(needle.slice(0, i+1));
}

实际使用的算法如下,又比较难理解。

1
2
3
4
5
6
7
8
9
10
11
var nexts = [-1, 0];
var j = 0;
for(var i = 1; i < nlen; i++) {
while(j > 0 && needle[j] !== needle[i]) {
j = nexts[j];
}
if(needle[j] === needle[i]) {
j += 1;
}
nexts[i+1] = j;
}

它的思路是这样的,因为我们是依次填充nexts,假设已经填充到ababa,此时nexts是[0, 0, 0, 1, 2, 3],已经得到的最长共同前后缀分别是””, “”, “a”=”a”, “ab”=”ab”, “aba”=”aba”,此时i=5,我们需要求next[5],如果needle[5] == needle[next[5]],也就是b,那我们就在前一个最长next值上加1就可以了,这个很好理解。但如果不是b,那我们就要在已有的对称’aba’=’aba’里找它的最长公共前后缀,然后比较,在这个例子里我们可以看清楚的看到是’a’=’a’,我们需要拿’a’后面的’b’与needle[i]比较,也就是needle[next[next[5]]]和needle[i]比较,这样直到找到0为止,这时表示没找到可用的对称,只好用needle[0]和needle[i]比较,如果相同,那么next[i]=1,否则为0。

这里nexts数组比needle多了1个长度,有的地方是这样,有的地方跟needle一样长,我觉得都可以,只是使用时候的区别。如果不是的话,欢迎指正。

开发中使用到的设计模式

单例模式 SINGLETON

单例在实际中使用很多,它保证一个类只有一个实例,并提供一个它的全局访问点。不论在cocos引擎还是自己的项目代码中,都有大量单例类的存在,但一般用于工厂类或者管理类,例如FileUtils, TextureCache等。保存一个全局变量并确保运行中只有一个实例,也可以看做是单例模式,比如在棋牌游戏中只会存在一个牌桌对象。

简单工厂模式 FACTORY

它在《设计模式》中叫做参数化工厂方法,它定义一个用于创建对象的接口,以一个参数作为标识符,来实例化对象。实际项目中有一个叫做WindowsManager::createWindow的方法,它根据传入的参数,创建出各种类型的弹出消息窗口。

外观模式 FACADE

这个模式也很好理解,它为复杂的子系统提供一个高层接口,以使子系统更加容易使用。举个很简单的例子,牌桌上收到某人出牌的消息,需要做很多事情,刷新他的剩余手牌数,展示他打出的牌,播放语音等等。我们把这些操作集合成一个接口,这样使用起来更方便,而且屏蔽了接口的内部实现,实现解耦合。但我们在写这个功能时,实际上并不一定需要创建一个facade类,而只是简单地封装一个函数就可以,只要理解这个概念就可以了。

观察者模式 OBSERVER

它定义了一种一对多的依赖关系,这个模式中的关键对象是目标(subject)和订阅者(observer)。这个模式不管是在cocos引擎内还是实际项目中都用到了。例如cocos引擎内,给一个Node添加触摸事件 就是订阅者向目标执行注册,当触摸事件发生时,目标会通知所有注册了的Node。它可以实现目标和订阅者的松耦合。

组合模式 COMPOSITE

将对象组合成树形结构,使得用户对单个对象和组合对象的使用具有一致性。 说到树形结构,第一反应就是cocos引擎的UI树了,它也确实是这个模式的应用,作为容器的类 Scene, Layer, Node,本身也是一个Node,使用起来就很方便。

适配器模式 ADAPTER

它将已有的接口转换成实际需要的接口。在《设计模式》中分为类适配器和对象适配器。在实际项目中,最明显的用到的地方,就是对不同平台sdk的封装,项目在接入各个平台时,他们的sdk功能都一样,无外乎登录,支付等等,但接口却不可能一样,我们通过构造一个adapter,把各种登录接口统一成一个,各种支付接口统一成一个,实际调用的时候就只需要调用这个统一的接口了。它的实现有两种方案:类适配器和对象适配器,前者使用继承,后者使用组合。

中介者模式 MEDIATOR

它用一个中介对象来封装一系列对象的交互,使得各对象不用显示的相互引用。在cocos引擎内,CCDirector就是一个中介者,通过它可以获取很多对象,比如getTextureCache, getActionManager等等,这些对象要交互的时候,彼此不需要持有引用,通过中介来获取就行了。实际项目中我将tableController作为一个中介者,牌桌上的各个子系统比如cardsController, operateController等等,都可以彼此不持有引用。

代理模式 PROXY

这个模式可以这样解释:我们想要一个实体subject,但出于某些原因,我们使用了一个代理proxy来代替它,为了能正常使用,显然我们构造的这个代理,必须与subject的接口保持一致。

在项目中有过一个例子,我们有一个通用的类soundManager,它有两个接口playMusic和playEffect。在不同的插件游戏中,播放声音和音效都是调用的这两个接口,但不同插件游戏中有不同的要求,A游戏要求播音乐时静音,B游戏要求播音效时静音,我们在不同插件中就构造不同的proxy类,它只为了代替soundManager而存在,同时也通过代替soundManager来实现了自定义的功能。当然proxy还适用于别的一些需要的场景。

享元模式 FLYWEIGHT

享元模式不要误认为是缓存池的概念,享元模式是在设计对象结构时,将可以共享的部分抽象出来进行建模。以达到减少存储开销的目的。被共享的flyweight不应直接实例化,而是通过FlyweightFactory来查找以保证共享。

装饰模式 DECORATOR

装饰模式用于动态的给对象添加额外的职责。因为装饰对象decorator要代替原组件component使用,所以接口要保持一致,在C++中通过公共父类的方式实现。装饰模式比使用继承更灵活,也避免在层次结构高层的类有太多特性。但decorator还是和component不是一样,decorator只是一个包装。

桥接模式 BRIDGE

桥接模式将抽象部分与它的实现部分分离,使它们可以独立变化。分离接口和实现部分有助于更好的结构化,比使用继承更为灵活。桥接模式适合用于分离不同维度的变化。

javaScript中的arguments

arguments

js中的每一个函数,都有一个默认的局部变量arguments,通过它可以获取到传给函数的所有实参,直接用序号获取即可,例如

1
2
3
4
5
6
7
function test(){
console.log(arguments.length); //2
console.log(arguments[0]); //1
console.log(arguments[1]); //haha
console.log(arguments[2]); //undefined
}
test(1, "haha");

虽然它有length属性和通过下标获取,但它并不是数组,它没有数组的其他属性和方法。如果想把它转成Array来使用,可以使用以下方法

1
2
3
4
var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);
var args = Array.from(arguments); (ES2015)
var args = [...arguments]; (ES2015)

一般我们在定义一个函数时,会设定参数的数量,但它可能与实际传入的参数数量不一致,在上面这个例子里,前者可以通过test.length获取到,后者则是arguments.length。 所以使用arguments的主要目的就是在不确定实际传入的参数数量的时候。

arguments与实参

arguments元素的值和实际参数的值,在没有rest parameters, default parametersdestructured parameters时,是会相互影响的

1
2
3
4
5
6
7
8
9
10
11
function test(a, b) {
console.log("a is " + a); // a is 1;
arguments[0] = 10;
console.log("a is " + a); // a is 10;

console.log("arguments[1] is " + arguments[1]) //arguments[1] is 2
b=10;
console.log("arguments[1] is " + arguments[1]) //arguments[1] is 10
}
test(1, 2);

否则就不会影响,上面例子改成 test(a, b, c=100) ,因为有了个参数c是default parameter,就导致arguments和实参互相不影响,结果就是显示

1
2
3
4
a is 1
a is 1
arguments[1] is 2
arguments[1] is 2

arguments.callee

callee是arguments的一个属性,它指向当前执行的函数。如果在一个没有函数名的闭包函数里,使用它可以实现递归,例如

1
2
3
[1, 2, 3, 4, 5].map(function(n) {
return !(n > 1) ? 1 : arguments.callee(n - 1) * n;
});

但是在ES5的严格模式里,是禁止使用callee的,而现在的很多主流浏览器,都实施了部分严格模式,所以要避免使用。

arguments.caller属性已经被废弃,不被支持了,可以使用Function.caller来代替,它指向调用当前函数的函数,如果值为null,则说明是global调用的。它虽然能被主流浏览器支持,但并没有进入标准,所以用的时候也要小心。以下是使用它实现记录当前调用栈

1
2
3
4
5
6
7
8
9
10
function stackTrace(){
var f = stackTrace.caller;
var s = "stack Trace:\n";
while(f) {
s += f.name;
s += "\n";
f = f.caller;
}
return s;
}

cocos之使用AssetsManager实现热更新

cocos提供了AssetsManager类进行热更新。这里大概介绍下它的使用方法和内部原理,引擎使用的是v3.13。首先是一段简单的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
string url = "https://.....test.zip";
string vurl = "https://.../version";
string root = CCFileUtils::getInstance()->getWritablePath();
string path =root + "test/";

FileUtils::getInstance()->createDirectory(path);

AssetsManager* am = AssetsManager::create(url.c_str(), vurl.c_str(), path.c_str(), [](int a){
CCLOG("update failed %d", a);
}, [](int b){
CCLOG("update progress %d", b);
}, [](){
CCLOG("update success");
});

am->retain();
am->checkUpdate();

这里有两个地方特别需要需要注意:

  • AssetsManager继承自Node,所以它的实例需要retain,否则无法正常工作
  • 更新包的存储路径,必须是已存在的路径,否则会导致拷贝失败而报错

可以看到使用还是很简单的,构造的参数包括更新包url地址,返回最新版本号的url地址,更新包存储路径,下载成功回调,下载失败回调,下载进度回调。 然后调用checkUpdate就开始了整个更新流程,我们按照流程看一下源代码。

AssetsManager内部通过一个_downloader来实现下载的

1
cocos2d::network::Downloader* _downloader = new Downloader();

它在构造函数里初始化,并设定好onTaskError, onTaskProgress, onDataTaskSuccess, onFileTaskSuccess四个回调。

  1. 当我们调用checkUpdate开始一次热更任务时,它使用_downloader创建一个下载任务,根据_versionFileUrl来获取需要更新资源的版本号。如果获取成功,会进入onDataTaskSuccess回调中,这里会获取本地存储的版本号,与之进行比较,如果相等,说明本地已经是最新版本,不需要更新。然后检查一下本地已下载版本,如果等于获取的版本号,说明更新包已经下载但尚未解压,直接执行解压操作。否则创建一个下载任务开始下载。
  2. 在下载进度回调中,获取到当前下载的百分比
  3. zip下载完成后,进行解压缩,如果失败,则把已下载成功的版本号记录在本地,避免再次下载。如果成功,则解压缩在我们设定的存储目录下,将本地版本目录更新至当前版本。删除已下载的zip文件,将存储目录添加到searchPath的最前面。

关于searchPath,它是一个队列,实现位于CCFileUtils内。cocos引擎在使用某个资源时,如果指定的资源路径不是绝对路径,就会依次使用searchPath内的路径合成一个绝对路径,如果找到了文件,就直接使用这个文件,所以我们热更新后的目录,设为searchPath的第一个,就可以实现替换掉老资源的功能。当然如果想节省空间,在下载成功的回调里,删除掉上个版本的目录就可以了。

cocos之Scene的切换

场景切换,牵涉到的是老scene善后处理和新scene的初始化,如果对各个函数调用的先后顺序不明的话,可能就会出问题(例如注册监听和取消监听,一般都写在onEnter和onExit方法内)

涉及到scene切换的方法,都在director内,常用的为

1
2
void Director::runWithScene(Scene *scene)
void Director::replaceScene(Scene *scene)

Director内通过_scenesStack, _runningScene,_nextScene进行管理。每帧都会调用drawScene方法,这个方法里,有一段代码。

1
2
3
4
5
6
/* to avoid flickr, nextScene MUST be here: after tick and before draw.
*/
if (_nextScene)
{
setNextScene();
}

这里还有一些注释,说明了切换scene的操作,在老scene的事件都处理完了之后,渲染新scene之前。这里setNextScene就是执行了切换scene的逻辑。

scene的切换分为有渐变和没有渐变两种情况

  • 没有渐变的时候,切换的回调会直接调用,具体可以看源代码。顺序为
1
2
3
4
5
_runningScene->onExitTransitionDidStart();
_runningScene->onExit();
//此时_runningScene已经指向了新scene
_runningScene->onEnter();
_runningScene->onEnterTransitionDidFinish();
  • 有渐变的情况其实也简单,渐变的效果是通过TransitionScene类来实现的,它将新scene存为_inScene, 需要被替换掉的老scene存为_outScene,然后它的draw函数会同时渲染这两个scene。为了将逻辑与视觉效果相符,新旧scene的执行回调也发生了变化,老scene的onExit延迟到新scene进场完毕后才调用
1
2
3
4
5
_outScene->onExitTransitionDidStart();
_inScene->onEnter();

_outScene->onExit();
_inScene->onEnterTransitionDidFinish();

关于TransitionScene的一些细节:

  • TransitionScene的各个子类,都只是实现了进场动画,并没有哪个实现有离场动画
  • TransitionScene的finish回调中会执行director->replaceScene(_inScene);设置真正的新scene。Director::setNextScene方法里,runningIsTransition只有在这时候才可能为true,否则如果老scene是个TransitionScene,意味着进场动画还没有结束,node的_running为true,此时不运行进行切换的,强制切换会触发assert。
  • 不要妄想使用连环Transition,原因和上面一样,进场动画还没执行完就切换scene,例如
1
2
3
4
TransitionFade* fade = TransitionFade::create(2, scene, Color3B::GREEN);
TransitionFade* fade1 = TransitionFade::create(2, fade, Color3B::GREEN);
TransitionFade* fade2 = TransitionFade::create(2, fade1, Color3B::GREEN);
Director::getInstance()->replaceScene(fade2);