cocos之使用jpg+mask合成png实现方式(二)

第二种方式就是使用shader,这种方式的优点是不需要修改源代码,但性能上不如第一种方式。

编写shader涉及到顶点着色器和片断着色器,前者使用sprite默认的即可。sprite使用哪个着色器,只要看一下CCSprite.cpp里这句就找到了

1
setGLProgramState(GLProgramState::getOrCreateWithGLProgramName(GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP, texture));

然后我们稍微改动一下片断着色器,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef GL_ES
precision lowp float;
#endif

varying vec4 v_fragmentColor;
varying vec2 v_texCoord;

uniform sampler2D mask;

void main()
{
gl_FragColor = v_fragmentColor * texture2D(CC_Texture0, v_texCoord);
gl_FragColor.a = texture2D(mask, v_texCoord).a;
}

我们通过代码将mask作为一个Texture传进来,然后取它的每个像素的alpha值拿来用。

使用代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GLProgram *gp = GLProgram::createWithFilenames("res/test.vert", "res/test.frag");
GLProgramState* gs = GLProgramState::create(gp);

Image* img = new Image();
img->initWithImageFile("res/zjz_pop_word_title_zhanji.png");
Texture2D* t1 = new Texture2D();
t1->initWithImage(img);

gs->setUniformTexture("mask", t1);

Sprite* s = Sprite::create("res/splash_tuyoo_m.jpg");
s->setGLProgramState(gs);
s->setPosition(480,320);
this->addChild(s);

这里只需要小心一点不要把s->setGLProgramState(gs);写成s->setGLProgram(gp);就行,node上同时提供了这两个接口,但使用后者会创建一个新的GLProgramState,我们已经创建的gs也就失去了作用了

cocos之使用jpg+mask合成png实现方式(一)

第一种方式,在CCImage内读取图片数据后,合并起来使用。改写Image类,增加initWithJpgAndPng方法

代码如下

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
bool Image::initWithJpgAndPng(const std::string& jpgpath, const std::string& pngpath) {
bool ret = false;
unsigned char* jd = nullptr;
do{
std::string jp = FileUtils::getInstance()->fullPathForFilename(jpgpath);
Data jpgdata = FileUtils::getInstance()->getDataFromFile(jp);

unsigned char* jdata = jpgdata.getBytes();
ssize_t jsize = jpgdata.getSize();

ret = initWithJpgData(jdata, jsize);
if(!ret) {
break;
}

int jwidth = _width;
int jheight = _height;

ssize_t jlen = _dataLen;
jd = static_cast<unsigned char*>(malloc(jlen * sizeof(unsigned char)));
memcpy(jd, _data, jlen * sizeof(unsigned char)); //将jpg数据暂存起来

std::string pp = FileUtils::getInstance()->fullPathForFilename(pngpath);
Data pngdata = FileUtils::getInstance()->getDataFromFile(pp);
unsigned char* pdata = pngdata.getBytes();
ssize_t psize = pngdata.getSize();
ret = initWithPngData(pdata, psize);
if(!ret) {
break;
}

int pwidth = _width;
int pheight = _height;
if(pwidth != jwidth || pheight != jheight) {
break; //要求长宽必须严格相同
}

int pindex = 0;
int jindex = 0;

for(int index = 0; index < pwidth * pheight; index++) {
unsigned char alpha =*(_data+(pindex+3));

*(_data+pindex) = *(jd + jindex) * alpha / 255;
*(_data+(pindex+1)) = *(jd + jindex+1) * alpha / 255;
*(_data+(pindex+2)) = *(jd + jindex+2) * alpha / 255;

pindex += 4;
jindex += 3;
}
}while(0);

if(jd) {
CC_SAFE_FREE(jd);
}

return ret;
}

tips:

  1. 这里为了验证思路,找了个RBGA8888的png做mask图,如果使用的mask图不是该格式,则需要修改_renderFormat, _fileType等属性。
  2. 因为cocos默认png图片是pre_multi_alpha的,所以我们在加入alpha数据时,需要同时将alpha乘到rgb上

