cocos2D主要是基于GLSurfaceView实现的,它是cocos2d架构的基础。下面就GLSurfaceView的实现进行一个分析。
GLSurfaceView是cocos2d里一个很重要的类,它负责创建render thread(即GLThread),处理事件(例如KeyEvent,MotionEvent),渲染视图等等工作。下面就GLSurfaceView的实现机制进行初步分析,由于才疏学浅,难免有错误或疏漏之处,请大家多多包涵和指正。
类层次图如下:
java.lang.Object |
|||
|
android.view.View |
||
|
|
android.view.SurfaceView |
|
|
|
|
org.cocos2d.opengl.GLSurfaceView |
SurfaceView是一个Android系统为需要绘制复杂画面的程序员提供的强有力的工具视图类,和OpenGL没有直接的关系,它的子类GLSurfaceView负责把OpenGL连接到Android视图系统。
下面主要谈谈SurfaceView的实现中比较重要的地方:
1.1.1 SurfaceView和WindowManager Service(下文中将用WMS来表示)的联系
SurfaceView有个内部类MyWindow,通过它和WMS建立连接关系。默认情况下,SurfaceView位于MyWindow的后面,代码在SurfaceView.java中,处理如下:
int mWindowType =WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
TYPE_APPLICATION_MEDIA的注释如下,
/**
* Window type: window for showing media(e.g. video). These windows
* are displayed behind their attachedwindow.
*/
public static final intTYPE_APPLICATION_MEDIA =FIRST_SUB_WINDOW+1;
大家可以参考。另外,可以通过SurfaceView.setZOrderMediaOverlay和SurfaceView. setZOrderOnTop来改变Surface在MyWindow中的位置。
SurfaceView和WMS建立连接关系的代码如下:
private voidupdateWindow(boolean force) {
……
if(mWindow == null) {
mWindow = new MyWindow(this);
mLayout.type = mWindowType;
mLayout.gravity = Gravity.LEFT|Gravity.TOP;
mSession.add(mWindow, mLayout,
mVisible ? VISIBLE : GONE, mContentInsets);
}
……
}
从此以后WMS就可以管理窗口的显示,把事件传递给窗口。
1.1.2 SurfaceView的Handler
SurfaceView有个内部成员mHandler,它的message来自于UI线程的Looper。代码如下:
final HandlermHandler = new Handler() {
@Override
public voidhandleMessage(Message msg) {
switch(msg.what) {
caseKEEP_SCREEN_ON_MSG: {
setKeepScreenOn(msg.arg1 != 0);
}break;
caseGET_NEW_SURFACE_MSG: {
handleGetNewSurface();
}break;
caseUPDATE_WINDOW_MSG: {
updateWindow(false);
}break;
}
}
};
通过这样的处理,是代码逻辑更加清晰,增强代码的模块化程度。
1.1.3 SurfaceView和SurfaceHolder的关系
SurfaceHolder是SurfaceView的代理,通过它可以添加新的SurfaceHolder.Callback,设置Surface的尺寸和格式,获取canvas和在canvas上描画。
几个需要注意的方法:
(1) abstract void addCallback(SurfaceHolder.Callback callback);
// 给SurfaceView当前的持有者一个回调对象。
(2) abstractCanvas lockCanvas();
// 锁定画布,一般在锁定后就可以通过其返回的画布对象Canvas,在其上面画图等操作了。
(3) abstractCanvas lockCanvas(Rect dirty);
// 锁定画布的某个区域进行画图等..因为画完图后,会调用下面的unlockCanvasAndPost来改变显// 示内容。 相对部分内存要求比较高的游戏来说,可以不用重画dirty外的其它区域的像素,可以提// 高速度。
(4) abstract voidunlockCanvasAndPost(Canvas canvas);
// 结束锁定画图,并提交改变。
1.1.4 SurfaceHolder.Callback的调用时机
先看代码,处理如下:
private voidupdateWindow(boolean force) {
……
if(visibleChanged && (!visible || mNewSurfaceNeeded)) {
reportSurfaceDestroyed();
}
……
if(visible) {
mDestroyReportNeeded = true;
SurfaceHolder.Callback callbacks[];
synchronized (mCallbacks) {
callbacks = new SurfaceHolder.Callback[mCallbacks.size()];
mCallbacks.toArray(callbacks);
}
if (visibleChanged) {
mIsCreating = true;
for (SurfaceHolder.Callback c : callbacks) {
c.surfaceCreated(mSurfaceHolder);
}
}
if (creating || formatChanged || sizeChanged
||visibleChanged || realSizeChanged) {
for (SurfaceHolder.Callback c : callbacks) {
c.surfaceChanged(mSurfaceHolder, mFormat,myWidth, myHeight);
}
}
……
}
private voidreportSurfaceDestroyed() {
if(mDestroyReportNeeded) {
mDestroyReportNeeded = false;
SurfaceHolder.Callback callbacks[];
synchronized (mCallbacks) {
callbacks = new SurfaceHolder.Callback[mCallbacks.size()];
mCallbacks.toArray(callbacks);
}
for(SurfaceHolder.Callback c : callbacks) {
c.surfaceDestroyed(mSurfaceHolder);
}
}
super.onDetachedFromWindow();
}
一般情况下,GLSurfaceView的事件处理很简单,描述如下:
首先,自定义一个GLSurfaceView的子类MyGLSurfaceView,重载View的函数OnTouchEvent,其中用函数queueEvent把事件传递到GLThread,GLThread在函数guardedRun中处理事件;
其次,在MyActivity的onCreate函数中,创建MyGLSurfaceView后用setContentView将之和WMS联系起来,从而使MyGLSurfaceView能从android系统中接受事件;
最后,当按键事件发生后,就会在GLThread的guardedRun中处理它。具体代码如下:
private voidguardedRun() throws InterruptedException {
……
if (! mEventQueue.isEmpty()) {
event =mEventQueue.remove(0);
break;
}
……
if(event != null) {
event.run();
event = null;
continue;
}
……
}
这里要特别说明一下GLThread存在的必要性:一般情况下,app的处理事件时往往会更新界面,界面图形绘制也是比较简单的,这个处理过程不能超过5秒。这是android系统的强制要求,原因是google考虑到app要及时的响应用户操作,避免由于app长时间无响应而造成假死现象。在游戏开发中,图形的绘制一般比较复杂,耗时多,所以为了避免app长时间处理图形绘制而被android系统强制杀死,就需要把图形的绘制放在另外的一个线程中,以提高渲染效率。GLSurfaceView就是这样处理,用UI Thread接受事件,GLThread根据事件更新图形。
但是cocos2d不是这样处理事件的,它考虑的更加细致,采用了一个固定的刷新率。为什么这样呢?大家想象一下,不同机器的硬件配置不同,刷新率不同。在配置高的机器上,由于刷新率很快,可能会超过人眼极限(120帧/秒),这时会出现无法分辨图形的现象;在配置低得机器上,如果刷新率低于24帧/秒,画面会出现明显的停顿现象;另外,如果根据机器的配置设置刷新率的话,会造成不同机器上游戏的速度不同,这样也不好;最后,如果按照机器的实际能力刷新的话,由于不同场景的复杂度不同,会造成复杂度高的画面刷新慢,复杂度低的画面刷新快的现象,这种快慢交替的现象会让用户极度不适应的。
由于cocos2d的刷新率是一定的,所以处理事件的速度也是一定的。这样它就采用了一种新的方案来处理事件,方法如下:把事件存储在CCTouchDispatcher等dispather中,在渲染器CCDirecor进行画面更新时处理事件。函数调用流程如下:
GLThread.guaredRun
-> CCDirector.onDrawFrame
->CCTouchDispatcher.update
->CCTouchDispatcher.touchesBegan
->CCTouchHandler.ccTouchesBegan
->CCTouchDelegateProtocol.ccTouchesBegan
Android app是基于事件驱动的。所谓的事件驱动,其实就是水来土挡,兵来将挡的意思。这里的水和兵指的是事件,土和将指的是事件处理函数。Android的framework给程序员制定了处理app的框架,通过该框架,程序员只要重载一些必要的回调函数就可方便的实现android程序框架,然后就可以集中精力于app的逻辑处理。
cocos2d是一个游戏引擎,为了方便游戏开发者的使用,它的android实现版本也制定了一个框架。看一下它在android系统中的位置:
图1 cocos2d在android系统中的位置
接下来我们看一个基于cocos2d的activity的启动,例子请参见cocos2d工程下的ClickAndMoveTest.java。从这里我们可以了解cocos2d框架为我们做了些什么,CCGLSurfaceView、CCDirector、CCNode、CCLayer、CCTouchDispatcher、CCScheduler、CCActionManager、CCAction等这些类是如何联系起来的。
图2 ClickAndMoveTest Activity的启动sequence图
由于流程复杂,图比较大,为了控制复杂度,对androidframework部分的处理进行了大幅度的简化。
大家知道,一般情况下,androidapp可以看做是一个大的循环结构,可以简化为
main()
{
event = getMessageFromPool();
While (event)
{
handleMessage(event);
}
}
这样的结构。结构中的handleMessage是由handler组成的。在此和我们关系比较密切的handler是ActivityThread.H和ViewRoot。ActivityThread.H主要负责activity的创建、启动、暂停和销毁等事件处理,ViewRoot负责View的描画处理和按键、触屏等事件处理。
通过图2,可以看出在activity的启动过程中,cocos2d的框架如何和android系统结合在一起。
大家先看张图,
图3 按键/触屏事件传递过程图
图3描述了keyEvent,TouchEvent从硬件到当前app的过程。我们从图中的focusview开始谈谈事件的处理过程。
在cocos2d中,用到的view是CCGLSurfaceView,它注册touchListener以便于处理touchEvent。touchListener的作用是从app的UI Thread中将touchEvent传递到CCTouchDispatcher中,等待GLThread处理。Sequence可参见从图2的第11步。
具体代码如下:
publicboolean onTouch(View v, MotionEvent event) {
mDispatcher.queueMotionEvent(event); -- 将touchEvent放入CCTouchDispatcher队列
synchronized(CCDirector.sharedDirector()) {
try{
CCDirector.sharedDirector().wait(20L);
}catch (InterruptedException e) {
//Do nothing
}
}
returntrue;
}
MainLayer的触屏事件处理注册可参见图2的第14步到第19步。
具体看一下addHandler的实现:
private voidaddHandler(final CCTouchHandler handler, final ArrayList array) {
// post to gl thread and no need to dosync
GLResourceHelper.sharedHelper().perform(newGLResourceHelper.GLResorceTask() {
@Override
publicvoid perform(GL10 gl) {
int i = 0;
for( int ind = 0; ind <array.size(); ind++ ) {
CCTouchHandlerh = (CCTouchHandler)array.get(ind);
if( h.getPriority() <handler.getPriority() )
i++;
if( h.getDelegate() ==handler.getDelegate() )
throw new RuntimeException("Delegatealready added to touch dispatcher.");
}
array.add(i, handler);-- 在这里将MainLayer作为一个CCTouchHandler加入到touchHandlers中
}
});
}
这样,就为MainLayer的触屏事件处理做好了准备。换句话说,我们的将已经准备好了,就等着敌兵的到来了。
点击屏幕,触屏事件这个敌兵出现,我们的将是如何处理敌兵的呢?处理事件的sequence图如下:
图4 事件处理sequence图
让我们看看图4中的第5步的实现:
private voidtouchesBegan(MotionEvent event) {
if(dispatchEvents ) {
for( intind = 0; ind < touchHandlers.size(); ind++ ) {
CCTouchHandler handler = touchHandlers.get(ind);-- 从touchHandlers中将MainLayer取出来
handler.ccTouchesBegan(event); -- 调用MainLayer.ccToucherBegan
// if(handler.ccTouchesBegan(event) == kEventHandled )
// break;
}
}
}
综上所述,我们看到了完整的touchEvent的注册和调用时序。keyEvent的原理也是一样的,在此不再叙述。
cocos2d的描画处理比较复杂,下面我们详细看一下,首先看一下sequence图,以获得一个整体影响。
图5 cocos2d的描画处理sequence图
说明如下:
图5第4步CCTouchDispatcher.sharedDispatcher().update();这个在上面已经说过,在这里处理触屏事件。
图5第5步CCKeyDispatcher.sharedDispatcher().update();和触屏事件处理类似,不过变成按键事件处理。
图5第10步,详细看一下setNextScene,代码如下:
public voidsetNextScene() {
booleanrunningIsTransition = runningCCScene_ instanceof CCTransitionScene;
boolean newIsTransition= nextCCScene_ instanceof CCTransitionScene;
// If it is nota transition, call onExit
if(runningCCScene_ != null && ! newIsTransition ) {
runningCCScene_.onExit();-- 通过重载onExit可进行node资源释放工作
// issue#709. the root node (CCScene) should receive the cleanup message too
//otherwise it might be leaked.
if(sendCleanupToCCScene_ )
runningCCScene_.cleanup();
}
runningCCScene_= nextCCScene_;
nextCCScene_ =null;
if( !runningIsTransition ) {
runningCCScene_.onEnter(); -- 通过重载onEnter可进行node初始化工作
runningCCScene_.onEnterTransitionDidFinish();
}
}
图5第13步,再详细看一下visit,代码如下:
/**
recursive methodthat visit its children and draw them
*/
public voidvisit(GL10 gl) {
// quick return if not visible
if (!visible_)
return;
gl.glPushMatrix();
if (grid_ !=null && grid_.isActive()) {
grid_.beforeDraw(gl);
transformAncestors(gl);
}
transform(gl); --进行translate,scale等变换处理
if (children_!= null) {
for (int i=0; i<children_.size();++i) {
CCNode child =children_.get(i);
if (child.zOrder_ < 0) { -- 当前窗口中位于Z轴原点后的node描画
child.visit(gl);
} else
break;
}
}
gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,GL10.GL_MODULATE);
draw(gl); --当前node的描画处理
if (children_!= null) {
for (int i=0; i<children_.size();++i) {
CCNode child =children_.get(i);
if (child.zOrder_ >= 0) { -- 当前窗口中位于Z轴原点前的node描画
child.visit(gl);
}
}
}
if (grid_ != null &&grid_.isActive()) {
grid_.afterDraw(gl, this);
}
gl.glPopMatrix();
}
注意:当前窗口的Z轴正向是向屏幕外的。
图5第20步waitForFPS();帧率处理。通过这个函数可以使画面按照设定的帧率进行刷新。
下面结合实例进行一个MainLayer的描画说明,详细代码请参见Cocos2Dapp的
org.cocos2d.tests.ClickAndMoveTest.java。考虑到图的清晰性,图5中只画出了sprite和layer。下面具体看一下代码:
publicMainLayer() {
this.setIsTouchEnabled(true);
CCSprite sprite = CCSprite.sprite("grossini.png");
CCLayer layer = CCColorLayer.node(new ccColor4B(255, 255,0, 255));
addChild(layer, -1); -- Z=-1处,即当前窗口最后面
addChild(sprite, 1, kTagSprite); -- Z=1处
sprite.setPosition(CGPoint.make(20, 150));
sprite.runAction(CCJumpTo.action(4, CGPoint.make(300, 48),100, 4));
CCLabellbl1 =CCLabel.makeLabel("Clickon the screen", "DroidSans", 24);
CCLabellbl2 =CCLabel.makeLabel("tomove and rotate Grossini", "DroidSans", 16);
addChild(lbl1, 0); -- Z=0处,注意sprite能盖住文字Click on the screen,sprite在Click on the screen上面
addChild(lbl2, 1); -- Z=1处,注意sprite不能盖住文字to move and rotate Grossini,它们同层
lbl1.setPosition(CGPoint.ccp(160, 240));
lbl2.setPosition(CGPoint.ccp(160, 200));
progressTimer = CCProgressTimer.progress("iso.png");
this.addChild(progressTimer, 10);-- Z=10处,当前窗口最前面,注意sprite一定位于progressTimer后
progressTimer.setPosition(160, 100);
progressTimer.setType(CCProgressTimer.kCCProgressTimerTypeVerticalBarTB);
progressTimer.setPercentage(50.0f);
layer.runAction(CCRepeatForever.action(CCSequence.actions(CCFadeIn.action(1),CCFadeOut.action(1))));
}
CCActionManager,顾名思义,它是Action的管理者。Action的实现其实是一个周期性的事件处理,由于代码比较简单,在此不详细分析了,它的调用在CCActionManager.update里,它的sequence可参见图2的第20步到第23步。
代码详细分析如下:
public voidupdate(float dt) {
for(ConcurrentArrayHashMap<CCNode, HashElement>.Entry e = targets.firstValue();
e != null; e =targets.nextValue(e)) {
HashElement currentTarget = e.getValue();
if(currentTarget == null)
continue;
if (!currentTarget.paused) {
synchronized(currentTarget.actions) {
// The 'actions' may change whileinside this loop.
for (currentTarget.actionIndex = 0;
currentTarget.actionIndex< currentTarget.actions.size();
currentTarget.actionIndex++){
CCActioncurrentAction = currentTarget.actions.get(currentTarget.actionIndex);
currentAction.step(dt);-- Action的调用点
if (currentAction.isDone()) {
currentAction.stop();
// removeAction(currentAction);
HashElement element =targets.get(currentTarget.target);
if (element != null && currentTarget.actionIndex >= 0) {
removeAction(currentTarget.actionIndex, currentTarget);
}
}
// currentTarget.currentAction = null;
}
currentTarget.actionIndex = -1;
}
}
if (currentTarget.actions.isEmpty())
deleteHashElement(currentTarget);
}
}
CCActionManager.update是由CCScheduler.tick调用起来的,它们建立连接的地方如下:
private CCActionManager() {
CCScheduler.sharedScheduler().scheduleUpdate(this,0, false);
targets= new ConcurrentArrayHashMap<CCNode, HashElement>();
}
CCScheduler.scheduleUpdate的实现如下:
public voidscheduleUpdate(UpdateCallback target, int priority, boolean paused) {
// TODO Auto-generatedmethod stub
if (ccConfig.COCOS2D_DEBUG>= 1) {
tHashSelectorEntry hashElement = hashForUpdates.get(target);
assert hashElement ==null:"CCScheduler: You can't re-schedule an 'update' selector'. Unscheduleit first";
}
// most of the updates aregoing to be 0, that's way there
// is an special list forupdates with priority 0
if( priority == 0 ) {
this.append(updates0, target, paused);
} else if( priority < 0 ) {
this.priority(updatesNeg, target, priority, paused);
} else { // priority > 0
this.priority(updatesPos, target, priority, paused);
}
}
CCScheduler.append的实现如下:
public voidappend(ArrayList<tListEntry> list, Object target, boolean paused) {
tListEntry listElement = newtListEntry();
listElement.target = target;
listElement.paused = paused;
if(target instanceofUpdateCallback) {
listElement.callback = (UpdateCallback)target;
} else {
try {
listElement.impMethod= target.getClass().getMethod(updateSelector, Float.TYPE);
} catch(NoSuchMethodException e) {
e.printStackTrace();
}
}
synchronized (list) {
list.add(listElement);
}
// update hash entry forquicker access
tHashSelectorEntryhashElement = new tHashSelectorEntry();
hashElement.target = target;
hashElement.list = list;
hashElement.entry =listElement;
hashForUpdates.put(target,hashElement);
}
从CCScheduler.scheduleUpdate和CCScheduler.append的实现可以看出,CCActionManager.update被添加到CCScheduler. updates0队列里,它在如下代码中被调用:
public voidtick(float dt) {
……
// updates withpriority == 0
synchronized(updates0) {
int len = updates0.size();
for(int i=0; i < len; ++i) {
tListEntrye = updates0.get(i);
currentEntry= e;
if( ! e.paused ) {
if(e.callback!=null) {
e.callback.update(dt);
}else {
try {
e.impMethod.invoke(e.target, dt); -- CCActionManager.update在此被调用。可参见图5的第8步
}catch (Exception e1) {
//TODO Auto-generated catch block
e1.printStackTrace();
}
}
if(currentTargetSalvaged){
updates0.remove(i);
i--;
len--;
currentTargetSalvaged= false;
}
}
}
currentEntry = null;
}
……
}
CCActionManager的其他部分代码相对来说比较简单,都是些一般的添加、删除处理,在此也不再详细分析了。
从上面的分析我们知道了CCActionManager.update是从CCScheduler.tick里调用起来的,那么CCScheduler .tick是哪里调用的呢?其实我们在前面已经提到过,是CCDirector. drawCCScene。Sequence可参见图5的第6步,代码如下:
public voiddrawCCScene (GL10 gl) {
……
/*tick before glClear: issue #533 */
if(!isPaused){
CCScheduler.sharedScheduler().tick(dt);
}
……
}
除了CCActionManager.update这个周期性处理外,在我们代码中还有一个非常有用的周期性处理点,这个处理点被用来改变CCNode的状态,例如sprite打怪动作的碰撞反应。举个实例以便于理解。WolfBoy的GameScene.java中就定义了一个update方法,代码如下:
public voidupdate(float aDeltaTemp) {
bgLayer.updateWithDelta(aDeltaTemp);
mainLayer.updateWithDelta(aDeltaTemp);
uiLayer.updateWithDelta(aDeltaTemp);
}
它是在GameScene初始化时和CCScheduler联系起来的,代码如下:
private voidinit() {
……
this.schedule("update",aDelta);
……
}
接着往下看,代码位于CCNode.schedule里,
/** schedules a custom selector with aninterval time in seconds.
If time is 0 it will be ticked everyframe.
If time is 0, it is recommended to use'scheduleUpdate' instead.
*/
public voidschedule(String selector, float interval) {
assert selector!= null : "Argument selector must be non-null";
assert interval>= 0 : "Argument interval must be positive";
CCScheduler.sharedScheduler().schedule(selector,this, interval, !isRunning_);
}
在这里,和CCScheduler关联起来,具体代码如下:
/** The scheduled method will be calledevery 'interval' seconds.
If paused is YES, then it won't be calleduntil it is resumed.
If 'interval' is 0, it will be calledevery frame, but if so, it recommened to use 'scheduleUpdateForTarget:'instead.
@since v0.99.3
*/
public voidschedule(String selector, Object target, float interval, boolean paused) {
assert selector!= null: "Argument selector must be non-nil";
assert target!= null: "Argument target must be non-nil";
tHashSelectorEntry element =hashForSelectors.get(target);
if( element ==null ) {
element =new tHashSelectorEntry();
element.target = target;
hashForSelectors.put(target, element);
// Is thisthe 1st element ? Then set the pause level to all the selectors of this target
element.paused = paused;
} else {
assertelement.paused == paused : "CCScheduler. Trying to schedule a selectorwith a pause value different than the target";
}
if(element.timers == null) {
element.timers = new ArrayList<CCTimer>();
}/* else if(element.timers.size() == element.timers )
ccArrayDoubleCapacity(element->timers);
*/
CCTimer timer = new CCTimer(target,selector, interval);
element.timers.add(timer);
}
这些CCTimer的调用点在CCScheduler.tick里,sequence可参见图5第9步,处理代码如下:
public voidtick(float dt) {
……
for(ConcurrentArrayHashMap<Object, tHashSelectorEntry>.Entry e =hashForSelectors.firstValue();
e != null; e =hashForSelectors.nextValue(e)) {
tHashSelectorEntry elt = e.getValue();
currentTarget = elt;
currentTargetSalvaged = false;
if( ! currentTarget.paused &&elt.timers != null) {
// The'timers' ccArray may change while inside this loop.
for(elt.timerIndex = 0; elt.timerIndex < elt.timers.size(); elt.timerIndex++) {
elt.currentTimer= elt.timers.get(elt.timerIndex);
elt.currentTimerSalvaged = false;
elt.currentTimer.update(dt);-- GameScene.update在此被调用
if(elt.currentTimerSalvaged ) {
// The currentTimer told the remove itself.To prevent the timer from
// accidentally deallocating itself before finishing its step, weretained
// it. Now that step is done, it's safe to release it.
elt.currentTimer = null;
}
elt.currentTimer = null;
}
}
// elt, at this moment, is stillvalid
// so it is safe to ask this here(issue #490)
// elt=elt->hh.next;
// only delete currentTarget if noactions were scheduled during the cycle (issue #481)
if(currentTargetSalvaged && currentTarget.timers.isEmpty()) {
// removeHashElement(elt);
hashForSelectors.remove(elt.target);
//[self removeHashElement:currentTarget];
}
}
currentTarget =null;
// }
……
}
文中的图内容多,看起来比较小,为了便于观看,这里把原图附上(http://download.csdn.net/detail/imyfriend/4640828)。图是用staruml画的。