Skip to content
标签
wx
字数
26567 字
阅读时间
105 分钟

一、概念

游戏编程

游戏编程(Gaming Programming)是一种通过编程来实现电子游戏中的逻辑的方法,包括渲染,物理,交互等。

编程语言的选择,如C编程语言、C++、Java, JavaScript,Lua等,编写计算机、手机或游戏机上的游戏。 目 前流行的游戏编程语言为C++和JavaScript编程语言,游戏编程底层接口为DirectX、OpenGL和SDL(Simple DirectMedia Layer)等。 多媒体编程接口 主要作用是让游戏或多媒体程序获得更高的执行效率,加强3D图形和声音效果,并提供设计人员一个共同的硬件驱动标准,让游戏开发者不必为每一品牌的硬件来写不同的驱动程序,也降低用户安装及设置硬件的复杂度。

现 在手机上玩的游戏分为Android与IOS两种平台,分别是用java和object-c(或swift)。当然时 下也流行一些跨平台的编程引擎,例如cocos creator、unity 3D等。

  • 游戏引擎是什么

    游戏引擎是指一些已编写好的可编辑电脑游戏系统或者一些交互式实时图像应用程序的核心组件。这些系统为游戏设计者提供各种编写游戏所需的各种工具,其目的在于让游戏设计者能容易和快速地做出游戏程式而不用由零开始。大部分都支持多种操作平台,如Linux、Mac OS X、微软Windows。游戏引擎包含以下系统:渲染引擎(即“渲染器”,含二维图像引擎和三维图像引擎)、物理引擎、碰撞检测系统、音效、脚本引擎、电脑动画、人工智能、网络引擎以及场景管理。

    简单来说, 引擎可以理解为一个开发游戏用的基本框架 ,对于一些游戏开发上通用的功能,不用再重复造轮子了,拿来就用。

  • 常见的游戏引擎

    一、Unity 3D

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

  • 代表作游戏:

  1. 诛仙
  2. 极限摩托车2 (Trial Xtreme 2)
  3. 择天记
  4. 王者荣耀
  5. 神庙逃亡2(Temple Run 2)
  6. 神庙逃亡:勇敢传说(Temple Run:Brave)
  7. 纪念碑谷(Monument Valley)
  8. 仙剑奇侠传6

二、虚幻引擎

虚幻引擎的设计目的非常明确,每个方面都具有较高的易用性,尤其侧重于数据生成和程序编写的方面,这样,美工只需要程序员的很少量的协助,就能尽可能多地开发游戏的数据资源,并且这个过程是在完全可视化环境中完成的,实际操作非常便利。

代表作游戏:

  1. 《绝地求生》(PLAYERUNKNOWN’S BATTLEGROUNDS)
  2. 《堡垒之夜》(Fortnight)

三、Cry Engine 3

Cry Engine3,是德国的CRYTEK公司出品一款对应最 新技术DirectX11的游戏引擎。2001年引入的这个“沙盒”是全球首款”所见即所玩”(WYSIWYP)游戏编辑器,现已发展到第三代,WYSIWYP功能将提升到一个全新层次,并扩展到了PS3和X360平台上,允许实时创作跨平台游戏,另外工具包内的创作工具和开发效率也都得到了全面增强。

代表作游戏:

  1. 《末日之战(Crysis)》
  2. 《战争前线》

四、白鹭(Egret)

Egret是一套完整的HTML5游戏开发解决方案。Egret中包含多个工具以及项目。Egret Engine是一个基于Type语言开发的HTML5游戏引擎,该项目在BSD许可证下发布。用Egret Engine开发的游戏可发布为HTML5版本,运行于浏览器之中。同时,也可以发布为iOS、Android和WindowsPhone原生程序。

五、Cocos 2D

Cocos2d-x是一个开源的移动2D游戏框架,MIT许可证下发布的。这是一个C++ Cocos2d-iPhone项目的版本。Cocos2d-X发展的重点是围绕Cocos2d跨平台,Cocos2d-x提供的框架。手机游戏,可以写在C++或者Lua中,使用API是Cocos2d-iPhone完全兼。Cocos2d-x项目可以很容易地建立和运行在iOS,Android,黑莓Blackberry等操作系统中。

代表作游戏:

  1. 刀塔传奇
  2. 捕鱼达人
  3. 开心消消乐
  4. 全民飞机大战
  5. 梦幻西游
  6. 传奇世界

二、Cocos Creator开发

Cocos 引擎 45% 的市场占有率在国内保持领先,全球 18% 的占有率排名次席。

目前市场份额最大的手机端游戏引擎

Cocos Creator 是以内容创作为核心,实现了脚本化、组件化和数据驱动的游戏开发工具。 具备了易于上手的内容生产工作流,以及功能强大的开发者工具套件,可用于实现游戏逻辑和高性能游戏效果。

  • 一体化编辑器: 包含了一体化、可扩展的编辑器,简化了资源管理、游戏调试和预览、多平台发布等工作。允许设计师深入参与游戏开发流程,在游戏开发周期中进行快速编辑和迭代。支持 Windows 和 Mac 系统。
  • 2D 和 3D: 同时支持 2D 和 3D 游戏开发,具有可满足您各种游戏类型特定需求的功能。并且深度优化了纯 2D 游戏的编辑器使用体验和引擎性能,内建了 Spine、DragonBones、TiledMap、Box2D、Texture Packer 等 2D 开发中间件的支持。
  • **开源引擎:**Cocos Creator 的引擎完全开源,并且保留了 Cocos2d-x 高性能、可定制、容易调试、易学习、包体小的优点。
  • 跨平台: Cocos Creator 深度支持各大主流平台,游戏可以快速发布到 Web、iOS、Android、Windows、Mac,以及各个小游戏平台。在 Web 和小游戏平台上提供了纯 JavaScript 开发的引擎运行时,以获得更好的性能和更小的包体。在其它原生平台上则使用 C++ 实现底层框架,提供更高的运行效率。
  • **JavaScript:**您可以完全使用 JavaScript 来开发游戏,在真机上进行快速预览、调试,对已发布的游戏进行热更新。同时支持 TypeScript。
  • **高效的工作流程:**Cocos Creator 预制件是预配置的游戏对象,可提供高效而灵活的工作流程,让设计师自信地进行创作工作,而无需为犯下耗时的错误担忧。
  • **UI:**内置的 UI 系统能够让您快速、直观地创建用户界面。
  • **自定义工具:**您可以借助各种您所需工具扩展编辑器功能以匹配团队工作流程。创建或添加自定义的插件或在插件商店中找到所需资源,插件商店中有上百种能够帮助您加快项目进程的范例、工具和插件。

组件化开发:

  • 传统开发模式 : 代码 驱动 数据、资源
  • 组件开发的好处
    • 先有数据节点
    • 给数据节点添加组件,赋予节点对象不同的功能
    • 组件重用,开发模式比较统一

下载地址:官网

使用介绍

在 Dashboard 中,打开 新建项目 选项卡,选中 Hello World 项目模板

在下面的项目路径栏中指定一个新项目存放路径,路径的最后一部分就是项目文件夹名称。

填好路径后点击右下角的 新建项目 按钮,就会自动以 Hello World 项目模板创建项目并打开。

可以在数据编辑中选择外部编辑器

项目目录结构

ProjectName(项目文件夹)
├──assets (资源文件夹)
	assets 将会用来放置游戏中所有的本地资源、脚本和第三方库文件。只有在 assets 目录下的内容才能显示在 资源管理器 中。assets 中的每个文件在导入项目后都会	生成一个相同名字的 .meta 文件,用于存储对应的资源配置和索引信息。.meta 文件需要一并提交到版本控制系统。
	一些第三方工具生成的工程或设计原文件,如 TexturePacker 的 `.tps` 文件,或 Photoshop 的 `.psd` 文件,可以选择放在 `assets` 里面来管理。
├──library (资源库)
	library 是将 assets 中的资源导入后生成的,在这里文件的结构和资源的格式将被处理成最终游戏发布时需要的形式。
	当 library丢失或损坏的时候,只要删除整个library文件夹再打开项目,就会重新生成资源库。
├──local (本地设置)
	local文件夹中包含该项目的本机上的配置信息,包括编辑器面板布局,窗口大小,位置等信息。开发者不需要关心这里的内容。
├──packages (拓展插件文件夹)
	packages文件夹用于放置此项目的自定义扩展插件。如需手动安装扩展插件,可以手动创建此文件夹。如需卸载扩展插件,在 `packages` 中删除对应的文件夹即可。
├──settings (项目设置)
	settings里保存项目相关的设置,如构建发布菜单里的包名、场景和平台选择等。
├──temp (临时文件夹)
	temp是临时文件夹,用于缓存一些 Cocos Creator 在本地的临时文件。这个文件夹可以在关闭 Cocos Creator 后手动删除,开发者不需要关心这里面的内容。
└──project.json
	project.json文件和assets文件夹一起,作为验证 Cocos Creator 项目合法性的标志,只有包括了这两个内容的文件夹才能作为 Cocos Creator 项目打开。开发		者不需要关心里面的内容。

构建目标(build)

在使用主菜单中的 项目 -> 构建发布... 使用默认发布路径发布项目后,编辑器会在项目路径下创建 build目录,并存放所有目标平台的构建工程。

基本概念

场景

Cocos Creator是由一个一个的游戏场景组成,场景是一个树形结构,场景是由有各种层级关系的节点组成;

场景编辑器 是内容创作的核心工作区域,您将使用它选择和摆放场景图像、角色、特效、UI 等各类游戏元素。在这个工作区域里,您可以选中并通过 变换工具 修改节点的位置、旋转、缩放、尺寸等属性,并可以获得所见即所得的场景效果预览。

节点和组件

Cocos Creator 的工作流程是以组件式开发为核心的,组件式架构也称作 组件-实体系统(或 Entity-Component System),简单的说,就是以组合而非继承的方式进行实体的构建。

在 Cocos Creator 中,**节点(Node)**是承载组件的实体,我们通过将具有各种功能的 **组件(Component)**挂载到节点上,来让节点具有各式各样的表现和功能。接下来我们看看如何在场景中创建节点和添加组件。

每个视觉元素都是一个节点,通常我们不会把所有节点平铺在场景上,而是会按照一定的分类和次序组织成如下图所示的节点树:

img

坐标

在Cocos Creator中采用的是右手坐标系,向右为X轴正方向,向上为Y轴正方向,延屏幕向外为Z轴正方向。

事件响应

触摸事件

javascript
this.node.on
// `TOUCH_START` 当手指触摸到屏幕时。
// `TOUCH_MOVE` 当手指在屏幕上移动时。
// `TOUCH_END` 当手指在目标节点区域内离开屏幕时。
// `TOUCH_CANCEL` 当手指在目标节点区域外离开屏幕时

事件的三个阶段和事件冒泡

鼠标或触摸事件会被系统调用 dispatchEvent 方法触发,触发的过程包含三个阶段:

  • 捕获阶段:派发事件给捕获目标(通过 _getCapturingTargets 获取),比如,节点树中注册了捕获阶段的父节点,从根节点开始派发直到目标节点。

  • 目标阶段:派发给目标节点的监听器。

  • 冒泡阶段:派发事件给冒泡目标(通过 _getBubblingTargets 获取),比如,节点树中注册了冒泡阶段的父节点,从目标节点开始派发直到根节点。 同时您可以将事件派发到父节点或者通过调用 stopPropagation 拦截它。 推荐使用这种方式来监听节点上的触摸或鼠标事件,请不要在节点上直接使用 cc.eventManager。 你也可以注册自定义事件到节点上,并通过 emit 方法触发此类事件,对于这类事件,不会发生捕获冒泡阶段,只会直接派发给注册在该节点上的监听器 你可以通过在 emit 方法调用时在 type 之后传递额外的参数作为事件回调的参数列表

键盘事件

javascript
// 事件类型
// KEY_DOWN 当按下按键时触发的事件
// KEY_UP 当松开按键时触发的事件

// 事件类型回调
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN,function (e) {
            cc.log("KEY_DOWN",e.keyCode)
},this);
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP,function (e) {
    cc.log("KEY_UP",e.keyCode)
     switch (e.keyCode) {
         case cc.macro.KEY.space : {
             cc.log("空格事件")
         }
     }
},this);

