Cocos小游戏开发-游戏开发中的重要概念

Cocos小游戏开发-游戏开发中的重要概念

Cocos 和游戏开发中的重要概念:

资源和工作流

管理资源配置文件(.meta)

所有 assets 路径下的资源都会在导入时生成一份 资源配置文件(.meta) 这份配置文件提供了该资源在项目中的唯一标识(uuid)以及其他的一些配置信息(如图集中的小图引用,贴图资源的裁剪数据等),非常重要。

在编辑器中管理资源时,meta 文件是不可见的,对资源的任意删除、改名、移动操作,都会由编辑器自动同步相应的 meta 文件,确保 uuid 的引用不会丢失和错乱。

注意在编辑器外部的文件系统中(Explorer,Finder)对资源文件进行删除、改名、移动时必须同步处理相应的 meta 文件。资源文件和其对应的 meta 文件应该保持在同一个目录下,而且文件名相同。

资源导入导出工作流程

Cocos Creator 是专注于内容创作的游戏开发工具,在游戏开发过程中,对于每个项目该项目专用的程序架构和功能以外,我们还会生产大量的场景、角色、动画和 UI 控件等相对独立的元素。对于一个开发团队来说,很多情况下这些内容元素都是可以在一定程度上重复利用的。

在以场景和 Prefab 为内容组织核心的模式下,Cocos Creator 内置了场景(.fire) 和预制 (.prefab) 资源的导出和导入工具。

资源导出

在主菜单选择 文件 -> 资源导出,即可打开资源导出工具面板,接下来可以用以下两种方式选择需要导出的资源:

  • 将场景或预制文件从 资源管理器 中拖拽到导出资源面板的资源栏中
  • 点击资源栏右边的 选择 按钮,打开文件选择对话框,并在项目中选取你要导出的资源

可以选择的资源包括 .fire 场景文件和 .prefab 预制文件。

确认依赖

导出工具会自动检查所选资源的依赖列表并列出在面板里,用户可以手动检查每一项依赖是否必要,并剔除部分依赖的资源。被剔除的资源将不会被导出。

确认完毕后点击 导出 按钮,会弹出文件存储对话框,用户需要指定一个文件夹位置和文件名,点击 存储,就会生成 文件名.zip 的压缩包文件,包含导出的全部资源。

资源导入

在新项目的主菜单里选择 文件 -> 导入资源,即可打开资源导入面板。

点击 Zip 文件路径 输入框右边的 选择 按钮,在文件浏览对话框中选择刚才导出的导出资源压缩包。

导入过程中也会让用户再次确认导入资源依赖,这时候可以通过取消某些资源的勾选来去掉不需要导入的资源。

设置导入位置

相比导出过程,导入过程中增加了 导入目标路径 的设置,用户可以点击旁边的 选择 按钮,选择项目的 assets 路径下的某个文件夹作为导入资源的放置位置。由于导出资源时所有资源的路径都是以相对于 assets 路径来保存的,导入时如果不希望导入的资源放入 assets 根目录下,就可以再指定一层中间目录来隔离不同来源的导入资源。

设置完成后点击 导入 按钮,会弹出确认对话框,确认后就会把列出的资源导入到目标路径下。

脚本和资源冲突

由于 Creator 项目中的脚本不能同名,当导入的资源包含和当前项目里脚本同名的脚本时,将不会导入同名的脚本。如果出现导入资源的 UUID 和项目中现有资源 UUID 冲突的情况,会自动为导入资源生成新的 UUID,并更新在其他资源里的引用。

场景资源

节点和组件

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

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

理解 组件-实体-系统 (ECS \CES)游戏编程模型

一般来说,我们实现游戏实体都是采用面向对象的方法进行编程。每一个实体都是一个对象,并且需要一个基于类的实例化系统,允许实体通过多态来扩展。但是,这样的方法,往往导致系统中出现大量的类,造成类爆炸的情况出现。随着新的实体出现,我们发现很难在类继承图中添加新的实体,特别是当这个实体需要很多不同类型的功能的时候。你可以看下下面的一个简单的类图继承。一个静态的敌人,并不能够很好的继承出来。

为了解决这样的问题,游戏开发人员想出了通过组合而不是继承的方法来进行实体的构建。一个实体,就是一群组件的聚合,通过这样的方式,它具有以下面向对象方法所不具有的好处:

1.容易添加新的复杂的实体类型
2.容易定义新的实体数据
3.更加的高效率

下面是如何实现实体的一种方式。注意,这里的组件都是纯粹的数据,没有任何的方法,在下面会详细解释为什么这么做。

组件

一个组件可以使用C中的结构体来进行设计。它没有方法,只是用来存储一些数据,并不在它之上进行动作。一个经典的实现方式是,每一个不同的组件都继承至一个抽象的Componet类,通过这样的方法我们能够在运行时动态的添加组件,识别组件。每一个组件都描述了实体的某个属性特征。当他们单独存在的时候,实际上是没有任何意义的,但是当多个组件通过系统的方式组织在一起,就能够发挥强大的力量。我们可以使用空的组件来对实体进行标记,从而能够在运行时动态的识别它。

实体

一个实体指的是存在于你的游戏世界中的物体。实体在代码上就是一个组件的列表。由于实体的结构实在是太简单了,所以很多实现都没有专门的设计一个实体的数据结构。相反的,一个实体就是一个ID,所有组成这个实体的组件将会被这个ID给标记,从而明确的知道哪些组件是属于哪个实体的。如果你想的话,你可以在运行时,动态的将组件从实体中移除或者增加一个或多个你感兴趣的组件。比如说,如果玩家发出了一个冰系魔法,将敌人冻住,你只要简单的将它的速度组件移除,那么敌人就静止住了。

系统

注意,我在上面没有提到任何和游戏逻辑相关的话题。游戏逻辑是系统需要进行的工作。一个系统就是对所有相关联的组件记性操作,比如说,同一个实体的组件。举个例子,人物的移动系统可能会对位置(Position),速度(Velocity),碰撞(Collider),和输入(Input)进行操作。每一个系统,都会在每一帧中按照逻辑上的顺序进行更新。如果要让一个角色跳起来,我们只要检测下Input中的keyJump按键是否被按下,如果是,那么系统就会查看下载Collider中是否有一个接触了地面,如果是,就将这个实体的Velocity的y速度设置一下,让这个物体跳起来。

