Cocos2d-x 的“HelloLua” 深入分析


注:本章需要一定Lua经验和内存管理编程相关经验

另:Cocos2d-x版本为​​http://cn.cocos2d-x.org/download​​​:
cocos2d-1.0.1-x-0.12.0 @ Mar 05, 2012


     我们来看一下Cocos2d-x的HelloLua示例工程。首先编译运行一下这个工程,当然,因为我在HelloWorld工程中改动了CCApplication的run函数和initInstance函数,所以这里要修改一下,与HelloWorld保持一致才能编译成功。哇!一个很COOL的农场游戏。

                    

Cocos2d-x 的“HelloLua” 深入分析_animation


   这几乎是我见过的最令人激动的示例了。农场类游戏两年前可是非常火的!怎么做的,马上来看。

       main.h和main.cpp与HelloWorld无异。不理会了。打开AppDelegate.cpp,可以看到它与HelloWorld工程中的AppDelegate.cpp的明显不同是使用了声音引擎类SimpleAudioEngine和脚本引擎类CCScriptEngineManager。下面我们来分析一下源码。

应用程序启动时调用的函数
bool AppDelegate::applicationDidFinishLaunching()
{
// 初始化显示设备
CCDirector *pDirector = CCDirector::sharedDirector();
// 设置显示设备使用initInstance函数创建的OpenGL视窗
pDirector->setOpenGLView(&CCEGLView::sharedOpenGLView());
//使用高清模式
// pDirector->enableRetinaDisplay(true);

// 设置显示FPS
pDirector->setDisplayFPS(true);
//设置设备的显示方向
// pDirector->setDeviceOrientation(kCCDeviceOrientationLandscapeLeft);
//设置FPS帧间隔时间
pDirector->setAnimationInterval(1.0 / 60);

// 通过CCLuaEngine的静态函数获取一个LUA脚本引擎实例对象指针
CCScriptEngineProtocol* pEngine = CCLuaEngine::engine();
// 通过CCScripEngineManager的静态函数sharedManager获取单件脚本引擎管理器的实例对象指针,并将上一句创建的LUA脚本引擎实例对象指针设为脚本引擎管理器当前进行管理的脚本引擎。
CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
//在ANDROID平台下,会通过文件处理API类CCFileUtils中的getFileData取得hello.lua文件并读取到char数组中。
unsigned long size;
char *pFileContent = (char*)CCFileUtils::getFileData("hello.lua", "r", &size);

if (pFileContent)
{
//将char数组数据放入到一个新的以’\0’结尾的char数组中形成一个LUA脚本字符串
char *pCodes = new char[size + 1];
pCodes[size] = '\0';
memcpy(pCodes, pFileContent, size);
delete[] pFileContent;
//让脚本引擎执行这个LUA脚本字符串
pEngine->executeString(pCodes);
//释放动态申请的char数组的内存
delete []pCodes;
}
#endif

#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) || (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) || (CC_TARGET_PLATFORM == CC_PLATFORM_MARMALADE)
//如果是Win32,IOS或MARMALADE平台,通过文件处理API类CCFileUtils中的fullPathFromRelativePath函数产生一个hello.lua在当前程序所在目录下的路径。具体实现可参看CCFileUtils_win32.cpp
string path = CCFileUtils::fullPathFromRelativePath("hello.lua");
//将这个路径的目录放入到脚本引擎的搜索目录
//path.substr会将路径中的目录取出来。
pEngine->addSearchPath(path.substr(0, path.find_last_of("/")).c_str());
//执行这个路径所指向的LUA文件
pEngine->executeScriptFile(path.c_str());
#endif

return true;
}



       我们没有在这里面发现任何关于农场或松鼠的只言片语。我们只知道程序执行了一下“hello.lua”文件。多COOL!

       我已经迫不急待的想要打开hello.lua文件一看究竟了。我们打开工程下的Resource目录,可以发现农场和松鼠的图片,还有一些声音文件,以及我们要找的lua文件,共有两个:hello.lua和hello2.lua。


Cocos2d-x 的“HelloLua” 深入分析_animation_02



点击打开hello.lua,我们来分析一下:

[Cocos2d-x相关教程来源于红孩儿的游戏编程之路 CSDN博客地址:javascript:void(0)] 
-- 设定LUAGC的拉圾回收参数
collectgarbage("setpause", 100)
collectgarbage("setstepmul", 5000)
--这里定义一个函数cclog,用来打印字符串参数
local cclog = function(...)
print(string.format(...))
end
--将hello2.lua包含进来,hello2.lua定义了myadd函数的实现
require "hello2"
--这里调用cclog打印一个和的结果,并没什么实际用处,可略过
cclog("result is " .. myadd(3, 5))