响应自定义事件

javascript
// 响应自定义事件
// 监听事件
// 自定义事件类型
this.node.on("fire",function (msg) {
    cc.log("自定义事件:","fire",msg)
},this);

// 发射事件

// 发射事件 emit 
// 只能派送给自己
this.node.emit("fire","开火!");

// 可以做事件传递
// 参数2--> 表示事件是否向上传递
let msgForDispatchEvent = new cc.Event.EventCustom("remoteFire",true);
msgForDispatchEvent.detail = "远程开火!";
this.node.dispatchEvent(msgForDispatchEvent)

游戏资源管理

从本质上说,每个游戏都是由种类繁多,数量庞大的资源构成的,因此,管理包括图片、声音、字体、粒子、地图等多媒体资源是一款游戏引擎的核心功能。资源管理的知识主要有几个方面:包括对于资源的组织,运行期对于资源的载入、清理及对于资源的操作以及资源的优化。

主要介绍图片、音乐音效、预置体、字体、粒子、瓦片地图等资源在Cocos Creator中的管理和使用,将从资源组织,资源操作及资源优化等方面分别介绍资源的管理方式。

图片资源

图片资源又称贴图,是游戏中,尤其是2D游戏中绝大部分对象的渲染源,图片资源一般由制图软件制作而成并输出成游戏引擎支持的格式,目前支持的格式包括JPG和PNG两种,除了单张图片外,Cocos Creator还支持图集资源的导入和自动图集资源的生成功能。

图片资源导入

将图片资源复制到资源管理器所在的目录下,在资源管理器中的对应目录下就可以看到相对应的图片资源,在对应的资源管理器中会显示自身图片的缩略图,以方便你找到想选的那个图片,单击三角形按钮就可以打开图片的子资源,也就是说单击开三角形以后可以看到同名的SpriteFrame,SpriteFrame是精灵类用来显示图像的对象,对于图片集资源来说,可以有多个图片资源在一个资源名下,直接将SpriteFrame或者图像资源从资源管理器中拖拽到场景编辑器,或者属性监视器的对应属性下时,就可以设置节点的图片显示。单击对应的图片资源文件或者SpriteFrame就可以看到对应的属性检查器中可以编辑的属性,但是目前这些属性的编辑暂时无法影响图片资源在游戏中使用时的属性,应该是为未来引擎的扩展预留的编辑接口:

图片资源导入属性编辑需要注意的是,在Cocos Creator 2.0版本后,WrapMode、FilterMode和PremultiplyAlpha这些参数才开始有效。

图集资源得制作和导入

图集(Atlas)也称作 Sprite Sheet,是游戏开发中常见的一种美术资源。图集是通过专门的工具将多张图片合并成一张大图,并通过 plist 等格式的文件索引的资源。可供 Cocos Creator 使用的图集资源由 plistpng 文件组成。下面就是一张图集使用的图片文件:

在游戏中使用多张图片合成的图集作为美术资源,有以下优势:

  • 合成图集时会去除每张图片周围的空白区域,加上可以在整体上实施各种优化算法,合成图集后可以大大减少游戏包体和内存占用
  • 多个 Sprite 如果渲染的是来自同一张图集的图片时,这些 Sprite 可以使用同一个渲染批次来处理,大大减少 CPU 的运算时间,提高运行效率。

更形象生动的解释可以观看来自 CodeAndWeb 的教学视频 What is a Sprite Sheet(什么是图集),需要使用 VPN 打开视频。

制作图集

要生成图集,首先您应该准备好一组原始图片,使用专门的软件生成图集如:

使用这些软件生成图集时请选择 cocos2d-x 格式的 plist 文件。最终得到的图集文件是同名的 plistpng

导入图集资源

将上述生成的 plistpng 文件同时拖拽到 资源管理器 中,就可以生成可以在编辑器和脚本中使用的图集资源了。

声音资源

WebAudio方式加载音频

在Cocos Creator中加载音频的方式和图片一样,在资源管理器的路径下添加音频文件即可,从属性检查器中可以设置加载模式,目前支持的加载模式有两种,一种是Web Audio方式,一种是Dom Audio方式,默认的是前者,当浏览器不支持第一种的时候才使用第二种方式。在属性检查器页面,可以选择加载方式,如图所示。

通过 Web Audio 方式加载的声音资源,在引擎内是以一个 buffer 的形式缓存的。 这种方式的优点是兼容性好,问题比较少。缺点是占用的内存资源过多。

DomAudio方式加载音频

通过生成一个标准的 audio 元素来播放声音资源,缓存的就是这个 audio 元素。

使用标准的 audio 元素播放声音资源的时候,在某些浏览器上可能会遇到一些限制。比如:每次播放必须是用户操作事件内才允许播放(Web Audio 只要求第一次),且只允许播放一个声音资源等。

如果是比较大的音频如背景音乐,建议使用 DOM Audio

预制体得创建和使用

预制体(Prefabs)这个概念是引自Unity引擎的,它是一种可以被重复使用的游戏对象。例如射击游戏中的子弹都来自于同一个子弹模型,当发射子弹时,就创建一个子弹预制体。也就是说,如果创建一个可以重复使用的对象,那么就该用到预制体了。它提供了一种创建对象的最佳方式。当直接创建对象的代价比较大时,则采用这种模式

创建和使用

创建预制体的方法非常简单,直接将编辑好的节点从层级管理器中拖到资源管理器就可以了,在资源管理器选中预设体就可以在属性检查器中设置,单击预制体资源,可以对预制体做一些简单的加载策略设置。

双击预制体资源可以查看预制体详细的属性。

字体资源管理

使用Cocos Creator制作游戏可以使用三类字体资源:系统字体,动态字体和位图字体。

系统字体

系统字体就是调用游戏运行平台上自带的系统字体渲染文字,不需要使用任何资源,只需要设置“Use System Font”属性。

动态字体

目前 Cocos Creator 支持 TTF 格式的动态字体。只要将扩展名为 TTF 的字体文件拖拽到 资源管理器 中,即可完成字体资源的导入。

位图字体

  • 位图字体的制作

    位图字体由fnt格式的字体文件和一张png图片组成,fnt文件提供了对每一个字符小图的索引,这种格式可以由专门的位图字体制作工具生成,目前主要的制作位图字体的工具包括hiero和bmfont等,具体配置文件的工具下载地址如下。

    1)http://www.n4te.com/hiero/hiero.jnlp(Java平台)

    2)http://slick.cokeandcode.com/demos/hiero.jnlp(Java平台)

    3)http://www.angelcode.com/products/bmfont/(Windows平台)

  • 位图字体的导入和使用

    位图字体资源由字体文件(.fnt)和字体图片(.png)组成。导入位图字体的时候,需要将两个文件放入到一个目录下

    2)使用字体文件首先要创建文字渲染对象“Label”。可以在层级管理器或者主菜单的节点子菜单的创建渲染节点选项中选择创建Label对象,在Label组件中可以看到一个“Font”属性

​ 将你想使用的字体文件拖入到“Font”属性中就可以使用该字体了,同时场景编辑界面也会刷新以显示相应的字 体,如果选择“Use SystemFont”,则“Font”属性中的文件便会消失,恢复到使用系统字的情况。

粒子资源管理

动画粒子系统会发射大量细小的粒子并且非常高效地渲染这些粒子,比渲染单个精灵要高效得多,它可以模拟随机的,栩栩如生的烟雾、闪电、风雨、雪花掉落的效果。

特点和构成

无论是通过主观地修改属性后观察运行效果来调试粒子系统,还是通过物理学和数学的公式推导来模拟粒子系统的运行效果,粒子系统都是用大量微粒无规则运动产生独特的视觉效果,因此所有物理引擎都涉及如下特点:

1)包含大量物理微粒对象(粒子)。 2)宏观特性:每一个粒子都符合主要的物理规律。 3)微观特性:在符合规律的基础上,每个粒子都有自己的随机性和独特性。 4)过程动态特性:每一个粒子都是动态的,在移动中不断变化,在每个模拟中都是不断自己更新自己的。

这样按照预先的设计不断产生新的粒子,每个粒子不断地随机变化运动,这样叠加的宏观效果就是粒子系统,可以达到栩栩如生的模拟视觉效果。

一个完整的粒子系统要包括粒子本身、粒子发射器和粒子的整体动态效果。首先介绍粒子。每一个粒子就是一个图形对象,可以使用一个色点或是一张图片来充当粒子。每个粒子都有自己的属性,这些属性不仅包括描述粒子本身的无规则运动的属性,同时也包括粒子在宏观整体运动中的属性,这两种属性共同决定了粒子的运动等性质。

粒子系统的发射器对象就是一个粒子系统的整体,如同一个整体的控制器一样,一片云、一团雾、一次闪电、一股烟都是由一个独立的粒子系统来模拟的,粒子和发射器对象描述了一个粒子系统的复杂性的全部属性,还可以根据自己的游戏设计内容,通过增加新的属性来增加整个粒子系统的复杂性,包括闪烁、随机波动等。

粒子系统的动态效果首先控制了粒子的生成,每个粒子在生成的时候被赋予不同的属性,这些属性增加了系统的随机性;另外动态效果还控制粒子系统的整体移动和变化,包括整体的颜色和位置等变化,比如整体需要有波动等效果。由于粒子是由一个原点喷发出来,所以原点附近集中很多粒子,粒子除了方向和速度以外,还有径向和切向的速度。

Cocos Creator中的粒子资源

  • 粒子系统可通过Particle Designer软件进行制作,也可以通过网页在线制作 http://www.effecthub.com/particle2dx .
  • 导入粒子资源到资源管理器。
  • 资源管理器 里将粒子资源直接拖到 层级管理器 或者 场景编辑器 ,就可以直接使用

Tiled地图集资源管理

在很多游戏中,整个游戏场景中除了主角精灵之外,游戏的地图背景也是一块“重头戏”。在手机游戏的开发中,为了节约内存空间,一般使用图素拼接的方法组成整个地图。也就是说,首先定下图素块(一般为正方形)的大小,然后美术绘制图素块,最后由策划和美术根据不同项目的需求使用地图编辑器将图素拼接成大地图,并将生成的数据文件和图素交给程序处理。

Tiled地图编辑器

Cocos Creator支持Tiled地图编辑器生成的地图数据文件,Tiled地图编辑器的下载地址为http://www.mapeditor.org/。Tiled地图编辑器是一个以普遍使用为目标的地图编辑器,它容易使用并且容易在不同的引擎中使用,目前它的最新版本是使用Qt框架进行开发的。之前也有Java的版本,使用二者的目的就是可以使编辑器跨平台,这点在Cocos2D-X的开发中也比较重要,因为Cocos引擎的跨平台特性,可能使用它的人所使用的操作系统各不相同,为了让大家都可以看到地图的效果,编辑器的“跨平台”也是必要的。Tiled地图编辑器的特性如下:

1)使用多种编码形式的地图数据文件使它可以在不同的游戏引擎中通用。 2)支持普通视角和45度两种视角。 3)可以把对象放在精确到像素的位置。 4)支持撤销/重做和复制/粘贴的操作。 5)图素、层次和对象等通用的概念。 6)自动重新载入图素集。 7)可以重置图素的大小和偏移。 8)支持图章刷和填充等高效工具的使用。 9)支持以通用的格式输入输出来打开和存储文件。

  • 地图资源文件的导入 (课件资源已经提供做好的瓦片图)

  • 地图所需资源有:

    • .tmx 地图数据
    • .png 图集纹理
    • .tsx tileset 数据配置文件(部分 tmx 文件需要)
  • 在cocos creator中使用地图资源

    • 第一种方式:从 资源管理器 里将地图资源拖动到层级管理器中
    • 第二种方式:从 资源管理器 里将地图资源拖动到场景中
    • 第三种方式:从 资源管理器 里将地图资源拖动到已创建 TiledMap 组件的 Tmx File 属性中

骨骼动画

骨骼动画资源是由 Spine 所导出的数据格式(Creator v2.0.7 及以下支持 Spine v2.5,Creator v2.0.8~v2.1 支持 Spine v3.6,Creator v2.2 支持 Spine v3.7,Creator v2.3 及以上支持 Spine v3.8)。

