cocos creator 3.x开发放置类策略游戏从零起步

最近有位网友咨询关于放置类策略游戏的逻辑(这里的放置不是指挂机,而是自走),就是角色自动行走攻击的那种,网上找了一圈相关资源还挺少的,于是花了一个钟撸了一个最简单的demo,效果如下:
1.对手召唤剑士,向我方主城移动
2.我方点击“召唤剑士”按钮,召唤出我方剑士(我方剑士3点HP,敌方2点HP)
3.当对方的剑士被消灭后,我方的剑士会继续前进,直至攻击敌方主城
4.敌方主城HP归零后,游戏结束(敌方主城3点HP)

请添加图片描述

在开发前,首先需要注意的是,如果是个人开发者(个人主体)要上线此类微信小游戏,注册小程序后选择的分类建议选择“休闲-塔防”类,因为严格来说这种放置自走的游戏属于策略类,而策略类的一级分类是角色类,个人是禁止上线角色类的(另两个是捕鱼和棋牌),别辛辛苦苦开发的游戏上不了线哦…另外虽然设置了塔防类,但审核人员还是会根据游戏的实际内容修改你的分类的,如果他们觉得你是角色类,那逃不掉的,可以考虑申请软著上线字节等其他平台。

言归正传,看看代码:

首先我们新建项目,横屏,因为用到了碰撞体判断范围内是否有敌人,所以需要设置分组,这里设置了我方mine和敌方enemy两个分组,不同的分组会互相碰撞

在这里插入图片描述

然后场景的节点如下,我方主城和敌方主城挂载碰撞组件,敌方主城的TAG设置为1:

在这里插入图片描述

我方剑士和敌方剑士,做成预制体,用于动态生成,剑士的TAG设置为0,这样在碰撞后可以知道自己攻击的是敌方剑士还是主城

在这里插入图片描述

剑士的动画准备两个,一个是移动(一跳一跳),另一个是攻击(挥剑)

在这里插入图片描述

移动的动画,就是设置节点中三张图片(身体,手,武器)的position,Y坐标从0到10再到0即可,这个动画我们设置为循环播放

在这里插入图片描述

攻击的动画,就是设置武器图片的旋转,从0到-88度再到0,这个设置只播放一次(可以看到demo里剑士攻击的时候身体还在空中,是因为攻击动画中我忘记把body的position的Y设置为0了,这边设置一下就行)

在这里插入图片描述

节点都设置完毕了,代码的话总共有三份,一份是全局脚本,挂载在DemoManage上,控制游戏的开始与结束等,另两份是剑士的脚本(我方剑士和敌方剑士),分别挂载在上面两个预制体上

全局脚本:

import { _decorator, Collider2D, Component, Contact2DType, instantiate, Node, Prefab, resources, Tween, UITransform } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('ScriptDemoManage')
export class ScriptDemoManage extends Component {
    //定义我方主城的血量,3为被打三下就游戏结束
    public myhomeHp = 3
    //定义敌方主城的血量,3为被打三下就游戏结束
    public enemyhomeHp = 3
    start() {
        //NodeSwordmanEnemy是敌方剑士,默认血量2点,这里在游戏开始时动态生成一个,如果有需要也可以随机生成多个
        resources.load("prefab/NodeSwordmanEnemy", Prefab, (err, prefeb) => {
            var nodeSwordmanEnemy = instantiate(prefeb);
            //把这个剑士挂载到SpriteBG节点中
            nodeSwordmanEnemy.parent = this.node.parent.getChildByPath('Canvas/SpriteBG')
            nodeSwordmanEnemy.active = true
        })
    }

    update(deltaTime: number) {

    }

    homeAttacked() {
        //我方主城被攻击
        this.myhomeHp--;
        if(this.myhomeHp <= 0){
            //我方主城血量为0,游戏结束
            Tween.stopAll()
            this.node.parent.getChildByPath('Canvas/NodeGameOver').active = true
        }
    }
    enemyhomeAttacked() {
        //敌方主城被攻击
        this.enemyhomeHp--;
        if(this.enemyhomeHp <= 0){
            //敌方主城血量为0,游戏结束
            Tween.stopAll()
            this.node.parent.getChildByPath('Canvas/NodeGameOver').active = true
        }
    }

    btnSendSwordmanClick(){
        //玩家点击了“召唤剑士”按钮,动态生成一个我方剑士,嘎嘎乱杀,我方剑士默认血量为3点,在ScriptSwordman脚本中能找到
        resources.load("prefab/NodeSwordman", Prefab, (err, prefeb) => {
            var nodeSwordman = instantiate(prefeb);
            nodeSwordman.parent = this.node.parent.getChildByPath('Canvas/SpriteBG')
            nodeSwordman.active = true
        })
    }
}

我方剑士脚本:

import { _decorator, Collider2D, Component, Contact2DType, IPhysics2DContact, Node, tween, Vec3, Animation, PhysicsSystem2D, Tween } from 'cc';
import { ScriptSwordmanEnemy } from './ScriptSwordmanEnemy';
import { ScriptDemoManage } from './ScriptDemoManage';
const { ccclass, property } = _decorator;

@ccclass('ScriptSwordman')
export class ScriptSwordman extends Component {
    //初始化我方剑士的血量为3点
    public hp: number = 3;
    start() {
        //监听碰撞体事件,这里只监听onBeginContact,也就是第一次接触
        let collider = this.getComponent(Collider2D);
        if (collider) {
            collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
        }
        //开始移动,我方剑士往右边移动
        this.startMoving()
    }

    update(deltaTime: number) {

    }