---------------
--通过CCDirector:sharedDirector()来取得显示设备实例对象,并调用其getWinSize函数取得窗口大小给变量winSize
local winSize = CCDirector:sharedDirector():getWinSize()

-- 定义createDog函数创建松鼠
local function creatDog()
--定义两个变量为每一帧图块的宽高
local frameWidth = 105
local frameHeight = 95

-- 创建松鼠的动画
-- 先使用CCTextureCache:sharedTextureCache()取得纹理块管理器,将dog.png放入纹理块管理器产生一张纹理返回给变量textureDog
local textureDog = CCTextureCache:sharedTextureCache():addImage("dog.png")
--创建一个矩形返回给变量rect
local rect = CCRectMake(0, 0, frameWidth, frameHeight)
--由这个矩形从纹理上取出图块产生一个CCSpriteFrame指针返回给变量frame0
local frame0 = CCSpriteFrame:frameWithTexture(textureDog, rect)
--换一个新的位置的矩形返回给变量rect中
rect = CCRectMake(frameWidth, 0, frameWidth, frameHeight)
--由第二个矩形从纹理上取出图块产生一个CCSpriteFrame指针返回给变量frame1
local frame1 = CCSpriteFrame:frameWithTexture(textureDog, rect)
--从frame0产生一个精灵返回给变量spriteDog(在C++中是CCSprite指针)
local spriteDog = CCSprite:spriteWithSpriteFrame(frame0)
--设置初始化时
spriteDog.isPaused = false
--设置精灵的位置在左上的位置
spriteDog:setPosition(0, winSize.height / 4 * 3)

--生成一个设定大小为2的CCMutableArray类的实例对象。用来存储CCSpriteFrame指针,将其指针返回给变量animFrames
local animFrames = CCMutableArray_CCSpriteFrame__:new(2)
--调用addObject将frame0和frame1放入animFrames
animFrames:addObject(frame0)
animFrames:addObject(frame1)
--由容器类实例对象的指针animFrames创建一个动画帧信息对象,设定每0.5秒更新一帧,返回动画帧信息对象指针给变量animation
local animation = CCAnimation:animationWithFrames(animFrames, 0.5)
--由animation创建出一个动画动作,将这个动画动作的指针给变量animate
local animate = CCAnimate:actionWithAnimation(animation, false);
--设置精灵循环运行这个动作
spriteDog:runAction(CCRepeatForever:actionWithAction(animate))

-- 每帧移动松鼠
local function tick()
--如果松鼠停止动作,则返回
if spriteDog.isPaused then return end
--取得松鼠的位置
local x, y = spriteDog:getPosition()
--如果松鼠的x值已经超出屏幕宽度将x位置变为0,否则加1,这样可以实现不断的往右移动,超出后就又回到最左边
if x > winSize.width then
x = 0
else
x = x + 1
end
--重新设置松鼠位置
spriteDog:setPositionX(x)
end
--这里设置每帧调用上面的函数tick
CCScheduler:sharedScheduler():scheduleScriptFunc(tick, 0, false)
--返回松鼠精灵
return spriteDog
end

-- 创建农场
local function createLayerFram()
--创建一个新的Layer实例对象,将指针返回给变量layerFarm
local layerFarm = CCLayer:node()

-- 由“farm.jpg”创建一个精灵实例,将指针返回给变量bg
local bg = CCSprite:spriteWithFile("farm.jpg")
---设置这个精灵实例的位置
bg:setPosition(winSize.width / 2 + 80, winSize.height / 2)
----将精灵放入新创建的Layer中
layerFarm:addChild(bg)

--在农场的背景图上的相应位置创建沙地块,在i从0至3,j从0至1的双重循环中,共创建了8块沙地块。
for i = 0, 3 do
for j = 0, 1 do
--创建沙地块的图片精灵
local spriteLand = CCSprite:spriteWithFile("land.png")
--设置精灵的位置,在j的循环中每次向右每次增加180个位置点。在i的循环中每次会跟据i与2取模的结果向左移90个位置点,向上移95的一半数量的位置点。这样最先绘制最下面的两个沙地块,再绘制上面两个。再上面两个直至最上面两个。注意:这里的位置计算数值不必太纠结,如果是依照land.png的图片大小182x94,则这里改成spriteLand:setPosition(200 + j * 182 – (i % 2) * 182 / 2, 10 + i * 94 / 2)会更好理解一些。
spriteLand:setPosition(200 + j * 180 - i % 2 * 90, 10 + i * 95 / 2)
--将沙地块的图片精录放入到新创建的Layer中
layerFarm:addChild(spriteLand)
end
end

