【cocos2dx面试题干货】--2021年度最新cocos2dx面试干货(引擎篇 )

 

  大家好,我是Lampard~~

  经过春招一番艰苦奋战之后,我终于是进入了心仪的公司。

  今天给大家分享一下我在之前精心准备的一套面试知识。

  由于我面试的岗位是cocos2dx的开发岗,今天就先给大家分享一下引擎方面的知识。

  本人亲测80%的引擎相关题目都是围绕着我总结出来的知识点提出的 。

 

  一.cocos的内存管理方式

cocos是通过引用计数方式管理内存,主要通过两种方式实现


1.手动管理内存

当我们创建一个实例时,基类Ref里面有一个叫referenceCount的参数就会置1,它代表我们引用的次数,使用retain()函数

可以使refernceCount+1release()函数会使referenceCount-1当这个参数减至0时会被引擎delete掉释放内存

 

2.自动回收池 

有手动就会有自动。我们在create()一个对象时,调用new函数创建对象并且还会利用autorelease()方法把该对象的指针加入自动回收池,主线程每一帧在Application::getInstance()->run()的过程中会调用 mainloop()方法

mainloop()方法中会利用单例的PoolManager类getCurPool()获取当前的自动回收池,并调用自动回收池中的clear()方法,

clear()方法里,会遍历移除所有与该自动回收池相关联的对象,并调用一次release()方法,如果这个对象没被调用且referenceCount被减至0就会被delete掉。若这个对象已经addChild到其他控件上呗引用的话,则referenceCount不为0,等其父节点被清理时再回收

 

好处:

1.无需手动的release()retain(),避免内存泄漏。

2.在每一帧结束后对无用的对象进行自动处理内存回收,方便可靠。

 

可优化的地方:

1.我们的PoolManager管理的是一个_releasePoolStack(存储自动回收池的栈),一般情况下,每一帧结束后我们只是把当前的池子给清空,然后执行下一帧的操作。但是我们也应该考虑到释放池本身维护着一个将要执行释放操作的对象列表,如果在一帧之内生成了大量的autorelease对象,将会导致释放池性能下降。因此,在生成autorelease对象密集的区域(通常是循环中)的前后,我们最好可以手动创建并释放一个回收池。

2.autorelease()只有在自动释放池被释放时才会进行一次释放操作,如果对象释放的次数超过了应有的次数,则这个错误在调用autorelease()时并不会被发现,只有当自动释放池被释放时(通常也就是游戏的每一帧结束时),游戏才会崩溃。在这种情况下,定位错误就变得十分困难了。因此,我们建议在开发过程中应该避免滥用autorelease(),只在工厂方法等不得不用的情况下使用,尽量以release()来释放对象引用。

 

二.cocos的渲染机制

3.x之前是通过调用每一个nodedraw方法来使用OpenGL ES代码进行渲染,3.x之后则使用了新的渲染机制,统一把所有需要渲染的node安放在一个队列中再进行自动批处理渲染。

主线程每一帧在Application::getInstance()->run()的过程中会调用 mainloop()方法,mainloop()方法中会调用一次drawScene()方法

方法名是drawScene,那么重点肯定就是对当前场景需要渲染的结点进行渲染。先清除渲染状态,然后调用render()方法。

接下来我们看看render函数具体是怎么实现的:它首先是使用visit方法让需要被渲染的结点进行排序并插入到渲染队列CommandQueue中,然后再一起自动批处理进行渲染。

我们接着看visit方法,首先它根据localZOrder使用sortAllChildren()来进行排序

紧接着对localZOrder<0的结点进行递归渲染。

然后是渲染本身结点,最后再递归渲染localZOrder>0的子节点。其本质就是按照(左,中,右)中序遍历进行渲染

往下看每个结点的draw函数,我们可以发现,相比于2.x的版本,3.x之后没有直接在draw函数中进行渲染,而是把渲染命令压入到渲染对列中去,最后再回到render函数中进行进行自动批处理渲染