由于系统只会对相关联的组件进行操作,所以组件就定义了一个实体所应该具有的行为。比如说,如果一个实体有一个Position组件,但是没有Velocity组件,那么我们就知道,这个物体是静止不动的,系统就不会对这个实体的Position组件进行操作了。当我们对这个实体增加了一个Velocity组件的时候,系统就会使用Velocity组件来对物体进行移动。这样的行为可以使用被标记的组件来进行,被标记的组件能够重复的使用在不同的上下文中。对一个实体,增加一个空的Player组件,将会为这个实体打上了Player的标签,那么PlayerControl系统,就会寻找带有这个标签的所有组件,然后使用Input中的数据,进行操作。

结论

使用了CES系统之后,我们就可以避免使用大量的类了。实体就是你游戏中存在的物体,它隐式的使用一系列的组件进行定义,这些组件都是纯粹的数据,只有系统才能够操作他们。

我希望我已经成功的向您解释了什么是CES系统,并且你有欲望在自己接下来的项目中试试这个系统的效果。如果你有任何关于本片文章的疑问,很感谢你在后面留下你的问题。

创建节点(Node)

要最快速的获得一个具有特定功能的节点,可以通过 层级管理器 左上角的 创建节点 按钮。我们以创建一个最简单的 Sprite(精灵)节点为例,点击 创建节点 按钮后选择 创建渲染节点 -> Sprite(精灵):

之后我们就可以在 场景编辑器 和 层级管理器 中看到新添加的 Sprite 节点了。新节点命名为 New Sprite,表示这是一个主要由 Sprite 组件负责提供功能的节点。您也可以尝试再次点击 创建节点 按钮,选择其他的节点类型,可以看到它们的命名和表现会有所不同。

组件(Component)

Sprite 组件

选中我们刚才创建的 New Sprite 节点,可以看到 属性检查器 中的显示:

接下来以 Sprite 标题开始的部分就是 Sprite 组件的属性,在 2D 游戏中,Sprite 组件负责游戏中绝大部分图像的渲染。Sprite 组件最主要的属性就是 Sprite Frame,我们可以在这个属性指定 Sprite 在游戏中渲染的图像文件。让我们试试从 资源管理器 中拖拽任意一张图片资源到 属性检查器 的 Sprite Frame 属性中:

可以看到刚才的默认 Sprite 图片变成了我们指定的图片,这就是 Sprite 组件的作用:渲染图片。

我们前面提到了组件式的结构是以组合方式来实现功能的扩展的,下图中就展示了节点(属性)和 Sprite 组件的组合。

添加其他组件

在一个节点上可以添加多个组件,来为节点添加更多功能。在上面的例子中,我们可以继续选中 New Sprite 这个节点,点击 属性检查器 面板下面的 添加组件 按钮,选择 UI 组件 -> Button 来添加一个 Button(按钮)组件。

坐标系和变换

Cocos Creator 坐标系

笛卡尔坐标系

笛卡尔坐标系中定义右手系原点在左下角,x 向右,y 向上,z 向外,我们使用的坐标系就是笛卡尔右手系。

屏幕坐标系和 cocos2d-x 坐标系

在 iOS、Android 等平台用原生 SDK 开发应用时使用的是标准屏幕坐标系,原点为屏幕左上角,x 向右,y 向下。

Cocos2d-x 坐标系和 OpenGL 坐标系一样,原点为屏幕左下角,x 向右,y 向上。

世界坐标系(World Coordinate)和本地坐标系(Local Coordinate)

世界坐标系也叫做绝对坐标系,在 Cocos Creator 游戏开发中表示场景空间内的统一坐标体系,「世界」就用来表示我们的游戏场景。

本地坐标系也叫相对坐标系,是和节点相关联的坐标系。每个节点都有独立的坐标系,当节点移动或改变方向时,和该节点关联的坐标系将随之移动或改变方向。

Cocos Creator 中的 节点(Node) 之间可以有父子关系的层级结构,我们修改节点的 位置(Position) 属性设定的节点位置是该节点相对于父节点的 本地坐标系 而非世界坐标系。最后在绘制整个场景时 Cocos Creator 会把这些节点的本地坐标映射成世界坐标系坐标。

要确定每个节点坐标系的作用方式,我们还需要了解 锚点 的概念。

锚点(Anchor)

锚点(Anchor) 是节点的另一个重要属性,它决定了节点以自身约束框中的哪一个点作为整个节点的位置。我们选中节点后看到变换工具出现的位置就是节点的锚点位置。

锚点由 anchorX 和 anchorY 两个值表示,它们是通过节点尺寸计算锚点位置的乘数因子,范围都是 0 ~ 1 之间。(0.5, 0.5) 表示锚点位于节点长度乘 0.5 和宽度乘 0.5 的地方,即节点的中心。

锚点属性设为 (0, 0) 时,锚点位于节点本地坐标系的初始原点位置,也就是节点约束框的左下角。

子节点的本地坐标系

锚点位置确定后,所有子节点就会以 锚点所在位置 作为坐标系原点,注意这个行为和 cocos2d-x 引擎中的默认行为不同,是 Cocos Creator 坐标系的特色!

假设场景中有三个节点:NodeA、NodeB、NodeC,节点的结构如下图所示:

当我们的场景中包含不同层级的节点时,我们按照以下的流程确定每个节点在世界坐标系下的位置:

  1. 从场景根级别开始处理每个节点,上图中 NodeA 就是一个根级别节点。首先根据 NodeA 的 位置(Position) 属性和 锚点(Anchor) 属性,在世界坐标系中确定 NodeA 的显示位置和坐标系原点位置(和锚点位置一致)。
  2. 接下来处理 NodeA 的所有直接子节点,也就是上图中的 NodeB 以及和 NodeB 平级的节点。根据 NodeB 的位置和锚点属性,在 NodeA 的本地坐标系中确定 NodeB 在场景空间中的位置和坐标系原点位置。
  3. 之后不管有多少级节点,都继续按照层级高低依次处理,每个节点都使用父节点的坐标系和自身位置锚点属性来确定在场景空间中的位置。

变换属性

管理节点层级和显示顺序

节点本地坐标系

我们把两个主要节点拖拽到 Parent 节点下面作为子节点,这时两个节点的 位置 属性会变成怎样:

由于 Parent 节点的锚点属性是 (0.5, 0.5),也就是以中心点作为本地坐标系原点,所以靠近父物体中心摆放的两个子节点的位置现在变成了 (-100, 0) 和 (100, 0),使用本地坐标系的位置信息能够直观的反应两个子节点的摆放逻辑,也就是「靠近背景中心左右对称摆放」。

使用场景编辑器搭建场景图像

使用 Canvas 作为渲染根节点

