1、现有的cocos2d-x的ui和点击事件分派机制是无法适应mmo的ui逻辑的。 mmo的ui元素很多,一两百个界面是很正常的。 规则既单一又复杂,这些界面肯定是由简单的对话框组成的,不会有什么amazing的东西,但是这些对话框之间的遮挡、层级关系又是复杂的。如果把这些东西丢给写ui或者写逻辑的程序员去操心,那就等着无穷无尽的bug吧。而且即便维护好现有的代码,这些代码结构也是脆弱的,每添加一个新功能都有可能对以前的逻辑造成影响。
2、一套完善的ui可以不是cocos2d-x的核心代码,但是一定要属于cocos2d-x引擎的一部分。 不要想着cocos2d-x只是一个渲染引擎,其他的东西都可以外部扩展。 这种思路就像是cocos2d和ogre的思路一样,并不利于引擎的发展。 什么东西都是灵活可选的,反而不如提供一套默认好用的。 就像是java和c#提供的标准库对比c++的标准库。 一个提供好你需要的基础套件,一个只提供最核心的容器语法,最后哪个容易学?哪个发展的好? 很多时候灵活可选对终端用户而言并不一定是一件好事。
3、最早cocos2d提供的控件只有CCMenu,其他控件需要的话就用UIKit。 这个勉强可以接受。因为那个时候ios平台独立开发者做的都是愤怒的小鸟,水果忍者什么的,这些ui元素几乎为0,一个CCMenu就可以走天下了。 如果需要其他控件的时候,UIKIt也可以和cocos2d引擎完美的结合起来,用起来还算方便。
但是转移到cocos2d-x就是另外一个天地了,因为用cocos2d的思路结合本地控件和游戏引擎在android平台上几乎是不可能实现的,那绝对是一个噩梦。 于是一套基于游戏引擎渲染的ui控件库便孕育而生。
4、但是这套新的ui控件库依然只是一个控件库,不是一个ui库。它依然无法承载一个mmo的需求。 mmo的开发者依然需要对其进行再封装和再开发。这里要表达两个观点,一是如果cocos2d-x提供一套方便好用的ui库,这些再开发是可以避免的,例如CEGUI。 二是,这样的一个好用方便的ui库并不是说一定限定于mmo使用,我们做水果忍者或者斗地主依然可以使用这套ui,依然会享受到其提供的便利。
5、现有的点击事件分发处理天然就不适应于复杂的ui框架。所有的控件平铺给CCTouchDispatcher处理,按照优先级处理点击事件。这个后期维护起代码来无比蛋疼,并且很有可能出现这些恶心的情况,窗体已经隐藏了,但是CCControlButton依然可以响应点击事件(注意Button本身依然是Visible的,只不过他的父窗体隐藏了); 看到的是一个界面,但是点击到的是另外一个界面。
6、原本另写一套ui库是更加漂亮的方法,但是一方面现有游戏已经发布,代码中大量CCNode作为节点容器存在的情况,不可能对其大动手术;另一方面再写一个ui库消耗的时间很多,短时间内不可能实现。 所以选择直接改写CCNode的代码,重写事件分发机制。 但是我没有对其进行更加完整的思考,所以这种修改适应于我们的游戏,但是不一定适应于所有的游戏。 当然,适应于大多数游戏并不是一件困难的事情,只不过我懒得做了而已。
核心代码:
enum { kEventModeNone = 1, kEventModeNormal, // 正常电击处理,大多数控件都是这种 kEventModeIgnoreEvent, // 不响应任何点击事件 kEventModeBackground, // 大多数node和layer是这种,本身不响应点击,但是会传递事件给子控件,如果没有子控件响应则穿透 kEventModePassDrag, // 一些子node会有这个属性,将move事件传递给父node kEventModeModel, // 模态对话框 };
CCNode* CCNode::getTouchedChildTarget(CCTouch* pTouch, uint eventType, bool includeIgnore) { if (!pTouch) { return NULL; } bool ret = false; if (!m_pChildren || m_pChildren->count() == 0) { return NULL; } CCPoint touchLocation = pTouch->getLocation(); for (int i = m_pChildren->count() - 1; i >= 0; --i) { CCNode* pNode = dynamic_cast<CCNode*>(m_pChildren->objectAtIndex(i)); if (!pNode || !pNode->isVisible()) { continue; } if (pNode->testEventMode(kEventModeNone)) { continue; } if (!includeIgnore && pNode->testEventMode(kEventModeIgnoreEvent)) { continue; } CCNode* pChildNode = pNode->getTouchedChildTarget(pTouch, eventType); if (pChildNode) { return pChildNode; } else { CCSize size = pNode->getContentSize(); if (size.width <= 0 || size.height <= 0) { continue; } // if (typeid(*this) == typeid(CCNode) || typeid(*this) == (CCSprite)) { // } CCPoint pt = pNode->convertToWorldSpace(CCPoint(0, 0)); CCRect bBox(pt.x, pt.y, size.width, size.height); if (pNode->testEventMode(kEventModeModel) || bBox.containsPoint(touchLocation)) { if (!pNode->testEventMode(kEventModeBackground)) { return pNode; } } } } return NULL; } bool CCNode::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) { return true; } void CCNode::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent) { } void CCNode::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent) { } void CCNode::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent) { } void CCNode::setEventMode(int mode) { m_eventMode = (1 << mode); } int CCNode::testEventMode(int mode) const { return(m_eventMode & (1 << mode)) != 0; }
void CCTouchDispatcher::touchesEnded(CCSet *touches, CCEvent *pEvent) { if (!m_bDispatchEvents) { return; } CCTouch *pTouch; CCSetIterator setIter; for (setIter = touches->begin(); setIter != touches->end(); ++setIter) { pTouch = (CCTouch *)(*setIter); if (!pTouch) { continue; } if (m_pFocusNode) { m_pFocusNode->ccTouchEnded(pTouch, pEvent); } } setFocusNode(NULL); } void CCTouchDispatcher::touchesCancelled(CCSet *touches, CCEvent *pEvent) { if (m_bDispatchEvents) { CCTouch *pTouch; CCSetIterator setIter; for (setIter = touches->begin(); setIter != touches->end(); ++setIter) { pTouch = (CCTouch *)(*setIter); if (!pTouch) { continue; } if (m_pFocusNode) { m_pFocusNode->ccTouchCancelled(pTouch, pEvent); } else if (m_pSceneRootNode) { m_pSceneRootNode->ccTouchCancelled(pTouch, pEvent); } } } setFocusNode(NULL); } void CCTouchDispatcher::setFocusNode(CCNode* node) { m_pFocusNode = node; } CCNode* CCTouchDispatcher::getFocusNode() const { return m_pFocusNode; } void CCTouchDispatcher::setUIRootNode(CCNode* node) { if (!node) { return; } m_pUIRootNode = node; } void CCTouchDispatcher::setSceneNode(CCNode* node) { if (!node) { return; } m_pSceneRootNode = node; }
void CCControl::sendActionsForControlEvents(CCControlEvent controlEvents) { retain(); // For each control events for (int i = 0; i < kControlEventTotalNumber; i++) { // If the given controlEvents bitmask contains the curent event if ((controlEvents & (1 << i))) { // Call invocations // <CCInvocation*> CCArray* invocationList = this->dispatchListforControlEvent(1<<i); CCObject* pObj = NULL; CCARRAY_FOREACH(invocationList, pObj) { CCInvocation* invocation = (CCInvocation*)pObj; invocation->invoke(this); } } } release(); }
思路和简单的说明:
1、在每个场景设置一个UIRoot,来进行统一的事件分发。 事件分发是由父节点传递给子节点,这样的层次结构。如果父节点是隐藏的或者不可点击的,那么子节点也不会接收到点击事件。
2、通过一个EventMode的属性来标志当前节点是否可以点击。比如大多数CCNode和CCLayer就是Background属性,这个属性的节点本身是不可点击的,但是它会把点击事件分派给他的子节点。如果子节点都没有响应,那么就传递给下一个界面节点。 而大多数控件都是Normal属性,它可以正常响应点击事件
3、查找到一个合适目标的节点,然后给这个目标节点发送点击事件,而不是遍历当前节点,响应点击事件,根据其返回值决定是否继续分发。 这样做的目的是防止响应事件的时候销毁当前窗体,造成一些恶心的内存问题(在遍历过程中修改容器的元素代码怎么写都不会非常的安全和稳定,还是采用现在的做法保险点)。
4、原本事件处理有个陷阱,那就是CCTouchDispatcher的Handler集合会给控件引用计数+1,所以事件分发的时候可以安心的remove掉,因为控件本身不会被delete。但是取消掉这个机制后,就要小心和多测试下。比如CCControl的事件分发代码就需要先retain然后release来防止响应事件时销毁控件造成崩溃。
5、事件处理是万里长征第一步,这一步完成后,再添加 1、xml配置自动创建界面的功能。(这个完成后就像模像样了) 2、添加一个模式可以在游戏中拖动ui控件的位置大小并且保存(游戏就是ui编辑器了) 3、一套lua和代码结合的方式,把大多数ui逻辑移动到lua(一切都是为了提高开发效率,所有代码都用lua写并不是一个好主意,但是部分体力活的代码挪到脚本里面确实会清爽不少)
二.Java的初始化机制、垃圾回收机制和内存分配机制
2.1初始化顺序:
public class Parent { static Tipout TIP = new Tipout("父类 static 成员 TIP 初始化"); Tipout tip = new Tipout("父类 成员 tip 初始化"); public Parent() { System.out.println("父类 构造函数 调用"); } } class Sub extends Parent { static Tipout SUB_TIP = new Tipout("子类 static 成员 SUB_TIP 初始化"); Tipout subTip = new Tipout("子类 成员 subTip 初始化"); public Sub() { System.out.println("子类 构造函数 调用"); } } class Tipout { public Tipout(String s) { System.out.println(s); } }
执行结果:
父类 static 成员 TIP 初始化
子类 static 成员 SUB_TIP 初始化
父类 成员 tip 初始化
父类 构造函数 调用
子类 成员 subTip 初始化
子类 构造函数 调用
由此可以总结出java初始化的顺序:
->所有静态成员初始化(父类->子类)
-->父类初始化(普通成员->构造函数)
--->子类初始化(普通成员-->构造函数)
!static成员初始化顺序只和类定义中的顺序有关。
调用静态数据和静态方法时,这个类中的所有静态成员都会被初始化(非static成员不会被初始化),前提是他们从未被初始化过。
测试代码:
public class Parent { public static Tipout TIP = new Tipout("父类 static 成员 TIP 初始化"); static { System.out.println("父类 static 代码块"); } Tipout tip = new Tipout("父类 成员 tip 初始化"); public Parent() { System.out.println("父类 构造函数 调用"); } public static void outMsg(){ System.out.println("父类 static 函数 调用"); } }
在Main()中调用:Parent.TIP.toString();
结果:
2.2垃圾回收
java中并不需要清除对象,也不存在C++中的析构函数,java的垃圾回收机制会自动释放无用的变量。
一个对象,可以有一个或多个引用变量指向它。当一个对象不再有任何一个引用变量指向它时,这个对象可以被垃圾回收机制回收了。
但是,并不是对象被抛弃后当即被回收的。JVM进程做空间回收有较大的系统开销。如果每当某应用进程丢弃一个对象,就立即回收它的空间,
势必会使整个系统的运转效率非常低下。
!不能操纵垃圾回收
使对象值为null,或者调用System.gc()
JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。
!垃圾回收器的运行时间是不固定的,清理工作的实际运行时间也是不能预知的。
2.3内存分配机制
1.栈内存,
基本类型的变量和对象的引用变量(存取速度比堆要快,仅次于寄存器)
2.堆内存。
堆内存用来存放由 new 创建的对象和数组。
!类跟数组一样,都是属于引用类型,引用类型就是指一堆对内存可以同时被多个栈内存指向。
java中主要存在4块内存空间:
栈内存空间:保存所有的对象名称(更准确地说是保存了引用的堆内存空间的地址)
堆内存空间:保存每个对象的具体属性内容。
全局数据区:保存static类型的属性。
全局代码区:保存所有的方法定义。
2.3.2 String缓冲池
缓冲池是java为了节省内存空间,会在内存中创建一个专门为String设计的缓冲池,用来保存已经存在的字符串,
如果2个字符串是一样的,则使用池中的字符串,不再创建新的对象
用下边的代码测试:
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
结果为true,说明str1和str2指向同一个对象
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
用new则在堆中创建了2个对象
2.4应该注意的问题:
1.尽量使用基本数据类型代替对象
String str = "hello";
上面这种方式会创建一个"hello"字符串,而且JVM的字符缓存池还会缓存这个字符串。
String str = new String("hello");
此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o
2.尽量减少对变量的重复计算
for(int i=0;i<list.size();i++)应该改为
for(int i=0,len=list.size();i<len;i++)并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。
3.避免在循环体中声明创建对象
for (int i = 0; i < 10000; ++i) { Object obj = new Object(); }虽然性能上差别不大,但这样的代码会浪费栈内存空间,上边提到过引用变量会存放在栈内存,这样会在内存中产生大量的对象引用。
应该写成下边的:
Object obj; for (int i = 0; i < 10000; ++i) { obj = new Object(); }
1楼wkupaochuan昨天 17:19lz总结的真好。学习了。
1、版本问题
phonegap 在2.0以前和2.0以后进行插件开发的方式不同,我这一次使用的是1.7的。
2、开发的步骤
第一、编写插件类:
com.ljp.laucher.util.TargetActivityPlugin.java package com.ljp.laucher.util; import org.apache.cordova.api.Plugin; import org.apache.cordova.api.PluginResult; import org.json.JSONArray; import android.app.Activity; import android.content.Intent; import android.os.Handler; import android.os.Message; import android.util.Log; public class TargetActivityPlugin extends Plugin { @Override public PluginResult execute(String action, JSONArray data, String callbackID) { if(action.equals("startActivity")){ PluginResult result = null; try { PluginResult.Status status = PluginResult.Status.OK; if(action.equals("startActivity")){ Log.e("test", "test plugin js -> java~~~~"+data.getString(0)); Log.e("test", "test plugin js -> java~~~~"); result = new PluginResult(status, data.getString(0)); Message msg=new Message(); msg.what=1; msg.obj=data.getString(0); handler.sendMessage(msg); } } catch (Exception e) { } return result; }else { return new PluginResult(PluginResult.Status.INVALID_ACTION); } } private Handler handler = new Handler() { public void handleMessage(Message msg) { if (msg == null) { return; } switch (msg.what) { case 1: String className=msg.obj.toString(); try { Class activityClass = Class.forName(className); Intent intent = new Intent(ctx.getContext(), activityClass);//浣犳兂鍘荤殑activity(exp:Temp) ctx.startActivityForResult(TargetActivityPlugin.this, intent, 1); } catch (ClassNotFoundException e) { e.printStackTrace(); } break; } }; }; @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { // TODO Auto-generated method stub if(requestCode==1){ if(resultCode == Activity.RESULT_CANCELED){ } }else{ super.onActivityResult(requestCode, resultCode, intent); } } }
第二、在MiLaucher\res\xml\plugins.xml中添加自己的插件
<?xml version="1.0" encoding="utf-8"?> <!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <plugins> <plugin name="App" value="org.apache.cordova.App"/> <plugin name="Geolocation" value="org.apache.cordova.GeoBroker"/> <plugin name="Device" value="org.apache.cordova.Device"/> <plugin name="Accelerometer" value="org.apache.cordova.AccelListener"/> <plugin name="Compass" value="org.apache.cordova.CompassListener"/> <plugin name="Media" value="org.apache.cordova.AudioHandler"/> <plugin name="Camera" value="org.apache.cordova.CameraLauncher"/> <plugin name="Contacts" value="org.apache.cordova.ContactManager"/> <plugin name="File" value="org.apache.cordova.FileUtils"/> <plugin name="NetworkStatus" value="org.apache.cordova.NetworkManager"/> <plugin name="Notification" value="org.apache.cordova.Notification"/> <plugin name="Storage" value="org.apache.cordova.Storage"/> <plugin name="Temperature" value="org.apache.cordova.TempListener"/> <plugin name="FileTransfer" value="org.apache.cordova.FileTransfer"/> <plugin name="Capture" value="org.apache.cordova.Capture"/> <plugin name="Battery" value="org.apache.cordova.BatteryListener"/> <plugin name="SplashScreen" value="org.apache.cordova.SplashScreen"/> <plugin name="test01" value="com.elt.phonegaodemo4.test01"></plugin> //<plugin name="给插件起个名字(有人说这个名字要和插件类名一致,经我测试,不需要)" value="包名.类名(插件类)"></plugin> <plugin name="TargetActivityPlugin" value="com.ljp.laucher.util.TargetActivityPlugin"></plugin></plugins>
第三、在MiLaucher\assets\www\js\cordova-1.7.0.js最后添加对应的js操作代码
var targetActivityAPI = function() { }; targetActivityAPI.prototype.startActivity = function(success, error, testData1) { return PhoneGap.exec(success, error, 'TargetActivity', // java类名,plugins.xml中注册的名字 'startActivity', // action,Java方法中用来匹配的字段 [ testData1 ] // params 传递的参数,Array形式 ); }; PhoneGap.addConstructor(function() { PhoneGap.addPlugin('targetActivityAPI', new targetActivityAPI()); });
第四、在MiLaucher\assets\www\index.html中就可以使用这个插件了
<script src="/blog_article/js/cordova-1.7.0.js"></script> <script type="text/javascript"> var startActivity = function() { window.plugins.targetActivityAPI.startActivity(null,null, "com.ljp.laucher.MiLaucherActivity"); } $("#back").live("click", null, startActivity); </script>
第五、最后就是要在activity中载入html界面
package com.ljp.laucher; import org.apache.cordova.DroidGap; import android.os.Bundle; public class MainActivity extends DroidGap { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.main); super.loadUrl("file:///android_asset/www/index.html"); } }
这样就可以使用这个插件了,
在开发这个插件的时候,自己也遇到了很多意想不到的问题,比如说js执行顺序问题,jquery绑定函数的问题,还有phonegap版本问题,等等!
简单记录一下吧:呵呵
jquery进行绑定函数的方式常用的有这两种:
$(".cell").bind("click", function() { location.href = "testinfo.html"; }); var startActivity = function() { window.plugins.targetActivityAPI.startActivity(null,null, "com.ljp.laucher.MiLaucherActivity"); } $("#back").live("click", null, startActivity);
记录结束!