【cocos creator】经典微信飞机大战

飞机大战

序章

最近在学习cocos creator, 教程常常是cocos creator 2.4.x, 3.x API很多发生变化, 故记录一下使用3.x练习实现过程。如果对您有用,倍感荣幸。

习作: 经典的微信小游戏飞机大战

软件: cocos creator 3.8.0

开源地址: https://gitee.com/depingl/cc_plane_war.git

体验地址:https://planewar.lisi.fun/

设置背景和音乐

背景

使用两张图片, 循环向下, 实现背景图图片切换

有一个技巧,将两张背景图放在一个空节点下,循环遍历两个背景图片, 交替下移

刚开始我直接在下侧图片溢出可视区域后, 重新设置位置到上方, 偶尔会出现黑边, 即两个图片之间出现缝隙。这是由于屏幕刷新率的不确定性导致的,直接在图片坐标上加最好。

    update(deltaTime: number) {
        // console.log('update ...')
        this.bgMove(deltaTime)
    }


    bgMove(dt: number) {
        for (const bgNode of this.node.children) {
            // console.log(bgNode.position.y);
            // bgNode.position.set(0, bgNode.position.y - 50 * dt, 0)

            let p= bgNode.getPosition()
            p.y -= 50 * dt * 5;
            
            if(p.y <= -850) {
                // 使背景图片网上移动拼接  比直接重新设置位置更好, 直接设置可能出现黑边
                p.y += 852 * 2
            }

            bgNode.setPosition(p)
        }
    }

背景音乐

背景音乐可以直接在背景图片组件上添加AudioSource组件实现,也可以用代码实现,不过为了后续可以添加设置功能,控制背景音乐,建议使用代码的方式控制。

官方提供了一个音频播放管理器示例可以直接使用:

//AudioMgr.ts
import { Node, AudioSource, AudioClip, resources, director } from 'cc';
/**
 * @en
 * this is a sington class for audio play, can be easily called from anywhere in you project.
 * @zh
 * 这是一个用于播放音频的单件类,可以很方便地在项目的任何地方调用。
 */
export class AudioMgr {
    private static _inst: AudioMgr;
    public static get inst(): AudioMgr {
        if (this._inst == null) {
            this._inst = new AudioMgr();
        }
        return this._inst;
    }

    private _audioSource: AudioSource;
    constructor() {
        //@en create a node as audioMgr
        //@zh 创建一个节点作为 audioMgr
        let audioMgr = new Node();
        audioMgr.name = '__audioMgr__';

        //@en add to the scene.
        //@zh 添加节点到场景
        director.getScene().addChild(audioMgr);

        //@en make it as a persistent node, so it won't be destroied when scene change.
        //@zh 标记为常驻节点,这样场景切换的时候就不会被销毁了
        director.addPersistRootNode(audioMgr);

        //@en add AudioSource componrnt to play audios.
        //@zh 添加 AudioSource 组件,用于播放音频。
        this._audioSource = audioMgr.addComponent(AudioSource);
    }

    public get audioSource() {
        return this._audioSource;
    }

    /**
     * @en
     * play short audio, such as strikes,explosions
     * @zh
     * 播放短音频,比如 打击音效,爆炸音效等
     * @param sound clip or url for the audio
     * @param volume 
     */
    playOneShot(sound: AudioClip | string, volume: number = 1.0) {
        if (sound instanceof AudioClip) {
            this._audioSource.playOneShot(sound, volume);
        }
        else {
            resources.load(sound, (err, clip: AudioClip) => {
                if (err) {
                    console.log(err);
                }
                else {
                    this._audioSource.playOneShot(clip, volume);
                }
            });
        }
    }

    /**
     * @en
     * play long audio, such as the bg music
     * @zh
     * 播放长音频,比如 背景音乐
     * @param sound clip or url for the sound
     * @param volume 
     */
    play(sound: AudioClip | string, volume: number = 1.0, loop: boolean = true) {
        if (sound instanceof AudioClip) {
            this._audioSource.clip = sound;
            this._audioSource.play();
            this.audioSource.volume = volume;
            this.audioSource.loop = loop;
        }
        else {
            resources.load(sound, (err, clip: AudioClip) => {
                if (err) {
                    console.log(err);
                }
                else {
                    this._audioSource.clip = clip;
                    this._audioSource.play();
                    this.audioSource.volume = volume;
                    this.audioSource.loop = loop;
                }
            });
        }
    }

    /**
     * stop the audio play
     */
    stop() {
        this._audioSource.stop();
    }

    /**
     * pause the audio play
     */
    pause() {
        this._audioSource.pause();
    }

