CocosCreator教程

场景管理

新建一个场景

1.点击左上角的文件,在对话框中点击新建场景。
2.ctrl+n直接创建新的场景。
在创建好一个新的场景之后,我们发现左上角的层级管理器被恢复到默认的状态了。这就表明我们创建新的场景成功了,并且切换到了新的场景当中。
创建好之后,点击保存,将场景文件保存起来。
然后在左下角的资源管理器当中就可以进行手动的场景切换了。

代码中进行切换场景的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 加载第二个场景
cc.director.loadScene("scene2",function(){
// 因为在加载场景的时候,如果场景资源特别大,就会长时间卡顿,会造成用户体验感下降,所以通常在加载新场景的时候会添加一个进度条作为过渡
})

// 预加载,对于一些有较大资源的场景,最好进行预加载,以节省场景切换的时间
cc.director.preloadScene("scene2", function () {
// 预加载完成之后,场景不会立即切换,只是提前加载如内存当中了
})

// 常驻节点的添加与移除,这个节点不会因为场景的切换而消失,比如可以实现游戏时间的计算
cc.game.addPersistRootNode(this.node);
// 只是将这个节点从常驻节点中移除了,并不是删除
cc.game.removePersistRootNode(this.node);

键鼠事件、触摸事件、自定义事件

鼠标事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
start(){
// 鼠标监听事件
this.node.on(cc.Node.EventType.MOUSE_DOWN, e => {
console.log("鼠标按下了,坐标是" + e.getLocation())
if (e.getButton() == cc.Event.EventMouse.BUTTON_LEFT) {
console.log("鼠标左键被按下了");
}
if (e.getButton() == cc.Event.EventMouse.BUTTON_RIGHT) {
console.log("鼠标右键被按下了");
}
})
this.node.on(cc.Node.EventType.MOUSE_ENTER, e => {
console.log("鼠标移入目标区域了,X的坐标是" + e.getLocationX());
})
// this.node.on(cc.Node.EventType.MOUSE_MOVE, e => {
// console.log("鼠标在目标区域移动了,Y的坐标是" + e.getLocationY());
// })
this.node.on(cc.Node.EventType.MOUSE_LEAVE, e => {
console.log("鼠标离开区域了,坐标是" + e.getLocation());
})
this.node.on(cc.Node.EventType.MOUSE_UP, e => {
console.log("鼠标松开了");
})
}

键盘事件

1
2
3
4
5
6
7
8
9
10
11
start(){
// 键盘监听事件
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, e => {
if (e.keyCode == cc.macro.KEY.w) {
console.log("w键被按下了");
}
if (e.keyCode == cc.macro.KEY.p) {
console.log("p键被按下了");
}
})
}

触摸事件

1
2
3
4
5
6
7
8
9
10
11
12
13
start(){
// 触摸事件
this.node.on(cc.Node.EventType.TOUCH_START, e => {
console.log("检测到用户触摸屏幕" + e.getID()); //getID用于获取用户是否进行了多点触摸
// this.node.emit("myMessage1")
this.node.dispatchEvent(new cc.Event.EventCustom("myMessage1", true)) //第二个参数布尔值为是否冒泡
})

// 监听自定义事件
this.node.on("myMessage1", e => {
console.log("自定义事件");
})
}

碰撞检测

在精灵的属性检查器中添加组件-碰撞组件
1.Box Collider:盒子型碰撞组件,是一个矩形框
2.Circle Collider:圆形碰撞组件,是一个圆形
3.Polygon Collider:多边形碰撞组件,非常耗费资源
他们的一些属性:
Tag:用于区分碰撞时,与哪个组件发生了碰撞
Offset:将碰撞框进行平移
Size:对碰撞框的大小进行修改

在代码中检测碰撞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
start() {
// 碰撞系统默认都是关闭的,需要手动打开
cc.director.getCollisionManager().enabled = true
}