使用时代码如下:

1
2
3
4
5
6
7
8
Image* image = new Image();
image->initWithJpgAndPng("res/test1.jpg", "res/test2.png");

Texture2D* t = new Texture2D();
t->initWithImage(image);
Sprite* s = Sprite::createWithTexture(t);
s->setPosition(480,320);
this->addChild(s);

cocos3.x 渲染机制简述

cocos2dx-3.x对绘制部分进行了重构,将绘制从UI树的遍历中分离了出来。首先进行UI树的遍历给每个元素生成一个绘制命令。等遍历完之后,render开始执行栈中所有renderCommand。

遍历UI树

遍历UI树很简单,就是调用每个Node的visit函数,这是个虚函数,除了部分类做了重写(目前源代码里只有三个类重写了,分别是CCAttachNode, CCBillBoard, CCSprite3D),其余就沿用了Node的实现。通过

1
void Node::visit(Renderer* renderer, const Mat4 &parentTransform, uint32_t parentFlags)

我们可以看到

  1. 如果一个Node是不可见(_visible为false),则不会生成渲染命令,因此隐藏的节点不会增加渲染负担,只会占用内存消耗。
  2. 在遍历时,会对所有子节点以localZorder从小到大进行排序,如果localZorder相同,则以_orderOfArrival从小到大排序,这个子节点被添加的先后顺序。排序后先执行所有localZorder小于0的子节点的visit,再执行自己的draw,再执行所有localZorder大于0的子节点的visit。

draw函数为虚函数,且Node内的实现为空,具体实现在各个子类中,它的作用就是生成RenderCommand

RenderCommand

RenderCommand有以下类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum class Type
{
/** Reserved type.*/
UNKNOWN_COMMAND,
/** Quad command, used for draw quad.*/
QUAD_COMMAND,
/**Custom command, used for calling callback for rendering.*/
CUSTOM_COMMAND,
/**Batch command, used for draw batches in texture atlas.*/
BATCH_COMMAND,
/**Group command, which can group command in a tree hierarchy.*/
GROUP_COMMAND,
/**Mesh command, used to draw 3D meshes.*/
MESH_COMMAND,
/**Primitive command, used to draw primitives such as lines, points and triangles.*/
PRIMITIVE_COMMAND,
/**Triangles command, used to draw triangles.*/
TRIANGLES_COMMAND
};

在CCRender.cpp的processRenderCommand函数里,有各个类型command的具体处理。例如 CUSTOM_COMMAND就是执行设定的回调函数

通过这里可以了解cocos2dx-3.x的autoBatch机制,Sprite使用的是TrianglesCommand,在处理时,会先统一放在队列里,在遍历队列时,比较每个Command的materialID,如果相同则统一处理。决定materialID的有glProgram, _textureID,_blendType。此外不同globalZorder的精灵因为不在一批处理,所以不会自动autoBatch,不同父节点,不同localZOrder则不会影响,会自动autoBatch

cocos-addImageAsync解析

当我们在cocos内需要做一些比较耗性能的事情时,我们可以一些很巧妙地方案,例如开启多线程,以及将任务分解到每一帧完成一部分。在TextureCache的addImageAsync方法里,就同时用到了这两种办法,把这部分源码看明白,对自己实现能有很大的帮助

类TextureCache维护一个队列_asyncStructQueue,它存放需要执行的任务,这些任务将在异步线程执行,加载生成Image类

1
std::queue<AsyncStruct*>* _asyncStructQueue;

一个双向队列_imageInfoQueue,他存放加载好的Image,这些加载好的图片资源就会每帧从队列中获取一个,用来生成纹理Texture2D,生成好的纹理就可以在游戏内直接使用了

1
std::deque<ImageInfo*>* _imageInfoQueue;

这两个队列都是先进先出的操作,我也不是很明白为什么要用deque,有知道的可以告诉我一下。然后这两个队列因为都需要在异步线程操作,所以操作时需要用锁锁住。

