cocos进阶【飞机大战】全流程【代码-笔记】

写完后的心得

用来写安卓小游戏和网页小游戏还是蛮不错的,但cocos似乎已经放弃电脑端了,各种报错,全网翻了个遍也没看几个靠谱的方案,不好评价

开始游戏的ui界面

1引入素材之中的背景图片【backgroud】,放置在画布节点下方。

2选择拖入的图片节点,在右侧的属性检查器查看长与宽。

3在项目设置-》项目数据-》设计宽度/高度,进行调整为图片节点的大小。

4添加logo,在资源管理器找到logo并拖入

5把图片【game_loaing1】拖入。

6把图片创建帧动画,选择图片-》创建动画组件-》添加动画剪辑资源-》属性列表-》cc.sprite-》sprite frame。

7把【game_loding1~4】拖放在动画剪辑资源轴里面。

8再次添加移动动画-》属性列表-》postion,使其移动时候并播放动画。

9使动画速度减少,在动画编辑器右下角,更改其播放速度。勾选加载后播放,记得保存。

10创建一个开始按钮,使其大小合适。

点击按钮跳转场景

1创建一个新的场景

2创建一个脚本,并挂在到场景下

3在脚本上创建一个公共的(public)函数,内部是Director.loadscene(“新的场景”)

oncilike(){

        director.loadScene("scene-2d");

    }

  • director:它是 cc.director 的引用,cc.director 是 Cocos Creator 里的导演对象,其职责类似于电影导演,掌控着游戏里场景的切换、游戏的运行和暂停等操作。
  • loadScene:这是 cc.director 对象的一个方法,专门用于加载新的场景。
  • "scene-2d":这是要加载的场景的名称。

4返回场景编辑器,对按钮的属性进行编辑,使其激活脚本的的公共函数。

添加游戏背景,控制背景移动。

1创建一个node节点,拖入背景图片,复制一次,使两个背景图片一上一下,下面的那张在摄像机完全。

2创建一个脚本,挂在到node节点下。

3使脚本引入两个背景图片。

@property(Node)

    public bg1:Node=null;

    @property(Node)

    public bg2:Node=null;

4设置一个值为速度,并属性化

 @property(Number)

    public sp:number=null;

5让每帧图片向下运动,在update里面进行修改。

update(deltaTime: number) {

let bei1=new Vec3();

        bei1 = this.bg1.position;

        // let bei1 = this.bg1.position;

        this.bg1.setPosition(bei1.x,bei1.y-this.sp*deltaTime,bei1.z);

}

let bei1=new Vec3();

bei1 = this.bg1.position;

这两句的意思是创建一个变量,用来存储当前bg1(之前属性化的图片)的位置。

 this.bg1.setPosition(bei1.x,bei1.y-this.sp*deltaTime,bei1.z);

改变bg1坐标,使其每帧执行一次向下运动(距离等于速度×时间)

注释掉的是原本教程的方法,我认为我写的更好便替换了。

在 JavaScript 里,对象是按引用传递的。如果你将 this.bg1.position 赋值给一个变量,后续对这个变量的修改可能会影响到 this.bg1.position 本身(因为它们指向同一个对象)。

但原本教程的方法,可读性更好,但我拆解的很细很细,所以就不用这种方法了。

6给第二张图片也添加移动,让背景循环动起来。

    let bei2=new Vec3();

        bei2 = this.bg2.position;

        // let bei1 = this.bg1.position;

        this.bg2.setPosition(bei2.x,bei2.y-this.sp*deltaTime,bei2.z);

---以上代码是让第二张添加移动---

let p1 =this.bg1.position;

        let p2 =this.bg2.position;

        if(this.bg1.position.y<-852){

            this.bg1.setPosition(p2.x,p2.y+852,p2.z);

        }

        if(this.bg2.position.y<-852){

            this.bg2.setPosition(p1.x,p1.y+852,p1.z);

        }

创建两个变量保存图片位置。每帧进行两个逻辑判断,如果位置小于-852,改变位置为另一个图片的y坐标的+852像素的地方。

首先图片的长为852,两个图片紧挨着,当一个图片坐标为-852,那么另一个图片正好完全出现在屏幕中,这时候该图片会瞬移到另一张图片的上方。另一个图片到达该位置时候也是如此。

至此,两个图片实现了循环播放的效果。

我的个人想法

if(this.bg1.position.y<-852){

            this.bg1.setPosition(bei2.x,bei2.y+852,bei2.z);

        }

        if(this.bg2.position.y<-852){

            this.bg2.setPosition(bei1.x,bei1.y+852,bei1.z);

        }

我的方法,代码更加简洁,但依赖前置代码逻辑理解稍难

Siki老师的方法逻辑清晰,数据独立,但代码冗余。

我在网上又找到了别的方法

 for (let item of this.node.children) {

            // 使用getPosition获取坐标信息

            const { x, y } = item.getPosition();

            // 计算移动坐标

            const moveY = y - 100 * deltaTime;

            item.setPosition(x, moveY);

            // 如果超出屏幕 重新回到顶部,也就是当前位置加上两倍的高度

            if (moveY < -852) {

              item.setPosition(x, moveY + 852 * 2);

            }

          }

这些都是放在update函数里面

For (...of...)

js之中的for遍历数组,与php之中的foreach相似。

for (let item of this.node.children)意思是,创建一个item变量,循环遍历 this.node.children(当前节点的所有孩子)

在 for...of 循环中,let item of this.node.children 这行代码把 this.node 的每个子节点依次赋值给变量 item。这里的 item 其实就是对当前子节点的一个引用,这里指的是两者都指向一个内存地址

 const { x, y } = item.getPosition();

运用了解构赋值的语法,从 item.getPosition() 方法返回的对象中提取 x 和 y 属性,并分别赋值给常量 x 和 y。

item.getPosition() 方法返回的是一个 cc.Vec3 对象,它代表节点在三维空间中的位置,包含 x、y、z 三个属性。不过这里只提取了 x 和 y 属性

--思考再定义--

 const { x, y } = item.getPosition();既然是把里面的值给与常量x,y,那么可否把常量x,y替换成a,b。

不可以

如果直接写 const { a, b } = item.getPosition();会报错

在 JavaScript 中,对象解构赋值是根据对象的属性名来匹配并赋值的。当使用 const { a, b, c } = item.getPosition(); 这样的语法时,JavaScript 会尝试从 item.getPosition() 返回的对象中寻找属性名为 abc 的属性,并将这些属性的值赋给对应的变量。

然而,item.getPosition() 返回的是一个 cc.Vec3 对象(在 Cocos Creator 中),这个对象的属性名是 xyz,并没有 abc 这些属性。所以如果直接写 const { a, b, c } = item.getPosition(); ,变量 abc 会被赋值为 undefined

而 const { x: a, y: b, z: c } = item.getPosition(); 这种写法使用了对象解构赋值的重命名语法。这里的 x: a 表示从 item.getPosition() 返回的对象中取出 x 属性的值,并将其赋值给变量 a

--思考结束--

const moveY = y - 100 * deltaTime;,创建一个常量 moveY,并让其等于常量y(已经去除当前节点的坐标值)减去100*deltaTime。

也就是速度乘以时间。

deltaTime;是一帧的时间,update函数里面的数值。

这个一百并不好,如果替换为可以替代的速度具体值就可以了,但考虑扩展太多,现在不写了。

item.setPosition(x, moveY);

更改item节点的位置,移动位置为x,movey。

if (moveY < -852) {

              item.setPosition(x, moveY + 852 * 2);

            }

这里和之前的差不多,目的都是为了回正图片。不过之前的例子是把852换成正在摄像机下的那个图片的坐标了。

--思考再定义

在二次复刻的过程中我想到了新的解决办法,不过更加的麻烦了,但是自己思考的,对于cocos的理解加深了。

 update(deltaTime: number) {

        for (let item of this.node.children) {

            let wz = item.position.clone();

            wz.y -= 10;

            item.setPosition(wz); // 将修改后的位置重新赋值给节点

            if(wz.y<-852){

                wz.y+=2*852;

                item.setPosition(wz);

            }

        }

    }

一开始还是通过for (let item of this.node.children) 来把子节点全部遍历出来。

let wz = item.position.clone(); 创建一个变量来存储当前遍历出来的子节点的位置。

这里犯了一个错误,当时我想直接wz=item、postion的,但是系统报错。

在 Cocos Creator 中,item.position 返回的对象的属性(如 x 和 y)被设计为只读属性,这意味着不能直接对其进行赋值修改操作。

可以使用 clone 方法创建对象的一个副本,对副本进行修改后再使用。

然后就把这副本的值传递回去,更改当前遍历出来的节点位置,每帧执行一次,两个图片节点就实现了往下走的效果。

在通过if来保证到底下在返回上面的位置,确保能够一直移动。

拓展一下.clone,除了这个方法还可以使用别的方法来实现。

for (let item of this.node.children) {

            // let wz = item.position.clone();

            // wz.y -= 10;

            // item.setPosition(wz); // 将修改后的位置重新赋值给节点

            let currentPos = item.position; // 获取当前位置

            let wz = currentPos.y - 10; // 计算新的 y 坐标

            item.setPosition(currentPos.x, wz); // 设置新的位置

            if(wz<-852){

                wz+=2*852;

                item.setPosition(currentPos.x, wz);

            }

        }

首先获取节点的当前位置 currentPos,然后计算出新的 y 坐标wz,最后使用 setPosition 方法将新的位置设置给节点。

--思考结束--

添加主角主机和动画

  1. 创建一个空节点,在空节点上挂载精灵图节点(“hero1”);
  2. 在精灵图节点创建帧动画,让其动起来。具体方法与开头的ui方法一致。
  3. 在动画编辑器左下角设置循环模式为循环播放。勾选加载后播放。

控制主角移动以及设置边界

  1. 创建脚本,挂载到之前的空节点上面。

   protected onLoad(): void {

       input.on(Input.EventType.TOUCH_MOVE,this.ontouchu,this);

   }

   protected onDestroy(): void {

    input.off(Input.EventType.TOUCH_MOVE,this.ontouchu,this);

}

   ontouchu(evet:EventTouch){

    const pp=this.node.position;

    this.node.setPosition(pp.x+evet.getDeltaX(),pp.y+evet.getDeltaY(),pp.z);

   }

protected onLoad(): void

protected 关键字:在 TypeScript 里,protected 是访问修饰符,表明该方法只能在类本身及其子类中被访问。

onLoad() 生命周期函数:onLoad 是 Cocos Creator 组件的生命周期函数之一,当组件所在的节点被加载时,这个函数会自动执行。通常会在 onLoad 里进行一些初始化操作。

