Cocos Creator开发笔记
前言
小白一个记录一下自己creator(开发版本2.0.x)开发过程中遇到的一些问题以及解决方案,如有错误和不足之处还请大佬指出,文章原创,转载注明出处,本文有部分内容转自其他优秀的博客,有些注明了原博客地址,还有些因地址丢失就没有注明出处,如有侵权,请联系我删除内容。
一、UI组件
1.Scrollview组件
(1)当scrollview的item数量比较少时,content的长度需要大于scrollview的长度才能滚动,预制体item的长度设置也需要合理,不能为0否则就会出现顶部子节点显示不完全的问题。content的layout模式选NONE就可以在数据很少的时候滑动,如果选择CONTAINER模式只有当content的长度大于scrollview的时候才能滑动。
(2)将 scrollview 下面的content容器的子节点拖出 scrollView 外时需要改变拖动的节点的父节点,否则cc.Node.EventType.TOUCH_END事件将不会触发。
(3)scrollview 的 content 上面挂载的 layout 组件的功能需要用自己的代码在 update 接口函数里面实现,使用自带的 layout 性能较差。
1、刚初始化完成时:此时在右侧按钮上提示有100行,实际上只创建了第1-15行,而玩家能看到的是第1-7行。如果玩家想要看到更多,必然会向上或向下滚动屏幕;
2、向上滚动时:在移动设备上,如果玩家想要看到下面的行,所做的操作是触摸往上滑动,则整个content区往上移动,也带动content区的item往上移动,update函数会不断遍历所创建的15项item,如果检测到某item的y坐标超出了缓冲区的上边界(该item已经被玩家看过或不想再看),则把该item往下移动一个缓冲区的高度(移动该item到玩家即将看到的位置),并更新它的显示ID;
3、向下滚动时:同理,content区的item往下移动,update不断遍历所创建的15项item,如果检测到某item的y坐标越过了缓冲区的下边界(该item已经被玩家看过或不想再看),则把该item往上移动一个缓冲区的高度(移动该item到玩家即将看到的位置),并更新它的显示ID;
(本段转自https://blog.csdn.net/foupwang/article/details/79103470)
// 返回item在ScrollView空间的坐标值
getPositionInView: function (item) {
let worldPos = item.parent.convertToWorldSpaceAR(item.position);
let viewPos = this.scrollView.node.convertToNodeSpaceAR(worldPos);
return viewPos;
},
// 每updateTimer调用一次,根据滚动位置动态更新item的坐标和显示(所以spawnCount可以比totalCount少很多)
update: function(dt) {
this.updateTimer += dt;
if (this.updateTimer < this.updateInterval) {
return; // we don't need to do the math every frame
}
this.updateTimer = 0;
let items = this.items;
// 如果当前content的y坐标小于上次记录值,则代表往下滚动,否则往上。
let isDown = this.scrollView.content.y < this.lastContentPosY;
// 实际创建项占了多高(即它们的高度累加)
let offset = (this.itemTemplate.height + this.spacing) * items.length;
let newY = 0;
// 遍历数组,更新item的位置和显示
for (let i = 0; i < items.length; ++i) {
let viewPos = this.getPositionInView(items[i]);
if (isDown) {
// 提前计算出该item的新的y坐标
newY = items[i].y + offset;
// 如果往下滚动时item已经超出缓冲矩形,且newY未超出content上边界,
// 则更新item的坐标(即上移了一个offset的位置),同时更新item的显示内容
if (viewPos.y < -this.bufferZone && newY < 0) {
items[i].setPositionY(newY);
let item = items[i].getComponent('Item');
let itemId = item.itemID - items.length; // update item id
item.updateItem(i, itemId);
}
} else {
// 提前计算出该item的新的y坐标
newY = items[i].y - offset;
// 如果往上滚动时item已经超出缓冲矩形,且newY未超出content下边界,
// 则更新item的坐标(即下移了一个offset的位置),同时更新item的显示内容
if (viewPos.y > this.bufferZone && newY > -this.content.height) {
items[i].setPositionY(newY);
let item = items[i].getComponent('Item');
let itemId = item.itemID + items.length;
item.updateItem(i, itemId);
}
}
}
// 更新lastContentPosY和总项数显示
this.lastContentPosY = this.scrollView.content.y;
this.lblTotalItems.string = "Total Items: " + this.totalCount;
},
将update()执行的代码放到滚动监听回调函数里面,减少开销。
2.Button组件
(1)button组件属性面板传参绑定的函数格式为 function(event, target) {}, 如果用 this.node.on 方法绑定监听函数传参,则使用
this.node.on(cc.Node.EventType.TOUCH_START, (event) => {
this._callback(arguments);
}, this);
(2)有触摸事件而且需要吞噬当前触摸的时候使用Button组件,只有吞噬当前触摸的时候用BlockInputEvents组件。
(3当 button 按钮使用 Color 或者 Sprite 模式时,点击按钮将btton.node.active设置为false后再将按钮设置为true时,按钮的颜色或者图片没有复位,时由于 this.on 触发的点击事件不能完全模拟 button 组件,所以当 button.node.active = false 时,该节点的过渡效果被中断了。
在你需要的地方加入类似这段代码:
this.closeButton.getComponent(cc.Button)._updateState();
另外建议直接使用 cc.Button 组件制作按钮点击
3.Label组件
(1)聊天气泡的背景图上面挂载一个 layout 组件,这样背景图的宽度和高度会随着 label 变化而变化,但需要先将 label 的 overflow 属性改为 NONE 当超过一定长度后代码动态改变 overflow 为自动换行。
(2)由于 label 中 _updateRenderData 处理开销过大,导致没办法设置 string 到时候去触发更新,只能在渲染到时候才可以获取到正确的 size,可以在设置 label 所有属性后在执行一次 label._updateRenderData(true); 就能带当帧获取大小。
4.ProgressBar组件
(1)
/* jshint esversion: 6 */
cc.Class({
extends: cc.Component,
properties: {
progressBar: {
default: null,
type: cc.Node,
},
},
// LIFE-CYCLE CALLBACKS:
onLoad () {
let onProgress = (cur, num, item) => {
console.log('已加载' + cur + '/' + num);
this.progressBar.getComponent(cc.ProgressBar).totalLength = this.progressBar.width * cur / num;
};
cc.director.preloadScene('helloworld', onProgress, () => {
cc.loader.onProgress = null;
console.log('ready');
cc.director.loadScene('helloworld');
});
},
});
另外2.0以上版本 removeComponent 被废弃了,建议使用 destroy 进行移除组件,但很多时候需要在当前帧就移除,就必须调用_destroyImmediate了,调用此方法会释放所有它对其它对象的引用。
二、逻辑层
1.碰撞系统
(1)当改变物体的碰撞分组之后,需要将该 node 的 active 先设置为 false 再设置为 true 才会刷新节点信息。
(2)
// 物理的时间慢放
cc.director.getScheduler().setTimeScale(0.5);
cc.director.getPhysicsManager().enabledAccumulator = true;
cc.director.getPhysicsManager().FIXED_TIME_STEP = 1 / 20;
(3)使用物理引擎要用力,向量,或设置速度来控制物体运动。
2.触摸系统
(1)新手引导代码,高亮圆圈点击
if (retWord.contains(point)) {
this.node._touchListener.setSwallowTouches(false); // 触摸事件往下层传递
this.node.active = false;
console.log("circle");
} else {
this.node._touchListener.setSwallowTouches(true);
console.log("maskBg");
}
(2)获取两点之间的角度
let angle = cc.v2(x1, y1).angle(cc.v2(x2, y2)) * 180 / Math.PI;
3.摄像机
(1).摄像机跟随缓动核心代码:
mainCamera.x = cc.misc.lerp(mainCamera.x, posX, rate);
mainCamera.y = cc.misc.lerp(mainCamera.y, posY, rate);
rate 是一个系数,可以设置为0-1之间,posX, posY是计算出来的角色位置,mainCamera是摄像机。
如果使用了物理系统,则 cc.follow() 不起作用,这个时候节点树需要如下图所示:
(2)屏幕最佳分辨率为 750*1334 。
(3)iPhonex适配
/* jshint esversion: 6 */
/**
* ResolutionMatch.js: 分辨率适配(过高按宽适配, 过宽按高适配)
*/
cc.Class({
extends: cc.Component,
properties: {
},
onLoad() {
let screenSize = cc.View.getFrameSize();
let canvas = this.node.getComponent(cc.Canvas);
canvas.fitWidth = false;
canvas.fitHeight = false;
if (screenSize.height / screenSize.width >= 1.77) {
canvas.fitWidth = true;
} else {
canvas.fitHeight = true;
}
},
});
4.定时器
(1)定时器和触摸监听传参函数的注册与取消
onLoad() {
// 用变量将回调函数保存起来
let _func = this.scheduleCahe.bind(this, arguments);
this.scheduele(_func, interval);
this.unschedule(_func);
},
scheduleCahe: function (arguments) {},
三、代码逻辑优化
1.随机数
(1)产生[min, max]随机数:(真正的随机数需要自己创建一个随机种子)。
let randNumber = Math.floor(Math.random() * (max + 1 - min) + min);
(2)产生随机数种子:
Math.seed = function(s) {
let m_w = s;
let m_z = 987654321;
let mask = 0xffffffff;
return function () {
m_z = (36969 * (m_z & 65535) + (m_z >> 16)) & mask;
m_w = (18000 * (m_w & 65535) + (m_w >> 16)) & mask;
let result = ((m_z << 16) + m_w) & mask;
result /= 4294967296;
return result + 0.5;
}
}
//usage
let myRandomFunction = Math.seed(1234);
let randomNumber = myRandomFunction();
2.单例模式
(1)将当前 let Singleton = cc.Class({}); 生成的构造函数赋给全局,就能通过全局访问到脚本内定义的 statics 变量,而将脚本 this 赋给全局变量则为单例。
(2)
let Singleton = cc.Class({
statics: {
_instance: null
getInstance(): {
if (this._instance === null) {
this._instance = new Singleton();
}
return this._instance;
},
},
});
module.exports = Singleton;
3.节点
(1)在脚本中将 node 的 active 被设置为 false 之后,后面的代码逻辑就无法生效,此时只需要先将 node 的 opacity 设置为0,然后等逻辑走完之后将 active 设置为 false 即可。
四、资源加载与优化
CCC资源加载特性:
- 资源动态加载都是异步的。在处理资源加载与释放时,就需要考虑加载中的资源如何释放的问题。
- 资源是互相依赖的,指定加载资源时,也会加载其所有依赖项。在处理资源释放时,需要考虑资源的依赖关系。
- 动态加载的资源都是不会自动释放的。就算切换场景,动态加载的资源依然需要手动释放。
- cc.loader.getDependsRecursively 接口可以获取到资源的所有依赖项,包括依赖的依赖。这个接口可以让我们方便地获取到资源的依赖关系。
- cc.loader.getDependsRecursively 接口获取到的数据,是每个资源对应的唯一的 reference id ,该值可以通过cc.loader的私有方法 _getReferenceKey 获取。释放时使用 cc.loader.release 直接传入资源的 reference id 进行释放。
- 按路径加载或释放资源时,需要指定目标资源的类型(简单的配置文件除外)。释放资源时,如果该路径下有多种资源类型(比如 spine 动画相关文件有 json/png/atlas ),你将不知道它会释放什么资源,而且释放也不完全(只会释放其中一种资源)。
- 类似于上一条,在使用 cc.loader.getDependsRecursively 接口获取依赖项时,不要使用文件路径作为参数获取。
五、组件化开发
1.ECS架构
- 目的: 降低不断增长的代码库的复杂度。
- 传统架构的弊端: 一般父类会有很多共享的属性和方法,子类继承父类去做具体的事情,这样随着项目规模的增长,代码库复杂度也不断增长,父类会越来越复杂,子类的功能会越来越不明确,与多个类相关的代码你不能太确切知道应该放在哪里,拓展功能的时候及其不灵活,如果后期需要增加新功能的话,就需要对整个继承树进行功能重构才能使其比较合理。
- ESC分别是:
- Entity(实体): 实体,组件的集合
- Component(组件):组件,储存游戏状态
- System(系统): 系统,实现游戏行为
- World(世界): 系统和实体的集合
- ESC架构设计
-
- 组件只有状态,没有行为
- 系统只有行为,没有状态
游戏行为,就是根据一定的规则去改变游戏状态,比如移动,就是根据是的方向和移动速度去改变这个实体的位置。
在系统实现的时候,要向整个游戏声明我关心哪些组件元组,“组件元组”其实就是用来实现框架筛选实体的功能,实体只需要根据自身功能需求挂载相应的组件元组就可以了。
this.enabled: 是否每帧执行该组件的 update 方法,同时也用来控制渲染组件是否显示。
编码规范:
(1)生命周期接口函数 start() 是在所有脚本的 onLoad() 执行完之后执行,所以将初始化工作放在start()里面就可以取到其他脚本的值。
(2)给预制体添加父亲节点的时候最好使用 parent.addChild(prefabNode); 而不是 prefabNode.parent = 父节点,这样如果父节点名字拼写错误 浏览器会报错 addChild is not definded。
(3)尽量在for循环里面用 let 替代 var ,比如远程从服务器上拉取图片的时候,如果使用var最后拉到的图片都是同一张,因为http请求是异步发生的。
(4)Javascript的Math三角函数浮点数精度问题: 如实现帧同步时,需要确定物理引擎位置等信息的一致性,需要将浮点数改为定点数,可以使用查表法,提前将需要用到的三角函数的值存在配置文件里,然后计算时直接使用这些值。
番外之骨骼动画:
1.Spine骨骼动画局部换装
(1)局部换装
/**
*
* @breif 局部换装 skeleton(脚本组件)
* (1) 获取得到当前需要更换附件的插槽数据(包括 index attachment);
* (2) 获取当前皮肤下用于替换的附件;
* (3) 在当前插槽下设置更换附件;
*
* @param slotName 需要替换图片的插槽名字
* @param targetSkinName 替换目标的皮肤名字
* @param targetAttaName 用于替换的附件名字 | null 为空则是将当前槽点的附件置空
*
*/
changeSkin(slotName, targetSkinName, targetAttaName = null) {
this.spine = this.getComponent('sp.Skeleton');
const slot = this.spine.findSlot(slotName);
const skeletonData = this.spine.skeletonData.getRuntimeData();
const slotIndex = skeletonData.findSlotIndex(slotName);
const skin = skeletonData.findSkin(targetSkinName);
const atta = skin.getAttachment(slotIndex, targetAttaName);
// console.log('change clothes:', slot, skin, slotIndex, atta);
slot.setAttachment(atta);
},
(2)整体换装
Spine 官方并不支持一套动画数据对应多套图片,使用 SkeletonTexture.setRealTexture() 设置为另一张图片资源就可以实现整体整体换装功能了。
2.龙骨
(1)龙骨动态添加碰撞盒
具体实现
- 获取龙骨骼的边界框插槽数据
- 在带龙骨骼组件的节点上添加子节点
- 字节点分组设为父节点分组
- 子节点位置为插槽位置
- 为子节点添加碰撞组件
- 通过插槽数据设置碰撞组件的碰撞点
- 在update里动态改变子节点的rotation、scale
cc.Class({
extends: cc.Component,
properties: {},
onLoad() {
this._armatureDisplay = this.node.getComponent(dragonBones.ArmatureDisplay);
this._armature = this._armatureDisplay.armature();
this.slots = this._armature.getSlots().filter(slot => {
return !!slot.boundingBoxData;
});
this.colliders = {};
},
start() {},
update(dt) {
this.updateColliders(this.slots, this.colliders);
},
updateColliders(slots, colliders) {
slots.map(slot => {
let [slotName, collider, node] = [slot.name, ,];
if (colliders[slotName]) {
collider = colliders[slotName];
node = collider.node;
node.parent = this.node;
node.active && (node.active = false);
} else {
node = new cc.Node(slotName);
node.groupIndex = this.node.groupIndex;
colliders[slotName] = collider = node.addComponent(cc.PolygonCollider);
}
let currentBone = slot.parent;
let transform = currentBone.global;
!node.active && (node.active = true);
node.x = transform.x;
node.y = -transform.y;
let rotation = 0;
let scaleX = 1;
let scaleY = 1;
while (currentBone) {
rotation += (currentBone.global.rotation * 180) / Math.PI;
scaleX *= currentBone.global.scaleX;
scaleY *= currentBone.global.scaleY;
currentBone = currentBone.parent;
}
node.rotation = rotation;
node.scaleX = scaleX;
node.scaleY = scaleY;
while (collider.points.length > slot.boundingBoxData.vertices.length / 2)
collider.points.pop();
for (let i = 0; i < slot.boundingBoxData.vertices.length / 2; ++i) {
collider.points[i] = cc.v2(
slot.boundingBoxData.vertices[i * 2] + slot.origin.x,
-(slot.boundingBoxData.vertices[i * 2 + 1] + slot.origin.y)
);
}
});
}
});
1.序列化
序列化/反序列化,官方解释是 序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
在creator里面用通俗的话讲就是序列化就是,这个属性会存到场景中的意思。不可序列化,就是每次启动场景,这个值都会保持默认值。
你都看到这了,不点个赞再走吗?