    /**
     * resume the audio play
     */
    resume() {
        this._audioSource.play();
    }
}

在背景图片控制脚本中调用:

    @property(AudioClip)
    bgAudio: AudioClip = null;

    // 音频管理器
    audioMgr: AudioMgr = null;

    start() {
        console.log('开始了..')

        // 播放背景音乐
        this.audioMgr = AudioMgr.inst;
        this.audioMgr.play(this.bgAudio, 0.6)
    }

英雄就位

添加英雄节点,设置图片和脚本。英雄随指尖移动,通过监听事件实现:

	// 移动
    heroMove() {
        input.on(Input.EventType.TOUCH_MOVE, this.heroMoveEvent, this)
    }

    // 英雄移动逻辑
    heroMoveEvent(event: EventTouch) {
        // 英雄的位置就是移动的位置
        let p: Vec2 = event.getUILocation();
        console.log(p);

        // 加一个判断, 飞机边缘也不能溢出屏幕, 这有一个问题, 敌机出现位置不应在英雄射击盲区
        if(p.x < this.nodeUI.width / 2) {
            p.x = this.nodeUI.width / 2
        }else if(p.x > this.viewSize.width - this.nodeUI.width / 2) {
            p.x = this.viewSize.width - this.nodeUI.width / 2
        }
        if(p.y < this.nodeUI.height / 2) {
            p.y = this.nodeUI.height / 2
        }else if(p.y > this.viewSize.height - this.nodeUI.height / 2) {
            p.y = this.viewSize.height - this.nodeUI.height / 2
        }

        this.node.setWorldPosition(new Vec3(p.x, p.y))
        

    }

注意:

  1. 这里加了一个判断, 使英雄不会部分躯干溢出屏幕
  2. 坐标系问题, 监听事件返回的是UI坐标, 需要使用setWorldPosition设置位置

英雄发射子弹

子弹通过预制体生成。

关于子弹的逻辑:

  1. 在英雄正前方周期生成
  2. 生成时播放发射音乐
  3. 向上飞行,超出可视区域销毁
  4. 碰到敌机,销毁,并通知敌机发生碰撞

2 3 4显然是子弹的行为,1是英雄行为,所以脚本逻辑写在对应组件上。新建子弹脚本Bullet.ts

生成时播放发射音乐

我们使用背景音乐播放中用的AudioMgr来播放:

// Bullet.ts

	// 音频路径
    _shootAudio: string = 'audio/shot';

    start() {
        this.playShootAudio()
    }
    /**
     * 播放发射音频
     */
    playShootAudio() {
        AudioMgr.inst.playOneShot(this._shootAudio)
    }

向上飞行,超出可视区域销毁

	update(deltaTime: number) {
        this.fly(deltaTime)
    }
    
    /**
     * 子弹飞行
     */
    fly(dt: number) {
        let p = this.node.getPosition()
        p.y += dt * 800;
        this.node.setPosition(p)

        if (p.y > 450) {
            // console.log('子弹销毁!!');
            this.node.destroy()
        }
    }

碰到敌机,销毁,并通知敌机发生碰撞

这里需要用到碰撞系统。

碰撞系统

  1. 可以碰撞的物体必须是刚体组件和碰撞体组件, 本例要将重力设置为0, 防止敌机和子弹受到重力影响
  2. 通过tag判断碰撞体分组
  3. 在碰撞回调中处理碰撞结果逻辑:
    1. 子弹销毁
    2. 敌机死亡
// Bullet.ts	
	start() {	
        // 注册碰撞回调方法
        let collider = this.getComponent(Collider2D)
        if (collider) {
            console.log('开启碰撞...');
            collider.on(Contact2DType.BEGIN_CONTACT, this.beginContact, this)
        }

    }
    
    // 死亡
    die() {
        console.log('子弹销毁...');
        this.node.destroy()
    }


    // 碰撞检测方法
    /**
     * 子弹 1
     * 敌机 2
     */
    beginContact(selfCollider: Collider2D, otherCollider: Collider2D) {

        // 敌机
        if (otherCollider.tag == 2) {
            // 必须使用director.once延迟销毁
            director.once(Director.EVENT_AFTER_PHYSICS, ()=> {
                // 销毁自身  即  子弹
                selfCollider.node.destroy()
                console.log(otherCollider);
                // otherCollider.node.destroy()
    			// 调用敌机销毁方法
                otherCollider.node.getComponent(EnemyControl).die()
            })

        }
       

    }

本例要将重力设置为0, 防止敌机和子弹受到重力影响:在背景脚本中添加:

	start() {
        console.log('开始了..')
        // 开启物理系统
        PhysicsSystem2D.instance.enable = true
        // 重力为0
        PhysicsSystem2D.instance.gravity = v2()
    }