-- 使用CCTextureCache:sharedTextureCache()取得纹理块管理器,将dog.png放入纹理块管理器产生一张纹理textureCrop
local textureCrop = CCTextureCache:sharedTextureCache():addImage("crop.png")
-- 由一个矩形从纹理取出一个图块frameCrop
local frameCrop = CCSpriteFrame:frameWithTexture(textureCrop, CCRectMake(0, 0, 105, 95))
-- 和刚才的沙地块一样,由图块创建出精灵并放在相应的位置上,这里不再赘述。
for i = 0, 3 do
for j = 0, 1 do
local spriteCrop = CCSprite:spriteWithSpriteFrame(frameCrop);
spriteCrop:setPosition(10 + 200 + j * 180 - i % 2 * 90, 30 + 10 + i * 95 / 2)
layerFarm:addChild(spriteCrop)
end
end

-- 调用createDog增加一个移动的松鼠精灵
local spriteDog = creatDog()
-- 将松鼠精录放入新创建的Layer中
layerFarm:addChild(spriteDog)

-- 定义变量touchBeginPoint,设为未使用
local touchBeginPoint = nil
--定义当按下屏幕时触发的函数
local function onTouchBegan(x, y)
--打印位置信息
cclog("onTouchBegan: %0.2f, %0.2f", x, y)
--将x,y存在变量touchBeginPoint中
touchBeginPoint = {x = x, y = y}
--暂停精灵spriteDog的运动
spriteDog.isPaused = true
--返回true
return true
end
--定义当保持按下屏幕进行移动时触发的函数
local function onTouchMoved(x, y)
--打印位置信息
cclog("onTouchMoved: %0.2f, %0.2f", x, y)
--如果touchBeginPoint有值
if touchBeginPoint then
--取得layerFarm的位置,将返回结果存放在cx和cy中。
local cx, cy = layerFarm:getPosition()
--设置layerFarm的位置受到按下移动的偏移影响
layerFarm:setPosition(cx + x - touchBeginPoint.x,
cy + y - touchBeginPoint.y)
--更新当前按下位置存放到变量touchBeginPoint中
touchBeginPoint = {x = x, y = y}
end
end
--当离开按下屏幕时
local function onTouchEnded(x, y)
--打印位置信息
cclog("onTouchEnded")
--将变量touchBeginPoint设为未用
touchBeginPoint = nil
--将变量spriteDog
spriteDog.isPaused = false
end
--响应按下事件处理函数
local function onTouch(eventType, x, y)
--如果是按下时,调用onTouchBegan
if eventType == CCTOUCHBEGAN then
return onTouchBegan(x, y)
--如果是按下并移动时,调用onTouchMoved
elseif eventType == CCTOUCHMOVED then
return onTouchMoved(x, y)
--松开时,调用onTouchEnded
else
return onTouchEnded(x, y)
end
end
--调用layerFarm的registerScriptTouchHandler函数注册按下事件的响应函数
layerFarm:registerScriptTouchHandler(onTouch)
--调用layerFarm的setIsTouchEnabled使layerFarm能够响应屏幕按下事件
layerFarm:setIsTouchEnabled(true)
--返回layerFarm
return layerFarm
end


-- 定义创建菜单层函数
local function createLayerMenu()
--创建一个新Layer,将其指针返回给变量layerMenu
local layerMenu = CCLayer:node()
--定义三个本地变量
local menuPopup, menuTools, effectID
--定义本地函数menuCallbackClosePopup
local function menuCallbackClosePopup()
-- 通过参数effectID关闭指定声音
SimpleAudioEngine:sharedEngine():stopEffect(effectID)
--设置menuPopup不显示
menuPopup:setIsVisible(false)
end
--定义本地函数menuCallbackOpenPopup
local function menuCallbackOpenPopup()
-- 循环播放声音文件“effect1.wav”,并返回对应的声音ID给变量effectID
effectID = SimpleAudioEngine:sharedEngine():playEffect("effect1.wav")
-- 设置menuPopup显示
menuPopup:setIsVisible(true)
end

