查看原文
其他

游戏开发者如何应用 CocosCreator 技术? | 技术头条

张晓衡 CSDN 2018-10-27

作者 | 张晓衡

责编 | 郭芮

本文详解介绍了Cocos Creator这一游戏开发方案。Cocos Creator包括 cocos2d-x 引擎的 JavaScript 实现,能让用户快速开发游戏所需要的各种图形界面工具。目前,其支持发布游戏到 Web、iOS、Android、各类"小游戏"、PC 客户端等平台,能够实现全平台运行。


CocosCreator基础教程(1)——从zIndex开始


从Cocos2d-x/lua/js过来的老程序员们肯定发现了,在CocosCreator属性检查器中Node节点竟然没有zIndex属性?

因为这一点,UI节点的遮挡关系控制不便,经常让策划、测试、甚至老板找程序员麻烦。不知道大家有没想过用编辑器去控制zIndex呢,请思考一下?我发现自己是用了CocosCreator快一年才去想到这个问题的。

要用编辑器控制,最简单的方案就是编写组件脚本。

/***SetZIndex.js 控制组件
**/
cc.Class({
    extends: cc.Component,    
    //编辑器属性定义
    properties: {
        zIndex: 0
    },
    onLoad () {        
        this.node.zIndex = this.zIndex;
    }
});

代码非常简单,将这个组件脚本挂载到任意节点上,通过zIndex属性就能控制节点的zIndex了,看下图:

SetZindex组件

但上面的代码有两个小问题,不仔细还不易被发现:

  • “zIndex:0”,这样定义zIndex属性,它是一个浮点数类型,你可以在编辑器设置0.1这样的值。运行在浏览器或H5环境没什么问题,但跑在原生环境zIndex对应的是cocos2d-x中的Node::setLocalZOrder(int localZOrder)函数,它的参数类型是整型。

  • 这个组件只在onLoad时设置了节点的zIndex,如果运行过程中,给这个组件的zIndex属性赋值没有任何作用,并且在编辑器中,你设置zIndex也看不到节点层级的变化。

知道问题了就好办了,看下面的代码:

/**
*SetZIndex.js 控制组件
**/
cc.Class({
    extends: cc.Component,    
    //编辑器属性定义
    properties: {
        zIndex: {
            type: cc.Integer, //使用整型定义
            default: 0,            
            //使用notify函数监听属性变化
            notify(oldValue) {                
                //减少无效赋值
                if (oldValue === this.zIndex) {               
                    return;
                }
                this.node.zIndex = this.zIndex;
            }
        }
    },
    onLoad () {        
        this.node.zIndex = this.zIndex;
    }
});

使用一个对象来定义zIndex属性,同时监听zIndex的修改,问题解决。

SetZIndex组件不依赖任何其它组件和节点,可以挂载任意节点之上,因此它是一个通用组件。不要小看了这个组件的设计,它蕴涵了CocosCreator的组件编程模式和思想。


CocosCreator基础教程(2)——聊聊scale与size属性


在CocosCreator引擎编辑中,节点的scale和size属性都可以改变节点内容的大小。如下图中可爱的椰子头,原图尺寸为512*512,在UI编辑时发现太大了,需要128*128的大小更适合。

scale&size

此时将节点scale属性设置为0.25好,还是将size属性的高\宽设置128好?回忆一下你在做UI编辑时,习惯用那个属性控制节点大小,思考一下怎样做才是UI开发的最佳实践?

scale与size的区别

  • scale: 节点整体的缩放比例,影响所有子节点。可使用scaleX、scaleY控制节点X\Y轴的缩放。

  • size:节点内容尺寸,以像素为单位,修改size不影响子节点。size是一个对象,使用width\height控制宽\高像素尺寸。

通过上面属性说明,比较容易看出scale与size的区别有两点:

  • scale使用比例单位,size使用像素单位;

  • scale影响子节点,size不影响子节点。

在API接口上,scale可以直接使用node.scale访问,但size却不行,需要过node.getContentSize()\node.setContentSize()这两个函数访问,不过size支持node.width\node.height属性访问控制宽高。

虽然scale/size两个属性都可以改变节点的大小,但是当这两个属性同时发生了变化 ,如何获取节点的实际像素大小用呢?比如说,将上面截图中的椰子头节点scale X\Y改为0.5,size W/H改为256,它会变成下面这样:

scale&size同时修改

你会发现,节点只有原来的1/16大小了,它的实际像素计算如下:

width = node.width * node.scaleX height = node.height * node.scaleY

再进一步,如果它的父节点也被缩放了,那当前节点的实际像素尺寸又怎么计算呢?还好有引擎提供有API获取节点包围盒的大小,也就是节点实际看到的像素尺寸:

//节点在父节坐标系下的轴向对齐的包围盒
rect1 = node.getBoundingBox()

getBoundingBox返回的是一个矩形cc.Rect对象的实例,其中的width\height就是节点的像素尺寸,x\y是矩形在父节点下的左下角位置。

有人可能会问,获取节点的实际尺寸有什么呢?最为常用情景就是做碰撞检测,简单的矩形碰撞并不会用到碰撞组件,而是使用cc.rectContainsPoint\cc.rectContainsRect这类函数做检测,例如:

  • 触摸一个节点时,检查触摸点是否在节点区域中;

  • 检查将一个节点是否在另一个节点之区域内。

检查一下你的项目代码,是否有直接使用getContentSize()或width\height获取节点大小做类似上面的碰撞检测,尝试修改节点的scale属性看看是否还能正常工作。

修改scale属性,节点的size并不会变化。由此也可以看出,使用scale修改节点外观大小不是一个好主意;简单的使用getContentSize()获取节点大小也不是一个安全之举,你不能保证UI编辑的同学不会使用scale属性,所以使用node.getBoundingBox()才是安全之道。

图片尺寸变化对精灵节点的影响

在游戏开发中,时常会遇到图片资源更改的情况,比如:有一系列的角色图片,切图为512*512的尺寸,但在游戏中只需要128*128或其它尺寸展示。

后来发现之前的切图过大,包体体积不理想,于是要求美术将其改为256*256的尺寸。这时做UI编辑的同学可能会被郁闷到,在UI编辑器中,他使用的是scale调整的精灵大小,那图片更新还得再全部重新调整,因为它会以图片原始尺寸的变化而按比列变化。