void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    // 判定纹理是否有效
    if (_texture == nullptr)
    {
        return;
    }

#if CC_USE_CULLING
    // Don't calculate the culling if the transform was not updated
    auto visitingCamera = Camera::getVisitingCamera();
    auto defaultCamera = Camera::getDefaultCamera();
    if (visitingCamera == defaultCamera) {
        _insideBounds = ((flags & FLAGS_TRANSFORM_DIRTY) || visitingCamera->isViewProjectionUpdated()) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
    }
    else
    {
        // XXX: this always return true since
        _insideBounds = renderer->checkVisibility(transform, _contentSize);
    }

    // 判定渲染的纹理是否在可见区域内
    if(_insideBounds)
#endif
    {    
        _trianglesCommand.init(_globalZOrder,
                               _texture,
                               getGLProgramState(),
                               _blendFunc,
                               _polyInfo.triangles,
                               transform,
                               flags);
        // 将绘制命令添加到renderer绘制栈RenderQueue中
        renderer->addCommand(&_trianglesCommand);
    }
}

render中,对队列中需要渲染的结点根据其GlobalZorder进行排序,globalZOrder 是一个 float (不是 int的参数。这个值在 渲染器 中用来给 RenderCommand 排序。较低的值拥有较高的优先级。这意味着一个 globalZorder 为 -10的节点会比一个 globalZOrder 为 10 的节点优先绘制.globalZOrder 为 0 (默认值)的节点将会根据 Scene Graph 顺序绘制。

然后再对队列中的渲染命令调用OpenGL的API进行渲染。自此完成了整个渲染流程。

总的来说:

导演类的mainLoop中会调用drawScene,在drawScene中会调用场景类的render,render中会递归执行节点类的visit,visit中会调用精灵类的draw,draw中会执行渲染类的addCommand。对所有节点执行完addCommand后,会执行渲染类的processRenderCommand,接下来执行渲染类的drawBatchedTriangles,最终在drawBatchedTriangles内会调用多个openGL API完成渲染。


三.cocos的cache缓存区

TextureCache:加载大纹理图如背景图片(pkm,png)

Director::getInstance()->getTextureCache()->addImage(filename)  // 存入

Texture2D *texture = Director::getInstance()->getTextureCache()->getTextureForKey(textureKeyName)  // 使用

Director::getInstance()->getTextureCache()->removeUnusedTextures() // 清理没被引用的
Director::getInstance()->getTextureCache()->removeAllTextures()  // 清理全部

SpriteFrameCache.:加载碎的精灵帧图片,plist文件

SpriteFrameCache *frameCache = SpriteFrameCache::getInstance() // 存入

frameCache->addSpriteFramesWithFile("boy.plist", 具体某张图) // 参数二可不填

auto frame_sp = Sprite::createWithSpriteFrameName("boy1.png") // 取出

SpriteFrameCache::getInstance()->removeUnusedSpriteFrames() // 清理没用的
SpriteFrameCache::getInstance()->removeSpriteFramesFromFile(const std::string &plist) // 清理某plist
SpriteFrameCache::getInstance()->removeSpriteFramesFromTexture(cocos2d::Texture2D *texture) // 清理某图片

AnimationCache:把像走路,奔跑等多次重复的动作加载进缓存中

AnimationCache::getInstance():addAnimation(Animation *animation, const std::string& name) // 存入

AnimationCache::getInstance():getAnimation(const std::string& name) // 取出

void removeAnimation(const std::string& name) // 移除

他们的作用都是把图片/动作进行预加载,cache首先会对每种缓存类型进行拆分成键值对(比如纹理则存纹理类型,精灵帧则存精灵帧类型,动画则存动画类型)

然后利用Map容器(Map是通过键值对的形式存储数据)储存之后,再次使用该资源时,就可从内存中直接取出,从而避免IO造成的卡顿现象

顺带一提的是,纹理缓存和精灵帧缓存之间的区别是如果精灵帧缓存在缓存区中找不到想要的图片时,它会报null,而TextureCache则会在系统路径中查找该图,精灵帧缓存是基于纹理缓存再进行的封装。

出于安全角度,建议的释放顺序是:动画缓存 先于 精灵帧缓存 先于 纹理缓存

 

四.SpriteBatchNode精灵批处理类

SpriteBatchNode* spriteBatchNode=  SpriteBatchNode::create();

spriteBatchNode.addchild(纹理);

对同一张纹理图进行多次重复的绘制,如子弹,粒子。而不用每一个粒子分开绘制,降低渲染批次从而提高效率。也就是说引擎只需要对spriteBatchNode这个对象进行渲染而不需要分开多次对不同精灵进行渲染。

在3.0之前,SpriteBatchNode是一个很好的优化游戏方式,然而到了3.0版本之后,由于有了新的渲染系统机制,我们将不再需要甚至不推荐再使用这个类。

限制

Sprite 对象的孩子只能是 Sprite (否则,Cocos2d-x 会触发断言)

  •  Sprite 的父节点是 SpriteBactchNode 时,不能添加 ParticleSystem 作为 Sprite的子节点。
  • 这将导致当 Sprite 的父节点是 SpriteBatchNode 时,不能使用 ParallaxNode
  • 所有的 Sprite 对象必须共享相同的纹理ID (否则,Cocos2d-x 会触发断言)
  • Sprite 对象使用 SpriteBatchNode 的混合函数和着色器。

v2.2 和 v3.0 最大的区别在于:

  • Sprite 对象可以有不同的纹理ID。
  • Sprite 对象可以有不同种类的 Node 作为子节点,包括 ParticleSystem
  • Sprite 对象可以有不同的混合函数和不同的着色器。

但是如果你这么做,渲染器 可能无法对它所有的子节点进行批处理(性能较低)。但是游戏仍然可以正常运行,不会触发任何断言。

优化

  • 保持将所有的精灵放在一张大的 spritesheet 中。
  • 使用相同的混合函数(使用默认)
  • 使用相同的着色器(使用默认)
  • 不要将精灵添加到 SpriteBatchNode

 

五.cocos2d-x程序的开始与结束流程

我们知道,无论是cocosluacocos2dx,还是cocosCreator,程序总是从AppDelegatedirector中调用mainloop进入主循环开始的。那么cocos引擎中是如何结束整个游戏进程的呢?

在正个mainloop过程中,引擎首先会进行一次是否结束游戏进程的判断操作,那我们看看这个参数是什么时候被赋值的

我们查询后发现,只有导演类调用end()函数的时候才会把该值赋为true,所以在这里我们知道了第一个结束进程的方式,就是手动调用导演类的end函数

那么有没有是引擎本身调用end函数的时候呢?我们查找到,当场景栈全部pop出来,没有场景在场景栈中的时候,就会执行导演类的end函数,之前在学习cocos的时候我们就知道场景仿佛就是一个舞台,layer是一个画布,现在舞台都没有了那当然就执行结束流程了呀。

我们回到mainLoop中,看到cocos是这样执行结束回收流程的:1.暂停计时器和事件派发器, 2.回收当前运行场景,3.销毁各种缓存区,4.对openGL执行end回收函数

(下图为OpenGLend函数)

CCEGLView* CCEGLView::sharedOpenGLView() 
{ 
    static CCEGLView* s_pEglView = NULL; 
    if (s_pEglView == NULL) 
    { 
        s_pEglView = new CCEGLView(); 
    } 
    return s_pEglView; 
} 
... 
 
// openglview 结束方法 
void CCEGLView::end() 
{ 
    /* Exits from GLFW */ 
    glfwTerminate(); 
    delete this; 
    exit(0); 
} 

cocos2d-x 程序的结束流程

程序运行时期,由 mainLoop 方法维持运行着游戏之内的各个逻辑,当在弹出最后一个场景,或者直接调用 CCDirector::end(); 方法后,触发游戏的清理工作,执行 purgeDirector 方法,从而结束了 CCEGLView(不同平台不同封装,PC使用OpenGl封装,移动终端封装的为 OpenGl ES) 的运行,调用其 end() 方法,从而直接执行 exit(0); 退出程序进程,从而结束了整个程序的运行。(Android 平台的 end() 方法内部通过Jni 方法 terminateProcessJNI(); 调用 Java 实现的功能,其功能一样,直接结束了当前运行的进程)

 

六.如何构建序列帧动画

之前我曾今分享过 一篇如何手动实现序列帧动画的博客

创建序列帧动画方法

大致的思路是:1.先把所用到的plist文件存入精灵帧缓存区

2.循环遍历所需帧的图片,把一帧一帧的图片加载进实例化的Animation动作中

3.把这个Animation作为参数,生成一个Animate动画。此时就已经生成出一个可播放的序列帧动画了。

 

七.cocos2dx的屏幕适配方案

常规做法:

pEGLView::sharedOpenGLView()->setDesignResolutionSize()

通过导演类调用OpenGl函数 设置屏幕的配置方案

里面接五个参数

KResolutionExactFit:拉伸变长宽比,全屏显示

kResolutionFixedHeight:不变长宽比(保持传入的设计分辨率高度不变,根据屏幕分辨率修正设计分辨率的宽度),全屏但是有一部分宽度不显示

kResolutionFixedWidth:不变长宽比(保持传入的设计分辨率宽度不变,根据屏幕分辨率修正设计分辨率的高度),全屏但是有一部分高度不显示

KResolutionNoBorder:不变长宽比(取Max(设计高度 / 实际高度, 设计宽度 / 实际宽度)的值作为缩放比例),全屏但是有一部分不显示

KResolutionShowAll:不变长宽比,有黑边

我们看看前辈们测试的demo:

 

以为这就已经问没解决问题了?

NONONO,对于以前那些老机子来说,确实可以解决问题了

但是现在手机换代越来越快,市面上推出了许许多多水滴屏,挖孔屏,刘海屏等奇奇怪怪的屏幕

这个时候我们就得更灵活的去处理问题了,不然你好好的一个充值按钮被刘海给挡住了,那直接举双手GG。

我们可以采取以下的方法解决问题:

1.从设计开始避免,把重要的信息,按钮不要摆放到太靠边的位置。

我们之后做的适配都是为了解决因为刘海屏,水滴屏而造成的游戏体验不好或者影响游戏操作的问题。那么既然如此为什么不从设计开始避免这些问题的出现呢?

2.一般采用下图这种适配方案:

 

  • 1.IOS苹果手机因为机型类型比较少,所以游戏正文所占屏幕区域的宽高比(safeArea)我们可以直接配置,比如["iPhone Xs"] = 2、["iPhone Xs Max"] = 2。
  • 2.安卓机器机型比较多而杂,无法通过配置写死safeArea,所以通过代码取得,指的一提的是安卓9.0以下的时候没有统一的获取方式,华为 oppo Vivo 小米需要各自厂商不同的接口分别获取,安卓9.0以上可以统一获取。(具体可自行百度或者参考AppActivity.java)
    取得safeArea以后:
    local safeWidth = realHeight * safeArea (游戏正文宽度)
    local offset = (realWidth - safeWidth)/2 (左右刘海宽度)
    g_UIMainSceneLayer:setPositionX(offset) (将所有界面的父节点 右移offset)
    local safeSize = cc.size(safeWidth,realHeight)
    g_UIMainSceneLayer:setContentSize(safeSize) (设置contentSize为正文大小)
    如果界面不需要适配,需要全屏显示。比如有的背景界面需要在刘海区域显示,就需要将界面左移回来,并设置contentSize大小为realSize

 

八.cocos事件分发机制

  • 事件源:场景中的对象,每个Node节点都可以触发事件
  • 事件监听者:监听器EventListener,负责监控对象是否触发某个事件, 如果触发就执行相应的回调函数。
  • 事件分发者:事件分发器EventDispatcher,负责分配事件监听器给对象。在游戏启动时,就会创建一个默认的EventDispatcher对象

  事件类型

enum class Type
{
    TOUCH,          // 触摸事件,单点触摸,多点触摸
    KEYBOARD,       // 键盘事件(按下,返回,弹出)
    ACCELERATION,   // 加速器事件(x,y,z轴的倾斜角度来判断加速状态)
    MOUSE,          // 鼠标事件
    FOCUS,          // 焦点事件
    CUSTOM          // 自定义事件
};

事件监听器类型

enum class Type
{
    UNKNOWN,            // 未知的事件监听器
    TOUCH_ONE_BY_ONE,   // 单点触摸事件监听器
    TOUCH_ALL_AT_ONCE,  // 多点触摸事件监听器
    KEYBOARD,           // 键盘事件监听器
    MOUSE,              // 鼠标事件监听器
    ACCELERATION,       // 加速器事件监听器
    FOCUS,              // 焦点事件监听器
    CUSTOM              // 自定义事件监听器
};

关于事件监听器的优先权

通过 addEventListenerWithSceneGraphPriority 添加的监听器,优先权为0。

通过 addEventListenerWithFixedPriority 添加的监听器,可以自定义优先权,但不能为0。

优先级越低,越先响应事件。

如果优先级相同,则上层的(z轴)先接收触摸事件

 

当使用场景图优先级时,实际上是在树的上方向后移动。如果一个事件被触发,H会看一眼,要么吞下它,要么让它传递给I

同样的事情,我要么消耗它要么让它传递给G,以此类推,先找监听器,再找Z轴最大的node结点,直到事件被它吞没或者没有得到回应,则整个事件触发机制结束

 

九.cocos调度器schedule

 Cocos2d-Lua 引擎中的调度器是用来周期执行某个函数或延时执行某个函数的。功能类似于定时触发器,但它又与游戏紧密结合。

schedule调度器利用了timer类来保存了调度任务的 时间线,重复次数,事件间隔,延迟等数据。timer把相关信息以结构体的形式存进了双向链表_hashForTimers

每帧更新后Timer做事件累加,根据时间线去触发或者取消更新回调。

每帧更新每帧更新的回调放在 _hashForUpdates结构中以target作为key,同时根据优先级放在不同的hash表中。

自定义更新更新回调放在_hashForTimers hash表中,以target作为key.

每帧开始绘制之前执行Scheduler::update(float dt)

在里面遍历hash表,执行update更新回调,完成后移出。

按照优先级<0 ==0 >0 自定义更新的顺序遍历执行。

 

使用方法:Cocos2d-Lua 中的调度器分两种:全局调度器和节点调度器

全局调度器模块提供了三种调度器以满足各种需求:

(1)全局帧调度器: schduleUpdateGlobal(listener)

 顾名思义,全局帧调度器是游戏的每一帧都会触发的调度器。主要是在碰撞检测等每一帧都需要计算的地方。全局帧调度器不依赖任何场景,因此可以在整个游戏范围内实现较为精确的全局计时。


示例代码:

lcoal scheduler = require(cc.PACKAGE_NAME..".scheduler")

local function onInterval(dt)

print("update")

end

scheduler.scheduleUpdateGlobal(onInterval)

回调函数onInterval的参数dt,是两次调度之间的时间间隔。

(2)全局自定义调度器:schduleGlobal(listener,interval)

全局帧调度器是全局自定义调度器的特例,自定义调度器可以指定调度时间,提供更高的灵活性

由于引擎的调度机制,自定义时间间隔必须大于两帧的间隔,否则两帧内的多次调用会被合并成一次调用。所以自定义时间间隔应在1/60s以上(引擎默认每秒刷新60帧)

示例代码:

lcoal scheduler = require(cc.PACKAGE_NAME..".scheduler")

local function onInterval(dt)

print("Custom")

end

scheduler.scheduleGlobal(onInterval,0.5)

每隔0.5秒控制台输出信息。

(3)全局延时调度器:performWithDelayGlobal(listener,time)

 若在游戏中某些场合,只想实现一个单次的延时调用,就需要延迟调度器。scheduler.performWithDelayGlobal()会在等待指定时间后执行一次回调函数,然后自动取消该scheduler

示例代码:

lcoal scheduler = require(cc.PACKAGE_NAME..".scheduler")

local function onInterval(dt)

print("once")

end

scheduler.performWithDelayGlobal(onInterval,0.5)

在控制台只会看到一次输出。

节点调度器

NodeCocos2d-Lua引擎中的基础类,它封装了很多基础方法与属性,其中调度器就是Node提供的方法之一。Node中的调度器只能在Node中使用,Node负责管理调度器的生命周期,当Node销毁的时候,会自动注销节点名下的所有调度器

1、节点自定义调度器

由于引擎的调度机制,自定义时间间隔必须大于两帧的间隔,否则两帧内的多次调用会被合并成一次调用。所以自定义时间间隔应在0.1s以上(引擎默认每秒刷新60帧)

节点自定义调度器示例代码:

self:schdule(function() print ("schdule") end,1.0)

可以在Scene或Layer中调用上面的代码。

2、节点延时调度器

延时调度器等待指定的时间后执行一次回调函数,示例代码:

self:performWithDelay(function() print("performWithDelay") end , 1.0)

 

十.如何对游戏的卡顿问题进行优化

造成卡顿也就是掉帧的原因主要是CPU计算量和GPU渲染压力过大,因而需要对资源进行合适的处理。

首先对于图片方面:

1.对资源进行预加载到缓存区避免游戏时出现卡顿

2.使用TexturePacket/cocoStdio打包图片,多次渲染,重复生成的精灵打包在同一张纹理中,利用3.x的渲染机制对UI进行自动批处理渲染

3.优化纹理大小、格式RGB8888(32位) -> RGB4444(16位))