使用一个_asyncRefCount来记录当前任务的数量,当往_asyncStructQueue添加一个任务asyncStruct时,计数+1,当一个纹理生成完成,销毁该任务asyncStruct,计数-1

1
int _asyncRefCount;

入口

需要异步加载一个图片时,首先调用

1
void TextureCache::addImageAsync(const std::string &path, const std::function<void(Texture2D*)>& callback)

首先判断一下这个path对应的texture是否已经在缓存里,如果在,则直接返回。否则如果_asyncStructQueue还未初始化,则执行初始化,然后生成一个AsyncStruct加入队列_asyncStructQueue中。

异步线程

类TextureCache维护一个异步线程,这个线程在_asyncStructQueue初始化时开启

1
std::thread* _loadingThread;

在这个线程执行的函数是

1
void TextureCache::loadImage()

这个线程通过std::condition_variable _sleepCondition来唤醒和休眠,当有任务加入任务队列_asyncStructQueue时,唤醒线程执行loadImage函数,在这个函数里发现_asyncStructQueue被清空时,休眠这个线程。

在执行loadImage时,会先看一下需要加载的图片是否已经在处理队列_imageInfoQueue中了。如果不在,则生成一个新的Image,执行

1
image->initWithImageFileThreadSafe(filename)

如果initWithImageFileThreadSafe返回true,则表示这张图片资源加载成功了,用这个image生成一个imageInfo放到_imageInfoQueue里。

定时回调

这个定时回调每帧都会被调用一次,它将上一步生成的image变成Texture2D。当_asyncRefCount从0变为1时,表示开始有任务需要完成,开启这个定时器,当_asyncRefCount变为0时表示任务全部完成,这个定时器会被关闭。

1
Director::getInstance()->getScheduler()->schedule(CC_SCHEDULE_SELECTOR(TextureCache::addImageAsyncCallBack), this, 0, false);

定时执行的函数就是addImageAsyncCallBack了,在这个函数里,通过

1
texture->initWithImage(image);

生成Texture2d,并存在缓存里,每次通过TextureCache加载纹理的时候,都会先看看是否已经存在缓存里了。
到这里,这个任务就完成了,执行下回调函数,然后把_asyncRefCount减一,搞定了

cocos图片资源加密

无意中看到hnliu’sblog上的方案,看完思路后,自己也照着写了一个,虽然不复杂,但是也花了将近两个小时。

python代码如下

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import os
import random

ENCRYPTBYTE = random.randint(1,255) #不能随机到0,否则等于没加密
FIRSTBYTE = 0x12
SECONDBYTE = 0X34
THIRDBYTE = 0x56

print("encrypt key is " + str(ENCRYPTBYTE))

def getNewFileName(path):
arr = os.path.split(path)
dirname = arr[0]
filename = arr[1]
nameArr = filename.split(".")
return os.path.join(dirname, nameArr[0]+"-en." + nameArr[1])

def encrypt(path):
rf = open(path, "r")
newpath = getNewFileName(path)

wf = open(newpath, "w")

bytes = bytearray(rf.read())
if bytes[0] == FIRSTBYTE and bytes[1] == SECONDBYTE and bytes[2] == THIRDBYTE:
print "encrypted already, return"
else:
index = 4
newarr = bytearray(len(bytes) + 4)
newarr[0]= FIRSTBYTE
newarr[1]= SECONDBYTE
newarr[2]= THIRDBYTE
newarr[3]= ENCRYPTBYTE
for byte in bytes:
newb = byte ^ ENCRYPTBYTE
newarr[index] = newb
index += 1
wf.write(newarr)
wf.close()
rf.close()

if __name__ == '__main__':
encrypt("/Users/yangguang/project/test/test_cocosx/Resources/HelloWorld.png")

