cocos2dx——项目启动流程与跨平台原理

最近大部分空余时间都在开始学习cocos2d游戏开发相关技术,作为一个游戏行业菜鸟级别的选手,我觉得基础很重要,所以我决定从cocos2dx启动流程和快平台原理开始!

注:这里只针对Lua做相关介绍,为什么是Lua,有过cocos2dx经验的朋友应该都知道Lua小,快,简单,而且比较大众化,而C++就不多多说了,大部分程序员都有畏惧心理,当然也有一些C++比较好,或者有独特爱好和专研朋友!

曹理鹏(iCocos)-梦工厂

关于其他平台基本上的流程和原理其实是一样的,可以直接参考!

什么是Lua

  • 百科:
    • Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
    • Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。
    • Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。

很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。这其中包括魔兽世界、博德之门、愤怒的小鸟、QQ三国、VOCALOID3、太阳神三国杀、游戏王ygocore等。

Cocos2d-lua,其实只是Cocos2d引擎添加了Lua绑定的版本。

Cocos2d这里就不解释了,懒得拷贝,只是关于cocos创建和使用的时候需要注意的是

cocos new TestProj -d Desktop/ -l lua,这里的引擎其实是同一套,只是创建工程时提供了不同语言的桥接层

  • 使用C++语言和Cocos2d-x引擎进行开发时,我们写的代码是直接调用引擎的API的,因为引擎也是用C++语言编写,不需要进行语言转换
  • 使用Lua语言和Cocos2d-x引擎进行开发时,我们写的代码通过LuaEngine执行,而LuaEngine封装了Cocos2d-x引擎的API,所以就相当于使用Lua脚本在调用Cocos2d-x的API了

各个平台的入口

iOS
#import <UIKit/UIKit.h>

int main(int argc, char *argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, @"AppController");
    [pool release];
    return retVal;
}
Mac OS
#import <Cocoa/Cocoa.h>

int main(int argc, char *argv[]) {
    return NSApplicationMain(argc, (const char **)argv);
}
Linux
int main(int argc,char *argv)
{
    AppDelegate app;
    return Application::getInstance()->run();
}
Android
void cocos_android_app_init(JNIENV* env)
{
    appDelegate.reset(new AppDelegate());
    //新版本 : AppDelegate *pAppDelegate = new AppDelegate();
}
  • Android启动流程概述
      1. 配置文件Manifest
      1. AppActivity
      1. onCreate
      1. super.onCreate
      1. onLoadNativeLibraries
      1. System.loadLibrary(libName);
      1. 触发cocos_android_app_init(在main.cpp)中
      1. 再由库执行调用对应的Lua代码
    • ……
由于笔者一直着力于iOS平台,所以这里以iOS为例,其他平台可参考:

1. main函数入口

iOSApp中打开程序,加载完动态库,和一些必备的初始化和准备(rebase, bind,SetUp)之后,会回到main函数开始执行真正的程序代码

int main(int argc, char *argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, @"AppController");
    }
}

Main函数里面会调用UIApplicationMain, UIApplicationMain会一路走完这些流程

+ 根据principalClassName传递的类名创建UIApplication对象
+ 创建UIApplication代理对象,给UIApplication对象设置代理
+ 开启主运行时间循环,处理事件,保持程序一直运行
+ 加载info.plist,判断下是否指定了main,如果指定了,就会去加载

我们可以看到这里设置的代理是AppController,然后看看AppController

2. 代理对象AppController

AppController里面有个 didFinishLaunchingWithOptions,这个是程序加载完毕的监听方法

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

cocos2d::Application *app = cocos2d::Application::getInstance();

// Initialize the GLView attributes
app->initGLContextAttrs();
cocos2d::GLViewImpl::convertAttrs();

// Override point for customization after application launch.

// Add the view controller's view to the window and display.
window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];

// Use RootViewController to manage CCEAGLView
_viewController = [[RootViewController alloc]init];
_viewController.wantsFullScreenLayout = YES;


// Set RootViewController to window
if ( [[UIDevice currentDevice].systemVersion floatValue] < 6.0)
{
// warning: addSubView doesn't work on iOS6
[window addSubview: _viewController.view];
}
else
{
// use this method on ios6
[window setRootViewController:_viewController];
}

[window makeKeyAndVisible];

[[UIApplication sharedApplication] setStatusBarHidden:true];

// IMPORTANT: Setting the GLView should be done after creating the RootViewController
cocos2d::GLView *glview = cocos2d::GLViewImpl::createWithEAGLView((__bridge void *)_viewController.view);
cocos2d::Director::getInstance()->setOpenGLView(glview);