4.支持pvr图片的情况下,使用pvr格式的图片

5.. 不要使用超大的纹理

然后是对于音乐方面:

1.对背景音乐进行裁剪,通过重复播放的形式节省内存

2.使用的音乐应该要经过压缩,通过降低音质的方式来减少占用的内存

最后是对于代码方面:

1.写算法时,不要造成内存泄漏,使用算法尽量要用最优方案

2.对于重复使用的纹理,精灵,动作要加载进缓存区,避免重复的删除创建

3.已经没用的场景和精灵要及时的删除

 

十一.cocos使用httpClient发送请求

游戏开发的过程中难免需要和服务端进行数据交互,我们可以利用cocos给我们封装好的httpClient类来进行数据的交互。

httpClient是一个单例的类,我们可以通过HttpClient:getInstrance()来获取这个单例。

数据的交互分为请求request和返回response两个部分,作为客户端来说我们首先要把玩家操作的数据上传给服务端,然后再根据服务端发回来的结果进行一系列操作。

(1)request请求

 coco2dx中我们可以直接new HttpRequest()来创造一个http请求,在quick中我们可以createHTTPRequest来实例一个request类。

request类继承与Ref,同样受到cocos内存管理机制的管理。

作为请求我们需要几个比较重要的信息:要连接的URL,回协议时的回调,与http交互的方式,自身的tag标签等等

 我们可以看看request类中的源码是怎么样对这些参数进行设置的

    inline void setRequestType(Type type)
    {
        _requestType = type;
    };

    inline void setUrl(const char* url)
    {
        _url = url;
    };

    inline void setTag(const char* tag)
    {
        _tag = tag;
    };

    inline void setUrl(const char* url)
    {
        _url = url;
    };

    inline void setResponseCallback(const ccHttpRequestCallback& callback)
    {
        _pCallback = callback;
    }