如果之前使用的是size属性控制的精灵尺寸,同时Script组件设置的sizeMode为CUSTOM(当修改精灵节点的size属性时Sprite组件的sizeMode会自动变为CUSTOM模块,默认为TRIMMED),那图片的尺寸变化就不会影响精灵在游戏中的尺寸变化,所以size属性在这次胜出。

通过上面的举例,还说明了一个问题,将游戏中的关键元素的尺寸预先规定下来非常的重要,这也就是在确定所说的设计尺寸。设计尺寸不仅仅只是屏幕设计尺寸用于规定背景图的大小,还包括统一的角色、图标、UI等等。

Sprite组件对图片大小的约束

上面提到了Sprite组件的sizeMode属性可以配合节点size对图片大小进行约束:

Sprite组件的SizeMode属性

当sizeMode设置为CUSTOM时,不论图片尺寸是多大,当精灵帧spriteFrame变化时(可以尝试拖动不同尺寸的图片到spriteFrame属性上)都不会影响当前节点的size大小。如果你选择的是其它值,当spriteFrame变化时节点size也会随之变化。

scale则不然,scale会在size的基础上再做缩放,所以scale保持为1是最安全的,size属性又得1分。

精灵的九宫模式

Sprite组件的type属性为SLICED时可开启精灵的九宫模式,当编辑好九宫属性后,用节点size属性可无限放大节点。

精灵九宫

需要特别注意的是,九宫属性只适合将精灵节点放大,而不适合将节点缩小,如果九宫的边缘像素占比较大,缩小后会导致精灵变形。

因此使用九宫属性的图片尺寸尽量可能的要小,同时最好不要叠加scale属性,这会让精灵变形更为严重,size属性再得1分。

scale属性的应用

从上面得分来看scale属性好惨,根据我这些年的经验来看将其保持为1是最安全的,所以scale属性尽量少用(默认为1)。

说scale属性一无事处,确实也不太地道,scale属性至少有下面3个用处:

  • 用于cc.ScaleTo/cc.ScaleBy的Action动画;

  • 用于有子节点的复杂界面的整体缩放,比如对一个预制件进行缩放;

  • 将scaleX或scaleY设置为负数,实现图片的左、右、上、下镜像减少资源量,比如下图中两个精灵这是同一张图片。

设置ScaleX为负数,实现向左镜像

所以scale属性的作用就如同它的名字一样:缩放!不仅可以放大、缩小,还可以向负数做缩放。

回到最初的问题,设置节点的大小使用size将是最佳的实践。这有助于在UI的编辑与设计,同时预先规划好游戏元素的设计尺寸、资源的文件名,无需太多考虑图片素材的尺寸,使用临时图片即可开始项目的开发。


CococsCreator基础教程(3)——meta的秘密



CocosCreator会为assets目录下的每一个文件和目录生成一个同名的meta文件,那meta文件有什么作用呢?下面我们就来说下meta,理解了CocosCreator生成meta文件的作用和机理,能帮助你和你的团队解决在多人开发时常会遇到的资源冲突、文件丢失、组件属性丢失等问题。


脚本丢失


先看下一个场景文件的meta长什么样子:


{  
 "ver": "1.0.0",  //版本  "uuid": "911560ae-98b2-4f4f-862f-36b7499f7ce3", //全局唯一id  "asyncLoadAssets": false,  //异步加载  "autoReleaseAssets": false,  //自动释放资源  "subMetas": {}  //子元数据
}

场景与预制件的meta都长的一个样,再看一个png图片的:

{  
  "ver": "1.0.0",
  "uuid": "19110ebf-4dda-4c90-99d7-34b2aef4d048",
  "type": "sprite",
  "wrapMode": "clamp",
  "filterMode": "bilinear",
  "subMetas": {    
      "img_circular": {      
      "ver": "1.0.3",      
      "uuid": "a2d1f885-6c18-4f67-9ad6-97b35f1fcfcf",      
      "rawTextureUuid": "19110ebf-4dda-4c90-99d7-34b2aef4d048",      
      "trimType": "auto",    
      "trimThreshold": 1,      
      "rotated": false,      
      "offsetX": 0,      
      "offsetY": 0,      
      "trimX": 0,      
      "trimY": 0,      
      "width": 100,      
      "height": 100,      
      "rawWidth": 100,      
      "rawHeight": 100,      
      "borderTop": 0,      
      "borderBottom": 0,      
      "borderLeft": 0,      
      "borderRight": 0,      
      "subMetas": {}    }  } }

图片文件meta信息比较多,除了基本的ver和uuid外,还记录了图片的高宽、偏移、九宫格等数据。上面这么多信息,我们这里只关心一个:uuid,通用唯一标识符(Universally Unique Identifier)。


uuid是CocosCreator用来管理游戏资源的,它会为每个文件分配一个唯一的id,图集会生成多个。由此可以了解在CocosCreator引擎中,识别一个文件不是简单地通过路径+文件名定位,而是通过uuid来引用文件。因此可以在编辑器资源管理中,随意删除、移动文件。


CocosCreator生成meta文件有以下几种情况:


  • 打开工程时。CocosCreator引擎在工程刚被打开时,先扫描assets目录,如果哪个文件还没有meta文件,此时就会生成。


  • 更新资源时。更新资源也会引发meta文件的更新:


  • 通过引擎编辑器资源管理窗口,可以对资源进行文件名修改、改变目录、删除文件,添加文件可以从桌面或操作系统的文件管理器将文件拖入引擎资源管理器中。


拖动图片到资源管理器


还有一种情况是在操作系统的文件管理器中对assets目录中的文件进行增、删、改之后,激活引擎编辑器窗口,此时可以看到资源管理器刷新的过程。


资源刷新


如果一个文件的meta文件不存在,上面两种情况都会触发引擎去生成meta文件。


下面我们分析下meta文件出错的几种可能情况。


uuid冲突


uuid是全局唯一的,产生冲突肯定是有不同的文件的uuid相同了,一旦出现这个问题会导致CocosCreator资源管理器目录结构加载不完整,看下图,遇到这种情况估计会让你吓出一身冷汗:

CocosCreator UUID冲突


从提示中可以看到冲突的uuid字符串,打开操作系统文件管理或代码编辑器,搜索这个uuid:

搜索uuid,找到两个相同的


这时先关闭CocosCreator,然后再任意删除其中一个meta文件,再打开CocosCreator问题可以解决。