-- 创建图片菜单按钮,设置其两个状态(普通和按下)的图片都相同是menu2.png,返回图片菜单按钮给menuPopupItem
local menuPopupItem = CCMenuItemImage:itemFromNormalImage("menu2.png", "menu2.png")
-- 设置图片菜单按钮的位置在0,0点
menuPopupItem:setPosition(0, 0)
-- 为图片菜单按钮注册响应函数menuCallbackClosePopup
menuPopupItem:registerScriptHandler(menuCallbackClosePopup)
-- 由图片菜单按钮menuPopupItem创建出菜单返回给变量menuPopup
menuPopup = CCMenu:menuWithItem(menuPopupItem)
-- 设置菜单menuPopup的位置为屏幕中央
menuPopup:setPosition(winSize.width / 2, winSize.height / 2)
--设置menuPopup不显示。
menuPopup:setIsVisible(false)
--将菜单放入layerMenu中
layerMenu:addChild(menuPopup)

-- 下面几行代码创建左下角的图片菜单按钮menuToolsItem及菜单menuTools,与上面的代码基本相似,不再赘述。
local menuToolsItem = CCMenuItemImage:itemFromNormalImage("menu1.png", "menu1.png")
menuToolsItem:setPosition(0, 0)
menuToolsItem:registerScriptHandler(menuCallbackOpenPopup)
menuTools = CCMenu:menuWithItem(menuToolsItem)
menuTools:setPosition(30, 40)
layerMenu:addChild(menuTools)
--返回layerMenu
return layerMenu
end


-- 注意:以上大部分都是函数的定义,以下才是真正的游戏逻辑。我在这里加个序号方便大家读懂。
-- 1。取得声音引擎的实例对象并调用其playBackgroundMusic函数加载并循环播放声音文件“background.mp3”。这里做为背景音乐
SimpleAudioEngine:sharedEngine():playBackgroundMusic("background.mp3", true);
-- 2。取得声音引擎的实例对象并调用其preloadEffect函数将声音文件“effect1.wav”预加载进内存。这里并不播放,预加载是为了在播放时不造成卡顿感。
SimpleAudioEngine:sharedEngine():preloadEffect("effect1.wav");

-- 3。创建一个场景返回给变量sceneGame
local sceneGame = CCScene:node()
-- 4。创建农场所用的Layer,并放入场景中
sceneGame:addChild(createLayerFram())
-- 5。创建菜单所用的Layer,并放入场景中
sceneGame:addChild(createLayerMenu())
-- 6。调用显示设备的单件实例对象的runWithScene函数运行场景sceneGame
CCDirector:sharedDirector():runWithScene(sceneGame)



       hello.lua读完了,我来做一下总结:

hello.lua定义了几个函数(1) . creatDog:创建松鼠动画精灵,并设置精灵每帧触发tick函数由左向右循环移动。 (2).createLayerFram:创建农场所用的Layer,然后使用双循环创建沙地块图片精灵和农作物图片精灵,并放在合适的位置构建出农场田园的景色。之后调用createDog。最后设定这个农场所用的Layer在接收到按下,按下移动和松开事件时响应的函数。(3).createLayerMenu:创建菜单所用的Layer及菜单,游戏共使用了两个菜单,第一个菜单是一个图片菜单按钮,处于屏幕中央,图片中是一些游戏工具按钮的内容,默认这个菜单不显示。定义这个菜单在按下时响应的函数是隐藏它自已。第二个菜单也是一个图片菜单按钮,处于屏幕左下角,默认这个菜单显示,定义这个菜单在按下时响应的函数功能为显示第一个菜单。游戏的逻辑在脚本文件的最后面。先加载播放背景音乐,之后创建场景并创建农场和菜单将各自返回的层放入场景,最后运行这个场景。

       我们已经知道,lua文件可以调用静态C函数,也可以通过tolua++访问类对象的成员函数进行游戏逻辑的处理。通过这个示例的分析,我们更加感受到了Lua脚本引擎的强大功能。

   

       做为一个负责任的程序员,虽然我很开心Cocos2d-x提供了Lua的脚本引擎,但是我发现在当前版本中有一个美中不足的问题。就是内存泄漏,如果你现在猛的一紧,哈哈,那就赶快来跟着我往下面进行吧。我们仍然回到HelloLua工程的AppDelegate.cpp:


AppDelegate::AppDelegate()
{
// 引擎作者在这里屏蔽了CRT对于内存泄漏的检查
//_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);
}


我们将前面的注释去掉,编译运行。可以发现在输出栏里有大量的内存泄漏:


Cocos2d-x 的“HelloLua” 深入分析_animation_03



我们姑切往后看。

AppDelegate::~AppDelegate()
{
//释放声音引擎
SimpleAudioEngine::sharedEngine()->end();
//这里本来是释放脚本引擎所用到的内存的,但奇怪的是作者把它注释掉了
// CCScriptEngineManager::purgeSharedManager();
}



       我暂且大着胆子把释放脚本引擎这一句的注释去掉,编译运行吧。发现关闭程序后在CCObject.cpp中会崩溃掉。