Canvas 节点是我们推荐大家使用的 渲染根节点,这个的意思就是希望大家将所有渲染相关的节点都放在 Canvas 下面,这样做有以下好处:

    1. Canvas 能提供多分辨率自适应的缩放功能,以 Canvas 作为渲染根节点能够保证我们制作的场景在更大或更小的屏幕上都保持较好的图像效果,详见多分辨率适配方案相关文档
    1. Canvas 的默认锚点位置是 (0.5, 0.5),加上 Canvas 节点会根据屏幕大小自动居中显示,所以 Canvas 下的节点会以屏幕中心作为坐标系的原点。根据我们的经验,这样的设置会简化场景和 UI 的设置(比如让按钮元素的文字默认出现在按钮节点的正中),也能让控制节点位置的脚本更容易编写。

如果您不希望使用我们默认配置的几种屏幕适配方案,或者不希望以屏幕中心作为坐标系原点,也可以直接删除 Canvas 节点,并使用您偏好的设置策略。

逻辑节点的归属

除了有具体图像渲染任务的节点之外,我们还会有一部分节点只负责挂载脚本,执行逻辑,不包含任何渲染相关内容。通常我们将这些节点放置在场景根层级,和 Canvas 节点并列,如下图所示:

可以看到除了 Canvas 下的背景、菜单、玩家角色等节点之外,我们还将包含有游戏主逻辑组件的 Game 节点放在了和 Canvas 平行的位置上,方便协作的时候其他开发者能够第一时间找到游戏逻辑和进行相关的数据绑定。

提高场景制作效率的技巧

  • 层级管理器 里选中一个节点,然后按 Cmd/Ctrl + F 就可以在 场景编辑器 里聚焦这个节点。
  • 选中一个节点后按 Cmd/Ctrl + D 会在该节点相同位置复制一个同样的节点,当我们需要快速制作多个类似节点时可以用这个命令提高效率。
  • 在 场景编辑器 里要选中多个节点,可以按住 Cmd/Ctrl 键依次点击你想要选中的节点,在 层级管理器 里也是一样的操作方式。
  • 场景编辑器 中将鼠标悬停在一个节点上(即使是空节点),会显示该节点的名称和约束框大小,这时点击就会选中当前显示名称的节点。在复杂的场景中选节点之前先悬停一会,可以大大提高选择成功率。

对齐节点

场景编辑器 左上角有一排按钮可以用来在选中多个节点时对齐这些节点,具体的对齐规则如下:

假设三个 Label 节点都已经选中,从左到右的对齐按钮会依次将这些节点:

  • 按照最靠近上面的边界对齐
  • 按照整体的水平中线对齐
  • 按照最靠近下面的边界对齐
  • 按照最靠近左边的边界对齐
  • 按照整体的垂直中线对齐
  • 按照最靠近右边的边界对齐

要注意对齐操作不管是一开始测定左右边界和中线还是之后将每个节点对齐时的参照,都是节点约束框的中心或某条边界,而不是节点的位置坐标。比如下图中我们将三个宽度不同的 Label 节点向右对齐后,得到下图中三个节点约束框的右边界对齐的情况,而不是让三个节点位置里的 x 坐标变成一致。

场景摄像机配置面板

场景摄像机配置面板可以配置编辑器场景中摄像机的操作以及渲染参数。

  • 2D Editor Camera:配置 2D 编辑模式下的 camera 参数。
  • 3D Editor Camera:配置 3D 编辑模式下的 camera 参数。

配置参数

  • Navigation Speed:编辑器移动或缩放摄像机的速度。
    • 2D 编辑模式下会影响 滚轮缩放 模式。
    • 3D 编辑模式下会影响 滚轮缩放 和 漫游 模式。
  • Near Clip:场景摄像机视椎体的近剪裁面,在视椎体外的物体会被裁减掉。
  • Far Clip:场景摄像机视椎体的远剪裁面,在视椎体外的物体会被裁减掉。

图像资源(Texture)

Texture 属性

属性 功能说明
Type 包括 Raw 和 Sprite 两种模式。Raw 模式表示只会生成贴图资源,Sprite 模式表示还会生成 SpriteFrame 子资源,详情请参考下文 Texture 和 SpriteFrame 资源类型 部分的内容。
Premultiply Alpha 是否开启 Alpha 预乘,勾选之后会将 RGB 通道预先乘以 Alpha 通道。
Wrap Mode 寻址模式,包括 Clamp(钳位)、Repeat(重复) 两种寻址模式。当 Type 设置为 Sprite 时,寻址模式是没有意义的,因为 Sprite 的 UV 不会超出 [0, 1]。
Filter Mode 过滤方式,包括 Point(邻近点采样)、Bilinear(双线性过滤)、Trilinear(三线性过滤) 三种过滤方式。
genMipmaps 是否开启自动生成 mipmap
packable 是否允许贴图参与合图

Premultiply Alpha

Texture 的 Premultiply Alpha 属性勾选与否表示是否开启 Alpha 预乘,两种状态分别表示:

  • Premultiply Alpha(预乘 Alpha):表示 RGB 在存储的时候预先将 Alpha 通道与 RGB 相乘,比如透明度为 50% 的红色,RGB 为(255, 0, 0),预乘之后存储的颜色值为(127,0,0,0.5)。
  • Non-Premultiply Alpha(非预乘 Alpha):表示 RGB 不会预先与 Alpha 通道相乘,那么上面所述的透明度为 50% 的红色,存储的颜色值则为(255,0,0,0.5)。

使用 Alpha 预乘并不仅仅简化颜色插值计算的效率, 而且Non-Premultiply Alpha 的纹理图像不能正确的进行线性插值计算

假设有两个相邻顶点的颜色,一个是顶点颜色为透明度 100% 的红色(255,0,0,1.0),另一个是顶点颜色为透明度 10% 的绿色(0,255,0,0.1),那么当图像缩放时这两个顶点之间的颜色就是对它们进行线性插值的结果。如果是 Non-Premultiply Alpha,那么结果为:

(255, 0, 0, 1.0) * 0.5 + (0, 255, 0, 0.1) * (1 - 0.5) = (127, 127, 0, 0.55)

如果使用了 Premultiply Alpha,绿色存储的颜色值变为(0,25,0,0.1),再与红色进行线性插值的结果为:

(255, 0, 0, 1.0)* 0.5 +(0, 25, 0, 0.1)*(1 - 0.5)=(127, 12, 0, 0.55)

对应的颜色值表现为:
premultiply_alpha2

观察上图之后可以看出,使用 Non-Premultiply Alpha 的颜色值进行插值之后的颜色偏绿,透明度为 10% 的绿色占的权重更多,透明度为100% 的红色占比反而更少,而使用 Premultiply Alpha 得到的插值结果才是正确并且符合预期的。因此,实际项目中可以根据图像的具体使用场景进行合适的选择。

Texture 和 SpriteFrame 资源类型