//run the cocos2d-x game scene
app->run();

return YES;
}

前面分别:获取Director,GLView设置GLView,最后执行run

3. run cocos2d delegate

我们看看run方法里面,这里的run方法很关键,

int Application::run()
{
    if (applicationDidFinishLaunching())
    {
        [[CCDirectorCaller sharedDirectorCaller] startMainLoop];
    }
    return 0;
}

有个applicationDidFinishLaunching,其实这里cocos2d默认的代理,在class中

4. cocos2d配置,lua加载

这里在coco2d的代理中,可以看到一堆的初始化配置和加载,然后就开始获取并执行脚本lua(通过lua脚本显示并处理Scene逻辑)

bool AppDelegate::applicationDidFinishLaunching()
{
    // set default FPS
    Director::getInstance()->setAnimationInterval(1.0 / 60.0f);

    // register lua module
    auto engine = LuaEngine::getInstance();
    ScriptEngineManager::getInstance()->setScriptEngine(engine);
    lua_State* L = engine->getLuaStack()->getLuaState();
    lua_module_register(L);

    register_all_packages();

    LuaStack* stack = engine->getLuaStack();
    stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));

    //register custom function
    //LuaStack* stack = engine->getLuaStack();
    //register_custom_function(stack->getLuaState());

    #if CC_64BITS
        FileUtils::getInstance()->addSearchPath("src/64bit");
    #endif
        FileUtils::getInstance()->addSearchPath("src");
        FileUtils::getInstance()->addSearchPath("res");
        if (engine->executeScriptFile("main.lua"))
        {
            return false;
        }

    return true;
}
原生集成补充

如果你是直接在原生嵌入而不是夸平台会看到这里其实是创建并返回Scene

bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();
    if(!glview) {
        glview = GLViewImpl::create("Fiction_Single");
        director->setOpenGLView(glview);
    }

    // turn on display FPS
    // director->setDisplayStats(true);
    // set FPS. the default value is 1.0/60 if you don't call this
    director->setAnimationInterval(1.0f / 60);
    register_all_packages();

    //SpriteFrameCache::getInstance()->removeSpriteFrames();
    //SpriteFrameCache::getInstance()->removeUnusedSpriteFrames();

    // create a scene. it's an autorelease object
    // 初始化与运行主场景
    auto scene = GameMainLayer::MainScene();    // 初始化与运行主场景
    director->runWithScene(scene);
    return true;
}

游戏逻辑就可以从这个Scene中的init函数开始,添加UI层,添加事件监听器,添加游戏层等等…如果我们有一些统计、资源管理器等,也可以在AppDelegate的applicationDidFinishLaunching函数中来进行。

5. Lua脚本初始化

在这之前,我们先先看看,项目的配置文件config.json

{
    "init_cfg":{
       "isLandscape": true,
       "isWindowTop": false,
       "name": "MyDemo",
       "width": 960,
       "height": 640,
       "entry": "src/main.lua",
       "consolePort": 6050,
       "uploadPort": 6060
    },
    "simulator_screen_size": [
        {
            "title": "iPhone 3Gs (480x320)",
            "width": 480,
            "height": 320
        },
         ......
    ]
}

可以看到”entry”: “src/main.lua”,也就是说入口文件是main.lua,由此可以得知,即使我们不从iOS的启动流程来看,整个启动也会从这里开始,进入。

再来看”main.lua”,这个时候我们就开始开发查看main.lua文件

从MyApp开始,初始化,然后执行run运行程序

cc.FileUtils:getInstance():setPopupNotify(false)

require "config"
require "cocos.init"

local function main()
    require("app.MyApp"):create():run()
end

local status, msg = xpcall(main, __G__TRACKBACK__)
if not status then
    print(msg)
end

这里很重要的一个方法是run(),run()方法是设置启动View,执行main函数,main函数里加载MyApp创建并运行,进而打开MyApp.lua:

6. MyApp.lua

我们看到MyApp仅仅是继承自AppBase,onCreate函数只是初始化了下随机数种子,也就意味着更多的操作在AppBase中,我们打开分析:

local AppBase = class("AppBase")