注意,我们常用的TYPE是GET和POST,其中GET是把请求的数据跟在网址后面以 ? 分割 URL 和传输数据,多个参数用 & 连接,而POST则url只填网址,把请求的指令放在setRequestData()中 

对于GET和POST的具体区别可以看看这个文章:

GET和POST的区别可以看看这个文章

当设置完成后我们就可以让httpClient类来发送这个请求。与服务端链接成功之后,交互的信息会封装在response类中随着回调函数返回来。

 

 (2)response回复

  这个类十分简单,我们首先来看一看这个类中究竟有什么元素:

    HttpRequest*        _pHttpRequest;  /// the corresponding HttpRequest pointer who leads to this response 
    bool                _succeed;       /// to indecate if the http reqeust is successful simply
    std::vector<char>   _responseData;  /// the returned raw data. You can also dump it as a string
    std::vector<char>   _responseHeader;  /// the returned raw header data. You can also dump it as a string
    long                _responseCode;    /// the status code returned from libcurl, e.g. 200, 404
    std::string         _errorBuffer;   /// if _responseCode != 200, please read _errorBuffer to find the reason 

  其实就是请求的指针,是否成功,返回的数据,返回码(链接失败发404),和报错的数据而已

  所有我们只需要在回调函数中,对是否请求成功做出判断,就可以输出错误日志或者执行之后的操作了。

 下面给大家看一个例子:

auto request = new HttpRequest();
request->setUrl("http://www.baidu.com");    // 设置请求连接的Url
request->setReqeustType(HttpRequest::Type::GET);    // 设置发送数据的模式:get/post/put/delete
request->setReqeustCallback(CC_CALLBACK_2(requestCallback, this));    // 设置连接成功时候的回调
HttpClient:getInstrance()->send(request);    // 发送这个请求

void requestCallback(HttpClient *sender, HttpResponse *response)
{
    if (response->isSucceed())
    {
        cout << "error msg:" << response->getErrorBuffer() << endl;
        return;
    } 
    
    vector<char> *buffer = response->getResponseData();    // 请求服务端得到的数据
    long statusCode = response->getResponseCode();    // 返回码
    
    /*
        do callBack
    */
}

至此就完成一个利用http协议和服务端交互数据的流程。

 

十二.cocos使用webSocket发送请求

WebSocketHTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

Cocos2d-x引擎集成libwebsockets,并在libwebsockets的客户端API基础上封装了一层易用的接口,使得引擎在C++, JS, Lua层都能方便的使用WebSocket来进行游戏网络通讯。

构建一个webSocket的过程如下:

  • 创建并初始化一个webSocket
  •  与服务端进行连接
  • 监听服务端的发送的数据&向服务端发送数据
  • 关闭socket连接