传统的动画,一般是对一个物体对象进行位移、旋转、缩放、变形,然后把关键帧的信息记录下来,在播放的时候按照关键帧时间对物体对象进行位移、旋转、缩放、变形,并在关键帧与关键帧之间做插值运算。

骨骼动画的特点是,需要做动画的物体对象本身不记录位移、旋转、缩放、变形信息,而是通过了第三方的“骨骼”物体记录动画信息,然后物体对象本身只记录受到骨骼物体影响的权重。在播放的时候,通过骨骼物体的关键帧和物体对象记录的权重,让动画重现。

好处:

1、骨骼动画是影响到顶点级别的动画,而且可以多根骨骼根据权重影响同一个顶点,不论是2D或者3D使用,都可以让动画做得更丰富。

2、做到了对象和动画分离,我们只需要记录了物体对于骨骼的蒙皮权重,就可以单独的去制作骨骼的动画,在保证蒙皮信息和骨骼信息一致的情况下,还可以多个物体之间共享动画。

3、相对于2D的逐帧动画大大的节省资源容量。

导入

骨骼动画所需资源有:

  • .json/.skel 骨骼数据

  • .png 图集纹理

  • .txt/.atlas 图集数据

创建骨骼动画

创建骨骼动画资源有以下三种方式:

  1. 资源管理器 中将骨骼动画资源拖动到 层级管理器:

  2. 资源管理器 中将骨骼动画资源拖动到 场景编辑器:

  3. 资源管理器 中将骨骼动画资源拖动到 属性检查器 Spine 组件的 Skeleton Data 属性中:

三、脚本编程

Cocos Creator 的脚本主要是通过扩展组件来进行开发的。目前 Cocos Creator 支持 JavaScript 和 TypeScript 两种脚本语言。通过编写脚本组件,并将它赋予到场景节点中来驱动场景中的物体。

在组件脚本的编写过程中,你可以通过声明属性,将脚本中需要调节的变量映射到 属性检查器(Properties) 中,供策划和美术调整。于此同时,你也可以通过注册特定的回调函数,来帮助你初始化,更新甚至销毁节点。

节点和组件

创建和使用脚本组件

  • 创建脚本组件

    在 Cocos Creator 中,脚本也是资源的一部分。你可以在资源编辑器中通过点击"创建"按钮来添加并选择 JavaScript 或者 TypeScript 来创建一份组件脚本。此时你会在你的资源编辑器中得到一份新的脚本:

    一份简单的组件脚本如下:

    javascript
    cc.Class({
        extends: cc.Component,
    
        properties: {
        },
    
        // use this for initialization
        onLoad: function () {
        },
    
        // called every frame, uncomment this function to activate update callback
        update: function (dt) {
        },
    });
  • 使用脚本组件

    将脚本添加到场景节点中,实际上就是为这个节点添加一份组件。我们先将刚刚创建出来的 NewScript.js重命名为 say-hello.js。然后选中我们希望添加的场景节点,此时该节点的属性会显示在 属性检查器 中。在 属性检查器 的最下方有一个 添加组件 的按钮,点击按钮并选择 添加用户脚本 -> say-hello 来添加我们刚刚编写的脚本组件。

    如果一切顺利,你将会看到你的脚本显示在 属性检查器 中:

    注意:用户也可以通过直接拖拽脚本资源到 属性检查器 的方式来添加脚本。

使用 cc.Class 声明类型

cc.Class 是一个很常用的 API,用于声明 Cocos Creator 中的类,为了方便区分,我们把使用 cc.Class 声明的类叫做 CCClass

  • 定义一个 CCClass

    调用 cc.Class,传入一个原型对象,在原型对象中以键值对的形式设定所需的类型参数,就能创建出所需要的类。

    javascript
    let Sprite = cc.Class({
        name: "sprite",
    });

    以上代码用 cc.Class 创建了一个类型,并且赋给了 Sprite 变量。同时还将类名设为 "sprite",类名用于序列化,一般可以省略。

  • 实例化

    Sprite 变量保存的是一个 JavaScript 构造函数,可以直接 new 出一个对象:

    javascript
    let obj = new Sprite();
  • 判断类型

    需要做类型判断时,可以用 JavaScript 原生的 instanceof

    javascript
    cc.log(obj instanceof Sprite);       // true
  • 实例方法

    js
    let Sprite = cc.Class({
        // 声明一个名叫 "print" 的实例方法
        print: function () { }
    });
  • 继承

    使用 extends 实现继承:

    js
    // 父类
    let Shape = cc.Class();
    
    // 子类
    let Rect = cc.Class({
        extends: Shape
    });

声明自定义属性

简单声明

在多数情况下,我们都可以使用简单声明。

(1)基本类型

  • 当声明的属性为基本 JavaScript 类型时,可以直接赋予默认值:

    javascript
      properties: {
          height: 20,       // number
          type: "actor",    // string
          loaded: false,    // boolean                
          target: null,     // object
      }
  • 当声明的属性具备类型时(如:cc.Nodecc.Vec2 等),可以在声明处填写他们的构造函数来完成声明,如:

    javascript
      properties: {
          target: cc.Node,
          pos: cc.Vec2,
      }

(2)数组类型

当声明属性是一个数组时,可以在声明处填写他们的类型或构造函数来完成声明,如:

js
properties: {
      any: [],      		// 不定义具体类型的数组
      bools: [cc.Boolean],
      strings: [cc.String],
      floats: [cc.Float],
      ints: [cc.Integer],
      values: [cc.Vec2],
      nodes: [cc.Node],
      frames: [cc.SpriteFrame],
}

完整声明

有些情况下,我们需要为属性声明添加参数,这些参数控制了属性在 属性检查器 中的显示方式,以及属性在场景序列化过程中的行为。

(1)基本类型

javascript
properties: {
    score: {
        default: 0,
        displayName: "得分",
        tooltip: "设置玩家初始得分",
    }
}

以上代码为 score 属性设置了三个参数 default, displayNametooltip。这几个参数分别指定了 score 的默认值为 0,在 属性检查器 里,其属性名将显示为:“Score (player)”,并且当鼠标移到参数上时,显示对应的 Tooltip。

下面是常用参数:

  • default: 设置属性的默认值,这个默认值仅在组件第一次添加到节点上时才会用到
  • type: 限定属性的数据类型
  • visible: 设为 false 则不在 属性检查器 面板中显示该属性
  • serializable: 设为 false 则不序列化(保存)该属性
  • displayName: 在 属性检查器 面板中显示成指定名字
  • tooltip: 在 属性检查器 面板中添加属性的 Tooltip

在属性检查器中展示

(2)数组类型

数组的 default 必须设置为 [],如果要在 属性检查器 中编辑,还需要设置 type 为构造函数,枚举,或者 cc.Integercc.Floatcc.Booleancc.String

javascript
properties: {
    names: {
        default: [],
        type: [cc.String]   // 用 type 指定数组的每个元素都是字符串类型
    },

    enemies: {
        default: [],
        type: [cc.Node]     // type 同样写成数组,提高代码可读性
    },
}

脚本生命周期的回调函数

游戏运行的时期自动执行相关脚本,用户不需要手工调用它们。

目前提供给用户的生命周期回调函数主要有:

  • onLoad
  • onEnable
  • start
  • update
  • lateUpdate
  • onDisable
  • onDestroy

onLoad

组件脚本的初始化阶段,我们提供了 onLoad 回调函数。onLoad 回调会在节点首次激活时触发,比如所在的场景被载入,或者所在节点被激活的情况下。在 onLoad 阶段,保证了你可以获取到场景中的其他节点,以及节点关联的资源数据。onLoad 总是会在任何 start 方法调用前执行,这能用于安排脚本的初始化顺序。通常我们会在 onLoad 阶段去做一些初始化相关的操作。例如:

js
cc.Class({
  extends: cc.Component,

  properties: {
    bulletSprite: cc.SpriteFrame,
    gun: cc.Node,
  },

  onLoad: function () {
    this._bulletRect = this.bulletSprite.getRect();
    this.gun = cc.find('hand/weapon', this.node);
  },
});

onEnable

当组件的 enabled 属性从 false 变为 true 时,或者所在节点的 active 属性从 false 变为 true时,会激活 onEnable 回调。倘若节点第一次被创建且 enabledtrue,则会在 onLoad 之后,start 之前被调用。

start

start 回调函数会在组件第一次激活前,也就是第一次执行 update 之前触发。start 通常用于初始化一些中间状态的数据,这些数据可能在 update 时会发生改变,并且被频繁的 enable 和 disable。

js
cc.Class({
  extends: cc.Component,

  start: function () {
    this._timer = 0.0;
  },

  update: function (dt) {
    this._timer += dt;
    if ( this._timer >= 10.0 ) {
      console.log('I am done!'); 
      this.enabled = false;
    }
  },
});

update

游戏开发的一个关键点是在每一帧渲染前更新物体的行为,状态和方位。这些更新操作通常都放在 update回调中。

js
cc.Class({
  extends: cc.Component,

  update: function (dt) {
    this.node.y+=1;
  }
});

lateUpdate

update 会在所有动画更新前执行,但如果我们要在动效(如动画、粒子、物理等)更新之后才进行一些额外操作,或者希望在所有组件的 update 都执行完之后才进行其它操作,那就需要用到 lateUpdate 回调。

js
cc.Class({
  extends: cc.Component,

  lateUpdate: function (dt) {
    this.node.rotation += 20;
  }
});

onDisable

当组件的 enabled 属性从 true 变为 false 时,或者所在节点的 active 属性从 true 变为 false时,会激活 onDisable 回调。

onDestroy

当组件或者所在节点调用了 destroy(),则会调用 onDestroy 回调,并在当帧结束时统一回收组件。

访问节点和其他组件

你可以在 属性检查器 里修改节点和组件,也能在脚本中动态修改。动态修改的好处是能够在一段时间内连续地修改属性、过渡属性,实现渐变效果。脚本还能够响应玩家输入,能够修改、创建和销毁节点或组件,实现各种各样的游戏逻辑。要实现这些效果,你需要先在脚本中获得你要修改的节点或组件。

我们将介绍如何

  • 获得组件所在的节点
  • 获得当前节点下的其它组件
  • 获得其它节点及其下的组件 (使用属性检查器设置)
  • 查找子节点

获得组件所在的节点

获得组件所在的节点很简单,只要在组件方法里访问 this.node 变量:

js
start: function () {
    let node = this.node;
    node.x = 100;
}

获得当前节点下的其它组件

你会经常需要获得同一个节点上的其它组件,这就要用到 getComponent 这个 API,它会帮你查找你要的组件。

js
start: function () {
    let label = this.getComponent(cc.Label);
    let text = this.name + 'started';

    // Change the text in Label Component
    label.string = text;
}

获得其它节点及其下的组件

仅仅能访问节点自己的组件通常是不够的,脚本通常还需要进行多个节点之间的交互。例如,一门自动瞄准玩家的大炮,就需要不断获取玩家的最新位置。Cocos Creator 提供了一些不同的方法来获得其它节点或组件。

  • 使用属性检查器设置节点

    最直接的方式就是在 属性检查器 中设置你需要的对象。以节点为例,这只需要在脚本中声明一个 type 为 cc.Node 的属性:

    js
    // Cannon.js
    
    cc.Class({
        extends: cc.Component,
        properties: {
            // 声明 player 属性
            player: {
                default: null,
                type: cc.Node
            }
        }
    });

    这段代码在 properties 里面声明了一个 player 属性,默认值为 null,并且指定它的对象类型为 cc.Node。这就相当于在其它语言里声明了 public cc.Node player = null;。脚本编译之后,这个组件在 属性检查器 中看起来是这样的:

    接着你就可以将层级管理器上的任意一个节点拖到这个 Player 控件:

    这样一来它的 player 属性就会被设置成功,你可以直接在脚本里访问 player:

    js
    // Cannon.js
    cc.Class({
        extends: cc.Component,
        properties: {
            // 声明 player 属性
            player: {
                default: null,
                type: cc.Node
            }
        },
    
        start: function () {
            cc.log("The player is " + this.player.name);
        },
    
        // ...
    });
  • 使用属性检查器设置组件

    在上面的例子中,如果你将属性的 type 声明为 Player 组件,当你拖动节点 Player Node属性检查器,player 属性就会被设置为这个节点里面的 Player 组件。这样你就不需要再自己调用 getComponent 啦。

    js
    // Cannon.js
    
    let Player = require("Player");
    
    cc.Class({
        extends: cc.Component,
        properties: {
            // 声明 player 属性,这次直接是组件类型
            player: {
                default: null,
                type: Player
            }
        },
    
        start: function () {
            let playerComp = this.player;
            player.fire();
        },
    
        // ...
    });

    你还可以将属性的默认值由 null 改为数组 [],这样你就能在 属性检查器 中同时设置多个对象。 不过如果需要在运行时动态获取其它对象,还需要用到下面介绍的查找方法。