这种方法虽然可以解决问题,但如果在编辑器中曾经引用过这个资源的地方将会出现资源丢失,你需要重新编辑或配置一次。最好是通过版本管理工具还原此meta文件。


据我观察,出现这种问题的原因有两个:


  • 在操作系统的文件管理器中移动文件时,将剪切、粘贴操作不少心弄成了复制、粘贴,同时也把meta文件也复制过去了。导致项目中同时出现两个相同的meta文件。

  • 在多人协作时,从版本管理工具中,更新资源时碰巧遇到别人生成的uuid与你的电脑上某个文件生成的uuid一样了,但这种情况非常罕见。


总的来说,要解少uuid冲突发生,最好在引擎资源管理工具中进行添加、移动文件。


uuid变化


还有种情况是uuid变了,你曾经编辑的界面将会出现资源、图片丢失,还可能出现组件属性丢失。


uuid变化,编辑器资源丢失


通过Creator控制台的警告可以看到,有曾经被使用过的资源uuid,但现在丢失了。提示还是很详细的,给出了所在的场景文件名、节点路径、组件、uuid,通过提示可以快速定位资源丢失的地方。


这种情况又是怎么造成的呢?一种情景是在新资源添加进项目时,忘记了激活一下CocosCreator让其生成meta文件,同时又将这些新增的文件提交到了版本管理中(不包含meta文件)。之后,有同学去更新了他提交的资源,同时打开或激活了CocosCreator进行编辑,这时Creator会检查到新资源没有meta便会立即生成。这样两个同学的电脑上为同一个文件,生成的meta文件中的uuid都不相同。


这种情况下,后面进行资源提交或更新的同学,肯定也会遇到冲突,如果不明就理就强行解决冲突,就会产生上面的问题,同时把问题蔓延到别的人身上。下面时序图,描述了这种错误的工作流程:



资源更新流程


上面就因第一个A同学忘记生成meta并提交,导致这个严重的问题,每个人都编辑过项目,但每个人生成的uuid都不同。如果不明其理,会陷入无限的资源出错中,做好的东西,一提交更新又出问题了。


要解决这个问题注意下面几点:


  • 提交前检查是否有新增文件,有新增文件时,注意是否有meta文件,需要一起提交。

  • 拉取文件时,注意是否有新增文件,并且是有meta文件成对,如果没有提醒之前提交文件的同学,把meta文件一并提交。

  • 提交时,如果发现只有新增的meta文件,那这个meta文件肯定是自己生成的,那注意是否使用过这个meta文件对应的资源(同名文件)。如果没用过,那请这个文件最早提交者把meta文件提交了。千万不能将这个meta文件提交上去。


注意上面几点基本上就可以杜绝meta文件uuid变化导致的工程出错了。


meta文件是CocosCreator用于资源管理的重要手段,但在多人协同开发中稍有不慎就容易产生资资源错误。要解决这个问题,不仅需要理解meta文件的产生机制和导致冲突的原因,同时还应该规范资源提交流程。




CocosCreator基础教程(4)——color属性的妙用



在CocosCreator中巧妙利用节点的color属性,改变精灵的颜色,可以有效减少美术资源。我们一起来看看CocosCreator的HelloWorld工程:


背景节点的Color属性