在 资源管理器 中,图像资源的左边会显示一个和文件夹类似的三角图标,点击就可以展开看到它的子资源(sub asset)。每个图像资源导入后,如果 Type 属性设置为 Sprite,则编辑器会自动在它下面创建同名的 SpriteFrame 资源。

SpriteFrame 是核心渲染组件 Sprite 所使用的资源,设置或替换 Sprite 组件中的 spriteFrame 属性,就可以切换显示的图像。

什么会有 SpriteFrame 这种资源?Texture 是保存在 GPU 缓冲中的一张纹理,是原始的图像资源。而 SpriteFrame 包含两部分内容:记录了 Texture 及其相关属性的 Texture2D 对象和纹理的矩形区域,对于相同的 Texture 可以进行不同的纹理矩形区域设置,然后根据 Sprite 的填充类型,如 SIMPLE、SLICED、TILED 等进行不同的顶点数据填充,从而满足 Texture 填充图像精灵的多样化需求。而 SpriteFrame 记录的纹理矩形区域数据又可以在资源的属性检查器中根据需求自由定义,这样的设置让资源的开发更为高效和便利。除了每个文件会产生一个 SpriteFrame 的图像资源(Texture)之外,我们还有包含多个 SpriteFrame 的图集资源(Atlas)类型。

使用 SpriteFrame

直接将 SpriteFrame 或图像资源从 资源管理器 拖拽到 层级管理器 或 场景编辑器 中,就可以直接用所选的图像在场景中创建 Sprite 节点。

之后可以拖拽其他的 SpriteFrame 或图像资源到该 Sprite 组件的 Sprite Frame 属性栏中,来切换该 Sprite 显示的图像。

在 动画编辑器 中也可以拖拽 SpriteFrame 资源到已创建好的 Sprite Frame 动画轨道上,详见 编辑序列帧动画 文档。

图片黑边问题

当图片资源导入到编辑器中时,默认使用的过滤方式(Filter Mode)为线性插值(Bilinear)。而对于 Sprite 组件,默认的 SrcBlendFactor 为 SRC_ALPHA。在这种条件下,对于有半透明像素的 PNG 图片,在编辑器及预览时半透明边缘通常会有黑边问题。原因是因为低分辨率的图片在显示到更高分辨率的显示设备上时,会进行上采样(upsampling),也就是图像插值(interpolating)。在做像素插值时,半透明边缘与透明像素(0,0,0,0)插值之后会产生低透明度的黑色像素。避免图片的黑边问题通常有如下几种方式:

过滤方式(Filter Mode)使用 Point 模式。(推荐使用, 需要能够接受该模式带来的锯齿问题)
当图片在 PS 等工具中制作时,增加背景图层,图层颜色设置成与半透明边缘相同的颜色,然后设置背景图层的透明度为很低的值,如 1/100。(推荐使用)
在图片导出时,设置为较高的分辨率,避免显示到设备时进行图像插值放大。(不推荐使用)
引擎的自动图集提供了扩边选项,勾选该选项时,编辑器会自动对半透明图片边缘进行扩边处理,以此来避免黑边问题。需要注意的是,自动图集只有构建之后才会生效,在编辑器及预览时不会生效。其他图集打包工具一般也会有类似的处理选项(推荐使用)
设置 Sprite 的 SrcBlendFactor 为 ONE,对图片进行预乘处理,但是可能会影响到图片的批次合并,需要开发者视使用场景决定。(需要同步勾选图片的 Premultiply Alpha 选项)

性能优化注意事项

使用单独存在的 Texture 作为 Sprite 资源,在预览和发布游戏时,将无法对这些 Sprite 进行批量渲染优化的操作。目前编辑器不支持转换原有的单张 Texture 引用到 Atlas 里的 SpriteFrame 引用,所以在开发正式项目时,应该尽早把需要使用的图片合成 Atlas(图集),并通过 Atlas 里的 SpriteFrame 引用来使用。

另外,引擎中的 cc.macro.CLEANUP_IMAGE_CACHE 字段表示是否将贴图上传至 GPU 之后删除 DOM Image 缓存。具体来说,我们通过设置 image.src 为空字符串来释放这部分内存。正常情况下,可以不需要开启这个选项,因为在 Web 平台,Image 对象所占用的内存很小。但是在微信小游戏平台的当前版本,Image 对象会缓存解码后的图片数据,它所占用的内存空间很大。所以我们在微信小游戏平台默认开启了这个选项,在上传 GL 贴图之后立即释放 Image 对象的内存,避免过高的内存占用。

预制资源

预制资源(Prefab)是预先配置好的游戏对象,可作为我们动态生成节点时使用的模板。

创建预制

在场景中编辑好节点后,直接将节点从 层级管理器 拖到 资源管理器:

即可创建一个 预制资源:

使用预制

将预制资源从 资源管理器 拖拽到 层级管理器 或 场景编辑器,即可在场景中生成一个 预制实例,预制实例节点在 层级管理器 中显示为蓝色。

如果对场景中预制实例的各项属性进行修改,然后保存场景,修改的数据会被存储在该预制实例中,不会影响到 资源管理器 中的预制资源,以及使用预制资源生成的其他预制实例的数据。

自动同步和手动同步

当预制实例对应的原始预制资源被修改后,每个场景中的预制实例都可以选择要自动同步还是手动同步预制资源。选中预制实例后,点击 prefab syn 按钮即可切换手动/自动同步,默认为手动同步。

  • 设为 手动同步 时,预制实例不会自动和原始预制资源同步更新。若需要同步更新,手动点击右上方的 回退 按钮即可。且该预制实例节点在 层级管理器 中显示为蓝色。
  • 设为 自动同步 时,预制实例会自动和原始预制资源保持同步,且该预制实例节点在 层级管理器 中显示为绿色。

将预制实例还原成普通节点

若不需要使用某个预制资源,并在 资源管理器 中将其删除,那么场景中通过该预制资源生成的预制实例可以还原成普通节点,点击顶部菜单栏中的 节点 -> 还原成普通节点 即可。

图像渲染

基本图像渲染

Sprite 组件参考

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

Sprite 属性