查找子节点

有时候,游戏场景中会有很多个相同类型的对象,像是炮塔、敌人和特效,它们通常都有一个全局的脚本来统一管理。如果用 属性检查器 来一个一个将它们关联到这个脚本上,那工作就会很繁琐。为了更好地统一管理这些对象,我们可以把它们放到一个统一的父物体下,然后通过父物体来获得所有的子物体:

js
// sya-hello.js

cc.Class({
    extends: cc.Component,

    start: function () {
        let children = this.node.children;
        // ...
    }
});

你还可以使用 getChildByName

js
this.node.getChildByName("Cannon");

如果子节点的层次较深,你还可以使用 cc.findcc.find 将根据传入的路径进行逐级查找:

js
cc.find("Cannon/Sub", this.node);

全局名字查找

cc.find 只传入第一个参数时,将从场景根节点开始逐级查找:

js
this.backNode = cc.find("Canvas/Hello/Cannon");

常用节点和组件接口

在通过 访问节点和组件 介绍的方法获取到节点或组件实例后,这篇文章将会介绍通过节点和组件实例可以通过哪些常用接口实现我们需要的种种效果和操作。

激活/关闭节点

节点默认是激活的,我们可以在代码中设置它的激活状态,方法是设置节点的 active 属性:

this.node.active = false;

设置 active 属性和在编辑器中切换节点的激活、关闭状态,效果是一样的。当一个节点是关闭状态时,它的所有组件都将被禁用。同时,它所有子节点,以及子节点上的组件也会跟着被禁用。要注意的是,子节点被禁用时,并不会改变它们的 active 属性,因此当父节点重新激活的时候它们就会回到原来的状态。

也就是说,active 表示的其实是该节点 自身的 激活状态,而这个节点 当前 是否可被激活则取决于它的父节点。并且如果它不在当前场景中,它也无法被激活。我们可以通过节点上的只读属性 activeInHierarchy来判断它当前是否已经激活。

this.node.active = true;

若节点原先就处于 可被激活 状态,修改 active 为 true 就会立即触发激活操作:

  • 在场景中重新激活该节点和节点下所有 active 为 true 的子节点
  • 该节点和所有子节点上的所有组件都会被启用,他们中的 update 方法之后每帧会执行
  • 这些组件上如果有 onEnable 方法,这些方法将被执行
this.node.active = false;

如该节点原先就已经被激活,修改 active 为 false 就会立即触发关闭操作:

  • 在场景中隐藏该节点和节点下的所有子节点
  • 该节点和所有子节点上的所有组件都将被禁用,也就是不会再执行这些组件中的 update 中的代码
  • 这些组件上如果有 onDisable 方法,这些方法将被执行

更改节点的父节点

假设父节点为 parentNode,子节点为 this.node

您可以:

js
this.node.parent = parentNode;

js
this.node.removeFromParent(false);
parentNode.addChild(this.node);

这两种方法是等价的。

注意:

  • removeFromParent 通常需要传入一个 false,否则默认会清空节点上绑定的事件和 action 等。
  • 通过 创建和销毁节点 介绍的方法创建出新节点后,要为节点设置一个父节点才能正确完成节点的初始化。

索引节点的子节点

this.node.children 将返回节点的所有子节点数组。 this.node.childrenCount 将返回节点的子节点数量。

注意 以上两个 API 都只会返回节点的直接子节点,不会返回子节点的子节点。

更改节点的变换(位置、旋转、缩放、尺寸)

(1)更改节点位置

分别对 x 轴和 y 轴坐标赋值:

this.node.x = 100;
this.node.y = 50;

使用 setPosition 方法:

this.node.setPosition(100, 50);
this.node.setPosition(cc.v2(100, 50));

设置 position 变量:

this.node.position = cc.v2(100, 50);

以上两种用法等价。

(2)更改节点旋转

this.node.rotation = 90;

this.node.setRotation(90);

(3)更改节点缩放

this.node.scaleX = 2;
this.node.scaleY = 2;

this.node.setScale(2);
this.node.setScale(2, 2);

以上两种方法等价。setScale 传入单个参数时,会同时修改 scaleXscaleY

(4)更改节点尺寸

this.node.setContentSize(100, 100);
this.node.setContentSize(cc.size(100, 100));

this.node.width = 100;
this.node.height = 100;

以上两种方式等价。

(5)更改节点锚点位置

this.node.anchorX = 1;
this.node.anchorY = 0;

this.node.setAnchorPoint(1, 0);

注意以上这些修改变换的方法会影响到节点上挂载的渲染组件,比如 Sprite 图片的尺寸、旋转等等。

(6)颜色和不透明度

在使用 Sprite, Label 这些基本的渲染组件时,要注意修改颜色和不透明度的操作只能在节点的实例上进行,因为这些渲染组件本身并没有设置颜色和不透明度的接口。

假如我们有一个 Sprite 的实例为 mySprite,如果需要设置它的颜色:

this.node.color = cc.Color.RED;

设置不透明度:

this.node.opacity = 128;

常用组件接口

cc.Component 是所有组件的基类,任何组件都包括如下的常见接口(假设我们在该组件的脚本中,以 this 指代本组件):

  • this.node:该组件所属的节点实例
  • this.enabled:是否每帧执行该组件的 update 方法,同时也用来控制渲染组件是否显示
  • update(dt):作为组件的成员方法,在组件的 enabled 属性为 true 时,其中的代码会每帧执行
  • onLoad():组件所在节点进行初始化时(节点添加到节点树时)执行
  • start():会在该组件第一次 update 之前执行,通常用于需要在所有组件的 onLoad 初始化完毕后执行的逻辑

创建和销毁节点

创建新节点

除了通过场景编辑器创建节点外,我们也可以在脚本中动态创建节点。通过 new cc.Node() 并将它加入到场景中,可以实现整个创建过程。

以下是一个简单的例子:

javascript
cc.Class({
  extends: cc.Component,

  properties: {
  },

  start: function () {
      let self = this;
      // 加载资源
      cc.loader.loadRes("sheep",cc.SpriteFrame,function (err,spriteFrame) {
          let node = new cc.Node("sprite");
          let sp = node.addComponent(cc.Sprite);
          sp.spriteFrame = spriteFrame;
          node.parent = self.node;
          node.setContentSize(100,100);
      });
  },
});

克隆已有节点

有时我们希望动态的克隆场景中的已有节点,我们可以通过 cc.instantiate 方法完成。使用方法如下:

javascript
cc.Class({
  extends: cc.Component,

  properties: {
    target: {
      default: null,
      type: cc.Node,
    },
  },

  start: function () {
    var node = cc.instantiate(this.target);
    node.parent = this.node;
    node.setPosition(0, 0);
  },
});

创建预制节点

和克隆已有节点相似,你可以设置一个预制(Prefab)并通过 cc.instantiate 生成节点。使用方法如下:

javascript
cc.Class({
  extends: cc.Component,

  properties: {
    target: {
      default: null,
      type: cc.Prefab,
    },
  },

  start: function () {
    var node = cc.instantiate(this.target);
    node.parent = this.node;
    node.setPosition(0, 0);
  },
});

销毁节点

通过 node.destroy() 函数,可以销毁节点。值得一提的是,销毁节点并不会立刻被移除,而是在当前帧逻辑更新结束后,统一执行。当一个节点销毁后,该节点就处于无效状态,可以通过 cc.isValid 判断当前节点是否已经被销毁。

使用方法如下:

javascript
cc.Class({
  extends: cc.Component,

  properties: {
    target: cc.Node,
  },

  start: function () {
    // 5 秒后销毁目标节点
    setTimeout(function () {
      this.node.destroy();
    }.bind(this), 5000);
  },

  update: function (dt) {
    if (cc.isValid(this)) {
      this.target.rotation +=  10.0;
    }
  },
});

destroy 和 removeFromParent 的区别

调用一个节点的 removeFromParent 后,它不一定就能完全从内存中释放,因为有可能由于一些逻辑上的问题,导致程序中仍然引用到了这个对象。因此如果一个节点不再使用了,请直接调用它的 destroy 而不是 removeFromParentdestroy 不但会激活组件上的 onDestroy,还会降低内存泄露的几率,同时减轻内存泄露时的后果。

总之,如果一个节点不再使用,destroy 就对了,不需要 removeFromParent 也不需要设置 parentnull 哈。

加载和切换场景

在Cocos Creator的脚本中,可以使用导演类的loadScene方法加载场景,loadScene可以传递一个或两个参数,第一个参数是场景名称,第二个参数是场景加载完毕后的回调函数,场景加载完毕后的回调函数可以进行必要的初始化或数据传递操作,代码如下所示。

js
cc.director.loadScene("MyScene");

有些场景的加载时间过长,会造成游戏的卡顿,这时我们可以使用预加载的方式,在时间充裕的时候加载场景数据,将这些数据缓存在内存中,这种方式本质上是一种“空间换时间”的方式。预加载的操作代码如下所示。

js
cc.director.preloadScene("table", function () {
    cc.log("Next scene preloaded");
});

通过常驻节点进行场景资源管理和参数传递

引擎同时只会运行一个场景,当切换场景时,默认会将场景内所有节点和其他实例销毁。如果我们需要用一个组件控制所有场景的加载,或在场景之间传递参数数据,就需要将该组件所在节点标记为「常驻节点」,使它在场景切换时不被自动销毁,常驻内存。我们使用以下接口:

cc.game.addPersistRootNode(myNode);

上面的接口会将 myNode 变为常驻节点,这样挂在上面的组件都可以在场景之间持续作用,我们可以用这样的方法来储存玩家信息,或下一个场景初始化时需要的各种数据。

如果要取消一个节点的常驻节点:

cc.game.removePersistRootNode(myNode);

资源管理

通过资源属性设置和加载资源

在 Creator 中,所有继承自 cc.Asset 的类型都统称资源,如 cc.Texture2D, cc.SpriteFrame, cc.AnimationClip, cc.Prefab 等。它们的加载是统一并且自动化的,相互依赖的资源能够被自动预加载。

例如,当引擎在加载场景时,会先自动加载场景关联到的资源,这些资源如果再关联其它资源,其它也会被先被加载,等加载全部完成后,场景加载才会结束。

脚本中可以这样定义一个 Asset 属性:

javascript
// NewScript.js

cc.Class({
    extends: cc.Component,
    properties: {
        sprite_frame: {
            default: null,
            type: cc.SpriteFrame
        },

    }
});

如何在属性检查器里设置资源

只要在脚本中定义好类型,就能直接在 属性检查器 很方便地设置资源。假设我们创建了这样一个脚本:

javascript
// NewScript.js

cc.Class({
    extends: cc.Component,
    properties: {
        sprite_frame: {
            default: null,
            type: cc.spriteFrame
        },

    }
});

将它添加到节点后,在 属性检查器 中是这样的:

接下来我们从 资源管理器 里面分别将一张 Texture 和一个 SpriteFrame 拖到 属性检查器 的对应属性中:

结果如下:

这样就能在脚本里直接拿到设置好的资源:

javascript
onLoad: function () {
     let sprite = this.node.getComponent(cc.Sprite);
     sprite.spriteFrame = this.sprite_frame;
}

属性检查器 里设置资源虽然很直观,但资源只能在场景里预先设好,没办法动态切换。

动态加载资源

Creator 提供了 cc.loader.loadRes 这个 API 来专门加载那些位于 resources 目录下的 Asset。调用时,你只要传入相对 resources 的路径即可,并且路径的结尾处 不能包含文件扩展名

