塔防(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();
}
}
});
添加脚本组件