(1) 创建并初始化一个webSocket

cocos2d::network::WebSocket* _wsiSendText = new network::WebSocket(); 

(2) 与服务端进行连接

 第一个参数是委托,传递自身的*this,第二个参数是要连接的URL

_wsiSendText->init(*this, "ws://echo.websocket.org") 

如果连接成功,WebSocket就会调用onOpen,告诉调用者,客户端到服务器的通讯链路已经成功建立,可以收发消息了。此时自身对的state会处在CONNECTING状态。

如果连接失败,会触发onError函数,并返回一个errorCodeerrorCode的类型有三种:

    enum class ErrorCode
    {
        TIME_OUT,           /** &lt; value 0 */
        CONNECTION_FAILURE, /** &lt; value 1 */
        UNKNOWN,            /** &lt; value 2 */
    };

 此时我们可以选择重连或者断开连接报错。

(3)监听服务端的发送的数据&向服务端发送数据

network::WebSocket::Data对象存储客户端接收到的数据, isBinary属性用来判断数据是二进制还是文本,len说明数据长度,bytes指向数据

客户端依靠onMessage来监听服务端发送的数据:

void WebSocketTestLayer::onMessage(network::WebSocket* ws, const network::WebSocket::Data& data) {
     if (!data.isBinary){ 
        _sendTextTimes++;
        char times[100] = {0};
        sprintf(times, "%d", _sendTextTimes);
        std::string textStr = std::string("response text msg: ")+data.bytes+", "+times;         
        log("%s", textStr.c_str());
        _sendTextStatus->setString(textStr.c_str());
} } 