javascript
let self = this;
// 加载 Prefab
cc.loader.loadRes("Player", function (err, prefab) {
    let newNode = cc.instantiate(prefab);
    self.node.addChild(newNode);
});

加载 SpriteFrame

图片设置为 Sprite 后,将会在 资源管理器 中生成一个对应的 SpriteFrame。但如果直接加载 test assets/image,得到的类型将会是 cc.Texture2D。你必须指定第二个参数为资源的类型,才能加载到图片生成的 cc.SpriteFrame:

javascript
// 加载 SpriteFrame
let self = this;
cc.loader.loadRes("sheep", cc.SpriteFrame, function (err, spriteFrame) {
    self.node.getComponent(cc.Sprite).spriteFrame = spriteFrame;
});

资源的释放

loadRes 加载进来的单个资源如果需要释放,可以调用 cc.loader.releaseResreleaseRes 可以传入和 loadRes 相同的路径和类型参数。

javascript
cc.loader.releaseRes("sheep", cc.SpriteFrame);
cc.loader.releaseRes("sheep");

此外,你也可以使用 cc.loader.releaseAsset 来释放特定的 Asset 实例。

javascript
cc.loader.releaseAsset(spriteFrame);

使用动作系统

Cocos Creator 提供的动作系统可以在一定时间内对节点完成位移,缩放,旋转等各种动作。

Cocos引擎的Action动作类并不是一个在屏幕中显示的对象,动作必须要依托于Node节点类及它的子类的实例才能发挥它的作用,Cocos中的动作不仅包括位置移动等,还包括跳跃、旋转,甚至是对象透明度的变化和颜色的渐变。这些基本动作可以构成各种复杂的动作,也可以通过sequence形成一个完整的动作序列。

基本使用API

动作系统的使用方式也很简单,在 cc.Node 中支持如下 API:

js
// 创建一个移动动作
let action = cc.moveTo(2, 100, 100);
// 执行动作
this.node.runAction(action);
// 停止一个动作
this.node.stopAction(action);
// 停止所有动作
this.node.stopAllActions();

动作类型

在 Cocos Creator 中支持非常丰富的各种动作,这些动作主要分为几大类:

基础动作

基础动作就是实现各种形变,位移动画的动作,比如 cc.moveTo 用来移动节点到某个位置;cc.rotateBy用来旋转节点一定的角度;cc.scaleTo 用来缩放节点。

基础动作中分为时间间隔动作和即时动作,前者是在一定时间间隔内完成的渐变动作,前面提到的都是时间间隔动作,它们全部继承自 cc.ActionInterval。后者则是立即发生的,比如用来调用回调函数的 cc.callFunc;用来隐藏节点的 cc.hide,它们全部继承自 cc.ActionInstant。

容器动作

容器动作可以以不同的方式将动作组织起来,下面是几种容器动作的用途:

  1. 顺序动作 cc.sequence 顺序动作可以让一系列子动作按顺序一个个执行。示例:

    js
     // 让节点左右来回移动
     let seq = cc.sequence(cc.moveBy(0.5, 200, 0), cc.moveBy(0.5, -200, 0));
     node.runAction(seq);
  2. 同步动作 cc.spawn 同步动作可以同步执行对一系列子动作,子动作的执行结果会叠加起来修改节点的属性。示例:

    js
     // 让节点在向上移动的同时缩放
     let spawn = cc.spawn(cc.moveBy(0.5, 0, 50), cc.scaleTo(0.5, 0.8, 1.4));
     node.runAction(spawn);
  3. 重复动作 cc.repeat 重复动作用来多次重复一个动作。示例:

    js
     // 让节点左右来回移动,并重复5次
     let seq = cc.repeat(
                 cc.sequence(
                     cc.moveBy(2, 200, 0),
                     cc.moveBy(2, -200, 0)
                 ), 5);
     node.runAction(seq);
  4. 永远重复动作 cc.repeatForever 顾名思义,这个动作容器可以让目标动作一直重复,直到手动停止。

    js
     // 让节点左右来回移动并一直重复
     let seq = cc.repeatForever(
                 cc.sequence(
                     cc.moveBy(2, 200, 0),
                     cc.moveBy(2, -200, 0)
                 ));
  5. 速度动作 cc.speed 速度动作可以改变目标动作的执行速率,让动作更快或者更慢完成。

    js
     // 让目标动作速度加快一倍,相当于原本2秒的动作在1秒内完成
     let action = cc.speed(
                     cc.spawn(
                         cc.moveBy(2, 0, 50),
                         cc.scaleTo(2, 0.8, 1.4)
                     ), 2);
     node.runAction(action);

从上面的示例中可以看出,不同容器类型是可以复合的,除此之外,我们给容器类型动作提供了更为方便的链式 API,动作对象支持以下三个 API:repeatrepeatForeverspeed,这些 API 都会返回动作对象本身,支持继续链式调用。我们来看一个更复杂的动作示例:

js
// 一个复杂的跳跃动画
this.jumpAction = cc.sequence(
    cc.spawn(
        cc.scaleTo(0.1, 0.8, 1.2),
        cc.moveTo(0.1, 0, 10)
    ),
    cc.spawn(
        cc.scaleTo(0.2, 1, 1),
        cc.moveTo(0.2, 0, 0)
    ),
    cc.delayTime(0.5),
    cc.spawn(
        cc.scaleTo(0.1, 1.2, 0.8),
        cc.moveTo(0.1, 0, -10)
    ),
    cc.spawn(
        cc.scaleTo(0.2, 1, 1),
        cc.moveTo(0.2, 0, 0)
    )
// 以1/2的速度慢放动画,并重复5次
).speed(2).repeat(5);

动作回调

动作回调可以用以下的方式声明:

js
let finished = cc.callFunc(this.myMethod, this, opt);

cc.callFunc 第一个参数是处理回调的方法,即可以使用 CCClass 的成员方法,也可以声明一个匿名函数:

js
let finished = cc.callFunc(function () {
    //doSomething
}, this, opt);

第二个参数指定了处理回调方法的 context(也就是绑定 this),第三个参数是向处理回调方法的传参。您可以这样使用传参:

js
let finished = cc.callFunc(function(target, score) {
    cc.log("动作回调");
}, this, 100);

在声明了回调动作 finished 后,您可以配合 cc.sequence 来执行一整串动作并触发回调:

js
let myAction = cc.sequence(cc.moveBy(1, cc.v2(0, 100)), cc.fadeOut(1), finished);

在同一个 sequence 里也可以多次插入回调:

js
let myAction = cc.sequence(cc.moveTo(1, cc.v2(0, 0)), finished1, cc.fadeOut(1), finished2);

注意: 在 cc.callFunc 中不应该停止自身动作,由于动作是不能被立即删除,如果在动作回调中暂停自身动作会引发一系列遍历问题,导致更严重的 bug。

四、UI系统

一个完整的游戏一般由不同的系统组成,从技术的角度看一般会包含UI系统、动画系统、物理系统和声音系统等。其中开发声音相关的内容在引擎工具的帮助下变得很简单,在之前资源管理的部分已经介绍。

对于目前市面上的手机游戏来说,特别是2D游戏,除了主要的游戏玩法和战斗以外,主要的工作量都在UI界面的开发上。由于使用率和开发量都比较高,Cocos Creator中的UI组件经过多个版本的迭代后,在易用性和功能完整度上都有较大的提升,本小节就来学习一下Cocos Creator中的UI系统。

基础渲染组件-精灵组件

再复杂的系统都是由基础的简单系统组成的,UI系统也是如此。复杂的UI组件也是由图片和文字等基础元素组成的,接下来就来介绍Cocos Creator中的基础渲染组件 Sprite 精灵组件。

Sprite(精灵)是 2D 游戏中最常见的显示图像的方式,在节点上添加 Sprite 组件,就可以在场景中显示项目资源中的图片。

点击 属性检查器 下面的 添加组件 按钮,然后从 渲染组件 中选择 Sprite,即可添加 Sprite 组件到节点上。

Sprite 属性

属性功能说明
AtlasSprite 显示图片资源所属的 Atlas 图集资源。(Atlas 后面的 选择 按钮,该功能暂时不可用,我们会尽快优化)
Sprite Frame渲染 Sprite 使用的 SpriteFrame 图片资源。
Type渲染模式,包括普通(Simple)、九宫格(Sliced)、平铺(Tiled)、填充(Filled)和网格(Mesh)渲染五种模式
Size Mode指定 Sprite 的尺寸 Trimmed 表示会使用原始图片资源裁剪透明像素后的尺寸 Raw 表示会使用原始图片未经裁剪的尺寸 Custom 表示会使用自定义尺寸。当用户手动修改过 Size 属性后,Size Mode 会被自动设置为 Custom,除非再次指定为前两种尺寸。
Trim勾选后将在渲染时去除原始图像周围的透明像素区域,该项仅在 Type 设置为 Simple 时生效。
Src Blend Factor当前图像混合模式
Dst Blend Factor背景图像混合模式,和上面的属性共同作用,可以将前景和背景 Sprite 用不同的方式混合渲染,效果预览可以参考 glBlendFunc Tool

添加 Sprite 组件之后,通过从 资源管理器 中拖拽 Texture 或 SpriteFrame 类型的资源到 Sprite Frame 属性引用中,就可以通过 Sprite 组件显示资源图像。

如果拖拽的 SpriteFrame 资源是包含在一个 Atlas 图集资源中的,那么 Sprite 的 Atlas 属性也会被一起设置。

渲染模式

Sprite 组件支持五种渲染模式:

  • 普通模式(Simple):按照原始图片资源样子渲染 Sprite,一般在这个模式下我们不会手动修改节点的尺寸,来保证场景中显示的图像和美术人员生产的图片比例一致。
  • 九宫格模式(Sliced):图像将被分割成九宫格,并按照一定规则进行缩放以适应可随意设置的尺寸(size)。通常用于 UI 元素,或将可以无限放大而不影响图像质量的图片制作成九宫格图来节省游戏资源空间。
  • 平铺模式(Tiled):当 Sprite 的尺寸增大时,图像不会被拉伸,而是会按照原始图片的大小不断重复,就像平铺瓦片一样将原始图片铺满整个 Sprite 规定的大小。
  • 填充模式(Filled):根据原点和填充模式的设置,按照一定的方向和比例绘制原始图片的一部分。经常用于进度条的动态展示。
  • 网格模式(Mesh):必须使用 TexturePacker 4.x 以上版本并且设置 ploygon 算法打包出的 plist 文件才能够使用该模式。

填充模式(Filled)

Type 属性选择填充模式后,会出现一组新的属性可供配置,让我们依次介绍他们的作用。

属性功能说明
Fill Type填充类型选择,有 HORIZONTAL(横向填充)、VERTICAL(纵向填充)和 RADIAL (扇形填充)三种。
Fill Start填充起始位置的标准化数值(从 0 ~ 1,表示填充总量的百分比),选择横向填充时,Fill Start 设为 0,就会从图像最左边开始填充
Fill Range填充范围的标准化数值(同样从 0 ~ 1),设为 1,就会填充最多整个原始图像的范围。
Fill Center填充中心点,只有选择了 RADIAL 类型才会出现这个属性。决定了扇形填充时会环绕 Sprite 上的哪个点,所用的坐标系和 Anchor 锚点 是一样的。

Fill Range 填充范围补充说明

HORIZONTALVERTICAL 这两种填充类型下,Fill Start 设置的数值将影响填充总量,如果 Fill Start 设为 0.5,那么即使 Fill Range 设为 1.0,实际填充的范围也仍然只有 Sprite 总大小的一半。

RADIAL 类型中 Fill Start 只决定开始填充的方向,Fill Start 为 0 时,从 x 轴正方向开始填充。Fill Range 决定填充总量,值为 1 时将填充整个圆形。Fill Range 为正值时逆时针填充,为负值时顺时针填充。

基础渲染组件-Lable组件

Label组件用来显示一段文字,文字可以是系统字,也可以是图片字或者艺术字体,Label组件将文字排好版并且渲染出来。单击属性检查器下面的添加组件按钮,然后就可以在渲染组件中选择Label,将Label组件添加到节点上,Label的属性编辑界面:

Label 属性