属性 功能说明
Atlas Sprite 显示图片资源所属的 Atlas 图集资源。(Atlas 后面的 选择 按钮,该功能暂时不可用,我们会尽快优化)
Sprite Frame 渲染 Sprite 使用的 SpriteFrame 图片资源。(Sprite Frame 后面的 编辑 按钮用于编辑图像资源的九宫格切分,详情请参考 使用 Sprite 编辑器制作九宫格图像
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 组件支持五种渲染模式:

  • 普通模式(Simple):根据原始图片资源渲染 Sprite,一般在这个模式下我们不会手动修改节点的尺寸,来保证场景中显示的图像和美术人员生产的图片比例一致。
  • 九宫格模式(Sliced):图像将被分割成九宫格,并按照一定规则进行缩放以适应可随意设置的尺寸(size)。通常用于 UI 元素,或将可以无限放大而不影响图像质量的图片制作成九宫格图来节省游戏资源空间。详细信息请阅读 使用 Sprite 编辑器制作九宫格图像 一节。
  • 平铺模式(Tiled):图像将会根据 Sprite 的尺寸重复平铺显示。如果 SpriteFrame 包含 九宫格配置,平铺时将保持周围宽度不变,而其余部分重复。
  • 填充模式(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 锚点 是一样的。

Label 组件参考

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

Mask(遮罩)组件参考

Mask 用于规定子节点可渲染的范围,带有 Mask 组件的节点会使用该节点的约束框(也就是 属性检查器 中 Node 组件的 Size 规定的范围)创建一个渲染遮罩,该节点的所有子节点都会依据这个遮罩进行裁剪,遮罩范围外的将不会渲染。

点击 属性检查器 下面的 添加组件 按钮,然后从 渲染组件 中选择 Mask,即可添加 Mask 组件到节点上。注意该组件不能添加到有其他渲染组件(如 Sprite、Label 等)的节点上。

Mask 属性

属性 功能说明
Type 遮罩类型。包括 RECT、ELLIPSE、IMAGE_STENCIL 三种类型,详情可查看 Type API
Inverted 布尔值,反向遮罩
Alpha Threshold Alpha 阈值,该属性为浮点类型,仅在 Type 设为 IMAGE_STENCIL 时才生效。只有当模板像素的 alpha 值大于该值时,才会绘制内容。该属性的取值范围是 0 ~ 1,1 表示完全禁用。
Sprite Frame 遮罩所需要的贴图,只在遮罩类型设为 IMAGE_STENCIL 时生效
Segements 椭圆遮罩的曲线细分数,只在遮罩类型设为 ELLIPSE 时生效

注意:节点添加了 Mask 组件之后,所有在该节点下的子节点,在渲染的时候都会受 Mask 影响。

外部资源渲染

摄像机

摄像机是玩家观察游戏世界的窗口,场景中至少需要有一个摄像机,也可以同时存在多个摄像机。创建场景时,Creator 会默认创建一个名为 Main Camera 的摄像机,作为这个场景的主摄像机。多摄像机的支持可以让你轻松实现高级的自定义效果,比如双人分屏效果,或者场景小地图的生成。

摄像机属性

backgroundColor

当指定了摄像机需要清除颜色的时候,摄像机会使用设定的背景色来清除场景。

depth

摄像机深度,用于决定摄像机的渲染顺序。值越大,则摄像机越晚被渲染。

cullingMask

cullingMask 将决定这个摄像机用来渲染场景的哪些部分。在 属性检查器 中的摄像机组件中的 cullingMask 会列出当前可以选择的 mask 选项,你可以通过勾选这些选项来组合生成 cullingMask。

例如下图中的 cullingMask 设置表示这个摄像机只用来渲染游戏中的 UI 部分,一般游戏中的 UI 部分都是不需要移动的,而游戏节点可能会往屏幕外移动,这时需要另外的一个摄像机去跟随这个游戏节点。

用户可以通过编辑器菜单栏中的 项目 -> 项目设置 -> 分组管理 来添加或者更改分组,这些分组即是对应的 mask。

clearFlags

指定渲染摄像机时需要做的清除操作。

rect

决定摄像机绘制在屏幕上的哪个区域,便于实现类似小地图那样的 Viewport,值为 0~1。

如上图所示,场景中创建了一个用来显示小地图的 camera,最终显示效果在 游戏预览 窗口的右上角可以看到。

zoomRatio

指定摄像机的缩放比例,值越大显示的图像越大。

alignWithScreen

当 alignWithScreen 为 true 的时候,摄像机会自动将视窗大小调整为整个屏幕的大小。如果想要完全自由地控制摄像机,则需要将 alignWithScreen 设置为 false。(v2.2.1 新增)

orthoSize

摄像机在正交投影模式下的视窗大小。该属性在 alignWithScreen 设置为 false 时生效。

targetTexture

如果设置了 targetTexture,那么摄像机渲染的内容不会输出到屏幕上,而是会渲染到 targetTexture 上。

如果你需要做一些屏幕的后期特效,可以先将屏幕渲染到 targetTexture,然后再对 targetTexture 做整体处理,最后再通过一个 sprite 将这个 targetTexture 显示出来。

3D 摄像机属性

这些属性在摄像机节点设置为 3D 节点 后才会显示在 属性检查器 中。

nearClip

摄像机的近剪裁面。

farClip

摄像机的远剪裁面。

ortho

设置摄像机的投影模式是正交(true)还是透视(false)模式。

fov

决定摄像机视角的高度,当 alignWithScreen 和 ortho 都设置为 false 时生效。

摄像机方法

cc.Camera.findCamera

findCamera 会通过查找当前所有摄像机的 cullingMask 是否包含节点的 group 来获取第一个匹配的摄像机。

1
cc.Camera.findCamera(node);

containsNode

检测节点是否被此摄像机影响。

render

如果你需要立即渲染摄像机,可以调用这个方法来手动渲染摄像机,比如截图的时候。

1
camera.render();

坐标转换

一个常见的问题是,当摄像机被移动、旋转或者缩放后,这时候用点击事件获取到的坐标去测试节点的坐标,这样往往是获取不到正确结果的。

因为这时候获取到的点击坐标是屏幕坐标系下的坐标了,我们需要将这个坐标转换到世界坐标系下,才能继续与节点的世界坐标进行运算。

下面是一些坐标系转换的函数

1
2
3
4
5
6
7
8
9
// 将一个屏幕坐标系下的点转换到世界坐标系下
camera.getScreenToWorldPoint(point, out);
// 将一个世界坐标系下的点转换到屏幕坐标系下
camera.getWorldToScreenPoint(point, out);

// 获取屏幕坐标系到世界坐标系的矩阵,只适用于 2D 摄像机并且 alignWithScreen 为 true 的情况
camera.getScreenToWorldMatrix2D(out);
// 获取世界坐标系到屏幕坐标系的矩阵,只适用于 2D 摄像机并且 alignWithScreen 为 true 的情况
camera.getWorldToScreenMatrix2D(out);

截图

截图是游戏中一个非常常见的需求,通过摄像机和 RenderTexture 我们可以快速实现一个截图功能。
参考 07_capture_texture GitHub

截取部分区域

当摄像机设置了 RenderTexture 并且 alignWithScreentrue 的时候,camera 视窗大小会调整为 design resolution 的大小。如果只需要截取屏幕中的某一块区域时,设置 alignWithScreenfalse,并且根据摄像机的 投影方式 调整 orthoSize 或者 fov 即可。

在原生平台上保存截图文件

首先先截图,然后在 readPixels 之后使用:

1
2
3
var data = renderTexture.readPixels();
var filePath = jsb.fileUtils.getWritablePath() + 'Image.png';
jsb.saveImageData(data, imgWidth, imgHeight, filePath)

微信中的截图

微信小游戏中由于不支持 createImageData,也不支持用 data url 创建 image,所以上面的做法需要一些变通。在使用 Camera 渲染出需要的结果后,请使用微信的截图 API canvas.toTempFilePath 完成截图的保存和使用。

案例

Github

动画系统

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

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

关于 Animation

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

Clip 动画剪辑

动画剪辑就是一份动画的声明数据,我们将它挂载到 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:删除当前所选中的关键帧
  • k:正向的播放动画,抬起后停止
  • j:反向播放动画,抬起后停止
  • ctrl / cmd + left:跳转到第 0 帧
  • ctrl / cmd + right:跳转到有效的最后一帧

创建 Animation 组件和动画剪辑

创建 Animation 组件

在每个节点上,我们都可以添加不同的组件。如果我们想在这个节点上创建动画,也必须为它新建一个 Animation 组件。创建的方法有两种:

  • 选中相应的节点,在属性检查器中点击右上方的 +,或者下方的 添加组件,在其他组件中选择 Animation。
  • 打开动画编辑器,然后在层级管理器中选中需要添加动画的节点,在动画编辑器中点击 添加 Animation 组件 按钮。

创建与挂载动画剪辑

现在我们的节点上已经有了 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,并回车

编辑动画曲线

我们刚刚已经在节点上挂载了动画剪辑,现在我们可以在动画剪辑中创建一些动画曲线了。

我们首先了解一下动画属性,动画属性包括了节点自有的 position、rotation 等属性,也包含了组件 Component 中自定义的属性。 组件包含的属性前会加上组件的名字,比如 cc.Sprite.spriteFrame。 比如下图的 position 那条就是属性轨道,而对应的蓝色菱形就是关键帧。

添加一个新的属性轨道

常规的添加方式,我们需要先选中节点,然后在属性区域右上角点击 +。 弹出菜单中,会将可以添加的所有属性罗列出来,选中想要添加的属性,就会对应新增一个轨道。

删除一个属性轨道

将鼠标焦点移动到要删除的属性轨道上,右边会显示一个按钮,点击按钮,在弹出菜单中选择 删除属性,选中后对应的属性就会从动画数据中删除。

添加关键帧

在弹出的菜单中选择 插入关键帧 按钮。

也可以在编辑模式下直接更改节点对应的属性轨道 - 例如直接在 场景编辑器 中拖动当前选中的节点,position 属性轨道上就会在当前的时间上添加一个关键帧。需要注意的是,如果更改的属性轨道不存在,则会忽略此次的操作,所以如果想要修改后自动插入关键帧,需要预先创建好属性轨道。

选择关键帧

点击我们创建的关键帧后关键帧会呈现选中状态,此时关键帧由蓝变白。如果需要多选,可以按住 ctrl 再次选择其他关键帧。或者直接在属性区域拖拽框选。

移动关键帧

此时我们将鼠标移动到任意一个被选中的关键帧上,按下鼠标左键并拖动,鼠标会变换成左右箭头,这时候就可以拖拽所有被选中的关键帧了。

更改关键帧

在时间轴上选中需要修改的关键帧,直接在 属性检查器 内修改相对应的属性即可(确保动画编辑器处于编辑状态)。例如属性列表中有 position、x、y 三个属性轨道,选中关键帧之后,则可以修改 属性检查器 中的 position、x、y 属性。

或者在时间轴上选择一个没有关键帧的位置,然后在属性检查器中修改相对应的属性,便会自动插入一帧。

复制/粘贴关键帧

在动画编辑器内选中关键帧之后,可以按下 ctrl + c(Windows)或 command + c(Mac)复制当前的关键帧。然后选中某一个时间轴上的点,按下 ctrl + v(Windows)或 command + v(Mac)会将刚刚复制的关键帧粘贴到选中的时间点上。

节点操作

动画是按照节点的名字来进行索引关联的,有时候我们会在 层级管理器 内改变节点的层级关系,而 动画编辑器 内的动画就会找不到当初指定对应的节点。
这时候我们需要手动更改一下动画上节点的搜索路径:

  1. 鼠标移动到要迁移的节点上,点击右侧出现的菜单按钮
  2. 选择移动节点数据
  3. 修改节点的路径数据

根节点的数据我们是不能改变的,我们可以修改后续的节点路径,比如我们要将 /root/New Node 的帧动画移动到根节点上,我们可以将 New Node 删除,剩下 /root/ 然后回车(/root/是无法修改的根节点路径)。再比如我们要将 /root 跟节点上的动画数据移动到 /root/New Node 上,我们只需要将路径改成 /root/New Node

编辑序列帧动画

编辑时间曲线

添加动画事件

在游戏中,经常需要在动画结束或者某一帧的特定时刻,执行一些函数方法。那么在动画编辑器中怎么实现呢

添加事件

首先选中某个位置,然后点击按钮区域最左侧的按钮(add event),这时候在时间轴上会出现一个白色的矩形,这就是我们添加的事件。

指定事件触发函数以及传入参数

双击刚刚出现的白色矩形,可以打开事件编辑器,在编辑器内,我们可以手动输入需要触发的 function 名字,触发的时候会根据这个函数名,去各个组件内匹配相应的方法。

如果需要添加传入的参数,则在 Params 旁点击 + 或者 -,只支持 Boolean、String、Number 三种类型的参数。

使用脚本控制动画

Animation 组件

Animation 组件提供了一些常用的动画控制函数,如果只是需要简单的控制动画,可以通过获取节点的 Animation 组件来做一些操作。

播放动画

1
2
3
4
5
6
7
8
9
10
11
12
13
var anim = this.getComponent(cc.Animation);

// 如果没有指定播放哪个动画,并且有设置 defaultClip 的话,则会播放 defaultClip 动画
anim.play();

// 指定播放 test 动画
anim.play('test');

// 指定从 1s 开始播放 test 动画
anim.play('test', 1);

// 使用 play 接口播放一个动画时,如果还有其他的动画正在播放,则会先停止其他动画
anim.play('test2');

Animation 对一个动画进行播放的时候会判断这个动画之前的播放状态来进行下一步操作。

如果动画处于:

  • 停止 状态,则 Animation 会直接重新播放这个动画
  • 暂停 状态,则 Animation 会恢复动画的播放,并从当前时间继续播放下去
  • 播放 状态,则 Animation 会先停止这个动画,再重新播放动画
1
2
3
4
5
6
7
8
var anim = this.getComponent(cc.Animation);

// 播放第一个动画
anim.playAdditive('position-anim');

// 播放第二个动画
// 使用 playAdditive 播放动画时,不会停止其他动画的播放。如果还有其他动画正在播放,则同时会有多个动画进行播放
anim.playAdditive('rotation-anim');

Animation 是支持同时播放多个动画的,播放不同的动画并不会影响其他的动画的播放状态,这对于做一些复合动画比较有帮助。

暂停、恢复、停止动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var anim = this.getComponent(cc.Animation);

anim.play('test');

// 指定暂停 test 动画
anim.pause('test');

// 暂停所有动画
anim.pause();

// 指定恢复 test 动画
anim.resume('test');

// 恢复所有动画
anim.resume();

// 指定停止 test 动画
anim.stop('test');

// 停止所有动画
anim.stop();

暂停、恢复、停止 几个函数的调用比较接近。

暂停 会暂时停止动画的播放,当 恢复 动画的时候,动画会继续从当前时间往下播放。
而 停止 则会终止动画的播放,再次播放这个动画时会重新播放动画。

设置动画的当前时间

1
2
3
4
5
6
7
8
9
var anim = this.getComponent(cc.Animation);

anim.play('test');

// 设置 test 动画的当前播放时间为 1s
anim.setCurrentTime(1, 'test');

// 设置所有动画的当前播放时间为 1s
anim.setCurrentTime(1);

你可以在任何时候对动画设置当前时间,但是动画不会立刻根据设置的时间进行状态的更改,需要在下一个动画的 update 中才会根据这个时间重新计算播放状态。

AnimationState

Animation 只提供了一些简单的控制函数,希望得到更多的动画信息和控制的话,需要使用到 AnimationState。

AnimationState 是什么?

如果说 AnimationClip 是作为动画数据的承载,那么 AnimationState 则是 AnimationClip 在运行时的实例,它将动画数据解析为方便程序中做计算的数值。
Animation 在播放一个 AnimationClip 的时候,会将 AnimationClip 解析成 AnimationState
Animation 的播放状态实际都是由 AnimationState 来计算的,包括动画是否循环、怎么循环、播放速度等。

获取 AnimationState

1
2
3
4
5
var anim = this.getComponent(cc.Animation);
// play 会返回关联的 AnimationState
var animState = anim.play('test');
// 或者直接获取
var animState = anim.getAnimationState('test');

获取动画信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var anim = this.getComponent(cc.Animation);
var animState = anim.play('test');

// 获取动画关联的 clip
var clip = animState.clip;

// 获取动画的名字
var name = animState.name;

// 获取动画的播放速度
var speed = animState.speed;

// 获取动画的播放总时长
var duration = animState.duration;

// 获取动画的播放时间
var time = animState.time;

// 获取动画的重复次数
var repeatCount = animState.repeatCount;

// 获取动画的循环模式
var wrapMode = animState.wrapMode

// 获取动画是否正在播放
var playing = animState.isPlaying;

// 获取动画是否已经暂停
var paused = animState.isPaused;

// 获取动画的帧率
var frameRate = animState.frameRate;

AnimationState 中可以获取到所有动画的信息,你可以利用这些信息来判断需要做哪些事情。

设置动画播放速度

1
2
3
4
5
6
7
8
var anim = this.getComponent(cc.Animation);
var animState = anim.play('test');

// 使动画播放速度加速
animState.speed = 2;

// 使动画播放速度减速
animState.speed = 0.5;

speed 值越大速度越快,值越小则速度越慢

设置动画的循环模式与循环次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var anim = this.getComponent(cc.Animation);
var animState = anim.play('test');

// 设置循环模式为 Normal
animState.wrapMode = cc.WrapMode.Normal;

// 设置循环模式为 Loop
animState.wrapMode = cc.WrapMode.Loop;

// 设置动画循环次数为 2 次
animState.repeatCount = 2;

// 设置动画循环次数为无限次
animState.repeatCount = Infinity;

AnimationState 允许动态设置循环模式,目前提供了多种循环模式,这些循环模式可以从 cc.WrapMode 中获取到。

如果动画的 WrapModeLoop 的话,需要与 repeatCount 配合使用才能达到效果。默认在解析动画剪辑的时候,如果动画循环类型为:

  • Loop 类型,repeatCount 将被设置为 Infinity,即无限循环。
  • Normal 类型,repeatCount 将被设置为 1

动画事件

动画编辑器支持可视化编辑帧事件(如何编辑请参考 这里),在脚本里书写动画事件的回调非常简单。动画事件的回调其实就是一个普通的函数,在动画编辑器里添加的帧事件会映射到动画根节点的组件上。

假设在动画的结尾添加了一个帧事件,如下图:

那么在脚本中可以这么写:

1
2
3
4
5
6
7
cc.Class({
extends: cc.Component,

onAnimCompleted: function (num, string) {
console.log('onAnimCompleted: param1[%s], param2[%s]', num, string);
}
});

将上面的组件加到动画的 根节点 上,当动画播放到结尾时,动画系统会自动调用脚本中的 onAnimCompleted 函数。动画系统会搜索动画根节点中的所有组件,如果组件中有实现动画事件中指定的函数的话,就会对它进行调用,并传入事件中填的参数。

注册动画回调

除了动画编辑器中的帧事件提供了回调外,动画系统还提供了动态注册回调事件的方式。目前支持的回调事件包括:

  • play:开始播放时
  • stop:停止播放时
  • pause:暂停播放时
  • resume:恢复播放时
  • lastframe:假如动画循环次数大于 1,当动画播放到最后一帧时
  • finished:动画播放完成时

当在 cc.Animation 注册了一个回调函数后,它会在播放一个动画时,对相应的 cc.AnimationState 注册这个回调,在 cc.AnimationState 停止播放时,对 cc.AnimationState 取消注册这个回调。

cc.AnimationState 其实才是动画回调的发送方,如果希望对单个 cc.AnimationState 注册回调,那么可以先获取到这个 cc.AnimationState 再单独对它进行注册。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var animation = this.node.getComponent(cc.Animation);

// 注册
animation.on('play', this.onPlay, this);
animation.on('stop', this.onStop, this);
animation.on('lastframe', this.onLastFrame, this);
animation.on('finished', this.onFinished, this);
animation.on('pause', this.onPause, this);
animation.on('resume', this.onResume, this);

// 取消注册
animation.off('play', this.onPlay, this);
animation.off('stop', this.onStop, this);
animation.off('lastframe', this.onLastFrame, this);
animation.off('finished', this.onFinished, this);
animation.off('pause', this.onPause, this);
animation.off('resume', this.onResume, this);

动态创建 Animation Clip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var animation = this.node.getComponent(cc.Animation);
// frames 这是一个 SpriteFrame 的数组.
var clip = cc.AnimationClip.createWithSpriteFrames(frames, 17);
clip.name = "anim_run";
clip.wrapMode = cc.WrapMode.Loop;

// 添加帧事件
clip.events.push({
frame: 1, // 准确的时间,以秒为单位。这里表示将在动画播放到 1s 时触发事件
func: "frameEvent", // 回调函数名称
params: [1, "hello"] // 回调参数
});

animation.addClip(clip);
animation.play('anim_run');

碰撞系统

编辑碰撞组件

当添加了一个碰撞组件后,可以通过点击 inspector 中的 editing 来开启碰撞组件的编辑,如下图。

多边形碰撞组件

如果编辑的是 多边形碰撞组件 的话,则会出现类似下图所示的 多边形编辑区域。区域中的这些点都是可以拖动的,拖动的结果会反映到 多边形碰撞组件 的 points 属性中。

当鼠标移动到两点连成的线段上时,鼠标指针会变成 添加 样式,这时点击鼠标左键会在这个地方添加一个点到 多边形碰撞组件 中。

当按住 ctrl 或者 command 键时,移动鼠标到多边形顶点上,会发现顶点以及连接的两条线条变成红色,这时候点击鼠标左键将会删除 多边形碰撞组件 中的这个点。

在 CocosCreator 1.5 中,多边形碰撞组件中添加了一个 Regenerate Points 的功能,这个功能可以根据组件依附的节点上的 Sprite 组件的贴图的像素点来自动生成相应轮廓的顶点。

Threshold 指明生成贴图轮廓顶点间的最小距离,值越大则生成的点越少,可根据需求进行调节。

圆形碰撞组件

如果编辑的是 圆形碰撞组件 的话,则会出现类似下图所示的 圆形编辑区域:

当鼠标悬浮在 圆形编辑区域 的边缘线上时,边缘线会变亮,这时点击鼠标左键拖动将可以修改 圆形碰撞组件 的半径大小。

矩形碰撞组件

如果编辑的是 矩形碰撞组件 的话,则会出现类似下图所示的 矩形编辑区域:

当鼠标悬浮在 矩形碰撞区域 的顶点上时,点击鼠标左键拖拽可以同时修改 矩形碰撞组件 的长宽;
当鼠标悬浮在 矩形碰撞区域 的边缘线上时,点击鼠标左键拖拽将修改 矩形碰撞组件 的长或宽中的一个方向。

按住 Shift 键拖拽时,在拖拽过程中将会保持按下鼠标那一刻的 长宽比例;
按住 Alt 建拖拽时,在拖拽过程中将会保持 矩形中心点位置 不变。

修改碰撞组件偏移量

在所有的碰撞组件编辑中,都可以在各自的 碰撞中心区域 点击鼠标左键拖拽来快速编辑碰撞组件的 偏移量。

碰撞分组管理

碰撞系统脚本控制

Cocos Creator 中内置了一个简单易用的碰撞检测系统,它会根据添加的碰撞组件进行碰撞检测。当一个碰撞组件被启用时,这个碰撞组件会被自动添加到碰撞检测系统中,并搜索能与之进行碰撞的其他已添加的碰撞组件来生成一个碰撞对。需要注意的是,一个节点上的碰撞组件,无论如何都是不会相互进行碰撞检测的。

碰撞检测系统的使用

碰撞系统接口

获取碰撞检测系统:

1
var manager = cc.director.getCollisionManager();

默认碰撞检测系统是禁用的,如果需要使用则需要以下方法开启碰撞检测系统:

1
manager.enabled = true;

默认碰撞检测系统的 debug 绘制是禁用的,如果需要使用则需要以下方法开启 debug 绘制:

1
manager.enabledDebugDraw = true;

开启后在运行时可显示 碰撞组件 的 碰撞检测范围,如下图:

如果还希望显示碰撞组件的包围盒,那么可以通过以下接口来进行设置:

1
manager.enabledDrawBoundingBox = true;

结果如下图所示:

碰撞系统回调

当碰撞系统检测到有碰撞产生时,将会以回调的方式通知使用者,如果产生碰撞的碰撞组件依附的节点下挂的脚本中有实现以下函数,则会自动调用以下函数,并传入相关的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 当碰撞产生的时候调用
* @param {Collider} other 产生碰撞的另一个碰撞组件
* @param {Collider} self 产生碰撞的自身的碰撞组件
*/
onCollisionEnter: function (other, self) {
console.log('on collision enter');

// 碰撞系统会计算出碰撞组件在世界坐标系下的相关的值,并放到 world 这个属性里面
var world = self.world;

// 碰撞组件的 aabb 碰撞框
var aabb = world.aabb;

// 节点碰撞前上一帧 aabb 碰撞框的位置
var preAabb = world.preAabb;

// 碰撞框的世界矩阵
var t = world.transform;

// 以下属性为圆形碰撞组件特有属性
var r = world.radius;
var p = world.position;

// 以下属性为 矩形 和 多边形 碰撞组件特有属性
var ps = world.points;
},
1
2
3
4
5
6
7
8
/**
* 当碰撞产生后,碰撞结束前的情况下,每次计算碰撞结果后调用
* @param {Collider} other 产生碰撞的另一个碰撞组件
* @param {Collider} self 产生碰撞的自身的碰撞组件
*/
onCollisionStay: function (other, self) {
console.log('on collision stay');
},
1
2
3
4
5
6
7
8
/**
* 当碰撞结束后调用
* @param {Collider} other 产生碰撞的另一个碰撞组件
* @param {Collider} self 产生碰撞的自身的碰撞组件
*/
onCollisionExit: function (other, self) {
console.log('on collision exit');
}

点击测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
properties: {
collider: cc.BoxCollider
},

start () {
// 开启碰撞检测系统,未开启时无法检测
cc.director.getCollisionManager().enabled = true;
// cc.director.getCollisionManager().enabledDebugDraw = true;

this.collider.node.on(cc.Node.EventType.TOUCH_START, function (touch, event) {
// 返回世界坐标
let touchLoc = touch.getLocation();
// https://docs.cocos.com/creator/api/zh/classes/Intersection.html 检测辅助类
if (cc.Intersection.pointInPolygon(touchLoc, this.collider.world.points)) {
console.log("Hit!");
}
else {
console.log("No hit");
}
}, this);
}

更多的范例

Collider 组件参考

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道