cocos端解析代码为把原来的initWithImageData改名为initWithImageDataInternal,然后重写initWithImageData方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool Image::initWithImageData(const unsigned char *data, ssize_t dataLen) {
char first = *data;
char second = *(data+1);
char third = *(data+2);
if(first == 0x12 && second == 0x34 && third == 0x56) {
char key = *(data+3);
unsigned char newarr[dataLen - 4];
ssize_t pos = 4;
while(pos < dataLen) {
char old = *(data+pos);
newarr[pos - 4] = old ^ key;
pos += 1;
}
return initWithImageDataInternal((const unsigned char*)(&newarr) , dataLen - 4);
}
return initWithImageDataInternal(data, dataLen);
}

中间出了一个比较土的问题,就是python代码中newarr[index] = newb这句,用了newarr.append(),因为newarr初始化了大小,导致newarr前面全是0,数据都被添加到后面去了

javaScript中的proto

__proto__

js的原型链继承很简单,对任意一个object,都有__proto__属性,当获取这个对象的属性或者方法时,现在对象本身去查找,如果不存在,则往它的__proto__上查找,因为__proto__本身也是一个object,所以如果没找到,则往__proto__的__proto__上查找,跟沿着一条链条走一样,直到找到返回,或者__proto__为null或undefined找到了尽头。

原型链不可能无止尽的延伸,所以js中有一个默认的object,它的__proto__就是undefined,除非我们强制打断原型链,否则最终都会查找到这个object上,它是长这个样子的,可以看到它是没有__proto__属性的

image

我们可以对一个object的__proto__属性随意赋值,但如果赋成null或者undefined,则它的__proto__属性变成了undefined,如果赋成基础数据类型,并不会改变其__proto__属性

__proto__来源

如果我们没对object的__proto__赋值,则它的__proto__来源于使用Object.create方法调用时传给他的参数,虽然构造一个object有好几种写法,例如

var a = {}
var b = new f();
var c = Object.create(proto)

但它们实际上都是执行了Object.create,例如var a = {}和var a = Object.create(Object.prototype)是一模一样的,而var b = new f()则基本可以认为等同于var b = Object.create(f.prototype)。所以上面例子里,a的__proto__是Object.prototype, b的__proto__是f.prototype, c的__proto__是proto

function的prototype

我们经常通过function来构造一个object,这种情况使用了该function的prototype来作为Object.create的参数,如果我们没有给function指定一个prototype,则它默认的prototype是这样的

img

可以看到,function默认的prototype有一个属性constructor指向函数自己,它的__proto__就是我们前面提到的原型链尽头的那个object(真该给他取个什么名字才好),当然我们可以给function指定一个prototype,而不使用它默认的,那就是我们在js中实现继承的办法。

function本身也是一个对象,所以上图里f也有__proto__属性,它的__proto__就是Function.prototype。Function是javascript解释器提供的实现,就不用再去追究它的prototype是什么了。

使用原型链实现继承

知道原型链是什么东西之后,就可以开始实现继承了。ES6开始有了class和extends关键字,就可以不用手动实现继承,但在ES6之前,我们需要自己来实现,例如

var base = {
    prop1:1,
    prop2:{
        a:1
    },
    func:function(){
        console.log("in base...");
    }
}

function Derived(){

}

Derived.prototype = base;
Derived.prototype.constructor = Derived;

var d1 = new Derived();
var d2 = new Derived();

d1.func();
d2.func();

console.log(d1.prop2.a);
d1.prop2.a = 2;
console.log(d2.prop2.a);

上面代码里,d1,d2是子类Derived的实例,它们继承了base里的属性和方法,所以可以直接调用d1.func(),也可以直接获取和设置d1.prop2.a的值,但也很容易发现问题,就是我们修改了d1.prop2.a的值,结果d2.prop2.a的值也改变了。prototype上的属性和方法,对于子类的实例是公共的,这是需要注意,容易出问题的地方。

这里顺便牵扯到一个问题,我们都知道object的constructor属性指向它的构造函数,所以在设定prototype时,必须再将prototype.constructor指向构造函数。就是上面代码里的

Derived.prototype.constructor = Derived;