英雄周期生成子弹

将上方字段作为预制体, 在英雄脚本中添加:

	start() {
        // 发射子弹
        this.shot()
    }
    // 周期发射子弹
    shot() {
        setInterval(() => {
            console.log('create a bullet');
            let myBullet = instantiate(this.bulletPrefab)
            myBullet.setParent(director.getScene().getChildByName('Canvas'))
            // director.getScene().addChild(myBullet)
            let nodePos = this.node.getPosition()
            myBullet.setPosition(new Vec3(nodePos.x, nodePos.y + this.nodeUI.height / 2))
        }, 400)
    }

敌机登场

  1. 敌机自上而下滑行, 超出屏幕下方时自动销毁

  2. 敌机被子弹碰撞后销毁,销毁时播放爆炸声和爆炸动画

  3. 敌机与英雄碰撞杀死英雄, 减少游戏机会或游戏结束

  4. 敌机有多种类型:

    1. 更大的飞机可以承受更多子弹
    2. 更大的飞机可以获取更多的分数
  5. 敌机随机出现, 出现位置在可视区域稍上方

飞行和自动销毁

敌机脚本:

	update(deltaTime: number) {
        // 向下移动
        this.move(deltaTime)
    }

    move(dt: number) {
        let p = this.node.getPosition()
        p.y -= dt * 50 * 4
        this.node.setPosition(new Vec3(p.x, p.y))
        if(p.y < -450) {
            this.node.destroy()
        }
    }

敌机被子弹碰撞后销毁

此处碰撞逻辑由子弹完成, 只用写一个死亡脚本。

	// 死亡方法
    die() {
        this._isActive = false

        // 播放击中死亡音乐
        AudioMgr.inst.playOneShot('audio/boom')

        // 显示死亡图片
        resources.load('enemy0_die/spriteFrame', SpriteFrame, (err, asset) => {
            if(err) console.log('图片加载失败: ', err);
            this.node.getComponent(Sprite).spriteFrame = asset
        })

        // 1s后销毁节点
        this.scheduleOnce(() => this.node.destroy(), 0.2)

        
    }

**特别注意: **

加载图片资源时, 需要在url中加入spriteFrame, 否则无法加载成功

击溃英雄

也是一次碰撞检测

	start() {
        // 注册碰撞回调方法
        let collider = this.getComponent(Collider2D)
        if (collider) {
            // console.log('开启碰撞...');
            collider.on(Contact2DType.BEGIN_CONTACT, this.beginContact, this)
        }
    }
    
    // 碰撞检测方法
    /**
     * 子弹 1
     * 敌机 2
     * 英雄 3
     */
    beginContact(selfCollider: Collider2D, otherCollider: Collider2D) {
        // 英雄
        if (this._isActive && otherCollider.tag == 3) {
            console.log('发生敌机碰撞..');
            
            director.once(Director.EVENT_AFTER_PHYSICS, ()=> {
                
                // 自我销毁
                selfCollider.node.destroy()

                otherCollider.node.getComponent(HeroControl).die()
            })

        }

    }

不同敌机

更多重复逻辑即可

敌机随机出现

使用预制体生成敌机, 在背景脚本中添加:

	/**
     * 
     * 生成敌机
     * 
     */
    generateEnemy() {
        this.schedule(() => this.randonEnemy(), math.randomRange(0.5, 3))
    }
    /**
     * 随机生成敌机
     */
    randonEnemy() {
        let enemy = instantiate(this.enemyPre0)
        enemy.setParent(director.getScene().getChildByName('Canvas'))

        // 左右不超过攻击范围
        let tmp = this.viewSize.width / 2 - 30
        enemy.setPosition(v3(math.randomRange(-tmp, tmp), 480))
    }

英雄的落幕

英雄之死, 同敌机。

	die() {
        console.log('you are die');

        // 播放击中死亡音乐
        AudioMgr.inst.playOneShot('audio/boom2')

        // 显示死亡图片
        resources.load('hero1_die/spriteFrame', SpriteFrame, (err, asset) => {
            if(err) console.log('图片加载失败: ', err);
            this.node.getComponent(Sprite).spriteFrame = asset
        })

        // 1s后销毁节点
        this.scheduleOnce(() => this.node.destroy(), 0.2)
    }

还应该有开始游戏、结束游戏场景切换,得分系统,多样敌机,随时间难度增加,子弹buff等功能。。。

有还录制了一个视频,欢迎围观:https://www.bilibili.com/video/BV187hVeoEf9

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值