function AppBase:ctor(configs)
    self.configs_ = {
        viewsRoot  = "app.views",
        modelsRoot = "app.models",
        defaultSceneName = "TitleScene",
    }

    for k, v in pairs(configs or {}) do
        self.configs_[k] = v
    end

    if type(self.configs_.viewsRoot) ~= "table" then
        self.configs_.viewsRoot = {self.configs_.viewsRoot}
    end
    if type(self.configs_.modelsRoot) ~= "table" then
        self.configs_.modelsRoot = {self.configs_.modelsRoot}
    end

    if DEBUG > 1 then
        dump(self.configs_, "AppBase configs")
    end

    if CC_SHOW_FPS then
        cc.Director:getInstance():setDisplayStats(true)
    end

    -- event
    self:onCreate()
end

function AppBase:run(initSceneName)
    initSceneName = initSceneName or self.configs_.defaultSceneName
    self:enterScene(initSceneName)
end

function AppBase:enterScene(sceneName, transition, time, more)
    local view = self:createView(sceneName)
    view:showWithScene(transition, time, more)
    return view
end

function AppBase:createView(name)
    for _, root in ipairs(self.configs_.viewsRoot) do
        local packageName = string.format("%s.%s", root, name)
        local status, view = xpcall(function()
                return require(packageName)
            end, function(msg)
            if not string.find(msg, string.format("'%s' not found:", packageName)) then
                print("load view error: ", msg)
            end
        end)
        local t = type(view)
        if status and (t == "table" or t == "userdata") then
            return view:create(self, name)
        end
    end
    error(string.format("AppBase:createView() - not found view \"%s\" in search paths \"%s\"",
        name, table.concat(self.configs_.viewsRoot, ",")), 0)
end

function AppBase:onCreate()
end

return AppBase

在前面的分析中知道main.lua是执行的是App的run函数,作为基类的AppBase,当然也要被调用run函数,因此直接看run函数:主要是创建并进入场景initSceneName,如果run的参数没有指定开始的场景则使用默认场景defaultSceneName,默认场景在构造函数的时候被初始化为MainScene,也就是说场景默认将从MainScene开始。

7. 指定Scene

如果想指定启动,而不是使用默认的Scene,那么项目启动后会直接进入该场景,这点有个好处是如果要调试设计某场景可以直接从这个场景进入,不必从其他场景进入了。

local function main()
    require("app.MyApp"):create():run("StartScene")
end

默认不设置会选择MainScene,如果仔细再AppBase里面会看到这样一行代码

function AppBase:ctor(configs)
self.configs_ = {
    viewsRoot  = "app.views",
    modelsRoot = "app.models",
    defaultSceneName = "MainScene",
}
.....

那么项目启动后会直接进入StartScene场景,而不再是默认的MainScene场景。

8. 根据脚本指定Scene,

上面说了,如果没有设置就会试着用默认的Scene,run里面的逻辑就是去真正指定Scene,

-- 创建完对象之后,就到了这一步
function AppBase:run(initSceneName)
    initSceneName = initSceneName or self.configs_.defaultSceneName
    self:enterScene(initSceneName)  -- 如果没有指定第一个Scene,则第一个Scene为MainScene
end
-- 生成并进入第一个Scene
function AppBase:enterScene(sceneName, transition, time, more)
    local view = self:createView(sceneName)     -- 前去生成View
    view:showWithScene(transition, time, more)  -- 因为MainScene继承自ViewBase类,这里就吊用ViewBase的方法了
    return view
end

建议仔细读一下AppBase,ViewBase!

9. 自定义Scene(StartScene)

这个时候就会首先从StartScene开始,

local StartScene = class("StartScene", cc.load("mvc").ViewBase)

function StartScene:onCreate()

    display.newSprite("HelloWorld.png")
    :move(display.center)
    :addTo(self)

    cc.Label:createWithSystemFont("cocos2dx Run StartScene", "Arial", 60)
    :move(display.cx, display.cy + 200)
    :addTo(self)

end

return StartScene

然后后面步骤就是从StartScene开始,写脚本代码,处理UI,网络和逻辑等,

到这里就基本上完成cocos2dx Lua启动流程,其他C++,JS原理和流程其实都差不多, 只是执行的方法或者代码不一样而已

总结

其实关于coocs2dx之前的版本我不了解,就我目前了解到的,其实就是由通用程序入口到跨平台程序入口

基本的流程如下

  • 初始化Director
  • 新建GLView,然后进行一些设置
  • 新建Scene
  • 使用Director运行这个场景