客户端通过send向服务端发送数据: send有文本和二进制两种模式

_wsiSendText->send("Hello WebSocket, I'm a text message."); 
_wsiSendBinary->send((unsigned char*)buf, sizeof(buf)); 

(4)主动关闭WebSocket

这是让整个流程变得完整的关键步骤, 当某个WebSocket的通讯不再使用的时候,我们必须手动关闭这个WebSocket与服务器的连接。close会触发onClose消息,而后onClose里面,我们释放内存

wsSendText:close() 

 至此就完成一个利用WebSocket协议和服务端交互数据的流程。

 

十三.cocos包体的压缩

对于上线 项目来说安装包每大一M都是钱啊!所以对包体要进行适当的压缩:删除没有用到的引擎代码块,对图片以及声音资源进行压缩,尽量使用系统的字库。采用分包下载的方式,将游戏资源进行拆分,等玩家进入游戏后进行二次下载。

 

十四.cocos的数据存储

Userdefault:本质是一颗XML文件,是cocos提供的方案优点时cocos自带的存储方案,调用比较方便缺点:都要遍历整个文件,效率不高&&不没有数据类型的安全

SQList:轻量的嵌入式数据库,可高速且安全的在本地存储数据。

FileUtils:用文件存储,比较麻烦和效率低

 