onDestroy() 生命周期函数:onDestroy 是 Cocos Creator 组件的另一个生命周期函数,当组件所在的节点被销毁时,这个函数会自动执行。通常会在 onDestroy 里进行一些清理操作。

.on 方法:该方法用于为指定的事件类型绑定一个回调函数。

.off 方法:该方法用于解除之前绑定的事件。它接收的参数和 on 方法相同

--思考再定义--

Start()与onLoad() 区别

我们可以把开发一个游戏场景想象成举办一场大型音乐会,组件就像是音乐会上的各种乐器和设备,onLoad 和 start 则是音乐会筹备和开场过程中的不同阶段

onLoad 阶段:后台准备

音乐会场景类比:onLoad 就相当于音乐会正式开场前,工作人员在后台进行的一系列准备工作。这个时候,场地已经确定好了(节点加载完成),所有的乐器和设备(组件)都已经搬运到了现场,但观众还没入场,舞台也还没布置好,音乐会还没有正式开始。

代码操作类比:在这个阶段, onLoad 函数里进行的资源初始化、变量赋值、事件监听绑定等操作。例如,给吉他调好音(变量赋值)、连接好麦克风和音响设备(事件绑定),这些准备工作必须在音乐会开始前完成,以确保后续演出的顺利进行。

start 阶段:开场准备与演出前奏

音乐会场景类比:start 就好比是音乐会正式开场前的最后准备阶段,此时观众已经入场就座,舞台灯光亮起,主持人即将宣布音乐会开始。这个时候,乐队成员要根据现场的氛围和舞台的实际情况,对乐器进行最后的微调,然后开始演奏开场曲目。

--思考结束--

ontouchu(evet:EventTouch){   }

传递进来一个eventtouchu的类型,形参为event

const pp=this.node.position;

把当前节点的坐标传给pp

this.node.setPosition(pp.x+evet.getDeltaX(),pp.y+evet.getDeltaY(),pp.z);

更改节点坐标位置,evet.getDeltaX() 是 EventTouch 对象的一个方法,用于获取从上次触摸事件到当前触摸事件在 x 轴方向上的偏移量。

--思考再定义--

 我又想到了两种方法实现同样的效果

// ontouchu(evet:EventTouch){

//     this.node.setPosition(this.node.position.x+evet.getDeltaX(),this.node.position.y+evet.getDeltaY(),this.node.position.z);

// }

// ontouchu(event: EventTouch) {

//     const pp = this.node.position;

//     const delta = new Vec3(event.getDeltaX(), event.getDeltaY(), 0);

//     const newPosition = new Vec3();

//     Vec3.add(newPosition, pp, delta);

//     this.node.setPosition(newPosition);

// }

原来方法:使用临时变量 pp 存储 this.node.position,可以让代码更具可读性。特别是在复杂的逻辑中,临时变量能够更清晰地表达代码的意图,让其他开发者更容易理解代码的作用。例如,如果后续还需要多次使用节点的位置信息,使用临时变量可以避免重复调用 this.node.position,使代码更加简洁明了。

方法一:直接使用 this.node.position 虽然代码更简短,但在复杂的表达式中可能会让代码显得冗长和混乱,降低代码的可读性。

方法二:结合了前两者的部分优点,使用临时变量 pp 存储当前节点的位置,提高了代码的可读性,同时避免了创建多个 Vec3 对象,代码相对简洁,性能也较好。通过临时变量 pp,代码的逻辑更加清晰,开发者可以很容易地理解代码的意图。

扩展性有限:虽然比方法二的可读性有所提高,但在处理复杂的向量运算场景时,仍然不如方法一易于扩展。

2限制飞机的移动距离,对之前代码进行更改,用vec3类型存储,用if限定条件。

注意子节点与父节点的位置,以及相对位置和绝对位置的基础知识。

ontouchu(evet:EventTouch){

    const pp=this.node.position;

    let weizhi = new Vec3(pp.x+evet.getDeltaX(),pp.y+evet.getDeltaY(),pp.z);

   

    if(weizhi.x<-230){

        weizhi.x=-230;

    }

    if(weizhi.x>230){

        weizhi.x=230;

    }

    if(weizhi.y>380){

        weizhi.y=380;

    }

    if(weizhi.y<-380){

        weizhi.y=-380;

    }

    this.node.setPosition(weizhi);

   }

设计一个vec3类型的变量weizhi,来存储当前向量与改变向量相加的结果。

然后进行四个判断,使其固定在一定范围内,最后移动其位置。

--思考再定义--

将多次if进行封装

function clamp(value: number, min: number, max: number): number {

    return Math.min(Math.max(value, min), max);}

ontouchu(event: EventTouch) {

    const pp = this.node.position;

    let weizhi = new Vec3(pp.x + event.getDeltaX(), pp.y + event.getDeltaY(), pp.z);

    weizhi.x = clamp(weizhi.x, -230, 230);

    weizhi.y = clamp(weizhi.y, -380, 380);

this.node.setPosition(weizhi);}

函数部分不能直接放在export class konghzi extends Component {}里面,在外面定义该函数。

function clamp(value: number, min: number, max: number): number {}

创建一个clamp函数,有三个形参,数值类型都为number。返回值也为number

return Math.min(Math.max(value, min), max);}

返回一个值,这行代码的主要作用是将一个数值 value 限制在 min 和 max 所指定的范围内,也就是实现数值的 “夹紧” 操作。

Math.max() 是 JavaScript(TypeScript 基于 JavaScript)内置的一个函数,它接受多个数值参数,并返回其中最大的那个数值。

Math.min()

Math.min() 同样是 JavaScript 内置的函数,它接受多个数值参数,并返回其中最小的那个数值。

当然,用if也可以实现。

function clamp(value: number, min: number, max: number): number {

    if(value<min){

    return min;

  }else if(value>max){

    return max;

  }else{

    return value;

  }

}

--思考结束--

制作子弹的预制体

1将资源管理器之中的bullet1放在层级管理器之中。

2创建一个脚本,将脚本挂载到bullet1所成为的精灵图节点上。

3属性化脚本里面的速度变量。让每帧移动一定的距离。

 @property(Number)

    public sudu:number=500;

使速度属性化。

之后让每帧移动一定距离

    update(deltaTime: number) {

       

        let dangq=this.node.position;

        this.node.setPosition(dangq.x,dangq.y+this.sudu*deltaTime,dangq.z);

       

    }

然后在资源管理器之中创建一个文件夹,把层级管理器之中的子弹拖入进去,这就成为了预制体。

将层级管理器之中的预制子弹复制一份,更改其 sprite frame为bullte2,速度更改600.

然后将其变成预制体,拖入文件夹之中。

4将层级管理器之中的两个预制体子弹删除,保留资源管理器之中的子弹预制体。

开发第一种子弹的发射功能

1创建一个空节点,挂载到之前创建的飞机节点上。

之前的飞机节点,是把图片和脚本分开,将空节点上挂在脚本,在空节点内部挂在精灵图飞机,这样实现了控制与显示的分离。

本次的空节点在飞机的空节点之下

进入飞机脚本,创建一个属性化的”攻击频率“,这将利用到update。

 @property(Number)

   gongjipinlv:number=0.5;

还需创建一个计时器,用来

    @property(Number)

    jishiqi:number=0;

去引用预制体子弹

 @property(Prefab)

    zidan:Prefab=null;

在画布(canvas)创建一个空的节点,让后期实例化的子弹都在此节点里面。

之后回到飞机脚本,让对其持有引用。

   @property(Node)

    bultikun:Node=null;

在去飞机节点下方创建一个空节点,用来让子弹定位。

 @property(Node)

    bultpotion:Node=null;

之后就是让定期生成子弹了,这需要在update里面,让其定期生成子弹。

update(deltaTime: number) {

    // 累加计时器的值,deltaTime 表示从上一帧到当前帧所经过的时间

    this.jishiqi += deltaTime;

    // 检查计时器的值是否超过攻击频率

    if (this.jishiqi > this.gongjipinlv) {

        // 若超过攻击频率,将计时器重置为 0

        this.jishiqi = 0;

        // 实例化子弹预制体,创建一个新的子弹节点

        const zi = instantiate(this.zidan);

        // 将新创建的子弹节点添加到子弹容器节点中

        this.bultikun.addChild(zi);

        // 设置子弹的世界位置为指定位置

        zi.setWorldPosition(this.bultpotion.worldPosition);

    }

}

worldPosition 属性

在游戏开发里,每个节点都有位置信息,而位置信息通常分为局部位置和世界位置:

局部位置(Local Position):是相对于该节点的父节点的位置。

世界位置(World Position):是相对于整个游戏世界坐标系的位置。

--思考再定义--

  update(deltaTime: number) {

        this.jishiqi += deltaTime;

        if (this.jishiqi > this.gongjipinlv) {

            this.jishiqi = 0;

            const bullet = instantiate(this.zidan);

            bullet.setParent(this.node.parent);

            const offset = new Vec3(0, 60, 0);

            bullet.setPosition(Vec3.add(new Vec3(), this.node.position, offset));

        }

    }

我的想法和教程有些许不同,我打算直接生成子弹。

const bullet = instantiate(this.zidan);

instantiate 是 Cocos Creator 提供的一个函数,用于根据预制体创建一个新的节点实例。

this.zidan 是我们在类属性中定义的子弹预制体,通过 instantiate 函数可以创建一个新的子弹节点。

bullet.setParent(this.node.parent);

setParent 方法用于设置节点的父节点。

将子弹节点的父节点设置为当前节点的父节点,这样可以避免子弹节点跟随当前节点移动,而是以相同的父节点坐标系来定位。

const offset = new Vec3(0, 60, 0);

Vec3 是 Cocos Creator 中用于表示三维向量的类,这里创建了一个偏移向量 offset,表示子弹相对于当前节点在 y 轴正方向上偏移 60 个单位。

bullet.setPosition(Vec3.add(new Vec3(), this.node.position, offset));

Vec3.add 方法用于将两个向量相加,这里将当前节点的位置向量 this.node.position 和偏移向量 offset 相加,得到子弹的最终位置。

new Vec3() 是一个临时的空向量,作为 Vec3.add 方法的第一个参数,用于存储相加后的结果。

最后使用 setPosition 方法将计算得到的位置应用到子弹节点上。

Ai修改了我的代码,一开始我的代码有一些逻辑错误。

节点父子关系与坐标系统

在 Cocos Creator 里,每个节点都有自己的坐标系统,子节点的位置是相对于其父节点来确定的。当你改变父节点的位置、旋转或者缩放时,子节点会跟随父节点进行相应的变换。

代码中不同设置的影响

1. zi.setParent(this.node);(原来的代码)