// 发生碰撞
onCollisionEnter(other) { //参数other就是发生碰撞的对象
console.log("碰撞发生了" + other.tag);
}
// 碰撞持续
onCollisionStay(other) {
console.log("碰撞持续中");
}
// 碰撞结束
onCollisionExit(other) {

}

音频播放

首先要将需要播放的音频放入resources文件夹当中
然后创建一个空的节点,新增一个其他组件-AudioSource
然后将音频文件直接拖入即可以简单进行播放音频
具体代码操作如下:

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
34
35
36
37
38
39
40
41
start() {
// // 通过组件的方式
// // 拿到播放器并存在player变量身上
// let player: cc.AudioSource = this.getComponent(cc.AudioSource)
// // 加载音频,需要将音频文件放入resources文件夹
// cc.loader.loadRes("Kevin MacLeod - If I Had a Chicken", cc.AudioClip, (res, clip) => {
// // 赋值音频
// player.clip = clip
// // 播放
// player.play()
// // 检查是否正在播放
// // player.isPlaying()
// // 暂停
// // player.pause()
// // 恢复
// // player.resume()
// // 停止
// // player.stop()
// // 是否循环播放
// // player.loop = true
// // 调整音量
// // player.volume = 1
// })

cc.loader.loadRes("Kevin MacLeod - If I Had a Chicken", cc.AudioClip, (res, clip) => {
// 播放
let audioId: number = cc.audioEngine.playMusic(clip, true)
// 是否正在播放
// cc.audioEngine.isMusicPlaying()
// 暂停
cc.audioEngine.pause(audioId)
// 恢复
cc.audioEngine.resume(audioId)
// 停止
cc.audioEngine.stop(audioId)
// 循环
cc.audioEngine.setLoop(audioId, true)
// 调整音量
cc.audioEngine.setVolume(audioId, 1)
})
}

物理系统

给一个精灵加上一个刚体即可为该节点加上物理引擎,添加组件->物理组件->Rigid Body
然后需要像开启碰撞检测一样,需要在代码中进行开启,必须写在onload中

1
2
3
onLoad(){
cc.director.getPhysicsManager().enable = true
}

开启物理碰撞盒绘制

1
2
3
4
5
6
cc.director.getPhysicsManager().debugDrawFlags = cc.PhysicsManager.DrawBits.e_aabbBit |
cc.PhysicsManager.DrawBits.e_pairBit |
cc.PhysicsManager.DrawBits.e_centerOfMassBit |
cc.PhysicsManager.DrawBits.e_jointBit |
cc.PhysicsManager.DrawBits.e_shapeBit
;

给精灵一个力或者速度

1
2
3
4
5
6
7
8
9
start(){
let rBody = this.getComponent(cc.RigidBody)
// 给一个力
rBody.applyForce(cc.v2(1000,0),cc.v2(0,0),true) //第一个参数是给X轴和Y轴上力的大小,第二个参数是给精灵哪里一个力,第三个参数是是否立即执行
rBody.applyForceToCenter(cc.v2(5000,0),true) //给中心一个力

// 给一个速度
rBody.linearVelocity = cc.v2(50,0) //给X轴上一个50像素每秒的速度
}

刚体属性

1.Enabled Contact Listener:开启碰撞检测
2.Bullet:如果刚体高速移动,可能会产生没有检测到碰撞的情况,所以需要勾选上这个框,这样系统就会检测的更加精确,但是会更加消耗性能
3.Type:默认是Dynamic,选择这项会让精灵获得重力;Static:不会受到重力以及速度的影响;Kinematic:这个类型不收到重力的影响,但是会受到速度的影响;Animated:通常与动画进行搭配。
4.Allow Sleep:允许睡眠,用于节省资源。
5.Gravity Scale:受到重力大小的影响。
6.Linear Damping:线性阻尼。
7.Angular Damping:角速度阻尼。
8.Linear Velocity:给一个速度X或Y方向上。
9.Angular Velocity:给一个角速度。
10.Fixed Rotation:保持角度不变。
11.Awake:保持刚体一直处于计算状态。