Cocos2d-x 的“HelloLua” 深入分析_脚本_04



     看一下堆栈,可以发现是在显示设备析构时进入基类析构造成的。我们可以在CCDirector和AppDelegate的析构里加断点来运行一下程序,可以看到在关闭程序后,是先执行AppDelegate的析构,后执行CCDirector的折构。因为在AppDelegate的析构中我们执行了对脚本引擎的调用。所以在CCObject的析构中。CCScriptEngineManager::shareManager()->getScriptEngine()返回的是个空指针,而空指针再调用removeCCObjectByID,那可不必崩嘛!好了,知道原理了,修改吧。




if (m_nLuaID)
{
CCScriptEngineManager* tpScriptEngineManage = CCScriptEngineManager::sharedManager() ;
if(tpScriptEngineManage)
{
CCScriptEngineProtocol* tpEngineProtocol = tpScriptEngineManage->getScriptEngine();
if(tpEngineProtocol)
{
tpEngineProtocol->removeCCObjectByID(m_nLuaID);
}

}

//CCScriptEngineManager::sharedManager()->getScriptEngine()->removeCCObjectByID(m_nLuaID);
}



再次运行程序,关闭后可以看到内存泄漏的数量大幅减少。


Cocos2d-x 的“HelloLua” 深入分析_引擎_05



      仍然有一些泄漏,先来判断一下这些泄漏的出处吧,既然是Lua引擎的使用示例,跟Lua多少可能有关,我们打开hello.lua,将文件开头用“- -[[” 结尾用“]] - -” 将整篇注释。之后运行程序,哈哈,在输出窗口没有提示任何内存泄漏。这就说明内存泄漏与Lua引擎有一定关系。好吧,我们一点一点打开hello.lua中的语句。

       将“- - [[”移到“local winSize”之前,运行,仍然没有任何内存泄漏,证明上面的语句都不会有问题,再移到下一行,运行。哦哦~,出现了!4字节的内存泄漏。

Delete CriticalSection spin count Detected memory leaks!
Dumping objects ->
{12911} normal block at 0x00481AD0, 4 bytes long.
Data: < > 00 00 00 00
Object dump complete.
The program '[8684] HelloLua.exe: Native' has exited with code 0 (0x0).


可以肯定的是在这一行Lua脚本引擎一定申请了4字节的内存。


local winSize = CCDirector:sharedDirector():getWinSize()



       这里面调用了两个函数,第一个是sharedDirector(),第二个是getWinSize()。我们先来看getWinSize()是否有内存申请。打开CCDirector.cpp中找到getWinSize函数加入断点。运行程序,中断后我们观察其调用堆栈,正好是Lua脚本调用。


Cocos2d-x 的“HelloLua” 深入分析_animation_06




       继续按F10运行,进入Lua调用的静态函数


Cocos2d-x 的“HelloLua” 深入分析_function_07




tolua_register_gc(tolua_S,lua_gettop(tolua_S));这一行其实是将指针数据注册到lua的拉圾收集器,由拉圾收集器负责处理。我们现在打开LuaCocos2d.cpp的最上面,会发现有一些供lua调用的静态函数,它们均以tolua_collect做函数名称的前缀,其意义是为了释放相应类形的内存块。我们找到下面一段:

static int tolua_collect_CCSize (lua_State* tolua_S)
{
CCSize* self = (CCSize*) tolua_tousertype(tolua_S,1,0);
Mtolua_delete(self);
return 0;
}



     顾名思义,这个函数对CCSize类型的指针进行delete,我们在这里加入断点。然后再启动程序。当我们关闭程序时,会断在这里。

 

Cocos2d-x 的“HelloLua” 深入分析_animation_08




       看一下调用堆栈,很明显,在Lua脚本引擎析构时进行了拉圾清空处理,这里可以看一下tolua_tousertype(tolua_S,1,0)返回的self的值,没错,是我们使用的CCSize。所以这里可以确定getWinSize是没有问题的。那4字节的内存泄漏应该是sharedDirecotor()函数造成的。

       我们在CCDirector.cpp中找到sharedDirector()函数,加断点运行。略过前20次中断,在第21次中断时,我们可以看到函数调用堆栈的情况跟lua有关,也就是上一句Lua在调用C函数触发的。这里已经是相当底层了,做为入门教程,我并不打算再对外有的内存进行一步一步的跟踪排查了。我们还是寄希望作者会在后面的版本中修复内存泄漏的问题吧。好了。有点累了,玩去~