看上图,这次我们的重点不在可爱的椰子头节点上,而是在背景background节点上。它是由一个高宽2像素的纯白色(#FFFFFF)图片渲染而成,但节点的color属性为#1B262E,同时注意,节点的高宽充满了整个画布,你可以通过size属性(没有使用scale哦)自由调整节点的尺寸大小。


回想一下,在你的工程中,有没有切出一大块纯色图片做背景?如果有的话这就是一个可优化的点!我们来看看充分发挥color的作用需要注意些什么。


颜色叠加


要想使用color属性精确控制精灵颜色,图片要尽量使用白色,因为color属性并不是简单地设置颜色,而是用纹理像素的rgb与节点的color的rgb相乘(r * color.r、g*color.g、b*color.b)。


节点的Color效果


看上图,在场景编辑器中,椰子头和一个纯红色的精灵节点,都设置为黄色(#FFFF00)。椰子头覆盖上了一层黄色,再看纯红色的精灵则没什么颜色变化,另外注意椰子头整体颜色变暗了,由此得出下面几条经验:


  • 最好在纯白色的精灵上使用color属性,可以精确控制颜色;

  • 在非纯色的精灵上使用color属性,整体色调会变暗;

  • 纯红、绿、蓝的三元色精灵使用color属性,颜色只能在当前图片颜色范围变化,应用范围有限。


color属性在字体上的应用


上图中,我不仅在精灵组件上设置了颜色,同时也设置了它们下方的Label文本节点的颜色。使用系统字体,引擎默认渲染出的文本是白色的,叠加任意color属性,可以精确控制颜色。


通过修改字体的color属性可以很方便实现一些效果,比如:使用红色Label做受伤时的hp减少;使用绿色Label实现hp的回复;


但是这里有个问题,项目中我们经常使用的并不是系统字体,而是位图字体,也就是由图片制作的字体,看下图:


艺术字体


上图使用的是Atlas艺术字体,关于自定义字体相关的内容我们以后再说。这里可看到绿色Label的文本是由字体文件中的图片构成,也使用了图片的颜色。但这里有个小小的遗憾,这个字体图片使用了个纯RGB三元色中的的绿色,它的颜色变化范围有限,只能用于hp回复这类场景,要给它叠加红色只会让你失望,看下图:

绿色字体叠色后变黑色了


所以在制作字体时,尽量先用纯白色,或者再用点浅灰色做字体外发光,这样可以让字体文件的使用范围更大,发挥更大的价值。


透明度对节点影响


透明度也是color属性的一个组成部分,但透明(opacity)会影响到子节点,RGB则值不会。


不知道你是否注意到美术切出的图片,应用到游戏被引擎渲染出来时,在颜色上总是觉得有所偏差,这里有一个很重要原因就是:透明度。如果一个精灵节点设置了透明度,你看到的并不是这个精灵所表现出来的颜色,而是当前这个精灵与它背后的颜色重叠后色彩,看下图:


透明度对图片的影响


中间和左边两个精灵透明(opactiy)为155,但中间的这个精灵节点放在了一个白色图片的上面,精灵节点的颜色与它的背景颜色做了叠加。最右边的精灵没有设置透明,与最左边对比,左边精灵的颜色要暗些,也是因为透过了当前节点加入了背景色的原因。


不仅设置节点的透明属性会影响到精灵的颜色表现,如果原始图片带有透明通道同样会影响到图片在布局时的颜色表现。它与不同的背景色重叠会产生不同的颜色偏差,因此用作背景的图片不论尺寸大小,纹理内容区域尽量不要设置透明(不规则边缘不在此列),这样做不仅避免颜色重叠产生的不一至,而且让图片所占用的磁盘空间、内存空间也会更小。


节点color可以控制精灵的渲染颜色,灵活运用可以减少图片资源。color属性不仅可以作用于精灵,更多的是应用于Lable标签,使用白色纹理,可以让图片更具灵活性。


另外需要注意,图片的透明和节点的透明度都会影响游戏最终渲染出的颜色效果,合理利用color、size、锚点、旋转、九宫等属性特性,扬长避短,可以让游戏更加出色。



Cocos Creator基础教程(5)—资源结构



对于游戏开发来说,除了编辑游戏界面、制作游戏动画、编写代码这些具体的工作外,大家还需要对游戏资源结构要非常清楚。如果马虎上阵,等你把项目运作做起来后,一是工作效率不会太高,二是难以精确控制资源,最后甚至会因此陷入混乱。


资源是指用于游戏内容创作所需要的素材,对于Cocos Creator工程来说就是assets目录下的文件,看下图:


资源目录结构


那资源结构就是将众多的资源文件按一定的规则存放和命名,以方便使用管理。


看上图所示,我把资源大致分为以下几类:


  • 动画:animation

  • 预制:prefab

  • 场景:scene

  • 脚本:scripts

  • 纹理:textures

  • 声音:sound

  • 配置:config


其中纹理可再细分为:动画帧、背景、字体、UI、图标等(将动画帧图片放到动画目录中会更好,这样可以将动画资源与项目做分离)。分类目录也不要过细,过细会增加重复文件(同名或不同名但内容)出现机率,同时将通用资源和专用资源分开存放,可以再次减少重复文件的产生。


代码的分类也需要注意,我通常将Cocos Creator通用组件(纯脚本组件)放在component目录下。如果组件脚本是与某个prefab紧密关联的,则将它们放在一起,如下图:


预制文件与组件脚本放在一起


上面有两个预制组件Chessboard是棋盘,Chessmain是棋子,它们各关联了一个同名的组件脚本。因为这两脚本不具备通用性,所以没有把它们放在component目录下,而是将它们与所关联的预制放在文件一起。


我上面只是举个例,不同公司、项目可以根据自己的具体情况设计资源结构,而且非常必要让参与项目的成员(策划、美术、程序)都清楚分类规则。


文件命名


一个普通的游戏项目,资源大致包括有:图片、代码脚本、声音、配置文件、场景预制文件等等,小项目至少也有上百个文件,大的项目甚至过万。仅对资源分类还不能进行快速定位,还需要有文件命名规则。


通常图片文件是游戏中用的最多的资源,我之前总结了一个简单的图片文件命名公式,供大家参考:命名公式:前缀+中缀+[后缀]。


前缀用于分类,中缀表示功能或特征,后缀可选一般是序号,举几个例子:


  • img_gold.png:一个金币图片,img表示图像是除UI交互元素之外的统称。

  • btn_blue_ok_0.png: 一个绿色按钮,btn表示一个UI按钮,blue_ok是颜色特征和功能特征(注意的是中缀可以由多个单词组成),后缀0是表示,正常、按下、禁用中的正常状态。

  • bg_login.png:登录界面的背景图片,bg表示背景,login是功能特征表示用于登录界面。


有了这样的文件命名,在编辑UI时就不会如大海捞针,把时间和精力消耗在资源管理里寻找图片了,直接使用Cocos Creator资源管理器模糊搜索前缀或中缀就能快速锁定目标,看下图:

定位资源并拖放到属性检查器中


上图不小心暴露了我的一个小技巧,如果你用心看了上图会发现,上面的窗口布局中将属性检查器、层级管理器、资源管理器放在一起,属性设置时减少了鼠标拖拽距离,减少了操作时间和出错机率,从而可以有效提高UI编辑效率,但我发现很少有人这样布局引擎编辑器。


文件命名与资源分类相同,可以根据自己实际情况制定自己的文件命名规则,一定要让参与项目的成员知晓且严格遵守,这样可以有效提高开发效率,躲避风险。


以上讲了资源分类与命名在游戏开发中的重要性,结构化不仅可用于资源管理,同时在分析问题时也可以使用结构化的思维。请用心观察体会自己现在或过去的项目,有没有在资源管理上遇到问题,是否注意到了资源结构对项目的影响。



Cocos Creator基础教程(6)——AudioSource组件



下面我们介绍cc.AudioSource音频播放组件的使用,使用cc.AudioSource组件不用写任何一行代码,就能控制音效的音量、播放、停止、恢复等操作。


在层级管理器里面创建一个空白节点,然后在下图示意位置添加AudioSource组件:


添加一个AudioSource组件


这里需要注意,有不少默认组件并不在组件库中或层级管理器的右键菜单中,但可以在属性检查器下方的添加组件按钮菜单中找到。


将AudioSource组件绑定到节点,可以看到它提供的属性接口,见下图:



简单解释一下组件属性:


  • Clip 音频资源,通过拖拽音频文件设置;

  • Volume 音量大小,范围0~1之间;

  • Mute 是否静音,静音后可以继续播放;

  • Loop 是否循环播放;

  • Play on load  加载完成是否立即播放;

  • preload 是否在未播放的时候预先加载。


接下来把资源目录下的音频文件拖到AudioSource的Clip属性,看下图:


设置音频文件


箭头2所指的Play On Load属性打勾,在游戏运行起来的时候就能自动播放了。不用任何代码,这对不会编程的策划同学来说是一个惊喜哦,不依赖程序员就能控制游戏音效,至少在做游戏原型时增加了声音这个维度!


下面我们讲下如何控制声音播放和停止,这里需要使用cc.Button组件来控制,同样无需编程。首先在层级管理器右键点击Canvas创建两个按钮:



也可以在控件库里面拖拽按钮:



接下来给按钮绑定事件:


  • 选中按钮,把我们之前设置的含有AudioSource节点拖到箭头指定的地方;

  • 然后在中间的选项卡里面选中我们的cc. AudioSource;

  • 最后在右边的选项卡里面找到我们的play函数。


这样就算绑定完成了! 快去运行起来试试看吧!



用同样的方法,给停止按钮绑定stop函数,与绑定play函数一样,在第3步选择stop就行了,这里附上AudioSource的实用函数接口,都可以使用cc.Button组件调用:


play()   //播放音频剪辑
stop()   //停止当前音频剪辑
pause()  //暂停当前音频剪辑
resume() //恢复播放
rewind() //从头开始播放

这里给大家分享了AudioSource组件的使用方法,不需要编写任何代码。不过AudioSource组件还有存在一点瑕疵, 它不适合播放背景声音,而且为AudioSource组件做全局控制音量控制也不方便。



Cocos Creator基础教程(7)——场景切换



在Cocos Creator中切换游戏场景可以像切换幻灯片页面一样简单,这次教程我们稍微进阶一点点,带着大家编写这个场景切换组件。


先看组件代码:


//场景加载组件
cc.Class({    extends: cc.Component,    properties: {       scene: cc.SceneAsset,  //定义场景资源    },    onLoad() {        
       //注册节点触摸事件,当触摸结束加载场景        this.node.on(cc.Node.EventType.TOUCH_END, () => {            
           //使用cc.director.loadScene引擎API加载场景            cc.director.loadScene(this.scene.name);        );    } });

新建一个测试场景,场景中添加一个Label,将LoadScene组件绑定到Label节点上,同时拖拽另一个场景到LoadScene的Scene属性上,看下图所示:

LoadScene组件


我们这个LoadScene组件可以挂载到任何节点上,配置好想加载的场景,启动预览下效果如何!


在Label上点击没有什么反馈效果,我们把节点换成按钮控件体验会更好,而且cc.Button组件还带有事件触发能力,可执行指定节点上的组件函数。


改造一下组件代码:


//增加loadScene函数,可被Button组件调用
cc.Class({    extends: cc.Component,    properties: {       scene: cc.SceneAsset,  //定义场景资源       clickable: true,       //是否可点击    },    onLoad() {        
       //开启点击,注册场景加载事件        if (this.clickable) {            
           this.node.on(cc.Node.EventType.TOUCH_END, this.loadScene, this);        }    },    loadScene() {        
       //场景存在,调用场景场景加载        if (this.scene) {            cc.director.loadScene(this.scene.name);        }    } });

增加了一个clickable属性,如果使用Button的事件触发来调用函数,就不要注册触摸事件了,不然会执行多次。我们把之前的触摸事件单独抽成了一个loadScene函数,同时做了属性检查,请看下图配置:

LoadScene组件关联Button


在场景中添加了一个Button节点,挂载好LoadScene组件,设置好要加载的场景,不要勾选Clickable属性(不与Button事件配合时勾选)。然后将重点放在cc.Button组件属性设置上:


  • 增加一个Click Events事件;

  • 事件第一个参数是指向一个节点,这里拖动Button节点到这里;

  • 事件第二个参数是选择这个节点上的一个组件,这里选择LoadScene;

  • 事件第三个参数是选择组件上的LoadScene函数。


与cc.Button组合在配置要繁琐一些,你可以只使用Button的点击过渡效果,不使用Button的事件,勾选下面的Clickable属性效果相同。



Cocos Creator基础教程(8)——加载预制件



我们上面讲了场景切换并编写了LoadScene场景加组件,这次我们在场景里面创建独立的子界面或子窗口。在Cocos Creator中实现子界面的最好方案就是: 预制件。


Cocos Creator并没有一个新建预制件的功能菜单项,我们可以在场景中先做一个大概的布局,然后在层级管理器中将节点拖动到资源管理器中:

层级管理器与资源管理器的本质是内存数据与磁盘文件的关系,从层级管理器将节点拖到资源管理器,就是从内存中将数据保存到磁盘上。


需要注意的是场景中的Dialog节点与资源管理器的Dialog预制文件并没有太多的联系,它们是同一个数据不同的表现形式而已,如果感兴趣可以用文本编辑器打开预制文件了解。


双击预制件文件,切换到预制件的独立编辑界面:

预制件的界面编辑与场景一样,但它们都应该保持逻辑清晰的层级结构,注意下面几点:


  • 有意义的节点命名,同层节点名尽量不要重复;

  • 建立节点之间在逻辑上的祖、父、子关系(例如:按钮上显示文字,就应该将Label节点放在Button节点的内部);

  • 将预制件根节点坐标位置设置为{x:0, y:0};

  • 建议预制文件名与预制件根节点名字保持一至。


接下来我们来实现LoadPrefab组件,先上代码:


cc.Class({
    extends: cc.Component,

    //组件属性定义
    properties: {
        PREFAB: cc.Prefab, //预制件
        parent: cc.Node,   //预制件实例化后所在的父节点
        autoLoad: false,   //自动加载
    },

    //组件加载时检查,是否自动加载预制件
    onLoad() {
        if (this.autoLoad) {
            this.loadPrefab();
        }
    },

    //实例化预制件,设置父节点
    loadPrefab() {
        let node = cc.instantiate(this.PREFAB);
        //当父节点不存在时,使用当前组件为父节点
        node.parent = this.parent || this.node;
    }
});


我们用一个按钮点击显示Dialog对话框,一起看看在编辑器上的配置,见下图:

  • 在场景中添加一个Button控件;

  • 将LoadPrefab组件脚本挂载到Button节点上;

  • 从资源管理器中将Dialog预制件拖动到DialogLoadPrefab组件PREFAB属性上,这是我们要加载的预制件;

  • 从层级管理器将Canvas节点拖动到DialogLoadPrefab组件Parent属性上,这是预制件实例化后的父节点;

  • 配置按钮事件,与上篇场景加载相同,就是调用Button节点上的LoadPrefab组件上的loadPrefab函数。


编辑器配置复杂了点,这里分享一个小小的经验,配置好一个复杂的组件后,你可以通过复制节点或复制组件,将其粘贴到界面中再做修改,这样比重头配置组件参数可以提高50%以上的效率。不过还好代码还是比较简单,你也可以在组件代码中监听触摸事件来调用loadPrefab函数,同样可以减化编辑器配置。


我们的组件上还提供了一个autoLoad属性,可以在宿主节点创建时自动创建预制件,这可以解决直接将预制件拖入场景,然后又去编辑预制件,导致场景中的预制节点与预制文件不同步的问题(预制件的嵌套问题)。


这一段我们讲解了预制件的生成,就是编辑的界面从内存保存到磁盘,之后可以通过cc.instantiate函数将预制件文件实例化为节点。同时介绍了我对编辑预制件的一点小经验供大家参考。最后编写了一个通用的LoadPrefab组件,可以方便非程序员同学使用。有了这些组件代码的积累相信以后不论是做游戏还是原型或是Demo,都能为我们提高生产效率。


Cocos Creator基础教程(9)——优化代码编辑器



Cocos Creator游戏开发主要是使用JavaScript语言,这里向大家推荐Visual Studio Code和Webstorm两款JavaScript神级编辑器。这两款编辑器的安装都很简单,这里主要介绍在Cocos Creator项目中如何调整编辑器配置,以提升开发效率。


我们知道Cocos Creatror会为项目资源文件生成同名的meta文件,在代码编辑器中很是碍眼,而且也不能更改里面的内容,严重干扰我们在代码编辑器中浏览文件,请看下图:

我们这里介绍VSCode和Webstorm如何屏蔽干扰文件。


首先用VSCode打开Cocos Creator项目,使用shift+ctrl+p/shift+cmd+p打开命令控制台。



在命令行中输入settings,在过滤出的选项中选择打开用户设置,在用户设置编辑区配置文件排除规则:


"files.exclude": {
     "**/*.meta"true
}


尝试保存此文件,你会看到VSCode的资源管理器中所有meta文件都不在了,下图是我惯用的文件排除配置:

除了过虑meta文件外,同时把git、svn和Cocos Creator的临时目录出排除了,这样可以通过ctrl+p/cmd+p在编辑器中快速准确地定位文件。


接下来我们看在Webstorm中怎么排除干扰文件,先在Webstorm中打开Cocos Creator项目,使用快捷键ctrl+,/cmd+,打开Preferences窗口,在左上角过滤框输入:File Types。



注意选中下方列表File Types选项,在右侧下方Ignore files and folder输入框中增加*.meta、.DS_Store等需要过滤的文件类型以分号隔开,然后点击下方Apply按钮,观察最左侧资源浏览器窗口,会看到相应要排除的文件不在了。


代码补全增强


代码补全是开发中提高效率的重要功能,对于JavaScript动态语言来说,代码补全确实要比c/c++、Java要差很多。但是经过配置VSCode和Webstorm也能提高不少我们的生产效率。


Cocos Creator集成有VSCode智能提示数据,可以通过Cocos Creator的主菜单:开发者->VS Code工作流->更新VS Code智能提示数据:



执行菜单命令后,Cocos Creator会在当前工程中添加一个creator.d.ts文件,此文件也是我们熟悉Cocos Creator API接口的重要文件,而且是中英两语的哦!


Webstorm除了像上述安装creator.d.ts文件外,还需要设置JavaScript语法为ES6,不然你可能会在IDE中看看到一大片红色的语法错误。进入Preferences设置窗口,在最左上角过滤框中输入JavaScript,定位到Languages & Frameworks下的JavaScript选项,在右边JavaScript language version选择ECMAScript 6,看下图:

配置上Cocos Creator的源码路径,可以进一步提高Webstorm代码提示精度,见下图:

点击Add…按钮,添加Cocos Creator源码路径:

  • 首先为导入的库设置名字;

  • 选择应用范围为Global所有工程有效;

  • 点击+按钮,选择Attach Directories… 浏览到Cocos Creator源码目录我用的是Mac系统设置的路径是:   /Applications/CocosCreator.app/Contents/Resources/engine/cocos2d;

  • 点击OK保存。


代码编辑器是程序员的一把利剑,这里介绍在VSCode和Webstorm中如何排除干扰文件、优化代码提示,以提高开发效率。



Cocos Creator基础教程(10)——预览调试



游戏预览是开发中的一个重要环节,Cocos Creator游戏引擎基于JavaScript语言有着丰富强大的预览调试能力,下面我们介绍预览调试相关的技术,了解一下这方面的知识相信对你也非常有帮助。


Cocos Creator是跨平台的游戏开发引擎,从类别上主要分为Nativet和H5两大平台,游戏预览也分为这两大模式:浏览器、模拟器。我们可以从Cocos Creator主窗口上选择预览模式、启动预览,也可以通过扫描二维码在手机浏览器中预览(注意IP地址为局域网地址,如不正确请在设置中修改)。

预览游戏


在浏览器中预览游戏是日常开发工作最为常用的功能,同时Cocos Creator为我们提供多种屏幕分辨率的模拟,查看游戏渲染性能参数,请看下图:

浏览器预览


在原生模拟器中也可以模拟不同的设备尺寸、设置横竖屏,看下图:


模拟器预选项


日常的开发中,我们用的最为频繁的还是在浏览器中预览,这里推荐大家使用Google Chrome浏览作为开发调试环境。


在浏览器中启动预览后,可以开启Chrome的开发者工具进行游戏代码的浏览、调试、日志查看等调试代码中的问题。在游戏画布窗口之外,点击鼠标右键,选择检查(快捷键:ctrl+shift+I/cmd+opt+I),打开开发者工具:

启动开发者工具


在Chrome开发者工具窗口中使用快捷键ctrl+p或cmd+p呼出文件搜索窗口,快速定位代码文件(与VSCode的文件查找一样)。

Chrome开发者工具


此处你可以利用开发者工具,对Cocos Creator的源码进行跟踪调试,查看API接口,这是学习Cocos Creator一个重要途径,看下图:

Chrome调试


Chrome的开发者调试工具非常强大,这里简单介绍几个常用的功能:


  • 点击行号设置断点,代码运行到此处程序会自动暂停下来;

  • 当代码被断点后,将鼠标移动变量之上查看变量值;

  • 使用快捷键ctrl+~呼出交互式命令控制台,可以查看变量值或执行代码;

  • 使用快捷键F10单步执行、F11单步跟入、Shift+F11跳出当前函数、F8运行;

  • 右侧Call Stack函数调用堆栈窗口,点击堆栈函数可以跳转到对应函数源码。


Cocos Creator支持多平台构建模板,如果你开发的是微信小游戏,一定要在微信开发者工具中预览调试。在构建发布窗口中,简单设置就可以构建出目标平台所需的文件,请看下图:

构建微信小游戏


启动微信开发者工具,选择小程序项目,你会看到与Chrome浏览器类似的预览调试窗口:

微信开发者工具


按照之前Chrome的快捷键用法,你就可以在微信开发者工具中断点调试游戏了,没什么太多区别,就是窗口太挤了,建议将调试窗口浮动出来形成一个独立的窗口。


Chrome是JavaScript的开发调试神器,熟练使用Chrome的调试工具是开发H5游戏的必备技能,我们这里只是介绍了Chrome的冰山一角,下来还请大家多多实践。



Cocos Creator基础教程(11)——可拖拽组件



在游戏中实现节点的可拖动是一个比较常见情况,比如:可以给小朋友做一个将果皮投进垃圾箱的教学练习、角色换装、物品包裹界面等。在Cocos Creator中实现一个可拖动组件,只需对目标节点拖拽配置就能让节点任意移动,这对策划、美术人员来说是不是很有杀伤力?


在实现一个组件代码之前最好新建一个测试场景,组件代码在测试场景中通过了基本测试之后再放入正式环境使用。而且在组件完成后,测试场景最好也不要丢弃了,等我们以后为组件升级或修改BUG时,可用于快速检验修改是否正确。

初始化工程


我们来看下组件代码非常简单,就算你不会编程,根着注释相信也能明白个大概:


cc.Class({
    extends: cc.Component,

    onLoad() {
        //注册TOUCH_MOVE事件
        this.node.on(cc.Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
    },

    _onTouchMove(touchEvent) {
        //通过touchEvent获取当前触摸坐标点
        let location = touchEvent.getLocation();
        //修改节点位置,注意要使用父节点进行对触摸点进行坐标转换
        this.node.position = this.node.parent.convertToNodeSpaceAR(location);
    }
});


代码主要是设置节点的触摸监听,在监听事件中修改节点的位置。将组件代码挂载到节点上,其它什么都不用做。



有了这个组件,可以控制节点任意移动了,但是很多情况下,需要将节点移动到指定位置,比如将果皮投进垃圾箱,我们增强一下组件代码:


cc.Class({
    extends: cc.Component,

    properties: {
        target: cc.Node,
    },

    onLoad() {
        //缓存原始父节点
        this._oldPosition = this.node.position;

        this.node.on(cc.Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
        this.node.on(cc.Node.EventType.TOUCH_END, this._onTouchEnd, this);
    },

    _onTouchMove(touchEvent) {
        let location = touchEvent.getLocation();
        this.node.position = this.node.parent.convertToNodeSpaceAR(location);
    },

    _onTouchEnd(touchEvent) {
        if (!this.target) {
            return;
        }
        //获取target节点在父容器的包围盒,返回一个矩形对象
        let rect = this.target.getBoundingBox();
        //使用target容器转换触摸坐标
        let location = touchEvent.getLocation();
        let point = this.target.parent.convertToNodeSpaceAR(location);
        //if (cc.rectContainsPoint(rect, targetPoint)) {
        //Creator2.0使用rect的成员contains方法
        if (rect.contains(point)) {
            //在目标矩形内,修改节点坐标  
            point = this.target.convertToNodeSpaceAR(location); 
            this.node.position = point;
            //修改父节点 
            this.node.parent = this.target;
            return;
        }
        //不在矩形中,还原节点位置    
        this.node.position = this._oldPosition;
    }
});


代码变复杂了,简单说明一下:


  • 是增加了一个target节点属性,它是节点要移动到的目标;

  • 增加TOUCH_END事件,当手指抬起时,检查当前节点是否在目标节点之中;

  • 在目标范围,修改节点父子关系;

  • 不在目标范围,还原节点位置(提前缓存节点原始坐标)。


组件有了锁定目标的功能,现在就可以实现将果皮投进垃圾箱了,当然也可以用来实现给角色换装、物品包裹之类的操作,请看下面的演示:



我给目标节点挂载了一个Layout组件,设置成GRID模式,实现自动网格排列,很像游戏中的物品包裹功能,这个组件真的是物超所值。


这次主要运用了节点的触摸事件监听,在触摸事件的touchEvent参数中获取当前触摸坐标点。同时还需要对坐标点在不同节点坐标系下进行转换,需要理解的是拖动节点的本质是:修改节点在父节点上的位置,需要使用this.node.parent.convertToNodeSpaceAR进行转换。同时还有使用了最简单的碰撞检测函数rect.contains(在Cocos Creator 1.9.3之前用cc.rectContainsPoint),检查一个坐标点是否在矩形内。



Cocos Creator基础教程(12)——精灵变身



在Cocos Creator中使用率最高的非精灵(Sprite)莫属了, 在游戏中我们经常会遇到将一张图片替换成另一张图片的情况,或者是在不同状态时来回切换图片。实现这个功能对程序员同学来说并不难,但是回头检视一下编写的代码,能否让美术、策划同学使用上吗?如果不能的话,相信下面的教程可能对你和你的伙伴有更多启发!


我们这里设计一个SpriteIndex组件,使用组件的index属性来控制Sprite组件的spriteFrame属性,从而得到图片变换的能力。


//SpriteIndex.js
cc.Class({    extends: cc.Component,               //编辑器属性,只在编辑状态有效    editor: CC_EDITOR && {        requireComponent: cc.Sprite,     //要求节点必须有cc.Sprite组件    },    properties: {        spriteFrames: [cc.SpriteFrame],  //定义一个SpriteFrames数组        _index: 0,                       //以下划线“_”开始的为私用变量              index: {                         //index属性控制图片切换            type: cc.Integer,            //定义属性为整数类型            //这次没使用notify方式实现属性值的变化监听,改用getter/setter方式            get() {                                          return this._index;            },
           //为负数退出
           set(value) {                                if (value < 0) {                    
                   return;                }                
               //根据spriteFrames组件长度计算this._index                this._index = value % this.spriteFrames.length;                
               //获取当前节点上的Sprite组件对象                let sprite = this.node.getComponent(cc.Sprite);                
               //设置Sprite组件的spriteFrame属性,变换图片                sprite.spriteFrame = this.spriteFrames[this._index];            },        }    },        /**    *next方法,调用index++切换图片,    *可以方便被cc.Button组件的事件调用    */    next() {        
       this.index++; //调用自身index属性,编号+1    } });        

代码比之前的教程要复杂了一点点,对没有编程经验的美术、策划同学来说可能残忍了点,但坚持跟着注释了解组件的实现意图,相信你会收获更多。


我们再看下图,SpriteIndex组件的用法:



  • 在编辑器场景中添加一个Sprite组件;

  • 然后挂载上SpriteIndex;

  • 添加SpriteFrames数组属性元素;

  • 将可能会出现的图片拖动到SpriteFrames数组属性下;

  • 尝试修改index属性,你会看到精灵图片的变化。


运行时图片切换


请出我们的按钮组件,通过点击时调用SpriteIndex.next方法进行切换,看下图配置:

此时启动预览,尝试点击这个精灵节点你就能看到图片在不断切换变化了。如果你想玩的再高级一点,可以在一个定时器中调用next方法,它立马就是成了一个序列帧动画了。


直接继承cc.Sprite


我们设计的是通用型组件,最好还是不要访问别的节点、组件的属性和方法,保持干净!这样更具有可扩展性和适应性。


SpriteIndex在这里就是给cc.Sprite做辅助的,它能不脱离cc.Sprite而存在。坚持做到最好,尽可能的让组件更多人能使用,限制越少越好,属性也是越少越好,只要能完成任务就行,看下在的做法,我们改进一下:


//SpriteEx.js

let SpriteEx = cc.Class({    
   extends: cc.Sprite,    //继承自cc.Sprite    properties: {        
       spriteFrames: [cc.SpriteFrame],        
       _index: 0,        
       index: {            
       type: cc.Integer,            set(value) {                
               if (value < 0) {                    
                   return;                }                
               this._index = value % this.spriteFrames.length;                //直接访问spriteFrame属性,因为this就是cc.Sprite                this.spriteFrame = this.spriteFrames[this._index];            },            get() {                
               return this._index;            }        }    },    next() {        this.index++    } });


//下面是控制SpriteEx组件在属性检查器中的属性显示

//不显示spriteFrame属性
cc.Class.Attr.setClassAttr(SpriteEx, 'spriteFrame', 'visible', false);
//不显示Atlas属性
cc.Class.Attr.setClassAttr(SpriteEx, '_atlas', 'visible', false);
//根据函数返回值控制属性显示、隐藏
cc.Class.Attr.setClassAttr(SpriteEx, 'fillType', 'visible', function() {    
   return this._type === cc.Sprite.Type.FILLED; }); cc.Class.Attr.setClassAttr(SpriteEx, 'fillCenter', 'visible', function() {    
   return this._type === cc.Sprite.Type.FILLED; }); cc.Class.Attr.setClassAttr(SpriteEx, 'fillStart', 'visible', function() {    
   return this._type === cc.Sprite.Type.FILLED; }); cc.Class.Attr.setClassAttr(SpriteEx, 'fillEnd', 'visible', function() {    
   return this._type === cc.Sprite.Type.FILLED; }); cc.Class.Attr.setClassAttr(SpriteEx, 'fillRange', 'visible', function() {    
   return this._type === cc.Sprite.Type.FILLED; }); cc.Class.Attr.setClassAttr(SpriteEx, 'srcBlendFactor', 'visible', function() {    
   return this._type === cc.Sprite.Type.FILLED; }); cc.Class.Attr.setClassAttr(SpriteEx, 'dstBlendFactor', 'visible', function() {    
   return this._type === cc.Sprite.Type.FILLED; });

上面的核心代码没几行,千万不要被吓到了,后面的属性控制函数主要是减少不必要的属性产生的干扰,提高组件使用时的体验,看下图:

红色框线是我们添加的属性,上面的是cc.Sprite组件原始属性,我们屏蔽了spriteFrame、Atlas属性的显示,这里已经看不到了。


节点下面再挂接一个Button组件,与之前的SpriteIndex的用法一样,运行起来效果相同。不过这里的节点挂载的组件少了一个,使用起来会简单一些,对策划、美术同学来说更贴心啦。


可能有程序员同学会怀疑,我是不是被美术、策划给收买了,尽为他们着想。请相信我这是在为程序员节省时间,将界面编辑的工作交给擅长的人,这样你就有时间可以去学习更有价值的东西了。


本段介绍了对cc.Sprite组件的扩展,有两种方式,一种是做辅助,一种是继承,它们没有绝对的优缺点,需要根据不同的情景取舍不同的方案。一是实现起来尽可能简单,二是用起来容易,三是能解决实际问题。


其实我们这里仍然是在讲组件化思维,合格的组件化组件将成为非程序员创作游戏内容的利器,它也是提高生产力的秘密。我在想以后是不是做代码审查,美术、策划也能为程序投上一票呢?


Cocos Creator基础教程(13)——组件与节点的秘密



有经验的同学,一上手Cocos Creator就能想到「装饰模式」,我们看下定义:装饰模式(Decorator)动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。


再看一下装饰模式的基本UML类图结构 :


大话设计模式-装饰模式UML


请注意,上图是我从《大话设计模式》中截取的,图中的Component在Cocos Creator对应的是Node,Decorator对应的是Cocos Creator中的Component。那么我对组件与节点关系的理解就是:组件为节点赋能。


组件为节点赋能


基于Cocos Creator这样的可视化引擎,组件代码有着巨大的意义。程序员开发出的组件代码不应该只是满足自己完成任务,而应该是为游戏设计师提供体验优良的工具,当然你在提供这些组件工具的同时,你的设计水平也在提高。


组件脚本就像是美术生产的图片、策划编写的文案一样,是游戏开发中的一项基础生产资料,游戏内容更多的是应该由游戏设计师去完成。


程序不仅仅是编写组件脚本,还应该向团队其他人员提供更多的帮助,比如:教他人使用Cocos Creator、教他人使用你编写的组件、编写项目辅助工具......目的是用程序的知识、思维和工具,去装饰身边的人,给他们提供他们本身不具备的能力,这样才能更多地发挥程序员的价值。如果程序员平时没有察觉到开发体验(各种不爽),做出的产品也很难谈的上有用户体验。

站在团队基础上,为他人赋予能力,将自己当成装饰品做为组件,去装饰他人让节点充分发挥能力,进行人与人之间的组件化协作,这是我最近从Cocos Creator组件化开发收获的启发。

作者:张晓衡,奎特尔工作室·cocos游戏开发工程师。10年开发经验,擅长cocos H5游戏开发和管理,技术培训和团队建设。

声明:本文为作者投稿,版权归对方所有。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存