物理组件-碰撞组件

添加组件->物理组件->Collider即可添加

碰撞属性

1.Friction:摩擦力
2.Restitution:弹性系数
3.Sensor:传感器,勾选之后就不会发生碰撞了,但是会执行碰撞函数中的方法,可以作为存档点,或者触发剧情等功能的使用

开启碰撞

首先要在刚体组件中,将Enable Contact Listener勾上
然后在脚本中的操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 开始碰撞
onBeginContact(contact,self,other){
// 得到碰撞点
let points = contact.getWorldManifold().points //返回的是一个数组
// 得到碰撞点的法线
let normal = contact.getWorldManifold().normal
}

// 结束碰撞
onEndContact(contact,self,other){

}

射线

打出一条射线

1
2
3
4
5
6
7
8
9
10
11
12
13
onLoad(){
// rayCast有三个参数,第一个是打出射线的位置,第二个是射线终点的位置,第三个是返回的碰撞点的类型,Closest是返回最近的一个碰撞点,AllClosest是返回所有接触面的近点,All是返回所有的碰撞点,Any是返回一个随机的碰撞点。
let results = cc.director.getPhysicsManager().rayCast(this.node.getPosition(),cc.v2(this.node.x,this.node.y+100),cc.rayCastType.Closest)
for(let i = 0; i < results.length; i++){
let res = results[i]
// 射线碰到的碰撞器
res.collider.tag
// 射线碰到的点
res.point
// 射线碰到点的法线
res.normal
}
}

动作系统Action(已被缓动系统Tween替代)

延时动作

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
34
35
36
37
38
39
40
41
start() {
// 定义动作
// 移动
let action = cc.moveTo(2, cc.v2(200, 200)) //移动到200,200的位置
action = cc.moveBy(2, cc.v2(200, 200)) //以自己为原点,移动200,200

// 旋转
action = cc.rotateTo(2, 100) //旋转100度

// 缩放
action = cc.scaleTo(2, 1.5, 0.5) //第二个参数是X轴的缩放倍率,第三个参数是Y轴的缩放倍率

// 跳跃
action = cc.jumpBy(2, 200, 0, 200, 3) //往右跳200的位置,第一个参数是时间,第二和第三个参数是要跳的位置,第四个参数是跳的高度,第五个参数是跳的次数

// 闪烁
action = cc.blink(3, 5) //第一个参数是时间,第二个参数是闪烁的次数

// 淡出
action = cc.fadeOut(3) //时间

// 淡入
action = cc.fadeIn(3)

// 指定一个渐变
action = cc.fadeTo(3, 100) //第一个参数是时间,第二个参数是透明度,取值范围是0-255

// 改变颜色
action = cc.tintTo(3, 100, 30, 100) //第一个参数是时间,后三位是rgb值

// 执行动作
this.node.runAction(action)
// 停止动作
this.node.stopAction(action)
this.node.stopAllActions() //停止这个节点上的所有动作
action.setTag(33)
this.node.stopActionByTag(33) //通过标签停止动作
this.node.pauseAllActions() //暂停所有动作
this.node.resumeAllActions() //恢复所有动作
}

瞬时动作

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
34
start() {
// 定义瞬时动作
// 显示
let action = cc.show()
// 隐藏
action = cc.hide()
//在显示和隐藏之间进行切换
action = cc.toggleVisibility()
// 翻转
action = cc.flipX(true) //在X轴上翻转
action = cc.flipY(true) //在Y轴上翻转
// 回调动作
action = cc.callFunc(() => {
})

action = cc.fadeOut(1)
let action2 = cc.fadeIn(1)

// 容器动作
// 队列动作
let seq = cc.sequence(action, cc.delayTime(1), action2) //cc.delayTime是延时动作

// 重复动作,第二个参数是重复次数,如果使用cc.repeatForever就会一直重复
let repeat = cc.repeat(seq, 3)
// 运行队列动作
this.node.runAction(repeat)

let move = cc.moveTo(3, 500, 500)
let color = cc.tintTo(3, 100, 100, 20)

// 运行并列动作
let spawn = cc.spawn(move, color)
this.node.runAction(spawn)
}

