infinite-world示例展示了小球顺着山坡凹凸做左右滚动的效果。
技术点
1、山坡由数量不等动态生成的的竖条状方块组成。
2、每个方块动态添加RigidBody组件和PolygonCollider组件,使小球和山坡产生物理碰撞效果。
3、摄像机根据山坡的凹凸高度做动态缩放。
4、通过键盘或触摸来控制小球的左右滚动。
源码分析
camera-control.js
该源文件功能是根据小球在屏幕上的位置高度来控制摄像机的缩放。
cc.Class({
extends: cc.Component,
properties: {
target: {
default: null,
type: cc.Node
}
},
// LIFE-CYCLE CALLBACKS:
onLoad () {
this.camera = this.getComponent(cc.Camera);
},
onEnable: function() {
// 将物理系统的调试绘制信息附加到指定摄像机上。
// 使用摄像机时,如果使用到了物理系统或碰撞系统等内置渲染节点的系统,
// 那么需要将它们的渲染节点也添加摄像机上。
cc.director.getPhysicsManager().attachDebugDrawToCamera(this.camera);
},
onDisable: function() {
// 将物理系统的调试绘制信息从指定摄像机上移除
cc.director.getPhysicsManager().attachDebugDrawFromCamera(this.camera);
},
lateUpdate: function(dt) {
// 此例中,this.target指小球,即将小球中心点转换为世界空间坐标系
let targetPos = this.target.convertToWorldSpaceAR(cc.Vec2.ZERO);
// 再转换为游戏Scene的(局部)空间坐标系,并调整摄像机到相应位置
this.node.position = this.node.parent.convertToNodeSpaceAR(targetPos);
// ratio的值区间将为 0 < ratio < 1
let ratio = targetPos.y / cc.winSize.height;
// 如小球位于屏幕中部,则摄像机缩放比例不变(即保持1),如屏幕上半部则缩小,如屏幕下半部则放大。
// 极端情况下,如小球位于屏幕顶部,则缩小25%,如果小球位于屏幕底部,则放大25%。
this.camera.zoomRatio = 1 + (0.5 - ratio) * 0.5;
},
// update (dt) {},
});
ball-control.js
该源文件功能是根据输入事件控制小球的运动方向和速度。
const MOVE_LEFT = 1; // 向左移动标志位
const MOVE_RIGHT = 2; // 向右移动标志位
cc.Class({
extends: cc.Component,
properties: {
maxSpeed: 1200
},
// LIFE-CYCLE CALLBACKS:
onLoad () {
// 注册键盘按下和释放事件的回调
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
// 注册触摸事件的回调
var canvas = cc.find('/Canvas');
canvas.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
canvas.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
this.moveFlags = 0;
},
start () {
// start 在 onLoad 之后,此时RigidBody组件已经被加载进来
this.body = this.getComponent(cc.RigidBody);
},
onKeyDown(event) {
switch(event.keyCode) {
case cc.KEY.a:
case cc.KEY.left:
this.moveFlags |= MOVE_LEFT; // 添加向左移动的标志位
this.updateMotorSpeed();
break;
case cc.KEY.d:
case cc.KEY.right:
this.moveFlags |= MOVE_RIGHT; // 添加向右移动的标志位
this.updateMotorSpeed();
break;
}
},
onKeyUp (event) {
switch(event.keyCode) {
case cc.KEY.a:
case cc.KEY.left:
this.moveFlags &= ~MOVE_LEFT; // 清除向左移动标志
break;
case cc.KEY.d:
case cc.KEY.right:
this.moveFlags &= ~MOVE_RIGHT; // 清除向右移动标志
break;
}
},
onTouchStart: function(event) {
let touchLoc = event.touch.getLocation();
if (touchLoc.x < cc.winSize.width/2) {
this.moveFlags |= MOVE_LEFT; // 添加向左移动的标志位
} else {
this.moveFlags |= MOVE_RIGHT; // 添加向右移动的标志位
}
this.updateMotorSpeed();
},
onTouchEnd: function(event) {
let touchLoc = event.touch.getLocation();
if (touchLoc.x < cc.winSize.width/2) {
this.moveFlags &= ~MOVE_LEFT; // 清除向左移动标志
} else {
this.moveFlags &= ~MOVE_RIGHT; // 清除向右移动标志
}
},
updateMotorSpeed() {
// 判断this.body是否可用
if (!this.body) {
return;
}
var desiredSpeed = 0;
if ((this.moveFlags & MOVE_LEFT) == MOVE_LEFT) {
desiredSpeed = -this.maxSpeed;
} else if ((this.moveFlags & MOVE_RIGHT) == MOVE_RIGHT) {
desiredSpeed = this.maxSpeed;
}
// 设置小球刚体角速度来控制小球的运动方向和速度
this.body.angularVelocity = desiredSpeed;
},
update (dt) {
// 判断标志位是否为空(避免在没有事件触发时也去改变小球运动)
if (this.moveFlags) {
this.updateMotorSpeed();
}
},
});
infinite-world.js
该源文件功能是随着小球方向动态生成N个高度不等的方块,从而组成凹凸不平的山坡,每个方块都动态添加了物理组件,每个方块宽度的粒度越小,则山坡越平滑。方块的颜色默认为青色,由物理引擎的调试绘制标志位决定。
cc.Class({
extends: cc.Component,
properties: {
pixelStep: 10, // 每个矩形的宽度(N个矩形组成一个山坡)
xOffset: 0, // 当前最新创建矩形的x坐标
yOffset: 240, // 山坡的最低高度
target: {
default: null,
type: cc.Node
}
},
// LIFE-CYCLE CALLBACKS:
onLoad: function () {
this.hills = [];
this.pools = [];
while (this.xOffset < 1200) {
this.generateHill(10);
}
},
// 生成一个竖条状矩形
generateHillPiece(xOffset, points) {
let hills = this.hills;
let first = hills[0];
// 若小球离第一个块的距离超过1000,则不再创建新的node,直接复用原有数组的第一个元素
if (first && ((this.target.x - first.node.x) > 1000)) {
first.node.x = xOffset;
first.collider.points = points;
first.collider.apply();
hills.push(hills.shift());
return;
}
let node = new cc.Node();
node.x = xOffset;
let body = node.addComponent(cc.RigidBody);
body.type = cc.RigidBodyType.Static;
let collider = node.addComponent(cc.PhysicsPolygonCollider);
collider.points = points;
collider.friction = 1;
node.parent = this.node;
hills.push({node:node, collider:collider});
},
// 生成山坡
// 每座山坡由N个竖条状矩形组成,
// 每座山坡的绘制都分成2步:第1步绘制上坡,第2步绘制下坡
generateHill () {
let pixelStep = this.pixelStep;
let xOffset = this.xOffset;
let yOffset = this.yOffset;
// 山坡宽度,值区间 120-640
let hillWidth = 120 + Math.ceil(Math.random()*26)*20;
// 计算山坡由多少个矩形组成
let numberOfSlices = hillWidth / pixelStep;
let j;
let points = [];
// first step
let randomHeight;
if (xOffset === 0) {
randomHeight = 0;
} else {
// make sure yOffset < 600
randomHeight = Math.min(Math.random() * hillWidth / 7.5, 600 - yOffset);
}
yOffset += randomHeight;
for (j = 0; j < numberOfSlices/2; j++) {
points.length = 0;
points.push(cc.v2(0, 0));
// 计算弧度
let rad = Math.cos(2*Math.PI/numberOfSlices*j);
points.push(cc.v2(0, yOffset-randomHeight*rad));
rad = Math.cos(2*Math.PI/numberOfSlices*(j+1));
points.push(cc.v2(pixelStep, yOffset-randomHeight*rad));
points.push(cc.v2(pixelStep, 0));
this.generateHillPiece(xOffset + j*pixelStep, points);
}
yOffset += randomHeight;
// second step
if (xOffset === 0) {
randomHeight = 0;
} else {
// make sure yOffset>240
randomHeight = Math.min(Math.random() * hillWidth / 5, yOffset - 240);
}
yOffset -= randomHeight;
for (j = numberOfSlices/2; j < numberOfSlices; j++) {
points.length = 0;
points.push(cc.v2(0, 0));
// 计算弧度
let rad = Math.cos(2*Math.PI/numberOfSlices*j);
points.push(cc.v2(0, yOffset-randomHeight*rad));
rad = Math.cos(2*Math.PI/numberOfSlices*(j+1));
points.push(cc.v2(pixelStep, yOffset-randomHeight*rad));
points.push(cc.v2(pixelStep, 0));
this.generateHillPiece(xOffset + j*pixelStep, points);
}
yOffset -= randomHeight;
this.xOffset += hillWidth;
this.yOffset = yOffset;
},
update: function (dt) {
if (!this.target)
return;
// 如果小球离x轴边界不足1200,则创建新的hill
while ((this.target.x + 1200) > this.xOffset) {
this.generateHill();
}
},
});