如果没有注释掉的那句,则d1.constructor就不是Derived,而是变成了function Object(){[native code]}了

因为ES6之前没有原生的继承机制,很多地方都使用了John Resig的一种简单实现方式,使用25行代码实现了extends功能。

缩短原型链

如果原型链过长,可能会带来性能问题。所以需要注意的是尽量不要去扩展内置类型的原型。直接获取属性和for in函数都会查询原型链,如果想避免查询原型链,可以使用hasOwnProperty方法来判断

使用var a = Object.create(null) 代替 var a = {}的区别,前者明确将__proto__设置为null,查找属性时就不会再往原型链上查找了,后者__proto__其实是Object.prototype

js对象与继承(二)

类继承与原型继承

先分别举个例子吧,首先是原型继承

1
2
3
4
5
6
7
var proto = {"a":1,"b":{"c":2}}
var f1 = function(){}
var f2 = function(){}
f1.prototype = proto
f2.prototype = proto
var t1 = new f1()
var t2 = new f2()

其次是类继承

1
2
3
4
5
6
7
8
9
10
var proto = function(){
this.a = 1;
this.b = {"c":2};
}
var f1 = function(){}
var f2 = function(){}
f1.prototype = new proto()
f2.prototype = new proto()
var t1 = new f1()
var t2 = new f2()

它们的区别就在于,类继承先定义了一个基类proto,在继承时,子类构造函数的prototype是根据这个基类new出来的一个对象。

这两种方式各有优劣:

正如上一篇说到的,如果执行代码

1
2
t1.b.c = 12
console.log(t2.b.c)

对于原型继承,因为proto被改变了,所以t2.b.c也变成了12,而对于类继承则不会有问题,这对于不够熟悉js的人来说绝对是个坑。

不过原型继承实现起来比类继承要更灵活,因为不需要做抽象基类的工作,但如果随意地使用原型继承,例如用作prototype的对象是一个非常庞大复杂的对象,那显然会产生问题。

js对象与继承(一)

大家都知道在js中创建一个对象的一种很基本的方式

1
2
3
4
function f(){
this.a = 1;
}
var test = new f();

当执行new的时候,系统实际做的事情分了几步

  1. var obj = Object.create(f.prototype)
  2. var result = f.call(obj)
  3. result && typeof result === ‘object’ ? return result : return obj

用语言描述就是,首先用f.prototype构造一个Object,这里命名为obj,然后将obj作为参数this去调用构造函数f,返回值为result,如果result是一个对象,则返回它,也就意味着test = result,否则 test = obj。

然后再看prototype是什么东西吧,举个例子

1
2
3
4
5
6
7
var p = {"a":1, "b":{"c":2}}
var f = function(){this.d = 3}
f.prototype = p
var t = new f()
console.log(t.d)
console.log(t.a)

这里p是一个对象,f是一个构造函数,将f的prototype设为p,当使用f构造出一个对象t出来时,t会有一个属性__proto__,它就指向了p

当我们获取t.d时,t是有d属性的,这个没问题
当我们获取t.a时,t没有a属性,此时会沿着它的原型链往上查找,也就是查找它的__proto__,也就是p,p是有a属性的,所以将a的值返回。我们可以使用Object类上的一个方法hasOwnProperty来判断一个属性或方法是对象本身的,还是原型链上的。例如

1
2
console.log(t.hasOwnProperty("a"))	//false
console.log(t.hasOwnProperty("d")) //true

上面说的是取值,赋值的时候就稍微复杂些,例如

1
2
t.a = 11;
t.b.c = 12

执行第一句话,也就是对于基础数据类型,在chrome控制台上可以看到t的属性列表里出现了a并且值为11,而p的a仍然是1不变。也就是对原型上的基础数据类型赋值,不会影响原型,而是自身多了一个这样的属性。但使用hasOwnProperty看,仍然为false,这应该是js的一些不完善的地方吧。