动画系统Animation

想要使用动画功能,必须为节点添加Animation组件,然后将动画片段拖到属性检查器的Animation组件当中进行使用

Animation属性

1.Default Clip:默认片段,默认播放的片段
2.Clips:片段数,后面的数字是几,就会为我们创建几个片段框以便我们进行动画的关联
3.Play On Load:在游戏开始时进行播放

动画制作

在动画编辑器当中点击第一个按钮进行动画的制作

脚本控制

1
2
3
4
5
6
7
8
9
10
11
start() {
let ani = this.getComponent(cc.Animation)
// 播放动画
ani.play("run")
// 暂停动画
ani.pause()
// 恢复动画
ani.resume()
// 停止动画
ani.stop()
}

Canvas、Label、RichText组件

1.Canvas就是用户的主界面,可以调整高适配或者宽适配
2.Label组件->简单的文字组件
3.RichText组件,可以让各种标签包住文字,以达到各种不同的效果
如:

1
2
3
4
5
6
<color="#"></color>  //可以定义文字的颜色
<on click="事件名"></on> //当用户点击被on标签包住的文字时会触发脚本中定义好的方法
<size=33></size> //定义文字的大小
<i></i> //斜体
<outline color=#FFFFFF width=3></outline> //文字描边,可同时配置边框的颜色和宽度
<img src=""></img> //图文混排

屏幕适配与遮罩

1.遮罩:

在存在子节点的父节点上添加一个渲染组件->Mask,就可以实现遮罩效果,子节点超出父节点的部分会被隐藏,
在遮罩属性框中,Inverted是反转,勾选上就会让遮罩实现一个反向遮盖的效果
Type属性可以将遮罩的形状定义为方形或者圆形。

2.屏幕适配:

首先,所有的UI的要放在Canvas下,
然后要为进行屏幕适配的组件添加一个组件->UI组件->Widget,然后要在指定位置固定好,这样在不同的设备中,该UI节点会显示在固定的位置,不会乱动。
属性中的Target,可以将其他的节点拖拽过来,然后拖拽过来的节点就会变成名义上的父节点,然后进行相关位置计算,一般不会用到。

UI组件

button组件

直接将控件库中的button拖到场景编辑器当中,或者在层级编辑器中创建button组件,就可以生成一个button按钮。
在button的属性栏中,可以定义按钮的默认图片、鼠标移入时的图片、被点击的图片以及被禁用的图片,还可以设计这四种状态不同颜色或者不同的缩放。

为按钮绑定点击事件:
首先创建好脚本并关联在button按钮上,然后在脚本中写好要触发的方法,然后将button属性栏中的click event的数字改成要触发的事件数,
然后将button节点拖入node节点框中,然后选择脚本文件以及方法名,即可绑定好点击事件。

Layout组件

就是一个布局,可以让其中的子节点进行一定方式的排列。

ScrollView组件

是一个可以滑动的框,可以将超出ScrollView组件外的内容通过滚动条的方式进行浏览。
ScrollView属性:
1.Horizontal:将滚动条水平放置。
2.Vertical:将滚动条垂直放置。
3.Inertia:是否开启滚动的惯性。
4.Brake:从0-1进行取值,选择惯性的大小,为1的时候没有惯性。
5.Elastic:反弹效果,当滚动条到达边缘的时候,是否可以继续拖动,松开再弹回。
6.Bounce Duration:反弹的速度。
7.Scroll Events:滑动触发事件,与按钮操作相同。

拖动这个组件到场景编辑器当中,会自动创建两个子节点,其中
scrollBar是控制滚动条的相关属性。
view是控制视框中相关属性。
view中的content节点是放置文本内容的。

PageView组件