属性功能说明
String文本内容字符串。
Horizontal Align文本的水平对齐方式。可选值有 LEFT,CENTER 和 RIGHT。
Vertical Align文本的垂直对齐方式。可选值有 TOP,CENTER 和 BOTTOM。
Font Size文本字体大小。
Line Height文本的行高。
Overflow文本的排版方式,目前支持 CLAMP,SHRINK 和 RESIZE_HEIGHT。
Enable Wrap Text是否开启文本换行。(在排版方式设为 CLAMP、SHRINK 时生效)
SpacingX文本字符之间的间距。(使用 BMFont 位图字体时生效)
Font指定文本渲染需要的字体文件,如果使用系统字体,则此属性可以为空。
Font Family文字字体名字。在使用系统字体时生效。
Cache Mode文本缓存类型(v2.0.9 中新增),仅对 系统字体 或 ttf 字体有效,BMFont 字体无需进行这个优化。包括 NONE、BITMAP、CHAR 三种模式。详情见下方的 文本缓存类型。
Use System Font布尔值,是否使用系统字体。

Label 排版

属性功能说明
CLAMP文字尺寸不会根据 Bounding Box 的大小进行缩放,Wrap Text 关闭的情况下,按照正常文字排列,超出 Bounding Box 的部分将不会显示。Wrap Text 开启的情况下,会试图将本行超出范围的文字换行到下一行。如果纵向空间也不够时,也会隐藏无法完整显示的文字。
SHRINK文字尺寸会根据 Bounding Box 大小进行自动缩放(不会自动放大,最大显示 Font Size 规定的尺寸),Wrap Text 开启时,当宽度不足时会优先将文字换到下一行,如果换行后还无法完整显示,则会将文字进行自动适配 Bounding Box 的大小。如果 Wrap Text 关闭时,则直接按照当前文字进行排版,如果超出边界则会进行自动缩放。注意:这个模式在文本刷新的时候可能会占用较多 CPU 资源。
RESIZE_HEIGHT文本的 Bounding Box 会根据文字排版进行适配,这个状态下用户无法手动修改文本的高度,文本的高度由内部算法自动计算出来。

文本缓存类型(Cache Mode)

属性功能说明
NONE默认值,Label 中的整段文本将生成一张位图。
BITMAP选择后,Label 中的整段文本仍将生成一张位图,但是会尽量参与 动态合图。只要满足动态合图的要求,就会和动态合图中的其它 Sprite 或者 Label 合并 Draw Call。由于动态合图会占用更多内存,该模式只能用于文本不常更新的 Label。补充:和 NONE 模式一样,BITMAP 模式会强制给每个 Label 组件生成一张位图,不论文本内容是否等同。如果场景中有大量相同文本的 Label,建议使用 CHAR 模式以复用内存空间。
CHAR原理类似 BMFont,Label 将以“字”为单位将文本缓存到全局共享的位图中,相同字体样式和字号的每个字符将在全局共享一份缓存。能支持文本的频繁修改,对性能和内存最友好。不过目前该模式还存在如下限制,我们将在后续的版本中进行优化: 1、该模式只能用于字体样式和字号固定(通过记录字体的 fontSize、fontFamily、color、outline 为关键信息,以此进行字符的重复使用,其他有使用特殊自定义文本格式的需要注意),并且不会频繁出现巨量未使用过的字符的 Label。这是为了节约缓存,因为全局共享的位图尺寸为 2048*2048,只有场景切换时才会清除,一旦位图被占满后新出现的字符将无法渲染。 2、Overflow 不支持 SHRINK。 3、不能参与动态合图(同样启用 CHAR 模式的多个 Label 在渲染顺序不被打断的情况下仍然能合并 Draw Call)

注意

  • Cache Mode 对所有平台都有优化效果。

Button-按钮组件

Button 组件可以响应用户的点击操作,当用户点击 Button 时,Button 自身会有状态变化。另外,Button 还可以让用户在完成点击操作后响应一个自定义的行为。

点击 属性检查器 下面的 添加组件 按钮,然后从 UI 组件 中选择 Button,即可添加 Button 组件到节点上。

Button 属性

属性功能说明
TargetNode 类型,当 Button 发生 Transition 的时候,会相应地修改 Target 节点的 SpriteFrame,颜色或者 Scale。
interactable布尔类型,设为 false 时,则 Button 组件进入禁用状态。
Transition枚举类型,包括 NONE、COLOR、SPRITE 和 SCALE。每种类型对应不同的 Transition 设置。详情见下方的 Button Transition 部分。
Click Event列表类型,默认为空,用户添加的每一个事件由节点引用、组件名称和一个响应函数组成。详情见下方的 Button 事件 部分。

注意:当 Transition 为 SPRITE 且 disabledSprite 属性有关联一个 spriteFrame 的时候,此时将忽略 Enable Auto Gray Effect 属性

Button Transition

Button 的 Transition 用来指定当用户点击 Button 时的状态表现。目前主要有 NONE、COLOR、SPRITE 和 SCALE 四种类型。

  • Color Transition

属性功能说明
NormalButton 在 Normal 状态下的颜色。
PressedButton 在 Pressed 状态下的颜色。
HoverButton 在 Hover 状态下的颜色。
DisabledButton 在 Disabled 状态下的颜色。
DurationButton 状态切换需要的时间间隔。
  • Sprite Transition

属性功能说明
NormalButton 在 Normal 状态下的 SpriteFrame。
PressedButton 在 Pressed 状态下的 SpriteFrame。
HoverButton 在 Hover 状态下的 SpriteFrame。
DisabledButton 在 Disabled 状态下的 SpriteFrame。
  • Scale Transition

属性功能
DurationButton 状态切换需要的时间间隔。
ZoomScale当用户点击按钮后,按钮会缩放到一个值,这个值等于 Button 原始 scale * zoomScale, zoomScale 可以为负数

Button 点击事件

Button 可以额外添加 Click 事件,用于响应玩家的点击操作。有以下两种方法。

通过属性检查器添加回调

序号属性功能说明
1Target带有脚本组件的节点。
2Component脚本组件名称。
3Handler指定一个回调函数,当用户点击 Button 时会触发此函数。
4CustomEventData用户指定任意的字符串作为事件回调的最后一个参数传入。
js
cc.Class({
    extends: cc.Component,

    properties: {

    },
    
    start () {

    },

    btnClick(event,customEventData) {
        let node = event.target;
        cc.log("通过属性检查器设置点击事件:",customEventData);
    }

});

通过脚本添加回调

通过脚本添加回调:

  1. 通过 button.node.on('click', ...) 的方式来添加,这是一种非常简便的方式,但是该方式有一定的局限性,在事件回调里面无法 获得当前点击按钮的屏幕坐标点。

    js
     // 假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理:
    
     cc.Class({
         extends: cc.Component,
    
         properties: {
             button: cc.Button
         },
    
         onLoad: function () {
             this.button.node.on('click', this.callback, this);
         },
    
         callback: function (button) {
             // do whatever you want with button
             // 另外,注意这种方式注册的事件,也无法传递 customEventData
         }
     });

五、动画系统

Cocos Creator 的动画系统,除了标准的位移、旋转、缩放动画和序列帧动画以外,这套动画系统还支持任意组件属性和用户自定义属性的驱动,再加上可任意编辑的时间曲线和创新的移动轨迹编辑功能,能够让内容生产人员不写一行代码就制作出细腻的各种动态效果。

注意:Cocos Creator 自带的动画编辑器适用于制作一些不太复杂的、需要与逻辑进行联动的动画,例如 UI 动画。如果要制作复杂的特效、角色动画、嵌套动画,可以考虑改用 Spine 或者 DragonBones 进行制作。

关于 Animation

在早期的游戏开发中,游戏工程师和设计师们使用简单的技巧开发动画,早期动画的动感由连续快速显示一连串静止的图片产生,这些静止的图片被称为帧,这种动画被称为帧动画。随着设备硬件技术的改进和开发工具的不断完善,更多的高级动画技巧和骨骼动画得到了使用,CocosCreator包含简单的动画编辑系统,可以通过编辑器制作简单的动画。

Animation 组件

之前我们了解了 Cocos Creator 是组件式的结构。那么 Animation 也不例外,它也是节点上的一个组件。

Clip 动画剪辑

动画剪辑就是一份动画的声明数据,是一种资源类型,我们将它挂载到 Animation 组件上,就能够将这份动画数据应用到节点上。

节点数据的索引方式

数据中索引节点的方式是以挂载 Animation 组件的节点为根节点的相对路径。 所以在同个父节点下的同名节点,只能够产生一份动画数据,并且只能应用到第一个同名节点上。

clip 文件的参数

sample: 定义当前动画数据每秒的帧率,默认为 60,这个参数会影响时间轴上每两个整数秒刻度之间的帧数量(也就是两秒之内有多少格)。

speed: 当前动画的播放速度,默认为 1

duration: 当动画播放速度为 1 的时候,动画的持续时间

real time: 动画从开始播放到结束,真正持续的时间

wrap mode: 循环模式

动画编辑模式

动画在普通模式下是不允许编辑的,只有在动画编辑模式下,才能够编辑动画文件。但是在编辑模式下,无法对节点进行 增加 / 删除 / 改名 操作。

打开编辑模式:

  • 选中一个包含 Animation 组件,并且包含有一个以上 clip 文件的节点。然后在动画编辑器左上角点击唯一的按钮。

退出编辑模式:

  • 点击动画编辑器上点击左上角的编辑按钮,或者在场景编辑器左上角的关闭按钮

熟悉动画编辑器

动画编辑器一共可以划分为 6 个主要部分。

  1. 常用按钮区域,这里负责显示一些常用功能按钮,从左到右依次为:开关编辑状态、返回第一帧、上一帧、播放/暂停、下一帧、新建动画剪辑、插入动画事件
  2. 时间轴与事件,这里主要是显示时间轴,添加的自定义事件也会在这里显示。
  3. 层级管理(节点树),当前动画剪辑可以影响到的节点数据。
  4. 节点内关键帧的预览区域,这里主要是显示各个节点上的所有帧的预览时间轴。
  5. 属性列表,显示当前选中的节点在选中的动画剪辑中已经包含了的属性列表。
  6. 关键帧,每个属性相对应的帧都会显示在这里

时间轴的刻度单位表示方式

时间轴上刻度的表示法是 01-05。该数值由两部分组成,冒号前面的是表示当前秒数,冒号后面的表示在当前这一秒里的第几帧。

01-05 表示该刻度在时间轴上位于从动画开始经过了 1 秒又 5 帧 的时间。

因为帧率(sample)可以随时调整,因此同一个刻度表示的时间点也会随着帧率变化而有所不同。

  • 当帧率为 30 时,01-05 表示动画开始后 1 + 5/30 = 1.1667 秒。
  • 当帧率为 10 时,01-05 表示动画开始后 1 + 5/10 = 1.5 秒。

虽然当前刻度表示的时间点会随着帧率变化,但一旦在一个位置添加了关键帧,该关键帧所在的总帧数是不会改变的, 假如我们在帧率 30 时向 01-05 刻度上添加了关键帧,该关键帧位于动画开始后总第 35 帧。之后把帧率修改为 10,该关键帧仍然处在动画开始后第 35 帧,而此时关键帧所在位置的刻度读数为 03-05。换算成时间以后正好是之前的 3 倍。

基本操作

更改时间轴缩放比例

在操作中如果觉得动画编辑器显示的范围太小,需要按比例缩小,让更多的关键帧显示到编辑器内怎么办?

  • 在图中2、4、6区域内滚动鼠标滚轮,可以放大,或者缩小时间轴的显示比例。

移动显示区域

如果想看动画编辑器右侧超出编辑器被隐藏的关键帧或是左侧被隐藏的关键帧,这时候就需要移动显示区域:

  • 在图中2、4、6区域内按下鼠标中键/右键拖拽。

更改当前选中的时间轴节点

  • 在时间轴(图 2 区域)区域内点击任意位置或者拖拽,都可以更改当前的时间节点。
  • 在图 4 区域内拖拽标示的红线即可。

播放/暂停动画

  • 在图 1 区域内点击播放按钮,按钮会自动变更为暂停,再次点击则是暂停。
  • 播放状态下,保存场景等操作会终止播放。