十五.中文乱码怎么解决

1.在Vs2017中另存项目,找到无签名的UTF-8进行储存

2.对于一些系统需要的字体logo,使用Plist文件把字体当作精灵贴图上去

3.先写一个XML文件,里面存储着所使用的字体,当需要调用时直接再XML文件中寻找

Dictionary* strings = Dictionary::createWithContentsOfFile("string.xml");
const char * str;
 str = ((String *)strings->objectForKey("menulogo"))->getCString();

 

十六.Cocos2d-x中所使用的设计模式

单例模式Dictor,OpenGl,cache,userfault等只需要一个实例

单例模式的实现方式:

饿汉模式

   在类中声明并实现了对象,如果调用getInstance则返回这个对象,立刻被加载,如果没有使用到会造成内存的浪费

懒汉模式

在类中声明了对象(但是还没有实例化),在调用getInstance才对其调用进行实例化

单例模式好处:只允许创建一个对象,因此节省内存,加快对象访问速度,操作方便,这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例避免对共享资源的多重占用

单例模式坏处:就是不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
 

工厂模式基类只定义接口(规定生产什么产品),由子类来具体实现。对于需求量大的类这样做简化代码,方便调用

管理者模式:如三个cache,对于体积大又要重复使用的数据,这样做可以避免多次加载 

观察者模式: 指被观察者含有一个数组,里边存放了所有观察者的引用,在被观察者的状态发生改变的时候,通过调用观察者的

函数来通知观察者(事件监听),实现了信息的传递。如血量变化。

 

十七.场景切换的内存处理过程是什么?

一般情况下先构建新场景,然后显示新场景,然后释放旧场景。

1、如果没有切换效果,则为B的ctor(),A的onExit(),B的onEnter()  

2、如果有切换效果,则为B的ctor(),B的onEnter(),A的onExit()  

 

十八.cocos2d 3.x的新特性

1.使用了新的渲染机制(上述一)

2.使用了C++11的新特性:如auto关键字,Lamdar表达式等(详见2020c++面试题)

3.去除了ObjectC模式,删除了CC前缀

 

OK,以上就是我呕心沥血总结出来的引擎常见的面试题

如果对大家有帮助记得点赞呀,谢谢大家 !


 


 

Logo

这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

更多推荐