执行第二句话,可以看到p的b变成了{“c”:12},也就是修改原型链上的非基础数据类型,会改变原型链本身。这也是javascript中使用类继承比使用原型继承更好的原因之一。下篇再分别讲这两种继承方式

svn忽略文件

在svn项目中,对于不需要加入版本管理的文件,可以使用svn propedit svn:ignore来进行设置

在执行这句之前,需要先设置好SVN_EDITOR环境变量,例如在~/.bash_profile内添加

1
export SVN_EDITOR=emacs

然后执行

1
svn propedit svn:ignore .

这里的.表示对当前目录进行设置,此时会打开你指定的编辑器界面,在里面写上你需要忽略的文件名或者正则表达式即可.
但这句命令,只会影响你指定的路径的子文件及文件夹,而不会递归影响到更深层次。例如在编辑界面填上

1
2
dir1
dir2/dir21/dir211

这里dir1会被忽略,而dir211不会,如果需要忽略dir211,要么在当前目录下执行svn propedit svn:ignore dir2/dir21 要么切换到dir2/dir21目录下执行svn propedit svn:ignore . 然后填写的内容都是dir211

在网上看到一个解释

1
2
3
4
5
6
7
8
9
Each and every directory in Subversion can be thought of its own
module, so there's no real way for Subversion to know that
foo/bar/barfoo is a directory in module /foo/bar, or a another
separate module module.

That means there's no way for Subversion to know how to handle
properties that can affect an entire directory tree. Plus, it would be
difficult to know exactly what parent directory is affecting a child
directory.

最后提醒一下,只有还没有被加入版本管理的文件会被忽略,如果文件已经加入了版本管理,那设置了忽略也没用。其次,设置了忽略的文件,仍然可以被添加进版本管理中。

cocos屏幕适配策略

cocos的屏幕适配包括两个方面,一个涉及到屏幕的宽高比,一个涉及到缩放因子。

宽高比

我打算尽量用通俗的语言来说这件事情,cocos处理不同宽高比的适配,使用了设计分辨率这么个东西,就是

1
void GLView::setDesignResolutionSize(float width, float height, ResolutionPolicy resolutionPolicy)

resolutionPolicy有好几种选择,但我们通常只使用FIXED_HEIGHT和FIXED_WIDTH。

设计分辨率的意义就是:你不用管实际上屏幕是什么样,你通过代码或者工具给一个node设定尺寸和坐标时,都只想象它是放在设计分辨率的屏幕里,然后给他设计尺寸和坐标,最后显示在实际屏幕上时,引擎会进行适当的比例缩放。

我们一般在项目中使用的ResolutionPolicy是FIXED_HEIGHT和FIXED_WIDTH,前者一般用于手机横屏,后者用于手机竖屏。我们拿FIXED_WIDTH举例,使用这个模式,意味着采用宽度的缩放比例,那么UI节点的x坐标和width就不用担心了,系统会帮你完美适配好来,y坐标和height则必须使用屏幕高度的百分比来进行设置,只要在需要适配的宽高比最大(通俗说就是最“扁”)的屏幕上不会发生冲突即可。FIXED_HEIGHT也是类似的道理,因为使用了高度的缩放比例,那么x坐标和width必须使用屏幕宽度的百分比来进行设置,保证在需要适配的宽高比最小的屏幕上不发生冲突即可。当然实际中我们对不管xy坐标还是width,height都习惯性采用百分比坐标。这两种方案的适用情况不能靠死记硬背,只要明白了原理,其实很容易理解。

缩放因子

缩放因子可以理解为图片资源尺寸与设计分辨率的比值。一般我们的资源都是配合设计分辨率而出的,但如果不是,就需要设置缩放因子了,比如一种情况就是,同样是1024*768的设计分辨率,我们有一套1024*768的资源,这套资源在2048*1536的屏幕上会被拉伸两倍显示,可能效果就不好了,那我们再准备一套2048*1536的资源,因为设计分辨率还是1024*768,所以就需要将缩放因子设为2了。

设置缩放因子的接口是:

1
void Director::setContentScaleFactor(float scaleFactor)