修改 clip 属性

  • 在插件底部,修改对应的属性,在输入框失去焦点的时候就会更新到实际的 clip 数据中。

快捷键

  • left:向前移动一帧,如果已经在第 0 帧,则忽略当前操作
  • right:向后移动一帧
  • delete:删除当前所选中的关键帧
  • ctrl / cmd + left:跳转到第 0 帧
  • ctrl / cmd + right:跳转到有效的最后一帧

创建与挂载动画剪辑

现在我们的节点上已经有了 Animation 组件了,但是还没有相应的动画剪辑数据,动画剪辑也有两种创建方式:

  • 在资源管理器中点击左上方的+,或者右键空白区域,选择 Animation Clip,这时候会在管理器中创建一个名为 'New AnimationClip' 的剪辑文件。 单单创建还是不够的,我们再次在层级管理器中点选刚刚的节点,在属性检查器中找到 Animation,这时候的 Clips 显示的是 0,我们将它改成 1。 然后将刚刚在资源管理器中创建的 'New AnimationClip' ,拖入刚刚出现的 animation-clip选择框 内。
  • 如果 Animation 组件中还没有添加动画剪辑文件,则可以在动画编辑器中直接点击 新建 AnimationClip 按钮,根据弹出的窗口创建一个新的动画剪辑文件。 需要注意的是,如果选择覆盖已有的剪辑文件,被覆盖的文件内容会被清空。

至此我们已经完成了动画制作之前的准备工作,下一步就是要创建动画曲线了。

剪辑内的数据

一个动画剪辑内可能包含了多个节点,每个节点上挂在多个动画属性,每个属性内的数据才是实际的关键帧。

节点数据

动画剪辑通过节点的名字定义数据的位置,本身忽略了根节点,其余的子节点通过与根节点的相对路径索引找到对应的数据。 有时候我们会在制作完成动画后,将节点重命名,这样会造成动画数据所以出现问题,如下图:

这时候我们要手动指定数据对应的节点,可以将鼠标移入节点,点击节点右侧出现的更多按钮,并选择 “移动数据”。 要注意的是,根节点名字是被忽略的,所以根节点名字是固定的,并不能修改,并且一直显示在页面左侧。 如上图,New Node/test 节点没有数据,我想将 /New Node/efx_flare 上的数据移到这里:

  1. 鼠标移到丢失的节点 - /New Node/efx_flare 上
  2. 点击右侧出现的按钮
  3. 选择移动数据
  4. 将路径改为 /New Node/test,并回车

制作一个完整的动画

让牛儿跑起来

用动画编辑器的方式做一个牛儿奔跑的动画

  1. 需要在动画编辑器里面添加属性 cc.Sprite.spriteFrame
  2. 将三张图片拖拽到关键帧区域
  3. 调整关键帧在时间轴中的位置,让动作更顺畅

编写脚本使用动作系统

需要使用动作系统让牛儿移动起来

js
cc.Class({
    extends: cc.Component,

    properties: {
    },

    start () {
        // 创建一个移动动作
        let action = cc.moveBy(5, -1200,0);
        // 执行动作
        this.node.runAction(action);

    },

});

让牛儿一直循环的移动

js
  start () {
        // 创建一个移动动作
        let action = cc.moveBy(5, -1200,0);
        // 设置回调,回到起始位置
        let finish = cc.callFunc(function () {
            this.node.x = 600;
        },this);
        // 顺序动作
        let sequence = cc.sequence(action,finish);
        // 循环动作
        let actionInterval = cc.repeatForever(sequence);
        // 执行循环动作
        this.node.runAction(actionInterval);

    },

六、案例开发

套牛是一款很有趣的敏捷小游戏 ,玩法简单有趣,非常适合微信小游戏平台。

主要操作就是触发套绳按钮,当牛儿不停的奔跑过来的时候,在合适的时机按下套牛按钮就可以捕捉到牛儿。

开发思路剖析

主要有几个技术点需要考虑:

  1. 主场景的搭建需要用到我们的UI系统,比如Sprite,Button。
  2. 如何让牛儿奔跑起来,可以用动作系统也可以用动画编辑器,那我们本案例主要采用动作系统配合动画编辑来使用,让大家能同时练习下动作系统&动画编辑器。
  3. 套绳动作,触发套绳动作后如何判断绳子正好触碰到牛儿的头部。
  4. 定时器开发,因为游戏总需要个结果,不能让玩家一直无止境玩下去,游戏需要个结束点,我们就可以设置限时操作,比如1分钟内结束游戏,然后计算玩家在一分钟内套取得牛儿数量作为游戏结果。

主界面UI搭建

主要元素

  • 背景图片我们用Sprite精灵组件渲染
  • 绳子(Sprite)
  • 捕捉按钮Button

提供的资源

包括背景图片和按钮两种状态,以及套绳等图片。

开始搭建场景

  1. 首先要在资源管理器中新建3个文件夹用来保存对应的资源 scenes,scripts,res

    分别用来保存场景、脚本资源和图片等资源

  2. 调整尺寸为竖屏640*960,ctrl+s 保存主场景

  3. 按顺序搭建,背景 --> 按钮 --> 牛儿 ....

奔跑的牛

动画实现步骤分析

  1. 如何让牛儿跑动起来?

    让牛儿跑起来只需要用帧动画就可以完成,但是还需要每完成一个回合就切换一种牛的类型。

  2. 牛除了跑起来还要让他移动

    移动可以使用动画编辑器来实现一个位移动画。

代码实现

定义一个数组实现奔跑动画
  1. 每一个牛有三张皮肤,可以通过循环轮换皮肤来实现牛儿奔跑的效果,首先需要用cc.class帮我们定义一个数组
js
// 用cc.class 生成一个对象,包含数组皮肤
const cow_skin = cc.Class({
    name:"cow_skin",
    properties:{
        cows:{
            default:[],
            type:[cc.SpriteFrame]
        }
    }
})
  1. 牛的类型数组
js
properties: {
    cow_sets: {
    default: [],
    type: [cow_skin]
    }
},
  1. update回调函数实现帧动画,更换皮肤
js
update (dt) {
    this.intervalTime += dt;
    // 每隔0.2秒更换皮肤
    let index  = Math.floor(this.intervalTime / 0.2);
    // 获取精灵组件
    let sprite = this.node.getComponent(cc.Sprite);
    // let spriteFrame = sprite.spriteFrame;
    // 获取牛的类型
    let cowSet = this.cow_sets[this.type];
    // 如果最后一个 重置index
    if (index > cowSet.cows.length -1) {
        index = 0;
        // 重置计时参数
        this.intervalTime = 0;
    }
    // 设置皮肤
    sprite.spriteFrame = cowSet.cows[index]
}
属性检查器按顺序拖入图片

将整个cow节点作为预制体

动画效果演示

套绳动作结果判定

套绳逻辑代码

js
 /**
     * 捕获按钮点击点击
     * @param event
     * @param customEventData
     */
    clickCapture: function (event, customEventData) {
        // 激活当前节点
        this.rope_node.active = true
        // siblingIndex是设置节点在父节点中的排序
        this.rope_node.setSiblingIndex(100);
        // 设置当前位置
        this.rope_node.y = -480;
        // 开始动作
        const up = cc.moveTo(0.5,this.rope_node.x,0);
        // 捕捉结果判定
        let result = cc.callFunc(function () {

            const cow_currentX = this.cow_ins.x;

            if (cow_currentX > -50 & cow_currentX < 50) {
                console.log("捕捉成功")
                this.node.removeChild(this.cow_ins)
                // 更换绳子
                let ropeType = this.cow_ins.getComponent("cow").type+1;
                this.rope_node.getComponent(cc.Sprite).spriteFrame = this.row_imgs[ropeType];
                // 生成新的牛
                this.cow_ins = cc.instantiate(this.cow_prefab);
                this.node.addChild(this.cow_ins)
            }
        },this);
        const down = cc.moveTo(0.5,this.rope_node.x,-600);
        let finish = cc.callFunc(function () {
            // 换绳子
            this.rope_node.getComponent(cc.Sprite).spriteFrame = this.row_imgs[0]
        },this);

        let capture_action = cc.sequence(up,result,down,finish);
        this.rope_node.runAction(capture_action)
    }

计分器开发

第一步:加上分数显示
  1. 创建一个label来显示得分

第二步:用位图字体让显示更美观

第三步:编写逻辑代码
js
onLoad () {
        // 定义初始化得分为0
        this.scoreNum = 0;
},
    
// 捕捉成功,分数+1
this.scoreNum++;
let scoreLabel = cc.find("Canvas/bg_sprite/score").getComponent(cc.Label);
scoreLabel.string = "Score: " + this.scoreNum;

游戏定时器开发

游戏不能无止境的进行下去,这样会让玩家疲劳且没有成就感,所以我们要加上一个成就系统。

在这个小游戏中我们采用计时制,让玩家在指定的时间内完成游戏。那我们下面首先就需要开发一个计时器功能,比如指定玩家在60s时间内完成游戏,并计算玩家在这段时间内的得分。

需要开发一个定时器并显示到界面

并添加位图字体样式和字体颜色#EB78E6

倒计时功能

js
start () {
    // 获得计时器组件
    let countDownLabel = cc.find("Canvas/bg_sprite/count_down").getComponent(cc.Label);
    let time = 60;
    // 倒计时
    this.schedule(function () {
        time--;
        countDownLabel.string = "Time: " + time + " s";
    },1);
},

最终成就系统

倒计时为0时游戏结束,并根据玩家的最终得分显示成就;比如小于等于3分就是套牛青铜,大于三分小于6分就是套牛高手,大于6分以上就是套牛王者。

制作一个显示用的弹窗

包含一个关闭按钮,标题和称号。

游戏结束,展示结果弹窗
  1. 代码逻辑
js
start () {

        this.cow_ins = cc.instantiate(this.cow_prefab);
        this.node.addChild(this.cow_ins);

        // 获得计时器组件
        let countDownLabel = cc.find("Canvas/bg_sprite/count_down").getComponent(cc.Label);
        let time = 60;
        // 倒计时
        this.schedule(function () {
            time--;
            countDownLabel.string = "Time: " + time + " s";
            if (time == 0) {
                // 获取结果弹窗节点
                let resultNode = cc.find("Canvas/result");
                // 通过getChildByName获得子节点, title 和 content
                let titleNode = resultNode.getChildByName("title");
                let contentNode = resultNode.getChildByName("content");
                // 最终得分显示
                titleNode.getComponent(cc.Label).string ="最终得分 "+this.scoreNum ;
                // 最终成就
                let contentLabel = contentNode.getComponent(cc.Label);
                switch (true) {
                    case this.scoreNum <= 3:
                        contentLabel.string = "套牛青铜";
                        break;
                    case this.scoreNum < 6:
                        contentLabel.string = "套牛高手";
                        break;
                    case this.scoreNum >=6:
                        contentLabel.string = "套牛王者";
                        break;
                }
                resultNode.active = true;
                // 暂停游戏
                cc.director.pause();
            }

        },1);
    },
  1. 最终成功展示

关闭按钮
js
closeBtn() {
    	// 游戏继续
        cc.director.resume();
    	// 重新加载整个场景
        cc.director.loadScene("game");
}
js
closeBtn() {
    	// 游戏继续
        cc.director.resume();
    	// 重新加载整个场景
        cc.director.loadScene("game");
}

七、微信小程序发布

微信小游戏账号注册

注册小程序

注册流程

注册微信公众平台账号

选择小游戏类型

微信公众平台官网:https://mp.weixin.qq.com/

点击【立即注册】按钮

选择小程序类型

按步骤完善信息即可

激活注册邮箱

注册成功后,会收到腾讯发给注册邮箱的一封激活确认邮件

需要准备一个邮箱、一个手机号、实名绑定微信

设置服务类目

登录公众平台->设置->服务类目->游戏->休闲游戏

安装微信开发者工具

安装并启动开发者工具

注册小游戏帐号后,应下载 开发者工具 并安装到计算机中。

打开已安装的开发者工具客户端,使用刚刚注册小游戏帐号登记的微信帐号“扫一扫”扫码即可进入小游戏开发环境。

选择左侧“小程序项目”中的“小游戏 Tab”,然后点击右侧“+”号,开始创建小游戏项目。