就是一个轮播图组件
可以在精灵属性中,进行每一页的颜色修改等。

ProgressBar组件

就是一个进度条组件
ProgressBar属性:
1.Mode:调整进度条的方向。
2.Total Length:当下面的Progress为0-1时,对应的进度条长度是多少,一般当Progress为1的时候,将此项属性的长度设置为父组件的长度。
3.Reverse:反向。

EditBox组件

就是一个输入框
EditBox节点:
1.TEXT_LABEL:用户输入的文本。
2.PLACEHOLDER_LABEL:默认显示的文本,即占位符,当用户未在输入框输入时,或者将输入框中的文字全部删除之后,就会显示本节点的内容。

EditBox属性:
1.String:输入的文本。
2.Background Image:设置背景。
3.KeyboardReturnType:在手机上,与电脑无关,点击输入框之后,弹出键盘,然后右下角的按键样式。
4.input Flag:输入的标识,比如密码域等。
5.input Mode:单行\多行\邮箱\数字等对键盘样式的控制。
回调:
1.Editing Did Began:开始编辑的时候调用指定的回调;
2.Text Changed:文本发生改变就会调用指定的回调;
3.Editing Did Ended:编辑模式结束(失去焦点)的时候调用指定的回调;
4.Editing Return:按下回车,就会调用指定的回调。

Slider组件

是一个用户可以滑动的一个条,比如将可以用来做音量条,进度条等。

属性:
Slide Events:触发滑动的回调事件。

Toggle组件

是一个单选框,如果将多个Toggle组件置于一个节点之下,然后为这个节点添加一个Toggle Container组件,就可以实现多个项目单选的操作了。

VideoPlayer组件

可以播放本地或者远程的视频,在代码中播放和音频操作一致,这个组件的延迟很大,一般不要使用。

WebView组件

打开指定的网页。

骨骼动画

使用代码控制骨骼动画

1
2
3
4
// 得到骨骼组件
let skeletonAnimation = this.node.getComponent(sp.Skeleton);
// 为骨骼组件设置动画,第一个参数是播放动画的管道,第二个参数是动画名字,第三个参数是是否重复
skeletonAnimation.setAnimation(0, "animation", false)

数据存储

以将键值对的方式将数据存储在本地内存当中。

1
2
3
4
5
6
7
8
9
10
11
12

// 保存数据
cc.sys.localStorage.setItem("key","value")

// 读取数据
cc.sys.localStorage.getItem("key")

// 删除指定数据
cc.sys.localStorage.removeItem("key")

// 清空所有数据
cc.sys.localStorage.clear()

数据格式-JSON

Json是一种数据格式,还有xml,csv等。

使用JSON.stringify()将对象转换为JSON格式,即Json字符串,然后就可以存入本地缓存当中在需要的时候读取出来,通过JSON.parse()转换为对象就可以正常使用了,常用于保存关卡数据、等非临时数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person{
name:string
id:number
sex:string
}

// 实例化一个对象
let person:Person = new Person()
person.id=1
person.name="小红"
person.sex="男"

// 将该对象序列化为JSON对象(就是字符串)
let json = JSON.stringify(person)

// 将JSON对象转换为普通对象
let save:Person = Object.assign(new Person(),JSON.parse(json))

网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
start() {
let url = "http://t.yushu.im/v2/movie/in_theaters?apikey=0b2bdeda43b5688921839c8ecb20399b"

let request = cc.loader.getXMLHttpRequest()
request.open("GET", url, true) //第三个参数代表是否为异步操作
request.onreadystatechange = () => {
// 请求状态改变
if (request.readyState == 4 && request.status == 200) {
console.log("请求完成了");
// 请求的资源就是response.responseText
console.log(request.responseText);
}
}
// 使用send发送请求
request.send()
}

Socket.io

安装好node.js环境之后,使用npm install –save express
和npm install –save socket.io即可安装好socket.io插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 服务端myServer
var app = require('express')()
var http = require('http').Server(app)
var io = require('socket.io')(http)