在你原来的代码里,使用的是 zi.setParent(this.node);,这意味着子弹节点成为了当前节点(也就是控制移动的那个节点)的子节点。那么子弹的位置就是相对于当前节点来确定的。当你移动当前节点时,子弹节点会跟着一起移动,因为它的位置是基于父节点的相对位置。这样一来,就可能出现子弹看起来 “乱飘” 的情况,尤其是当你期望子弹在一个固定的全局坐标系中发射时。

2. bullet.setParent(this.node.parent);(优化后的代码)

优化后的代码使用 bullet.setParent(this.node.parent);,把子弹节点的父节点设置为当前节点的父节点。这就使得子弹和当前节点处于同一层级,它们的位置都是相对于同一个父节点(通常是场景根节点)来确定的。这样,当你移动当前节点时,子弹不会受到当前节点移动的影响,而是会在全局坐标系中按照你设定的规则发射,避免了因为父子关系导致的位置异常。

也就是说,当我把bullet.setParent(this.node)时候,移动飞机,发射出去的子弹也会更这一起移动。

// Emm,以后如果想做那种肉鸽玩法的游戏似乎可以

但我写的bug不只是如此

 update(deltaTime: number) {

        this.jishiqi+=deltaTime;

        if(this.jishiqi>this.gongjipinlv){

            this.jishiqi=0;

            const zi= instantiate(this.zidan);

            zi.setParent(this.node.parent);

            let pp=this.node.position;

            zi.setPosition(pp.x,pp.y+60,pp.z);

        }

       

    }

zi.setParent(this.node.parent);看样子正常运行,没有bug,但zi.setParent(this.node);时候bug就出现了,弹道不稳,到处乱飞...

子弹乱飘的原因

在 Cocos Creator 中,子节点的位置是相对于其父节点的。当你使用 zi.setParent(this.node); 时,子弹节点成为了飞机节点(this.node)的子节点。这意味着子弹的位置会随着飞机节点的移动、旋转等变换而改变。

弹道不在飞机正中心的原因

子弹的初始位置是相对于其父节点(也就是飞机节点)来设置的。飞机节点自身可能有锚点(Anchor Point)和位置的偏移,这会影响子弹相对于飞机的实际位置。

--思考结束--

双发模式

1定义一个枚举类型

enum shoottype{

onshoot,

towshoot

};

2属性化一个值为该枚举类型的变量。

    @property

    sootype:shoottype=shoottype.onshoot;

3在update函数里面创建一个swich语句,当一发子弹状态时候,当两发子弹的时候。

update(deltaTime: number) {

        switch(this.sootype){

            case shoottype.onshoot:

                break;

            case shoottype.towshoot:

                break;

        }

switch 语句用于根据 this.sootype 的值来决定执行哪个 case 分支。

4将之前的代码封装为一个函数,用当前update的值去传递参数。

update(deltaTime: number) {

    // 根据当前的射击类型进行不同逻辑的处理

    switch(this.sootype){

        // 当射击类型为单发射击时

        case shoottype.onshoot:

            // 调用单发射击处理方法,并传入时间

            this.onshut(deltaTime);

            // 跳出 switch 语句,防止继续执行后续 case

            break;

        // 当射击类型为双发射击时

        case shoottype.towshoot:

            // 目前此分支暂未实现具体逻辑,可后续补充

            break;

    }

}

/**

 * 单发射击处理方法,控制单发射击的频率和子弹创建。*/

onshut(deltaTime: number){

    // 累加攻击计时器的值,记录时间的流逝

    this.jishiqi += deltaTime;

    // 检查攻击计时器是否超过攻击频率

    if(this.jishiqi > this.gongjipinlv){

        // 若超过攻击频率,将攻击计时器重置为 0,以便下一次计时

        this.jishiqi = 0;

        // 根据子弹预制体实例化一个新的子弹对象

        const zi = instantiate(this.zidan);

        // 将新创建的子弹对象添加到子弹容器节点中

        this.bultikun.addChild(zi);

        // 设置子弹的世界位置为指定的发射位置

        zi.setWorldPosition(this.bultpotion.worldPosition);

    }

}

5创建一个函数,用来存储两发子弹的发射

twoshut(deltaTime: number){

}

在之前的switch代码,两发的时候调用这个函数。

case shoottype.towshoot:

            this.twoshut(deltaTime);

            break;

6返回层级管理器,像是之前创建空节点来保存子弹发射位置一样,创建两个空节点,来保存两发时候的子弹位置。

7在代码之中创建两个属性话的子弹预制体变量,在引用里面把之前的预制体子弹2拖入其中。

    @property(Prefab)

    zidan2:Prefab=null;

8代码之中写出两个属性化的节点位置,用来存放新的子弹位置,记得拖入。

  @property(Node)

    bultpotion2:Node=null;

    @property(Node)

    bultpotion3:Node=null;

  1. 把单发时候的代码复制过去,相应的改一下。

子弹的自动销毁

1让子弹超出屏幕之外自动销毁,去子弹预制体挂载的脚本上.

 update(deltaTime: number) {

       

        let dangq=this.node.position;

        this.node.setPosition(dangq.x,dangq.y+this.sudu*deltaTime,dangq.z);

       

        if(dangq.y>440){

            this.node.destroy();

        }

    }

 if(dangq.y>440){this.node.destroy();}

当前节点的y大于440时候,销毁当前节点。

制作敌人

1创建一个空节点“enemy0”,把资源管理器之中的enemy0图片拖进空节点“enemy0”。并对拖入资源管理器的图片节点进行重新命名。

2把敌人节点移动到屏幕外面。因为敌人在屏幕外生成。

3创建一个脚本,让其成为敌人通用脚本。将创建好的脚本拖入敌人节点。

4实例化速度属性,在update函数里面进行修改,让其每帧向下运动。

@property(Number)

    sudu:number=100;

 update(deltaTime: number) {

        let pp=this.node.position;

        this.node.setPosition(pp.x,pp.y-this.sudu,pp.z);

    }

5将其变成预制体。

制作敌人销毁动画

1进入预制体内部,添加动画。

2给其图片节点添加动画,sprite-》sprite.frame

3在资源管理器全选enemy0_down1~4,放置到动画编辑器之中。

4将最后一帧动画后面添加一帧空白动画,先在动画编辑器之中在最后一帧后面创建一个sprite.frame关键帧,然后将其中的动画编辑器里面的sprite.frame关键帧清除。

制作敌人2

1将预制体1复制后更改名字为敌人2(enemy2)

2更改其sprite frame 为-》“enemy1”

3新建动画剪辑资源enemy1_down。

4在资源管理器全选enemy1_down1~4,放置到动画编辑器之中。将最后一帧动画后面添加一帧空白动画

5再创建一个受到攻击动画。在动画编辑器,新建剪辑资源。因为只有一帧,所以只有一个贴图。但要在这一帧后面在添加一个正常的帧图片,要不然受到攻击后就一直是第一帧的动画了。

6将其速度略小于第一个敌人,因为体积更大,这样显得更加合理。

速度之前已经属性化了,在面板可以更改。

制作敌人3

1将预制体1复制后更改名字为敌人3(enemy3)

2更改其sprite frame 为-》“enemy2”

3新建动画剪辑资源enemy2_down。

4与2相似。

敌人的自动生成

1创建一个空节点,和ts,ts挂载其上。

2属性话一个值为生成速度,在属性化生成一个引用预制体的值。

 @property(Number)

    shengsudu:number=1;

    @property(Prefab)

    diren1:Prefab=null;

3创建一个方法,首先实例化预制体,在设置预制体挂载到哪个位置。

 start() {

        this.shenghengdiren();

    }

 shenghengdiren(){

        const xx=instantiate(this.diren1);

        this.node.addChild(xx);

       

    }

4让生成的x坐标随机。并更改其位置为那个坐标

const xx_x=math.randomRangeInt(-215,215);

        xx.setPosition(xx_x,450,0);

5定时让其激活该函数。

(周期性激活该函数)

 start() {

        this.schedule(this.shenghengdiren,this.shengsudu);

    }

 this.schedule会定期的调用。

This.shengsudu是之前属性话的速度。

this.shenghengdiren是调用的函数。

//三次复刻时候出现了问题

 this.schedule(this.generate(),this.generatespeed) ;

//有括号就不会激活了,但是编译器之中没有报错。

给子弹添加碰撞器和刚体

1进入子弹预制体,在其属性检查器之中,添加一个 cc.box

Collider2d+

2点击旗下的editing,进行体积编辑,调整到合适的大小

3勾选sensor

4添加刚体,在其属性处添加一个rigidbody2d+,旗下type属性,选择

Kinematic。

5给另一个子弹也添加上

给敌人飞机添加多边形碰撞器和组价

1进入敌人1预制体,在其属性检查器之中,添加一个cc.polygoncollider2d(多边形碰撞器)

2通过修改旗下属性points,让其形状与小飞机图片相等。

3勾选旗下属性sensor

4添加一个cc.rigidbody2d,更改旗下属性type为kinematic

5更改cc.rigidbody2d旗下属性,勾选enabled contact list...,如果不勾选,将不会触发碰撞回调

6取消勾选allow sleep ,不让其休眠

碰撞事件的执行

对敌人小飞机的代码部分进行修改,增加一些

具体代码如下

@property(Animation)

     cuihui:Animation=null;

     //引入被摧毁的动画

     @property

     hp:number=1;

    start() {

        let collider =this.getComponent(Collider2D);

        collider.on(Contact2DType.BEGIN_CONTACT,this.onbegin,this);

        //getComponent:这是节点对象的一个方法,用于获取该节点上挂载的指定类型的组件。

        //Collider2D:这是 Cocos Creator 中用于处理 2D 碰撞检测的组件类。

        // 通过 getComponent(Collider2D),可以获取当前节点上挂载的 2D 碰撞器组件实例。

        //Contact2DType.BEGIN_CONTACT,定义了 2D 碰撞检测中的各种接触事件类型。BEGIN_CONTACT 表示两个碰撞器开始接触的事件。

    }

    onbegin(){

        this.hp-=1;

        //当每次被子弹接触的时候,血量减少

        if(this.hp<=0){

            this.cuihui.play("enemy0_xiaohui");

            //当血量没了就播放摧毁动画

        }

       

    }

    update(deltaTime: number) {

        if(this.hp>0){

            let pp=this.node.position;

            this.node.setPosition(pp.x,pp.y-this.sudu,pp.z);

        }

        //血量大于0时候正常移动

    }

//三次复刻的时候遇见的小问题

update(deltaTime: number) {

        if(this.hp>0){

            let pp=this.node.position;

            this.node.setPosition(pp.x,pp.y-this.speed,pp.z);

            if(pp.y<-568.279){

                this.node.destroy();

            }

        }

     if (this.hp <= 0 ) {

            setTimeout(() => {

                console.log("xx2");

                if (this.donghua) {

                    this.donghua.play("scene-2d_enemy0_down2");

                    console.log("xxscene-2d_enemy0_down3");

                }

                console.log("xx3");

            }, 200);

        }

    }

我把判断this.hp <= 0也放在了update里面,(我当时在想,this.hp>0 能够放在这里,那hp判断放在一起方便管理)但是没有考虑到每帧都激活导致动画永远卡在第一帧的问题。又因为第一帧动画我写的是原本的图片,第二帧动画才播放摧毁动画,就导致一系列的排查问题。

解决敌人小飞机重复播放的问题

让敌人小飞被子弹触发时候,播放动画,并且禁用掉Collider2D组件,这样就不会产生碰撞回调事件了。

将之前的onbegin(){}函数里面内容进行简单更改

 onbegin(){

        this.hp-=1;

        //当每次被子弹接触的时候,血量减少

        if(this.hp<=0){

            this.cuihui.play("enemy0_xiaohui");

            //当血量没了就播放摧毁动画

            let collider =this.getComponent(Collider2D);

            collider.enabled = false;

        }

       

    }

collider.enabled = false;当组件的enabled属性等于false时候就会被禁用掉。

之前小方块代码之中用过类似方法。

//拓展

This.node.active=false.

是让当前的节点消失,不显示。

而 collider.enabled = false;是让当前节点的某一个组件关闭。

在整个代码段之中,let collider =this.getComponent(Collider2D);重复了多次,可以让其变成私有属性。

Collider:collider2d=null;

把代码之中部分进行简单替换.

import { _decorator, Animation, Collider2D, Component, Contact2DType, Node } from 'cc';

const { ccclass, property } = _decorator;

@ccclass('NewComponent')

export class NewComponent extends Component {

    @property(Number)

     sudu:number=10;

     @property(Animation)

     cuihui:Animation=null;

     //引入被摧毁的动画

     @property

     hp:number=1;

     collider:Collider2D|null=null;

    start() {

        this.collider =this.getComponent(Collider2D);

        if(this.collider){

        this.collider.on(Contact2DType.BEGIN_CONTACT,this.onbegin,this);

        }

        //这里直接this.collider.on(Contact2DType.BEGIN_CONTACT,this.onbegin,this);能用,但会报错。

        //加上if后就不会报错了,可能是因为this。collider某一个时刻为空或者未定义

    }

    onbegin(){

        this.hp-=1;

        if(this.hp<=0){

            this.cuihui.play("enemy0_xiaohui");

            this.collider.enabled = false;

        }

       

    }

    update(deltaTime: number) {

        if(this.hp>0){

            let pp=this.node.position;

            this.node.setPosition(pp.x,pp.y-this.sudu,pp.z);

        }

    }

    protected onDestroy(): void {

        this.collider.off(Contact2DType.BEGIN_CONTACT,this.onbegin,this);

        //当脚本结束时候停止监听。

    }

}

销毁敌人小飞机

1当子弹接触导致血量为0时候,播放完动画自身销毁

2超出屏幕外自动销毁

代码如下

    onbegin(){

        this.hp-=1;

        if(this.hp<=0){

            this.cuihui.play("enemy0_xiaohui");

            this.collider.enabled = false;

            //this.scheduleOnce(this.node.destroy(),1);

            // this.scheduleOnce(() => {

            //     this.node.destroy();

            // }, 1);

            this.scheduleOnce(

                function(){

                    this.node.destroy();

                },1

            );

        }

       

    }

    update(deltaTime: number) {

        if(this.hp>0){

            let pp=this.node.position;

            this.node.setPosition(pp.x,pp.y-this.sudu,pp.z);

        }

        if(this.node.worldPosition.y<-50){

            this.node.destroy();

        }

    }

其中this.scheduleOnce(this.node.destroy(),1);为什么不行呢

  • 方法调用错误this.scheduleOnce 方法期望接收一个函数作为第一个参数,而传入的是 this.node.destroy() 的返回值。在 JavaScript 里,this.node.destroy() 会立即调用 destroy 方法,并且把该方法的返回值(一般为 undefined)传递给 this.scheduleOnce,这并非想要的结果。

解决方案

要解决这个问题,你需要传递一个函数给 this.scheduleOnce,可以使用箭头函数或者普通函数来实现:

使用箭头函数

typescript

this.scheduleOnce(() => {

    this.node.destroy();}, 1);

在这个代码中,箭头函数 () => { this.node.destroy(); } 被传递给 this.scheduleOnce,这样 this.node.destroy 方法就会在 1 秒之后被调用。

使用普通函数

 this.scheduleOnce(

                function(){

                    this.node.destroy();

                },1

            );

为什么我没有根据教程去使用postion,而是用worldPosition,postion位置在我第一次复刻之中并未准确,而是用world坐标就不会考虑这个问题.

添加中型飞机敌人

1给中型飞机添加刚体和碰撞器

2因为中型飞机不是一发子弹就打掉的存在,有着受攻击动画和摧毁动画,需要对脚本进行更改。

代码部分

     @property(Animation)

     cuihui:Animation=null;

     @property(String)

     cuihuiname:string="";

     @property(String)

     shougongjiname:string="";

创建两个字符串,用来保存动画的名字

 onbegin(){

        this.hp-=1;

        if(this.hp<=0){

            this.cuihui.play(this.cuihuiname);

            this.collider.enabled = false;

            this.scheduleOnce(

                function(){

                    this.node.destroy();

                },1

            );

        }else{

            this.cuihui.play(this.shougongjiname);

        }

       

    }

把之前的固定动画名变成了this.成员名。

回到属性编辑器,在外面对其进行更改,对属性化的cuihuiname 与 shougongjiname 进行修改,不要忘记更改中型飞机敌人的hp。

子弹与飞机的多次碰撞

上述代码完成后会发现,子弹接触中型飞机时候会减掉很多的血,这是因为二者发生了多次碰撞导致。

我们需要让碰撞的子弹消失

在代码部分

  this.collider.on(Contact2DType.BEGIN_CONTACT,this.onbegin,this);

之中,Contact2DType.BEGIN_CONTACT 会传输一些属性,通过这个属性可以销毁接触到飞机的子弹。

onbegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

          setTimeout(() => {

                otherCollider.node.destroy();

            }, 0.2);

       // otherCollider.destroy();

        //siki老师教程里是写的 otherCollider.node.destroy();出了bug,我这样写otherCollider.node.destroy;就正常了,siki老师后来用 otherCollider.enabled=false;了

        //但是后来问题了,只是不报错,但也没有用,后来我发现问题了,算是cocos特性吧,只要加入一个延迟就好了。

        this.hp-=1;

        if(this.hp<=0){

            this.cuihui.play(this.cuihuiname);

            this.collider.enabled = false;

            this.scheduleOnce(

                function(){

                    this.node.destroy();

                },1

            );

        }else{

            this.cuihui.play(this.shougongjiname);

        }

       

    }