    startMoving() {
        //匀速朝右边移动
        let tweenDuration: number = 10.0;
        tween(this.node)
            .to(tweenDuration, { position: new Vec3(this.node.position.x + 1200, -90, 0) })
            .start();
        //显示动画,一跳一跳的,动画设置的是循环播放
        const animationComponent = this.node.getComponent(Animation);
        animationComponent.play('effectMove')
    }

    onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        //只在两个碰撞体开始接触时被调用一次
        let _this = this
        //停止该节点的所有缓动,目的是让剑士停下来别走了,准备干架
        Tween.stopAllByTarget(this.node);
        //动画也停止,别一跳一跳了
        const animationComponent = this.node.getComponent(Animation);
        animationComponent.stop()
        //判断碰撞体的tag,这个tag是在目标node的组件里,0表示剑士,1表示主城,如果后续有其他兵种,记得设置不同的tag方便区分
        if (otherCollider.tag == 0) {
            //定义一个回调函数,用来循环执行攻击指令
            let callback = function () {
                //如果对手已经挂了,停止攻击,继续往前走
                if (!otherCollider.node) {
                    _this.unschedule(callback);
                    _this.startMoving()
                    return
                }
                //显示攻击动画
                const animationComponent = _this.node.getComponent(Animation);
                animationComponent.play('effectAttack')
                //获得敌方节点挂载的脚本,以便调用BeAttacked扣除它的血量
                const enemy = otherCollider.node.getComponent(ScriptSwordmanEnemy)
                enemy.BeAttacked()
                //判断血量为0,直接往前走,不用等一个循环了,但是上面的判断也需要因为可能存在并发,也就是两个剑士同时攻击一个目标
                if (enemy.hp <= 0) {
                    _this.scheduleOnce(() => {
                        _this.startMoving()
                    }, 0.5)
                    return
                }
            }
            //见面立即攻击一次,因为schedule不会立刻执行
            callback()
            //设置循环,2秒攻击一次
            this.schedule(callback, 2);
        }
        else if (otherCollider.tag == 1) {
            //tag==1表示攻击的是敌方主城
            let callback = function () {
                if (!otherCollider.node) {
                    _this.unschedule(callback);
                    return
                }
                //显示攻击动画
                const animationComponent = _this.node.getComponent(Animation);
                animationComponent.play('effectAttack')
                //主城没有单独挂载脚本,是在DemoManage节点统一处理的,所以这边找到DemoManage的脚本组件触发enemyhomeAttacked方法
                _this.node.parent.parent.parent.getChildByName('DemoManage').getComponent(ScriptDemoManage).enemyhomeAttacked()
            }
            callback()
            this.schedule(callback, 2);
        }
    }

    BeAttacked() {
        //我方被攻击
        this.hp--
        if (this.hp <= 0) {
            //没血了,0.2秒(挥剑动作正好碰到)后销毁当前节点
            this.scheduleOnce(() => {
                this.node.destroy()
            }, 0.2)
        }
    }
}



敌方剑士脚本:

import { _decorator, Collider2D, Component, Contact2DType, IPhysics2DContact, Node, PhysicsSystem2D, tween, Vec3, Animation, Tween } from 'cc';
import { ScriptSwordman } from './ScriptSwordman';
import { ScriptDemoManage } from './ScriptDemoManage';
const { ccclass, property } = _decorator;

@ccclass('ScriptSwordmanEnemy')
export class ScriptSwordmanEnemy extends Component {
    //初始化敌方剑士的血量为2点,比我方少一点
    public hp: number = 2;
    //脚本的逻辑和ScriptSwordman基本一致,只是换了目标,详细注释请阅读ScriptSwordman里面的注释
    //也可以只用一份脚本,根据变量区分敌我,这边是偷懒了copy了一份,违反了DRY原则,管它呢
    start() {
        let collider = this.getComponent(Collider2D);
        if (collider) {
            collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this);
        }
        this.startMoving()
    }

    update(deltaTime: number) {

    }

    startMoving() {
        //匀速朝左边移动
        let tweenDuration: number = 10.0;
        tween(this.node)
            .to(tweenDuration, { position: new Vec3(this.node.position.x - 1200, -90, 0) })
            .start();
        const animationComponent = this.node.getComponent(Animation);
        animationComponent.play('effectMove')
    }

    onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        // 只在两个碰撞体开始接触时被调用一次
        let _this = this
        Tween.stopAllByTarget(this.node);
        const animationComponent = this.node.getComponent(Animation);
        animationComponent.stop()
        if (otherCollider.tag == 0) {
            let callback = function () {
                if (!otherCollider.node) {
                    _this.unschedule(callback);
                    _this.startMoving()
                    return
                }
                const animationComponent = _this.node.getComponent(Animation);
                animationComponent.play('effectAttack')
                const mine = otherCollider.node.getComponent(ScriptSwordman)
                mine.BeAttacked()
                if (mine.hp <= 0) {
                    _this.scheduleOnce(() => {
                        _this.startMoving()
                    }, 0.5)
                    return
                }
            }
            callback()
            this.schedule(callback, 2);
        }
        else if (otherCollider.tag == 1) {
            let callback = function () {
                if (!otherCollider.node) {
                    _this.unschedule(callback);
                    return
                }
                const animationComponent = _this.node.getComponent(Animation);
                animationComponent.play('effectAttack')
                _this.node.parent.parent.parent.getChildByName('DemoManage').getComponent(ScriptDemoManage).homeAttacked()
            }
            callback()
            this.schedule(callback, 2);
        }
    }

    BeAttacked() {
        this.hp--
        if (this.hp <= 0) {
            this.scheduleOnce(() => {
                this.node.destroy()
            }, 0.2)
        }
    }
}



以上,素材用的均是kenney(https://kenney.nl/assets)的CC0资源,逻辑很简单仅供参考,需要此demo源码打包的可私信,随便写的有BUG请自行修复啊哈哈哈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰机大大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值