// 在某个端口开始监听
http.listen(3000, function () {
console.log('server listen on 3000')
})

// 监听与客户端的连接
io.on('connection', function (socket) {
// 监听消息
// socket.emit('message', '连接成功了')
// 监听客户端发来的消息
console.log("有客户端连接")
socket.on('message', function (data) {
console.log('客户端发来消息' + data)
})
})

如何运行服务端脚本:
在cmd中进入到脚本文件所在位置,然后使用node myServer.js即可运行服务端脚本文件。

项目中经验

动态修改刚体的重力,以控制静止或者受重力下落

在编辑器中初始设置刚体为static则可以让其保持静止,然后经过每种操作让其受重力下降

1
this.node.getComponent(cc.RigidBody).type = cc.RigidBodyType.Dynamic

强制类型转换

1
GameModel._ins.posArr[i] = (this.node.name as any) as number

使用json文件画格子

  1. 首先创建一个map.json的文件,在其中编写行列的信息,如下所示为九行五列
1
2
3
4
5
6
7
8
9
{
"map":[
[1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1]
]
}
  1. 然后在MainGame.ts文件中创建格子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 先在开头声明引入的属性类型
@property(cc.Asset)
map: cc.Asset = null
// 创建一个数组用于存放所有格子位置
posArray: cc.Vec2[] = []
// 画格子,并将格子的位置信息存入数组中
mapBorn() {
let map = this.map.json.map
console.log(map);
for (var i = 0; i < map.length; i++) {
for (var j = 0; j < map[i].length; j++) {
if (map[i][j] == 1) {
let block = Tools.newPrefab("Block", this.node)
block.x = -240 + 120 * i
block.y = -500 + 120 * j
this.posArray.push(cc.v2(block.position))
block.destroy()
}
}
}
console.log(this.posArray);
}
  1. 使用随机数取得数组中格子的位置信息,并且使用完之后将对应格子的位置删除
1
2
3
4
5
let num = Math.floor(Math.random() * GameModel._ins.mainGame.posArray.length)
eagle.x = GameModel._ins.mainGame.posArray[num].x + 360
eagle.y = GameModel._ins.mainGame.posArray[num].y + 640
eagle.angle = Math.random() * 360
GameModel._ins.mainGame.posArray.splice(num, 1)
  1. 然后再进入下一关的时候再重新生成一次格子

生成脚印覆盖住父节点的情况

因为物体生成的脚印必须是保留在原地的,所以不能让脚印绑定在父节点的下面,否则脚印会随着父节点的移动而移动,但是如果直接将脚印绑定在根节点,又会让脚印覆盖到物体的上方,这显然也是不符合逻辑的。

解决办法:在根节点创建一个节点,并将节点置于最上方,然后让父节点生成脚印之后,将脚印的父节点绑定到该节点,就可以完美解决上面的两个问题。

让碰撞体的框体显示出来

1
2
3
cc.director.getCollisionManager().enabled = true
cc.director.getCollisionManager().enabledDebugDraw = true;
cc.director.getCollisionManager().enabledDrawBoundingBox = true;

动态加载图片资源,节省预制体的制作数量

需要把动态加载的图片资源放入resources文件夹下

1
2
3
4
5
6
7
8
9
// 加载飞机爆炸的图片
cc.loader.loadRes("enemy0_die", cc.SpriteFrame, (err, res) => {
this.node.getComponent(cc.Sprite).spriteFrame = res
cc.director.getCollisionManager().enabled = false
setTimeout(() => {
this.node.destroy()
cc.director.getCollisionManager().enabled = true
}, 300);
})

在属性框中生成一个预制体\节点数组列表

1
2
@property([cc.Prefab])
public PrefabArr: Array<cc.Prefab> = [];

效果:
效果

开启一个间隔时间不确定的计时器

1
2
3
4
5
6
7
8
timer() {
//用于确定取随机时间的范围
let time: number = Tools.random(3, 5, false);
this.scheduleOnce(() => {
console.log("do something...");
this.timer();
}, time);
}