此外,还需要对属性进行设置,回到小飞机的页面,进入刚体2d组件,勾选allow sleep属性,这能确保刚体不会被休眠。

对子弹也进行取消。

因为刚体是比较消耗性能的组件,默认是开启自动休眠的,这会导致问题,所以记得关掉它。

大型飞机敌人添加后出现的问题

1给大型飞机敌人添加刚体和碰撞器,并设置好血量和动画

2解决飞机之前相互碰撞产生的问题。

//按照我的写法大飞机敌人可能直接被销毁,siki老师的写法则是飞机与子弹无法碰撞,不论是那种bug,本质是飞机之间相互碰撞导致的。

3对敌人的刚体2d属性之中的group进行分组

项目设置-物理-碰撞矩阵

4添加两个组,bullet,enemy。横竖交叉则意思可以碰撞。

5回到场景,给预制体们分好组,设置为enemy

给玩家飞机添加受伤动画和坠毁动画以及碰撞器和刚体

1选中玩家节点,添加准备好的坠毁动画。

2新建一个动画,通过调整透明度制造出来闪烁效果,用作受伤动画

3选中小飞机,添加碰撞器polygoncollider2d,记得勾选sensor

4添加刚体,记得勾选Enabled Contact Listener,还有取消勾选Allow Sleep,以及属性更改为kinematic

5对代码进行更改

@property

    hp:number=1;

    //设置一个血量

    @property(Animation)

    cuihui:Animation=null;

    //这里需要重新引入一下animation,因为cocos重名的缘故,默认复制过来的不是想要的那个animation,不然底下涉及到动画的部分会报错。

    //设置一个动画属性

    @property(String)

    cuihuiname:string="";

    @property(String)

    shougongjiname:string="";

//这里的用法和敌人部分一样,挺巧思的方法,有时间可以拿回去改一下之前写的galgame

在飞机限制位置的代码添加如下代码,因为结束的话飞机还能够移动就很不正常

  if(this.hp<=0)return;

//就是之前让飞机限制在固定位置的代码最上方这样写。

 collider:Collider2D|null=null;

//设置一个成员变量,属性为Collider2D,默认值为空,稍后会为其赋值

//下面的写start函数里面

 this.collider =this.getComponent(Collider2D);

                if(this.collider){

                this.collider.on(Contact2DType.BEGIN_CONTACT,this.onbegin,this);

                }

        //复用了之前的代码,进行碰撞监听,那个老师写在了onload里面,我直接写start里面了,我觉着都一样

这里和之前的敌人碰撞代码有些许不同,但是整体思路一样

   onbegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

      setTimeout(() => {

                otherCollider.node.destroy();

                 //让敌人禁用也行,一会写完尝试一下

                 }, 0.2);

//之前ai在这里瞎编了一个函数,我以为是定时效果,结果酷酷报错,没有直接审核是这样的。

        //siki老师教程里是写的 otherCollider.node.destroy();出了bug,siki老师后来用 otherCollider.enabled=false;了

        this.hp-=1;

        if(this.hp<=0){

            this.cuihui.play(this.cuihuiname);

            //this.collider.enabled = false;

            //这里小飞机碰撞到后不需要让销毁掉模块

            // this.scheduleOnce(

            //     function(){

            //         this.node.destroy();

            //     },1

            // );

            //玩家被摧毁游戏结束,所以不考虑销毁的问题了

        }else{

            this.cuihui.play(this.shougongjiname);

        }

    }

6.设置分组,让玩家可以和敌机发生碰撞,分组方法和敌人方法一样。

//注意是刚体的分组,碰撞器也有组,不要选错了,虽然经过我的尝试都可以触发。

7在飞机限制位置的代码添加如下代码,结束的话飞机还能够移动就很不正常