bool AppDelegate::applicationDidFinishLaunching()//程序入口 -> 跨平台程序入口
{
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();
    if(!glview)
    {
        glview = GLViewImpl::create("my test");
        director->setOpenGLView(glview);
    }

    //初始化、资源适配、屏幕适配、运行第一个场景...
    glview->setDesignResolutionSize();
    auto scene = Hellow::scene();
    director->runWithScene(scene);
    return scene;
}

通过上面我们其实可以知道,cocos2d_lua_bindings库提供了Lua对Cocos2d引擎的绑定,相当于通过注册Module的方式对Cocos2d引擎提供的(相关的)API进行了一次封装(把常用的功能封装成一个函数newScene)。

相对于Cocos2d-x C++工程来说,Cocos2d-x生成的Lua语言工程提供了对Cocos2d引擎的Lua语言封装。将Cocos2d引擎API绑定到对应的Lua语言函数,在调用到这些函数时,会执行对应的Cocos2d引擎API, 其实最终还是调用的C++代码和对应的引擎代码。

跨平台原理

  1. AppDelegate 作为跨平台程序入口,在这之上做了另一层的封装,封装了不同平台的不同实现。比如我们通常认为一个程序是由 main 函数开始运行,那我们就去找寻,我们看到了在 proj.linux 目录下存在 main.cpp 文件。
  1. 在main.cpp 中 CCApplication::sharedApplication()–>run(); 这一句看起,这一句标志着, cocos2d-x 程序正式开始运行.
  1. 定位到 sharedApplication() 方法的实现,在CCAplication类中我们可以看到从 sharedApplication() 方法,在调用 run() 方法,在这之前,我们需要调用到它的构造函数,否则不能运行,这就是为什么在 CCApplication::sharedApplication()–>run(); 之前,我们首先有语句 AppDelegate app;

    创建 AppDelegate 变量的原因是 AppDelegate 是 CCApplication 的子类,在创建子类对象的时候,调用其构造函数的同时,父类构造函数也会执行,然后就将 AppDelegate 的对象赋给了 CCApplication 的静态变量,而在 AppDelegate 之中我们实现了 applicationDidFinishLaunching方法,所以在 CCApplication 中 run 方法的开始处调用的就是 AppDelegate 之中的实现。

  1. 我们在此方法中我们初始化了一些变量,创建了第一个 CCScene 场景等,之后的控制权,便全权交给了CCDirector::sharedDirector()–>mainLoop(); 方法了。

曹理鹏(iCocos)-梦工厂

  • 在cocos2d-x的文件夹下,有一个platform文件夹,里面存放了跨平台的封装接口。

  • 当前目录下有CCApplicationProtocol.h头文件,子目录有win32,Android,IOS三个文件夹,里面分别存放跨平台需要的函数,其中包括CCApplication。

  • 而AppDelegate 类则是继承自CCApplication。CCApplication又继承自CCApplicationProtocol。

  • 在CCApplicationProtocol中定义了applicationDidFinishLaunching虚方法,由CCApplication 继承, AppDelegate 实现的。以此实现了跨平台。

为了充分发挥硬件性能,手机游戏通常使用Native App开发模式,这就造成开发商要为iOS 和Android平台用户开发不同的应用,无论是产品迭代还是运行维护都非常麻烦。

Cocos2d-x在iOS,Android等移动平台之上,封装了一层C++接口,从而屏蔽了平台的差异性,通过平台宏来控制使用哪个平台的代码,向开发者提供C++接口调用。这些接口主要包括UI、事件和网络,封装UI主要是使用OpenGL ES的接口来写UI,封装事件和网络,均是使用C++接口对原生接口进行一层封装。

其实对应cocos2dx跨平台在说,我们这么理解就可以了

  • Lua底层是通过C编写实现的
  • Android通过JNI技术调用C
  • iOS也是完全兼容C语言
我们打开对应的工程可以看到
  • Java项目中,有些关于Lua的文件,

    • luajava.jar是Java代码封装包,
    • libluajava-1.1.so在底层封装了.C 文件,实现了lua相关底层功能。
  • 但是在iOS项目中,我们发现

    iOS允许开发者使用C语言文件和objective-c文件混合编程。但是,如果你在Objective-C的代码中调用C文件中的函数,你不能直接将.c文件import到你的OC文件中,这样是不起作用的。你需要先创建一个.h 头文件 里面包含你的函数申明,同时将这个.h 头文件import到.c文件中,.c文件负责实现要调用的函数。最后将你新创建的.h头文件import到OC文件中,这样你就可以在OC的文件中调用C的方法了

  • 具体更多启动相关细节,可以参考这里

坚持原创技术分享,您的支持将鼓励我继续创作!