Cocos Creator入门(一)之查漏补缺

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)龙骨动态添加碰撞盒

具体实现

  1. 获取龙骨骼的边界框插槽数据
  2. 在带龙骨骼组件的节点上添加子节点
  3. 字节点分组设为父节点分组
  4. 子节点位置为插槽位置
  5. 为子节点添加碰撞组件
  6. 通过插槽数据设置碰撞组件的碰撞点
  7. 在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里面用通俗的话讲就是序列化就是,这个属性会存到场景中的意思。不可序列化,就是每次启动场景,这个值都会保持默认值。

 

你都看到这了,不点个赞再走吗?

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值