CocosCreator塔防

塔防(TowerDefence)

场景搭建

设置CocosCreator布局为经典布局

规范项目资源目录结构

目录 描述
scene 场景目录
script 脚本目录
texture 纹理目录
anim 动画片段

保存当前场景到scene目录下并命名为game.fire

将游戏资源拖拽到texture目录下

  • 搭建游戏场景
  • 游戏场景中创建路径导航

将地图纹理拖拽到Canvas渲染节点上释放后会在画布节点下生成新的地图节点,修改画布节点的设计分辨率为地图自身的宽高。

创建地图

设置主角hero节点,在其下创建单色渲染精灵节点,并命名为blood血条,调整血条的背景色为红色。设置blood`血条的宽高尺寸为30x3。

在blodd节点下创建子节点,即选择创建节点中的创建渲染下选中Sprite单色节点,同时命名为value。设置value节点中Sprite属性中Type为FIELD,同时设置Fill Range为1。

制作敌人

制作敌人与血条

制作角色与血条.gif

制作路径

使用动画编辑器创建补间动画完成路径的绘制

制作路径.gif
补间动画

使用自定义的组件类从补间动画中获取所有节点的坐标

贝塞尔曲线

贝塞尔曲线是应用于2D图形的数学曲线,该曲线的定义由4个点组成,分别是起始点p0,终止点p3(又称为锚点),以及两个相互分离的中间点控制点p1和p2。

  • p0 起始点
  • p1 中间控制点1
  • p2 中间控制点2
  • p3 终止点
贝塞尔曲线

创建path.js用户脚本组件,用于将路径动画片段中的贝塞尔曲线转化为坐标点。

$ vim /assets/script/path.js

cc.Class({
    extends: cc.Component,
    properties: {
        debug:true
    },
    onLoad () {
        //获取当前节点上的动画组件
        this.anim = this.node.getComponent(cc.Animation);
        //获取动画片段
        const clips = this.anim.getClips();
        //获取当前片段
        const clip = clips[0];

        //获取贝塞尔曲线路径
        this.pathdata = [];
        const paths = clip.curveData.paths;
        for(let k in paths){
            const frames = paths[k].props.position;
            this.genPathData(frames);
        }

        //根据调试模式绘制节点
        if(this.debug){
            //添加绘图组件
            this.graphics = this.node.addComponent(cc.Graphics);
            this.graphics.fillColor = cc.color(255, 0, 0, 255);
            this.graphic();
        }
    },
    graphic(){
        this.graphics.clear();
        for(let i=0; i<this.pathdata.length; i++){
            const path = this.pathdata[i];
            for(let j=0; j<path.length; j++){
                const node = path[j];
                this.graphics.moveTo(node.x - 1, node.y + 1);
                this.graphics.lineTo(node.x -1, node.y - 1);
                this.graphics.lineTo(node.x + 1, node.y - 1);
                this.graphics.lineTo(node.x + 1, node.y + 1);
                this.graphics.close();//封闭路径
            }
        }
        this.graphics.fill();//填充
    },
    genPathData(frames){
        let bezier = [];
        //贝塞尔曲线定义四个点:起点、终点、两个相互分离中间控制点点 [begin, ctrl1, ctrl2, end]
        let begin = null, ctrl1 = null, ctrl2 = null, end = null;
        //遍历关键帧获取贝塞尔曲线
        for(let i=0; i<frames.length; i++){
            const frame = frames[i];
            //获取控制起点
            if(ctrl1 !== null){
                bezier.push([begin, ctrl1, ctrl1, cc.v2(frame.value[0], frame.value[1])]);
            }
            //获取起点坐标
            begin = cc.v2(frame.value[0], frame.value[1]);
            //获取关键帧的运动路径
            const motionPath = frame.motionPath;
            //遍历关键帧运动路径获取贝塞尔曲线的四个点
            for(let j=0; j<motionPath.length; j++){
                const item = motionPath[j];
                //获取终点
                end = cc.v2(item[0], item[1]);
                //获取控制终点
                ctrl2 = cc.v2(item[2], item[3]);
                //判断控制起点是否为空
                if(ctrl1 === null){
                    ctrl1 = ctrl2;
                }
                //添加贝塞尔曲线中各点坐标
                bezier.push([begin, ctrl1, ctrl2, end]);
                //获取控制起点
                ctrl1 = cc.v2(item[4], item[5]);
                //设置下一个节点的起点为上一个节点的终点
                begin = end;
            }
        }
        //将贝塞尔曲线转换为坐标点
        const first = bezier[0][0];//获取起始点坐标
        let data = [first];
        for(let k=0; k<bezier.length; k++){
            const item = bezier[k];
            begin = item[0];
            ctrl1 = item[1];
            ctrl2 = item[2];
            end = item[3];
            //使用微元法获取曲线长度
            const length = this.getBezierLength(begin, ctrl1, ctrl2, end);
            //将每一段贝塞尔曲线转化为16个坐标点
            const count = Math.floor(length / 16);
            const delta = 1 / count;
            let t = delta;
            for(let m=0; m<count; m++){
                const x = begin.x * (1 - t) * (1 - t) * (1 - t) + 3 * ctrl1.x * t * (1 - t) * (1 - t) + 3 * ctrl2.x * t * t * (1 - t) + end.x * t * t * t;
                const y = begin.y * (1 - t) * (1 - t) * (1 - t) + 3 * ctrl1.y * t * (1 - t) * (1 - t) + 3 * ctrl2.y * t * t * (1 - t) + end.y * t * t * t;
                data.push(cc.v2(x, y));
                t += delta;
            }
        }
        this.pathdata.push(data);
    },
    //获取贝塞尔曲线节点 t [0, 1] t 分成20等分 1 / 20 = 0.05
    getBezierLength(begin, ctrl1, ctrl2, end) {
        const count = 20;
        const delta = 1 / count;
        //贝塞尔插值
        let length = 0;
        let prev = begin;
        let t = delta;
        for (let i = 0; i < count; i++) {
            let x = begin.x * (1 - t) * (1 - t) * (1 - t) + 3 * ctrl1.x * t * (1 - t) * (1 - t) + 3 * ctrl2.x * t * t * (1 - t) + end.x * t * t * t;
            let y = begin.y * (1 - t) * (1 - t) * (1 - t) + 3 * ctrl1.y * t * (1 - t) * (1 - t) + 3 * ctrl2.y * t * t * (1 - t) + end.y * t * t * t;
            let point = cc.v2(x, y);
            
            let dir = point.sub(prev);
            prev = point;
            length += dir.mag();
            
            t += delta;
        }
        
        return length;
    },
    getPathData(){
        return this.pathdata;
    }
});

path路径节点添加自定义的用户脚本组件path.js

设置脚本组件

预览观察调试模式下路径

路径

敌人导航

控制敌人沿着路径移动

$ vim assts/script/enemy.js
/**控制敌人按照路径导航移动 */
const Path = require("./path");
cc.Class({
    extends: cc.Component,
    properties: {
        path:{type:Path, default:null, tooltip:"路径组件"},
    },
    pause(){
        this.walking = false;
    },
    reset(){
        this.walking = true;
    },
    //读取配置文件获取初始化变量
    initConfig(config){
        this.config = config;
        this.speed = config.speed;
        this.index = config.index;//路径索引
    },
    start(){
        this.walking = false;
        //读取配置初始化参数
        this.initConfig({speed:200, min_blood:100, max_blood:1000, index:0});
        //生成路径节点坐标
        this.getPath(this.index);
        //设置敌人验证路径节点移动
        this.walk();
        //行走中的暂停,5秒内走2秒暂停3秒
        this.scheduleOnce(this.pause.bind(this), 3);//3秒后暂停
        this.scheduleOnce(this.reset.bind(this), 5);//移动5秒
    },
    //生成路径节点
    getPath(index){
        //获取目标路径节点的集合
        const pathdata = this.path.getPathData();
        if(index < 0 || index >= pathdata.length){
            return;
        }
        this.pathnode = pathdata[index];
        //cc.log("pathnode", this.pathnode);
    },
    //设置敌人验证路径节点移动
    walk(){
        if(this.pathnode.length < 2){
            return;
        }
        //设置敌人移动到起点位置
        this.node.setPosition(this.pathnode[0].x, this.pathnode[0].y);
        //记录下一步
        this.next = 1;
        //从当前点移动到下一点
        this.walkToNext();
    },
    //从当前点移动到下一步
    walkToNext(){
        //是否走到最后一点
        if(this.next > this.pathnode.length){
            this.walking = false;
            return;
        }
        //获取起点和终点
        const src = this.node.getPosition();
        const dst = this.pathnode[this.next];
        if(dst == undefined){
            return;
        }
        //获取起始点和终止点之间的方向向量
        const dir = dst.sub(src);
        //计算向量长度,取模
        const distance = dir.mag();
        //判断长度是否超出
        if(distance <= 0){
            this.next ++;
            this.walkToNext();//跳入下一步
            return;
        }
        //将速度分解
        this.vx = this.speed * dir.x / distance;
        this.vy = this.speed * dir.y / distance;
        //计算从起点移动到终点所需要耗费的时间长度
        this.walktime = distance / this.speed;

        //定义定时器用于判断行走过程中的累加时间是否大于总消耗时间
        this.timer = 0;

        //设置行走状态
        this.walking = true;
    },
    //敌人移动 dt是每次更新的时间间隔
    update(dt){
        if(!this.walking){
            return;
        }
        //累加时间
        this.timer += dt;
        //重新设置当前节点的坐标
        if(this.timer >= this.walktime){
            dt -= (this.timer - this.walktime);
        }
        this.node.x += this.vx * dt;
        this.node.y += this.vy * dt;
        //判断计时器时间进入下一步
        if(this.timer >= this.walktime){
            this.next ++;
            this.walkToNext();
        }
    }
});
添加脚本组件
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355