声明二维数组

声明一个存放节点坐标的二维数组,因为没有初始化,所以后续进行push操作的时候会报错。
目前的一种解决办法:先把外层数组的长度确定了。

1
2
// 创建一个二维数组用于存放所有格子位置
posArr: Array<Array<cc.Vec2>> = [[], [], [], [], []]

在碰撞回调中修改节点的属性会报错

比如在开启物理碰撞之后,在碰撞回调中修改节点的角度属性,会报错,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
onBeginContact(contact, self, other) {
// 皮筋下落碰到边界且自身没有进入队列
if (other.tag == 10) {
setTimeOut(() => {
if (GameModel._ins.gameState) {
// 下面这行代码会报错
this.node.angle = 90
this.play()
}
}, 10);
if (!this.isInQuery) {
this.isInQuery = true
setTimeout(() => {
cc.find("/Canvas/bgSpr").getComponent(bgController).query.push(this.node)
}, 200);
}
}
}

解决办法,让修改节点属性的代码延迟执行即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
onBeginContact(contact, self, other) {
// 皮筋下落碰到边界且自身没有进入队列
if (other.tag == 10) {
// 在碰撞回调中修改属性会报错,延迟执行即可
setTimeout(() => {
if (GameModel._ins.gameState) {
this.node.angle = 90
this.play()
}
}, 10);
if (!this.isInQuery) {
this.isInQuery = true
setTimeout(() => {
cc.find("/Canvas/bgSpr").getComponent(bgController).query.push(this.node)
}, 200);
}
}
}

spine动画关闭了alpha预渲染,透明地方还是有色块

解决办法:在资源压缩的地方,将存放skeleton数据的文件夹排除掉即可。

摘下满天果点击物体下落到指定位置进行三消的解决思路

当点击天上的物体之后,将其按顺序压入数组中,当做栈来使用,每次按顺序选中水果的时候就可以直接使用弹栈(数组中的第一个元素)进行操作,这样就可以按照顺序拿到操作的水果。

动态更新碰撞盒大小

1
2
otherNode.getComponent(cc.PhysicsBoxCollider).size = cc.size(214, 323);
otherNode.getComponent(cc.PhysicsBoxCollider).apply();

更改完尺寸之后一定要调用碰撞盒.apply方法,这样才会修改生效。

刚体碰撞回调函数没有持续检测

如果想要让两个紧贴的刚体重新开始碰撞,则让其中一个刚体的碰撞盒大小发生改变即可,记得使用apply方法实现。
这样他们两个之间的碰撞函数就会重新调用。

左右移动物体时有一种缓动的效果

1
2
3
4
5
6
// 通过触摸回调实时获取位置,以施加力的方向,在此处可以同时加上左右边界,控制可以移动的范围
let delta = e.getDeltaX();
this.touchTargetP = this.touchTargetP.add(cc.v2(delta * 1.3, 0));
// 然后在update函数中实时施加对应的力
this.node.getComponent(cc.RigidBody).linearVelocity =
cc.v2(this.touchTargetP.sub(this.node.getPosition()).mul(1250 * dt).x, 0);

动态修改节点的分组信息

获取\修改节点分组:this.node.group;

1
2
3
4
5
6
7
8
9
// 在结束碰撞之后,将sensor关闭,同时修改自己的节点分组,用于正常碰撞
onEndContact(contact, self, other): void {
// 发射水果之后修改水果的分组
if (self.tag == 3 && self.node.group == "shootFruit" && other.node.group == "default") {
self.node.group = "commonFruit";
this.innerCircle.sensor = false;
this.innerCircle.apply();
}
}