填写合适的项目名称,选择合适的本机空目录后,登录 刚刚注册的小程序管理平台,如下图找到小程序的 AppID,填入表单的“AppId”文本框中。

发布到微信平台

项目-构建发布-微信小游戏

如果出现不能打开微信开发者工具,需要到微信开发者工具里面去设置。

微信开发者工具常用介绍

常用按钮包含

编译

点击工具栏中的编译按钮或者使用快捷键 Ctrl(⌘) + B,可以编译当前代码,并自动刷新模拟器。

同时为了帮助开发者调试从不同场景值进入具体的页面,开发者可以添加或选择已有的自定义编译条件进行编译和代码预览(如图)。

注:编译条件跟项目相关,每个项目可以保存自己相关的编译条件

编译异常信息

在预览或上传的过程中,可能会出现编译异常信息。

忽略上传的文件:在项目文件夹下,某些文件可能不是小程序运行所需的,例如 readme 文档、.gitignore 文件等。为了优化大小,在预览和上传的过程中,这些文件将不会被打包。

体积过大的文件:如果勾选了 ES6 转 ES5 或代码压缩的选项,为了优化编译速度,对于某些体积很大的 JS 文件,工具会跳过对这些文件的处理。

预览

真机调试

要发起一个真机远程调试流程,需要先点击开发者工具的工具栏上 “真机调试” 按钮。

此时,工具会将本地代码进行处理打包并上传,就绪之后,使用手机客户端扫描二维码即可弹出调试窗口,开始远程调试。

切后台

工具栏中前后台切换帮助开发者模拟一些客户端的环境操作。例如当用户从小程序中回到聊天窗口,会触发一个小程序被设置为后台的回调

清缓存

上传

上传完之后需要进入微信公众平台,在开发版本里面会看见自己刚刚提交的版本,点击提交之后会进入微信后台审核,由微信官方人员决定是否允许上线。

微信开放能力

获取用户信息

显示按钮的方式
js
const button = wx.createUserInfoButton({
  type: 'text',
  text: '获取用户信息',
  style: {
    left: 10,
    top: 76,
    width: 200,
    height: 40,
    lineHeight: 40,
    backgroundColor: '#ff0000',
    color: '#ffffff',
    textAlign: 'center',
    fontSize: 16,
    borderRadius: 4
  }
})
button.onTap((res) => {
  // 此处可以获取到用户信息
  console.log(res);
})
隐式按钮的方式

设置完全透明的方式

js
let sysInfo = wx.getSystemInfoSync();
//获取微信界面大小
let width = sysInfo.screenWidth;
let height = sysInfo.screenHeight;

let button = window.wx.createUserInfoButton({
    type: 'text',
    text: '',
    style: {
        left: 0,
        top: 0,
        width: width,
        height: height,
        backgroundColor: '#00000000',//最后两位为透明度
        color: '#ffffff',
        fontSize: 20,
        textAlign: "center",
        lineHeight: height,
    }
});

在主界面显示用户头像和昵称(如何加载网络头像)

js
// 获取微信用户信息
let userInfo = res.userInfo;
// 获取头像组件
let icon = cc.find("Canvas/bg_sprite/icon").getComponent(cc.Sprite);
cc.loader.load({url:userInfo.avatarUrl,type:"png"},function (err,text) {
    icon.spriteFrame = new cc.SpriteFrame(text);
})
已经授权的情况下直接获取用户信息
js
wx.getUserInfo({
    success(res) {
        // 此处可以获取到用户信息
        console.log(res.userInfo);
    },
    fail(err) {
        console.log("接口调用失败!");
    }
})

分享

用户通过点击分享按钮来分享小程序。

主动转发
js
wx.shareAppMessage({
    title: "用户点击按钮转发!",
    imageUrl: "http://img.zhubohome.com.cn/game_share.png",
    success(res) {
        console.log(res)
    },
    fail(res) {
        console.log(res)
    }
});

Banner 广告组件是由客户端原生的图片、文本控件组成的原生组件,层级最高,会覆盖在上屏 Canvas 上。

开发者可以调用 wx.createBannerAd 创建 Banner 广告组件。Banner 广告组件在创建后会自动拉取广告数据并进行渲染,开发者只需要控制 Banner 广告组件的位置和显示/隐藏即可。

javascript
let bannerAd = wx.createBannerAd({
    adUnitId: 'XXX',
    style: {
        left: 27.5,
        top: 80,
        width: 320
    }
});
bannerAd.onError(err => {
    console.log(err)
});

bannerAd.show();

小游戏后台开发

搭建Java后台

本课件后台使用的是常用的ssm框架+mysql数据库。

user 表结构

idBIGINT(20)ID自增
nickNameVARCHAR(20)昵称
avatarUrlVARCHAR(200)头像
openidVARCHAR(100)微信开放id
topScoreINT(11)玩家最高分

openid 介绍

用户唯一标识, 为了识别用户,每个用户针对每个公众号或小程序/小游戏 等应用会产生一个安全的OpenID,公众号或应用可将此ID进行存储,便于用户下次登录时辨识其身份

对应的domain、dao、和service层

java
package com.itheima.domain;

import java.util.List;

public class User {

    private Long id;

    private String nickName;

    private String avatarUrl;

    private String openid;

    private int topScore;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public String getAvatarUrl() {
        return avatarUrl;
    }

    public void setAvatarUrl(String avatarUrl) {
        this.avatarUrl = avatarUrl;
    }

    public String getOpenid() {
        return openid;
    }

    public void setOpenid(String openid) {
        this.openid = openid;
    }

    public int getTopScore() {
        return topScore;
    }

    public void setTopScore(int topScore) {
        this.topScore = topScore;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", nickName='" + nickName + '\'' +
                ", avatarUrl='" + avatarUrl + '\'' +
                ", openid='" + openid + '\'' +
                ", topScore=" + topScore +
                '}';
    }
}
java
package com.itheima.mapper;

import com.itheima.domain.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface UserMapper {

    /**
     * 根据openid 获取用户
     * @param openid
     * @return
     */
    @Select("select * from user where openid = #{openid}")
    User findByOpenid(@Param("openid") String openid);

    @Insert("insert into user values(null,#{nickName},#{avatarUrl},#{openid},#{topScore})")
    void insert(User user);
}
java
package com.itheima.service;

import com.itheima.domain.User;

public interface UserService {
    /**
     * 根据openid查询用户
     * @param openid
     * @return
     */
    User findByOpenid(String openid);

    /**
     * 保存用户
     * @param user
     */
    void insert(User user);
}

微信后台登录

请求地址

text
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

请求参数

属性类型默认值必填说明
appidstring小程序 appId
secretstring小程序 appSecret
js_codestring登录时获取的 code
grant_typestring授权类型,此处只需填写 authorization_code

返回值

Object

返回的 JSON 数据包

属性类型说明
openidstring用户唯一标识
session_keystring会话密钥
unionidstring用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回,详见 UnionID 机制说明
errcodenumber错误码
errmsgstring错误信息
java后台代码
java
package com.itheima.controller;

import com.alibaba.fastjson.JSONObject;
import com.itheima.domain.User;
import com.itheima.help.HttpRequestHelp;
import com.itheima.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpSession;

@RestController
public class UserController {

    private static final String WX_APPID = "wx053c63b43f5458dc";
    private static final String WX_SECRET="bd3736dc6ae4dfe98e34cb623ea66bb6";
    private static final String WX_URL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";

    @Autowired
    private UserService userService;

    /**
     * 登录页面
     * @return
     */
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    public Object login(String code,String nickName,String avatarUrl) {
        String url = String.format(WX_URL,WX_APPID,WX_SECRET,code);
        JSONObject jsonObject = HttpRequestHelp.httpGet(url);
        String openid = jsonObject.getString("openid");
        /** 根据openid 获取登录信息 */
        User user = userService.findByOpenid(openid);
        if (user == null) {
            // 保存新用户
            user = new User();
            user.setAvatarUrl(avatarUrl);
            user.setNickName(nickName);
            user.setOpenid(openid);
            userService.insert(user);
        }
        System.out.println(user);
        return "success";
    }
}
前端代码
javascript
// 微信后台登录
wx.login({
    success (res) {
        if (res.code) {
            //发起网络请求
            wx.request({
                url: 'http://localhost:8080/login',
                method:"POST",
                header: {
                    'content-type': 'application/x-www-form-urlencoded',
                },
                data: {
                    code: res.code,
                    nickName:userInfo.nickName,
                    avatarUrl:userInfo.avatarUrl
                }
            })
        } else {
            console.log('登录失败!' + res.errMsg)
        }
    }
})

保存最高得分

java后台代码
java
@Update("update user set topScore=#{topScore} where openid = #{openid} and #{topScore} > topScore")
void updateScore(User user);
java
@RequestMapping("/updateScore")
public Object updateScore(String code,int score) {
    // 拼接微信api请求地址
    String url = String.format(WX_URL,WX_APPID,WX_SECRET,code);
    JSONObject jsonObject = HttpRequestHelp.httpGet(url);
    String openid = jsonObject.getString("openid");
    // 根据获取的微信openid 去判断,判断是否已经存在
    User user = userService.findByOpenid(openid);
    if (user == null) {
        return "error";
    }
    user.setTopScore(score);
    userService.updateScore(user);
    return "success";
}
前端代码
js
let score = this.scoreNum;
wx.login({
    success(res) {
        if (res.code) {
            // 发起网络请求给游戏后台
            wx.request({
                url:"http://localhost:8080/updateScore",
                method: "POST",
                header:{
                    'content-type':'application/x-www-form-urlencoded'
                },
                data:{
                    code:res.code,
                    score:score
                }
            })
        } else {
            console.log("保存分数失败"+ res.errMsg);
        }
    }
})

使用原始的XMLHttpRequest替代wx.request

为了保证cocos creator的跨平台特性 ,应当尽量的减少使用第三方平台的api。多使用底层api来代替,比如wx.request()。

js
request(option) {
    if (String(option) !== '[object Object]') return undefined
    option.method = option.method ? option.method.toUpperCase() : 'GET'
    option.data = option.data || {}
    var formData = []
    for (var key in option.data) {
        formData.push(''.concat(key, '=', option.data[key]))
    }
    option.data = formData.join('&')

    if (option.method === 'GET') {
        option.url += location.search.length === 0 ? ''.concat('?', option.data) : ''.concat('&', option.data)
    }

    var xhr = new XMLHttpRequest()
    xhr.responseType = option.responseType || 'json'
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                if (option.success && typeof option.success === 'function') {
                    option.success(xhr.response)
                }
            } else {
                if (option.error && typeof option.error === 'function') {
                    option.error()
                }
            }
        }
    }
    xhr.open(option.method, option.url, true)
    if (option.method === 'POST') {
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
    }
    xhr.send(option.method === 'POST' ? option.data : null)
}

微信小游戏发布注意事项

小游戏审核过程中的注意事项,微信小游戏提交审核要注意哪些问题

1)服务类目需与内容保持一致

很多小游戏核不通过的主要原因是因为游戏类目与小游戏的内容不一致。

2)小游戏名称需与内容相关

小游戏的名称具有唯一性,但取名时也必须要与小游戏的简介信息和内容相关,否则也会审核不通过。

3)小游戏发布需要提供自审报告和软著

自审报告

《XXX》游戏软件著作权人为XXX,该游戏于2020年3月25日获得软件著作权登记证书,登记号为XXXXX。

已在游戏开始前全文登载《游戏健康忠告》,如下图所示:

(上面有健康游戏公告的内容:

抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。)

我承诺上述内容真实、全面、有效。并保证此作品上网出版运营后不添加违反《出版管理条例》、《互联网信息服务管理办法》、《网络出版服务管理规定》、《移动游戏内容规范》等规定的内容。

​ XX年XX月XX 日

4)不可诱导分享

小程序的页面内容中,存在诱导类行为,包括但不限于诱导分享、诱导添加、诱导关注公众号、诱导下载等,要求用户分享、添加、关注或下载后才可操作的程序,含有明示或暗示用户分享的文案、图片、按钮、浮层、弹窗等的小程序,通过利益诱惑诱导用户分享、传播的小程序,用夸张言语来胁迫、引诱用户分享的小程序,强制或诱导用户添加小程序的,都将会被拒绝。