//不过我前期修改了代码的逻辑,正常根据教程是如下写法,但我把限制位置和控制分离了,所以放到位置应该是this.node.setPosition那边

 xianzhiweizhi(pp: Vec3){

        if(this.hp<=0)return;

       if(pp.x<-230){

            pp.x=-230;

       }

......

//就是之前让飞机限制在固定位置的代码最上方这样写。

//我设置的限制位置和移动分离的代码如下。

start() {

        input.on(Input.EventType.TOUCH_MOVE,this.otto,this);

    }

otto(even:EventTouch){

        if(this.hp<=0)return;

        let ikun=this.node.position;

        this.node.setPosition(ikun.x+even.getDeltaX(),ikun.y+even.getDeltaY(),ikun.z);

        let pp = new Vec3(ikun.x+even.getDeltaX(),ikun.y+even.getDeltaY(),ikun.z);

        this.xianzhiweizhi(pp);

    }

    xianzhiweizhi(pp: Vec3){

   

       if(pp.x<-230){

            pp.x=-230;

       }

       if(pp.x>230){

        pp.x=230;

        }

        if(pp.y<-380){

            pp.y=-380;

       }

       if(pp.y>380){

        pp.y=380;

        }

        this.node.setPosition(pp);

    }

8,回到enemy的程序之中,对代码进行简单修改

 obeg(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

       

        if(otherCollider.getComponent(bullet)){

           this.hp-=1;

            setTimeout(() => {

                otherCollider.node.destroy();

                 }, 0.2);

               

        }

        //没有放到一起是觉着这样看方便理解,都放在一个if(otherCollider.getComponent(bullet)里面也可以。

        if(otherCollider.getComponent(bullet)){

            if (this.hp <= 0 ) {

                if (this.donghua) {

                    this.donghua.play(this.cuihuiname);

                }

               

                this.collider.enabled = false;

                this.scheduleOnce(

                    function(){

                    this.node.destroy();

                    },1

                   );

        }else{

            if (this.donghua) {

                this.donghua.play(this.shougongjiname);

            }

        }

        }

       

    }

    }

就是加了一个判断,如果获取的节点,挂载了名字为 bullet的组件,那么进行销毁,也就是遇见子弹进行销毁。玩家节点是没有这个的。

 getComponent()获取节点上指定类型的组件,如果节点有附加指定类型的组件,则返回,如果没有则为空。 传入参数也可以是脚本的名称。

//补充,之前代码写完后,hp<=0时候,碰撞器检测到小飞机触发后又会播放摧毁动画,对其进行简单修改,使其检测器为hp《=0的时候被禁用

  onbegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

            setTimeout(() => {

                otherCollider.node.destroy();

                 }, 0.2);

        this.hp-=1;

        if(this.hp<=0){

         this.cuihui.play(this.cuihuiname);

         if(this.collider){

            this.collider.enabled=false;

         }

         //禁用collider成员变量所指向的组件(之前声明的碰撞器),使其为false;这样当hp《=0时候,就不会被重复触发摧毁动画了。

        }else{

         this.cuihui.play(this.shougongjiname);

        }

    }

设置玩家受伤后的无敌时间

在此时,我写的代码和siki老师的代码有些许差别了,但呈现效果是差不多的。主要还是对代码有没有理解透彻

  1. 回到玩家飞机脚本,属性化一个无敌时间。

 @property

    Invincible_time:number=1;

  1. 创建一个成员变量,让其充当计时器

Invincible_timer:number=0;

  1. 创建一个布尔成员变量,用来后期判断当前是不是无敌时间

 invincibility:boolean=false;

4,更改玩家飞机代码,在碰撞触发部分,对其更改,如果是被碰撞,更改之前3的状态,在这个状态时候不执行检测代码。

 onbegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

       

        if(this.invincibility)return;//当其为真的时候,说明现在是无敌的,不减血。

        this.invincibility=true;//由上判断,那现在不是无敌状态,被碰撞了,那就更改为无敌状态。

            setTimeout(() => {

                otherCollider.node.destroy();

                 //让敌人禁用也行,一会写完尝试一下

                 }, 0.2);

        this.hp-=1;

        if(this.hp<=0){

         this.cuihui.play(this.cuihuiname);

         if(this.collider){

            this.collider.enabled=false;

         }

         //禁用collider成员变量所指向的组件(之前声明的碰撞器),使其为false;这样当hp《=0时候,就不会被重复触发摧毁动画了。

        }else{

         this.cuihui.play(this.shougongjiname);

        }

    }

5.无敌的状态是有时间限制,所以在update里面进行计数。

和之前写的跳跃牢猫代码部分差不多。

 update(deltaTime: number) {

        this.wudishijian(deltaTime);

    }

wudishijian(deltaTime:number){

        if(this.invincibility){

            this.Invincible_timer+=deltaTime;

            if(this.Invincible_timer>=this.Invincible_time){

                this.invincibility=false;

                this.Invincible_timer=0;

            }

        }

    }

6.使死亡后子弹不发射

回到玩家小飞机的顶部,将子弹的触发类型新增加一个状态,none

enum shoottype{

    onebullet,

    twobullet,

    none

}

在碰撞检测地方,让其hp<0时候,让其状态为none

 if(this.hp<=0){

         this.cuihui.play(this.cuihuiname);

         

         this.shoot=shoottype.none;

         if(this.collider){

            this.collider.enabled=false;

         }

         //禁用collider成员变量所指向的组件(之前声明的碰撞器),使其为false;这样当hp《=0时候,就不会被重复触发摧毁动画了。

        }else{

         this.cuihui.play(this.shougongjiname);

        }

创建一种奖励物品的预制体

1.找到prop_type_0与1,这是即将制成预制体的图片素材

2.拖入项目之中,给其增加碰撞器和刚体,与敌人类似。注意不要犯小错。

3.更改其碰撞组,让其和玩家碰撞不与子弹和敌人碰撞。

4创建脚本,与其挂载

  @property

    speed:number=1;

 update(deltaTime: number) {

        const pp =this.node.position;

        this.node.setPosition(pp.x,pp.y-this.speed*deltaTime,pp.z);

    }

5.创建一个摇摇晃晃的动画

动画编辑器-》rotation(eulerangles),反复循环播放动画,开局播放动画

创建第二种奖励的物品预制体

1.将刚才所做的预制体复制一份,重新命名。

2.更改其Sprite Frame 的图片

3.更改其碰撞器的体积

4.在奖励预制体的脚本之中,创建枚举类型,用来区分是那个奖励物品

enum reward{

    twoshoot,

    boom

}

5.属性化其枚举类型,让其可以在外部进行更改。

@property

    rewardType:reward=reward.twoshoot

6.跟据奖励物品是boom还是双发在外面进行设置值为0还是1。

控制奖励物品的生成

1回到生成敌人飞机的脚本处,增加代码。

  @property

    propspeed:number=15;

    @property(Prefab)

    prop1:Prefab|null=null;

    @property(Prefab)

    prop2:Prefab|null=null;

2再二次复刻过程中,我实现了效果但没有把代码集成,在此处重新集成一下代码,意思是一样的。

这里我拿了三个作为示范,第一个generateenemy0是我没有集成的写法,第二个generateenemy1是正常情况下用集成的写法,第三个是特殊奖励物品。

 start() {

       this.schedule(this.generateenemy0,this.generatespeed) ;

this.schedule(this.generateenemy1,this.generatespeed1) ;

       this.schedule(this.generprop,this.propspeed) ;

    }

//上面是未集成的,下面标红的是集成的写法

generateenemy0(){

        const xx = instantiate(this.enemy0);

        this.node.addChild(xx);

        const xx_x=math.randomRangeInt(-215,215);

        xx.setPosition(xx_x,450,0);

    }

//可见,未集成的每次都需要写这一段代码,和跳跃牢猫代码之中的生成地板类似。

 generateenemy1(){

        this.enemyspawn(this.enemy1,-215,215,450);

    }

//enemyspawn是自己写的函数,这样通过该函数就不用每次都写上面的一堆了。

 enemyspawn(enemypre:Prefab,mixx:number,maxx:number,Y:number){

        const xx = instantiate(enemypre);

        this.node.addChild(xx);

        const xx_x=math.randomRangeInt(mixx,maxx);

        xx.setPosition(xx_x,Y,0);

    }

//通过脚本之间的传递参数实现特定效果

 generprop(){

        const randnumber = math.randomRangeInt(0,2);

        let preprop = null;

        if(randnumber==0){

            preprop=this.prop1;

        }else{

            preprop=this.prop2;

        }

        this.enemyspawn(preprop,-207,207,474);

    }

math.randomRangeInt 函数生成一个介于 0 和 2 之间(包含 0,但不包含 2)的随机整数,并将其赋值给变量 randnumber。也就是说,randnumber 的值可能是 0 或者 1。

通过该随机数,来让preprop随机变成prop预制体的1或者2,以达到随机的效果。

再通过集成好的函数来快速生成预制体并实例化。

开发玩家与奖励物品的碰撞

1.返回玩家飞机脚本,对其修改,找到碰撞部分代码,在此处新增逻辑判断,如果是奖励物品,如何如何,若是敌人飞机,如何如何。

但这时候代码冗余很多了,不方便阅读,将之前的碰见敌人以及设计无敌时间的部分进行打包(红色区域),做成函数(蓝色区域),让之后进行调用。

 onbegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

        const rewardx=otherCollider.getComponent(prop)

        if(rewardx){

            console.log("碰到奖励物品啦!");

        }else{

            this.collisionEnemy(selfCollider, otherCollider, contact);

        }

    }

    collisionEnemy(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

        if(this.invincibility)return;        this.invincibility=true;

            setTimeout(() => {

                otherCollider.node.destroy();

                               }, 0.2);

        this.hp-=1;

        if(this.hp<=0){

         this.cuihui.play(this.cuihuiname);

         

         this.shoot=shoottype.none;

         if(this.collider){

            this.collider.enabled=false;

         }

        }else{

         this.cuihui.play(this.shougongjiname);

        }

    }

2.奖励物品有两种,要在代码之中确定是那个,是boom还是双发子弹,又因为之前的奖励物品代码之中,没有将枚举类型export声明,所以需要返回美剧类型之中,将其export。

export 是 JavaScript/TypeScript 里的关键字,其用途是把模块里的变量、函数、类等导出,从而让其他模块能够对其进行导入和使用。

export enum reward{

    twoshoot,

    boom

}

3.回到玩家脚本,对步骤1里面的onbegin(){}里面的内容进行修改,目的是让其判断碰撞的是什么奖励物品。

写了两个方法实现效果,都是一样的

onbegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

        const rewardx=otherCollider.getComponent(prop)

        if(rewardx){

            // if(rewardx.rewardType==reward.boom){

            //     console.log("炸弹来了");

            // }

            // if(rewardx.rewardType==reward.twoshoot){

            //     console.log("双发豌豆");

            // }

            switch(rewardx.rewardType){

                case reward.twoshoot:

                    console.log("双发豌豆");

                    break;

                case reward.boom:

                    console.log("炸弹来了");

                    break;

            }

        }else{

            this.collisionEnemy(selfCollider, otherCollider, contact);

        }

    }

实现双发子弹的奖励效果

1.在碰撞检测的代码段之中,双发的代码下边新增如下代码。

  switch(rewardx.rewardType){

                case reward.twoshoot:

                    console.log("双发豌豆");

                    this.twoshootjiangli();

                    break;

//这样,当碰到双发的时候就会把发射模式改变为双发的。

twoshootjiangli(){

        this.shoot=shoottype.twobullet;

    }

2.设置双发时间限制。

@property

    twoshoottime:number=5;//奖励的双发时间

    twoshoottimer:number=0;//计时器。

twoshootjiangli(){

        this.shoot=shoottype.twobullet;

    }

update(deltaTime: number) {

        this.shuangfashijian(deltaTime);

    }

    shuangfashijian(deltaTime:number){

        if(this.shoot==shoottype.twobullet){

            this.twoshoottimer+=deltaTime;

            if(this.twoshoottimer>=this.twoshoottime){

                this.shoot=shoottype.onebullet;

                this.twoshoottimer=0;

            }

        }

    }

//逻辑如下,当碰撞到双发奖励物品时候,状态变为双发,update函数每帧检测一遍状态,当发现当前为双发时候,启动if语句,当计时器大于设定好的双发持续时间,恢复为单发,计时器归零。

3.销毁碰到的奖励物体,防止多次碰撞。

switch(rewardx.rewardType){

                case reward.twoshoot:

                    console.log("双发豌豆");

                    this.twoshootjiangli();

                    setTimeout(() => {

                        otherCollider.node.destroy();

                         }, 0.2);

                    break;

实现炸弹奖励物品

1,明确炸弹奖励的使用条件(双击屏幕),炸弹还可以存储多个(有上限)。

创建一个用来存储炸弹的游戏管理器,siki老师说玩家hp等这种出现一次的都可以放到这里,当然也可以直接把这些数值写入玩家脚本,涉及到单例模式。这是一种游戏开发过程中常见的模式。

2.创建一个脚本,命名为gamemanagerTS。

3.创建一个空节点,命名为gamemanager_node,将刚才脚本拖放进来。

4,编写脚本,涉及到软件开发的 设计模式 ,之中的单例模式

@ccclass('gamemanagerTS')

export class gamemanagerTS extends Component {

private static instance:gamemanagerTS;

定义了一个静态变量 instance,用于存储 gamemanagerTS 的唯一实例。静态变量属于类本身,而不是类的实例。

    public static getinstance():gamemanagerTS{

        return this.instance;

};

定义了一个静态方法 getinstance,用于返回 instance 的值。这个方法是单例模式的核心,通过它可以全局访问 gamemanagerTS 的唯一实例。

    start() {

        gamemanagerTS.instance=this;

}

start() 是 Cocos Creator 的生命周期方法,当脚本附加的节点被激活时,start 方法会被调用。

在这里,start 方法将当前实例(this)赋值给 gamemanagerTS.instance,从而初始化单例。

}

//了解单例模式

单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。

//理解单例模式

cocos手册-》功能模块-》音频系统-》全局音频管理示范。

5.编写脚本,创建一个声明化的数字类型,用于存储炸弹数量,一个函数用于接触到奖励物品时候让炸弹增加。

    @property

    boomshuliang:number=0;

  public boomzengjia(){

        this.boomshuliang+=1;

    }

6.返回玩家飞机的节点,修改代码。

switch(rewardx.rewardType){

                case reward.twoshoot:

                    console.log("双发豌豆");

                    this.twoshootjiangli();

                    setTimeout(() => {

                        otherCollider.node.destroy();

                     

                         }, 0.2);

                    break;

                case reward.boom:

                    console.log("炸弹来了");

                    gamemanagerTS.getinstance().boomzengjia();

                    setTimeout(() => {

                        otherCollider.node.destroy();

                       

                         }, 0.2);

                    break;

            }

奖励物品多次添加的bug

被重复触发可能是系统认为发生了多次碰撞,这里采用如下方法。

 lastrewad:prop =null;//创建一个成员变量,属性为碰撞敌人里面的奖励系统里面的脚本组件,默认为空。

    onbegin(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null){

        const rewardx=otherCollider.getComponent(prop);

        if(rewardx){

           

            if(rewardx==this.lastrewad){

                return;

            }

            this.lastrewad=rewardx;

            //当第一次触发的时候,将会把内存地址指向reward的变量,第二次触发的时候因为二者内存地址相同,不执行操作。

           

            switch(rewardx.rewardType){

                case reward.twoshoot:

                    console.log("双发豌豆");

                    this.twoshootjiangli();

                    setTimeout(() => {

                        otherCollider.node.destroy();

                     

                         }, 0.2);

                    break;

                case reward.boom:

                    console.log("炸弹来了");

                    gamemanagerTS.getinstance().boomzengjia();

                    setTimeout(() => {

                        otherCollider.node.destroy();

                       

                         }, 0.2);

                    break;

            }

        }else{

            this.collisionEnemy(selfCollider, otherCollider, contact);

        }

    }

炸弹ui的显示

1.回到场景之中,新建一个画布命名为canvas-ui

2.选择之前的画布,canvas,点击node组件的layer,旁边的Edit。

3.对其layer0进行编辑,起名为game。

4,回到一开始的画布节点,选择layer,勾选新建的game。(连同修改子节点)

5,去预制体之中,修改层级layer为game

6,选中画布节点下的摄像机camera,对其属性visibtiy只勾选game。让其之渲染game的layer内容。

7,去新画布的图层,对layer进行修改为ui,找到摄像机节点,对其属性visibletiy勾选ui。

8.在新画布之下创建一个精灵节点,图标为小炸弹。移动位置到右下角。

9.在小炸弹精灵节点添加组件widget,可以使其自适应对齐。

10.在下炸弹底下创建ui文本,一个为“x”,一个用来存储数量的文本

11.文本数量的文本调整其属性,调整水平对其,居左。取消自动换行。溢出处理选择CLAMP.

12.文本数量的文本调整其属性,用T工具调整边框。适当调整字体大小。

13.现在运行会发现底下的关于ui画布内容无法被显示,也就是说小炸弹图标无法显示,这是因为相机的执行顺序问题导致的。

将ui画布里面的摄像机,priority属性改大,如1.

//拓展

Widget 组件参考 | Cocos Creator

对齐策略 | Cocos Creator

炸弹数量变化的事件接收与发射

1进入之前写的 游戏单例 那个脚本,对其进行修改。

之前使用start来进行对单例进行赋值,为了防止出现生命周期相关的错误,对其进行修改,使用onlad()来进行使用。

protected onLoad(): void {

        gamemanagerTS.instance=this;

    }

   

    public boomzengjia(){

        this.boomshuliang+=1;

        this.node.emit("onbechenge");

        //emit:这是节点对象的一个方法,其功能是触发指定名称的自定义事件。

    }

2.在脚本ts文件夹之中创建一个ui空文件夹,用于存放ui相关的脚本。

3.在该文件夹创建一个boomui脚本。挂在到ui画布上的炸弹图标里面。

4.返回游戏单例脚本,进行简单修改。因为siki老师说直接调用bommshuliang不安全。

    @property

    private boomshuliang:number=0;

    public getboomshuliang():number{

        return this.boomshuliang;

    }

5.返回新建的boomui脚本,进行简单修改

 @property(Label)

    bommuishuliang:Label|null=null;

    start() {

        gamemanagerTS.getinstance().node.on("onbechenge",this.onboom,this);

    }

    onboom(){

        console.log("以触发炸弹");

       

            this.bommuishuliang.string=gamemanagerTS.getinstance().getboomshuliang().toString();

       

     

    }

6.第二种方法,这是siki老师的想法。

(1)返回单例模式的脚本,删除this.node.emit("onbechenge");

(2)回到boomui,假设现在是新的脚本,没有上面我写的那些。

 (3)引入label标签@property(Label)

     bommuishuliang:Label|null=null;

(4)新建一个updateui函数,具体如下

 updateui(cont:number){

        this.bommuishuliang.string=cont.toString();  }

(5)回到单例模式的脚本,引入boomui脚本

 @property(boomui)

    boomui:boomui=null;

(6)简单修改代码

public boomzengjia(){

        this.boomshuliang+=1;

        this.boomui.updateui(this.boomshuliang);

    }

解决之前控制台的警告问题

@property(String)

   cuihuiname:string="";

如之前写的小飞机和敌人被摧毁的脚本,很多地方是这样写的,可以改成如下

@property

   cuihuiname:string="";

就不会报错了。

在3/31还发现其中的奖励物品不会自动销毁,明天自己补上笔记和代码

 update(deltaTime: number) {

        const pp =this.node.position;

        this.node.setPosition(pp.x,pp.y-this.speed*deltaTime,pp.z);

        if(pp.y<-568.279){

            this.node.destroy();

        }

    }

生命值ui的显示

1.在ui画布之中创建一个图片节点,取名为LifecountUI

2.为其添加一个widget,右上对其。

3.更改图片节点的精灵图。

4.将之前做显示炸弹ui的两个文本内容复制后拖到精灵图下方,并且放置到合适的位置。修改一下显示数字的label大小,调整合适即可。

(调整layer为对应的层级)

5.创建一个脚本,放置地点为之前脚本文件夹之中的ui文件夹部分,名称为lifeUi。

6.把之前给炸弹ui写的脚本大体上复制过去。

 @property(Label)

    bommuishuliang:Label|null=null;

 updateui(cont:number){

        this.bommuishuliang.string=cont.toString();

    }

7.回到小飞机脚本,增加些许代码

@property(lifeUi)

    lifeconUI:lifeUi=null;

在start()函数里面新增这一段

this.lifeconUI.updateui(this.hp);

之后就是在涉及到hp减少的时候进行传递当前hp,因为我的代码和siki老师不一样,所以还是具体问题具体分析。

思维拓展(当一个成员变量发生改变时候需要触发某些事件的时候可以进行集成。)

Siki老师在hp这里进行了些许拓展,当对于某个成员变量每次的更改都会产生相应变化的话,那么如果像上方一样,那么需要在每次出现变化的地方增加代码,可有别的方法。

创建一个函数

Changelife(cont:number){

This.hp+=cont;

//更新ui的代码this.lifeconUI.updateui(this.hp);//

//除此之外还可以进行别的方法//

}

那么比如hp减少就可以这样写了。

之前的 this.hp-=1;

现在的 changelife(-1);

警告,脚本找不到问题

警告,命名问题

警告,cocos特性问题

当新建一个脚本后,在对其命名进行修改,点开当前脚本,会发现脚本外部(也就是刚才重命名的地方)与代码内部的@ccclass('NewComponent')这里,与export class NewComponent。都没有被修改,需要手动进行修改。

添加分数的属性

1.回到单例脚本里面,添加一个属性化的私有的属性,类型是number,默认为0的,名称为score。

2.创建一个公有的成员函数addscore,形参为s,类型为number,内容为score+=s;

  @property

    private score:number=0;

    addscore(s:number){

        this.score+=s;

    }

3.进入敌人飞机脚本,在hp《=0时候调用2的函数。

gamemanagerTS.getinstance().addscore(this.fenshu);

4.因为不同的敌人让其给的分数不一样,所以属性化一个成员变量sc,number类型。

5.将其3后面加入(this.sc)

@property

    fenshu:number=0;

6.返回游戏引擎,调整不同飞机的参数。

添加分数及ui,以及更新

1.在ui画布下创建一个空节点,scoreUI

2.添加widget,左上角对其

3.复制之前的血量ui里面的label,放置到当前节点旗下。调整大小。

4.在脚本文件夹的ui文件夹之中,创建一个脚本,scoreUIts,将其挂在scoreUI其上。

5.复制血量或者之前的炸弹脚本的代码。

 @property(Label)

       shuliang:Label|null=null;

     updateui(cont:number){

        this.shuliang.string=cont.toString();

    }

6.在引擎之中让label赋值。

7.回到单例脚本,

创建一个属性化的scoreuits类型的成员变量。

在每次分数增加的部分调用,将分数传递过去。

 @property

    private score:number=0;

    @property(scoreUIts)

    scoreui:scoreUIts=null;

    addscore(s:number){

        this.score+=s;

        this.scoreui.updateui(this.score);

    }

暂停按钮的ui部分

1.在ui画布创建一个按钮,命名pausebutton。

2.移动到左上角,更改按钮贴图,修改该按钮节点上的cc.UITransform属性的宽高。

3.删除按钮默认带的label,调整宽高后按钮位置会发生偏移,移动到理想位置。

4.添加widget,左上角对其。返回按钮的点击属性部分,cc.button,修改按下时候的贴图,删除多余的按钮贴图状态为空。

5.复制当前的按钮,新的命名为resumebutton。调整贴图和按钮贴图。

6.返回原按钮,属性最上方取消勾选,让其失效。

暂停按钮的代码部分

1.返回游戏单例代码部分创建两个名为onpausebuttonclike和onresumebuttonclike的函数。

2.onpausebuttonclike函数内部的代码为director.pause();onresumebuttonclike函数内部的代码为director.resume();

//当点击暂停的时候,本质是所有的update函数内停止了,之前让其移动的代码本质是根据这个每帧执行×速度,为0时候就没有用了。

3.返回游戏引擎,将二者与按钮进行绑定。

4.因为控制移动不被update里面,所以需要对代码进行修改,让暂停时候玩家飞机不能移动。

返回小飞机代码,新增一个私有成员变量private cancontrol:boolean=true;

创建两个公共函数,(disablecontrol、enablecontrol)分别是改变上面的布尔值为真或假。

5.在控制移动的代码前面增加一个if判断,if(!this.cancontrol)return;

6.返回游戏单例代码,引入小飞机代码部分

@pr(小飞机的代码部分)

Xx;xx=null

7.在游戏单例的代码之中,暂停函数onpausebuttonclike调用小飞机里面的公共函数,进行暂停。在引擎之中进行赋值。

8.返回单例代码,对暂停ui图标进行引用。两个暂停图标以node形式进行引用。

9.对单例之中的onpausebuttonclike和onresumebuttonclike函数内部进行修改,当点击时候,8之中的active的属性修改为true或者false。

设计游戏结束的ui界面

1.在ui画布下创建一个图片节点,命名为gameoverui贴图使用资源之中的特定图纸。

2.创建两个文本标签,适当调整,用于显示历史最高分和最终得分。Higscore,curscore。

3.创建两个按钮用作返回主菜单和重新游戏。Rebutton(重新开始)quitbutton(退出游戏),使用特定的贴图

4.默认gameoverui节点隐藏,选择节点时。最上方取消勾选

5.layer设置不要搞错

判断游结束的代码部分

1.创建一个脚本(gameoveruiTs),挂载到gameoverui上面

2.代码内引入两个文本(higscorelabel,curscorelabel),以及一个新建函数(showGameOverUi),函数两个形参,number类型。

3.函数代码解析,首先是让当前脚本显示,然后让文本label的string属性等于两个形参,对形参进行类型转换.tostring()

4.返回引擎之中,对进行赋值

 @property(Label)

    higscorelabel:Label=null;

    @property(Label)

    curscorelabel:Label=null;

showGameOverUi(hig:number,cur:number){

 this.node.active=true;

        this.higscorelabel.string=hig.toString();

        this.curscorelabel.string=cur.toString();

    }

   

5.在游戏单例之中创建一个函数gameover,里面对游戏分数进行传参,去执行之前的showGameOverUi函数,当小飞机没血时候执行该函数,该函数再去执行ui脚本里面的函数

需要在游戏单例之中引入游戏结束的ui部分脚本。

当游戏结束时候还在不断地生成敌人,在gameover函数之中调用一下之前写的暂停函数

 @property(gameoveruiTs)

    gameoverui:gameoveruiTs=null;

    gameover(){

        this.onpausebuttonclike();

        this.gameoverui.showGameOverUi(1,2);//先传入两个固定数作为测试

    }

6.在小飞机脚本之中,血量为0时候执行游戏单例里面的函数,但是考虑动画部分,需要延迟零点几秒执行

 setTimeout(()=>{

            gamemanagerTS.getinstance().gameover();

         },0.6);

分数的更新

1.之前测试传递的参数是1与2,2是最终得分,1是历史得分,将其修改,2改为this。Score,之前写分数的时候属性化的。这时候进行测试,最终得分已经没有问题了。

 gameover(){

        this.onpausebuttonclike();

        this.gameoverui.showGameOverUi(1,this.score);

    }

2.涉及到本地数据存储与分数比较,因为之前做galgame的时候解除到一些,所以理解起来也不算是复杂

gameover(){

        this.onpausebuttonclike();

       let hscroe = localStorage.getItem("higscore");

       let hscroeint = 0;

        if(hscroe!=null){

            hscroeint= parseInt(hscroe,10);

        }

        if(this.score>hscroeint){

            localStorage.setItem("higscore",this.score.toString());

        }

        this.gameoverui.showGameOverUi(hscroeint,this.score);

    }

localStorage 是浏览器提供的一个对象,用于在本地存储数据,这些数据会一直保留,直到手动清除。

getItem("higscore") 是 localStorage 的一个方法,用于获取键为 "higscore" 的值。如果该键存在,则返回对应的值;如果不存在,则返回 null。

let hscroe 声明了一个变量 hscroe,用于存储从本地存储中获取的最高分。

声明一个变量 hscroeint,并将其初始值设为 0。这个变量将用于存储转换后的整数类型的最高分。

if(hscroe!=null) 检查 hscroe 是否不为 null,即本地存储中是否存在 "higscore" 这个键。

如果存在,则使用 parseInt(hscroe, 10) 将 hscroe 转换为十进制整数,并将结果赋值给 hscroeint。parseInt 函数用于将字符串转换为整数,第二个参数 10 表示使用十进制进行转换。

if(this.score>hscroeint) 检查当前得分 this.score 是否高于存储的最高分 hscroeint。

如果当前得分更,则使用 localStorage.setItem("higscore",this.score.toString()) 更新本地存储中的最高分。

setItem 方法这里将 "higscore" 作为键,this.score.toString() 将当前得分转换为字符串后作为值存储。

调用 this.gameoverui 对象的 showGameOverUi 方法,传入存储的最高分 hscroeint 和当前得分 this.score 作为参数,用于显示游戏结束界面并展示这些信息。

游戏的重新开始

1.在游戏单例脚本之中写两个函数onrebuttonclike,onquitbuttonclike

2.返回游戏结束ui的按钮部分,对按钮进行绑定

3.对onrebuttonclike函数进行修改

onrebuttonclike(){

        director.loadScene(director.getScene().name);

        this.onresumebuttonclike();

    }

director.getScene():这是 director 对象的一个方法,调用它会返回当前正在运行的场景对象。场景对象包含了场景中的各种节点、组件等信息。

director.getScene().name:通过 director.getScene() 得到场景对象后,使用 .name 属性可以获取该场景的名称。

director.loadScene(...):这也是 director 对象的一个方法,它的作用是加载指定名称的场景。这里传入的参数是当前场景的名称,所以最终效果就是重新加载当前正在运行的场景。

this.onresumebuttonclike();是之前暂停和继续里面的继续函数,因为游戏结束是时候运行了暂停,所以这里要进行继续。

双击事件的检测

1.在敌人生成的代码之中,进行双击的检测,因为之后触发炸弹能方便一点,不用在引用了。

2.对敌人生成代码进行简单新增

doubleclikeinterval:number=0.2;//设计双击的间隔时间

    lastcliketimer:number=0;//用于存储上次点击的时间戳

    protected onLoad(): void {

        this.lastcliketimer=0;//初始化

        input.on(Input.EventType.TOUCH_END,this.onTouchEnd,this);//对点击进行监听

    }

        //这段代码是用来检测“双击”事件的,就像你敲门一样——如果两次敲门的时间间隔很短,主人就会知道你是双敲门;如果时间间隔太长,主人就会觉得你只是普通的两次敲门。

    onTouchEnd(event) {

        // 记录当前时间(就像主人记录你第二次敲门的时间)

        let currentTime = Date.now();

       

        // 计算两次敲门的时间差(单位是秒)

        let timeDiff = (currentTime - this.lastcliketimer) / 1000;

   

        // 如果时间差小于双击的阈值(就像两次敲门时间太短)

        if (timeDiff < this.doubleclikeinterval) {

            this.ondoubleclike(); // 主人开门

        }

   

        // 更新记录,保存这次敲门的时间(为下一次判断做准备)

        this.lastcliketimer = currentTime;

    }

    ondoubleclike(){

            console.log("双击666");

    }

激活炸弹并使消灭屏幕所有的敌人

1返回游戏单例脚本,设置一个炸弹减少的函数

 public boomjianhsao(){

        this.boomshuliang-=1;

        this.boomui.updateui(this.boomshuliang);//ui更新

    }

2.回到敌人生成代码进行简单修改

ondoubleclike(){

            if(gamemanagerTS.getinstance().getboomshuliang()>0){

//判断数量是不是大于0,大于0在执行,避免出现负数错误

                gamemanagerTS.getinstance().boomjianhsao();

//触发上方的的炸弹减少函数

                const children = this.node.children;

                for(const child of children){

//通过遍历循环来让每一个子节点都销毁

                    if (child) {

//这里的if是避免bug,延迟销毁亦是如此。

                        setTimeout(() => {

                            child.destroy();

                             }, 0);

                    }

                }

            }

    }

3.如果这样会突然的消失,就很突兀,对代码进行简单修改,不让突然消失,而是让其爆炸

ondoubleclike(){

            if(gamemanagerTS.getinstance().getboomshuliang()>0){

                gamemanagerTS.getinstance().boomjianhsao();

                const children = this.node.children;

                for(const child of children){

                    if (child) {

                        if(child.getComponent(enemy)){//判断当前的子节点是不是带了enemy组件

                            const childComponent = child.getComponent(enemy);

//让一个常量等于这个拥有enemy的组件

                            childComponent.hp=0;

                            childComponent.cuihuipanduan();

//对血量进行修改,和新修改的摧毁函数,下面展开。

                        }

                        // setTimeout(() => {

                        //     child.destroy();

                        //      }, 0);

                    }

                }

            }

    }

4.对之前的敌人代码进行简单修改,让其执行摧毁动画

cuihuipanduan(){

        if (this.hp <= 0 ) {

            if (this.donghua) {

                this.donghua.play(this.cuihuiname);

            }

           

            this.collider.enabled = false;

            this.scheduleOnce(

                function(){

                this.node.destroy();

                },1

               );

            gamemanagerTS.getinstance().addscore(this.fenshu);

    }else{

        if (this.donghua) {

            this.donghua.play(this.shougongjiname);

        }

    }

    }

就是把之前的代码拆分整理为函数了,要不然炸弹调用的时候hp为0但是不执行动画,拆分后,让炸弹重新调用一下检测状态,让其销毁,动画,等等。

//拓展

1了解cocos之中的碰撞检测和触发检测

在使用碰撞检测和触发检测之前,需要先启用物理系统。

 PhysicsSystem.instance.enable = true;

 添加碰撞组件

为需要进行碰撞检测的节点添加碰撞组件。Cocos Creator 3.x 提供了多种碰撞组件,如BoxColliderSphereColliderCapsuleCollider等。

代码添加碰撞组件示例

 start() {

        // 获取当前节点

        const node = this.node;

        // 添加BoxCollider组件

        const collider = node.addComponent(BoxCollider);

        // 设置碰撞体的大小

        collider.size.set(1, 1, 1);

    }

 碰撞检测

要实现碰撞检测,需要监听碰撞事件。Cocos Creator 3.x 提供了三个碰撞事件:onCollisionEnter(碰撞开始)、onCollisionStay(碰撞持续)和onCollisionExit(碰撞结束)。

碰撞检测代码示例

start() {

        // 获取当前节点的碰撞组件

        const collider = this.node.getComponent(Collider);

        if (collider) {

            // 监听碰撞开始事件

            collider.on('onCollisionEnter', this.onCollisionEnter, this);

            // 监听碰撞持续事件

            collider.on('onCollisionStay', this.onCollisionStay, this);

            // 监听碰撞结束事件

            collider.on('onCollisionExit', this.onCollisionExit, this);

        }

    }

    // 碰撞开始事件处理函数

    onCollisionEnter(event: ICollisionEvent) {

        console.log('Collision Enter');

    }

    // 碰撞持续事件处理函数

    onCollisionStay(event: ICollisionEvent) {

        console.log('Collision Stay');

    }

    // 碰撞结束事件处理函数

    onCollisionExit(event: ICollisionEvent) {

        console.log('Collision Exit');

    }

触发检测

触发检测与碰撞检测类似,但触发检测不会产生物理碰撞效果,只是在物体进入、停留或离开触发区域时触发事件。要启用触发检测,需要将碰撞组件的isTrigger属性设置为true

触发检测代码示例

  start() {

        // 获取当前节点的碰撞组件

        const collider = this.node.getComponent(Collider);

        if (collider) {

            // 启用触发检测

            collider.isTrigger = true;

            // 监听触发开始事件

            collider.on('onTriggerEnter', this.onTriggerEnter, this);

            // 监听触发持续事件

            collider.on('onTriggerStay', this.onTriggerStay, this);

            // 监听触发结束事件

            collider.on('onTriggerExit', this.onTriggerExit, this);

        }

    }

    // 触发开始事件处理函数

    onTriggerEnter(event: ITriggerEvent) {

        console.log('Trigger Enter');

    }

    // 触发持续事件处理函数

    onTriggerStay(event: ITriggerEvent) {

        console.log('Trigger Stay');

    }

    // 触发结束事件处理函数

    onTriggerExit(event: ITriggerEvent) {

        console.log('Trigger Exit');

    }

2将预制体子弹放入画布之中

在了解完后,需要对其进行测试,因为不需要对于飞机发生物理碰撞,这里使用触发检测。

在预制体之中,添加碰撞组件。“boxcolider2d+”

勾选“boxcolider2d+”之中的editing,调整碰撞体的触发检测体积。

更改其sensor属性,让其变成触发器(传感器),这将会产生碰撞回调,但不会触发物理碰撞效果。

3了解刚体

刚体(Rigidbody)是物理模拟中的一个重要概念,它是具备物理属性的游戏对象,能够模拟真实世界中物体的物理行为。

在 Cocos Creator 3.x 里,刚体主要分为以下三种类型:

  • 静态刚体(Static Rigidbody)
    • 特性:质量为无穷大,位置和旋转不会受外力影响而改变,在物理模拟中始终保持静止。不过可以通过代码手动改变其位置和旋转。
    • 适用场景:常用于创建游戏中的地形、墙壁、固定的平台等不会移动的物体。
  • 运动刚体(Kinematic Rigidbody)
    • 特性:不受重力和其他外力的直接影响,但可以通过代码控制其运动,能够精确控制其位置、旋转和速度。
    • 适用场景:适用于一些需要按照特定路径或规则运动的物体,例如自动移动的平台、电梯等。
  • 动态刚体(Dynamic Rigidbody)
    • 特性:具有质量,会受到重力、碰撞、外力等因素的影响,能够模拟真实世界中物体的运动,会与其他刚体产生碰撞和交互。
    • 适用场景:可用于模拟各种可移动的物体,如游戏中的角色、道具、炮弹等。

4给预制体添加刚体组件

给预制体添加一个“rigidbody2”,更改其“rigidbody2”的type属性。

//当type值为static时候,代表这是一个静态的

//当type值为dynamic时候,代表这是一个动态的,会向下掉落。

将值改为kinematic

5将两个预制体子弹都加上碰撞组件和刚体。

拓展

碰撞组件和刚体有着密切的联系,它们相互协作来实现游戏中的物理交互效果。

  • 碰撞组件:碰撞组件用于定义游戏对象的碰撞边界,也就是确定该对象在物理空间中占据的区域。当两个带有碰撞组件的对象的碰撞边界发生重叠时,就会触发碰撞事件。常见的碰撞组件有BoxCollider(盒子碰撞体)、SphereCollider(球体碰撞体)、CapsuleCollider(胶囊体碰撞体)等。
  • 刚体:刚体是一种具备物理属性的游戏对象,它可以模拟真实世界中物体的物理行为。刚体分为静态刚体、运动刚体和动态刚体,不同类型的刚体在物理模拟中有不同的表现。

给敌人小飞机添加多边形碰撞器和刚体组件。

1进入小飞机敌人的预制体,添加碰撞器属性。

添加组件-》physick2d-》coliders-》poly...coliders(多边形碰撞器)

//box...coliders,方向碰撞器,cirdecoliders,圆形碰撞器。

2修改poly...coliders的points属性,默认为四个,表示为四个顶点。

因为小飞机敌人是倒三角形,所以设置为三个点方便调整。

3poly...coliders(多边形碰撞器)的sensor勾选

4给小飞机敌人添加刚体。physice2d

修改刚体的type属性为kinematic。原因和子弹相似,这样可以避免受到添加刚体后默认重力的影响。

碰撞事件的注册

1点击敌人通用脚本,更改其start函数内容。

//就让敌人一直向下的那个。

添加一个动画,小飞机敌人噶掉时候播放。

@property(Animation)

    anim:Animation=null;

设置一个hp,小飞机就一滴血,所以等于1.。

    hp:number=1;

在start里面进行一个赋值,监听。

start() {

        let collider=this.getComponent(Collider2D);

        if(collider){

            collider.on(Contact2DType.BEGIN_CONTACT,this.onbeginco,this);

        }

    }

 let collider=this.getComponent(Collider2D);

这行代码的作用是尝试获取当前节点上的 Collider2D 组件,并将其赋值给变量 collider。

 on 是事件监听方法,用于为指定事件类型添加回调函数。

         Contact2DType.BEGIN_CONTACT 是一个常量,表示 2D 碰撞开始的事件类型。

         this.onbeginco 是一个回调函数,当 Collider2D 组件检测到碰撞开始时,会调用这个函数。

修改了原update里面的代码,增加了一个逻辑判断,当hp>0时候才执行

update(deltaTime: number) {

        if(this.hp>0){

            let pp=this.node.position;

            this.node.setPosition(pp.x,pp.y-this.sudu,pp.z);

        }

     

    }

在start函数里面写了一个监听,当碰撞的时候激活该函数

onbeginco(){

        this.hp-=1;

        const animationComponent = this.getComponentInChildren(Animation);

        animationComponent.play();

    }

这里与siki老师写的不一样,我按照自己理解写了,倒是也实现了效果。

因为我将预制体的贴图和脚本分离了,所以只能这样了。我是设置了一个空节点来保存脚本,将贴图挂载到底下了。

这段代码将会在碰撞检测的时候触发,首先hp会减少,然后就会因为hp减少激活了update的if()就销毁了。

const animationComponent = this.getComponentInChildren(Animation);

    // getComponentInChildren 是节点对象的一个方法,用于查找当前节点及其所有子节点中第一个匹配指定类型的组件。

    // Animation 是 Cocos Creator 中用于处理动画的组件类型。

    // 这行代码尝试在当前节点及其子节点中查找第一个 Animation 组件,并将其赋值给常量 animationComponent。

    animationComponent.play();

    // play 是 Animation 组件的一个方法,用于播放该组件所包含的动画。

    // 这行代码调用 animationComponent 的 play 方法,开始播放动画。

    // 如果没有指定具体的动画名称,通常会播放默认的动画。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值