生成一个n等分圆(同时可以在随机范围内进行波动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 /**
* 以某点为圆心,生成圆周上等分点的坐标
*
* @param {number} r 半径
* @param {cc.Vec2} pos 圆心坐标
* @param {number} count 等分点数量
* @param {number} [randomScope=80] 等分点的随机波动范围
* @returns {cc.Vec2[]} 返回等分点坐标
*/
getCirclePoints(r: number, pos: cc.Vec2, count: number, randomScope: number = 60): cc.Vec2[] {
let points = [];
let radians = (Math.PI / 180) * Math.round(360 / count);
for (let i = 0; i < count; i++) {
let x = pos.x + r * Math.sin(radians * i);
let y = pos.y + r * Math.cos(radians * i);
points.unshift(cc.v3(x + Math.random() * randomScope, y + Math.random() * randomScope, 0));
}
return points;
}

生成一个自动居中的格子地图

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
createMap(): void {
this.initPosArr();
// 根据行列数将整体格子进行居中
let deltaY: number = (-this.line * this.blockSize + this.blockSize) / 2;
let deltaX: number = (-this.line * this.blockSize + this.blockSize) / 2
for (let i: number = 0; i < this.line; i++) {
for (let j: number = 0; j < this.line; j++) {
let block: cc.Node = Tools.newPrefab("block", this.mapNode);
block.width = this.blockSize;
block.height = this.blockSize;
block.x = deltaX + this.blockSize * i;
block.y = deltaY + this.blockSize * j;
block.getChildByName("label").getComponent(cc.Label).string = i + "," + j;
this.blockInfo[i][j] = new Block(i, j, cc.v2(block.getPosition()));
// block.destroy();
}
}
// console.log(this.blockInfo);
}

initPosArr(): void {
this.blockInfo = [];
for (let i: number = 0; i < this.line; i++) {
this.blockInfo.push([]);
for (let j: number = 0; j < this.line; j++) {
this.blockInfo[i].push(null);
}
}
}

缓动系统的精细应用

对数字增长进行缓动,比如数字的增长动画,将数字作为对象的属性,然后对对象进行缓动操作。
同时添加onUpdate事件,可以在缓动的同时进行相应的更新(比如更新Label的内容)。

注意:在onUpdate函数中,this指代Window,而不是当前的脚本,需要在外层使用that来记录一下当前的this指向。

1
2
3
4
5
6
7
8
9
10
11
12
let currentNum = {
a: 0
}
let that: MainCtrl = this;
cc.tween(currentNum)
.to(0.8, { a: 100 }, {
onUpdate() {
that.furniture.getChildByName("progress").getComponent(cc.Label).string = Math.floor(currentNum.a).toString();
that.furnitureMask.getComponent(cc.Sprite).fillRange = 1 - (Math.floor(currentNum.a) / 100);
}
})
.start()

在屏幕中央或其他节点中使图片样式的(关卡、分数)与Label样式的数字共同实现居中

创建一个含有Layout组件的父节点,并且将类型设置为container,然后将图片和label节点拖入即可,随着label的长度的改变,图片和文本会自动向两侧移动,实现整体的居中。

抖音平台相关开发经验

检测用户在切换游戏到前台时是否是从侧边栏进入小游戏(用于特定游戏发放游戏奖励)

注意:这个方法类似于检测触摸,所以无法检测到用户第一次启动游戏,只能用于游戏从后台切换到前台的时候进行检测。并且调用该方法要在ooLoad方法当中,尽早的开启检测。

1
2
3
4
5
6
tt.onShow(op => {
// op是返回的包含用户的场景值等信息用于判断用户是从哪里进入游戏的。
if (op.scene == "021036" || op.scene == "021001") {
// 021036是用户从侧边栏进入的游戏,021001是用户从个人中心的侧边栏进入的小游戏。
}
})

检测用户启动游戏时(非后台切换到前台)的启动场景

1
2
// 获得用户冷启动小游戏时的场景值
tt.getLaunchOptionsSync().scene;

标签外挂提示块标签

默认 提示块标签

default 提示块标签

primary 提示块标签

success 提示块标签

info 提示块标签

warning 提示块标签

danger 提示块标签