“好记性不如烂笔头,记录开发过程中的点点滴滴”——xshu
“书写是对思维的缓存” ——佚名
进入正题
在游戏开发过程中经常会遇到渲染元素的尺寸大于你所能展示的区域,需要玩家自行进行操作、拖动以及点击,比如地图通常会超出玩家所示区域,这个时候需要一个能够缩放、拖动以及点击的组件来完成这个需求。
1.效果展示
先来看看实际效果(由于电脑无法操作双指缩放,所以是先在手机上录屏然后再在电脑上做成gif的,文末有demo地址,大家可以下载下来自行体验一下)
2.属性配置
1.组件的一些配置参数,可以直接在编辑器里面填写数值来调试效果。
2.再来看看组件的节点以及属性定义块
下图是编辑器里面的设置
3.在start生命周期接口里面会先对map和mapContainer进行判断,如果为空则取默认值。
3.代码逻辑的实现细节
private addEvent(): void {
this.map.on(cc.Node.EventType.TOUCH_MOVE, (event: any) => {
if (this.operLock) return; // 如果触摸操作暂时锁定则不响应
let touches: any[] = event['getTouches'](); // 获取所有触摸点
// 1.x当多点触摸的时候 第二个触摸点即使不在node上也能进来 而且target也是当前node
// 通过rect是否包含当前触摸点来过滤无效的触摸点
touches
.filter(v => {
let startPos: cc.Vec2 = v.getStartLocation(); // 触摸点最初的位置
let worldPos: cc.Vec2 = this.mapContainer.convertToWorldSpaceAR(cc.Vec2.ZERO);
let worldRect: cc.Rect = cc.rect(
worldPos.x - this.mapContainer.width / 2,
worldPos.y - this.mapContainer.height / 2,
this.mapContainer.width,
this.mapContainer.height
);
return worldRect.contains(startPos);
})
.forEach(v => { // 将有效的触摸点放在容器里自行管理
let temp: any[] = this.mapTouchList.filter(v1 => v1.id === v.getID());
if (temp.length === 0) {
this.mapTouchList.push({ id: v.getID(), touch: v });
}
})
;
if (this.mapTouchList.length >= 2) { // 如果容器内触摸点数量超过1则为多点触摸,此处暂时不处理三点及以上的触摸点,可以根据需求来处理
this.isMoving = true;
this.dealTouchData(this.mapTouchList, this.map);
} else if (this.mapTouchList.length === 1) {
// sigle touch
let touch: any = this.mapTouchList[0].touch;
let startPos: cc.Vec2 = touch.getStartLocation();
let nowPos: cc.Vec2 = touch.getLocation();
// 有些设备单点过于灵敏,单点操作会触发TOUCH_MOVE回调,在这里作误差值判断
if ((Math.abs(nowPos.x - startPos.x) <= MOVE_OFFSET ||
Math.abs(nowPos.y - startPos.y) <= MOVE_OFFSET) &&
!this.isMoving) {
return cc.log('sigle touch is not move');
}
let dir: cc.Vec2 = touch.getDelta();
this.isMoving = true;
this.dealMove(dir, this.map, this.mapContainer);
}
}, this);
this.map.on(cc.Node.EventType.TOUCH_END, (event) => {
if (this.operLock) return cc.log('operate is lock');
// 需要自行管理touches队列, cocos 的多点触控并不可靠
if (this.mapTouchList.length < 2) {
if (!this.isMoving) {
let worldPos: cc.Vec2 = event['getLocation']();
let nodePos: cc.Vec2 = this.map.convertToNodeSpaceAR(worldPos);
this.dealSelect(nodePos);
}
this.isMoving = false; // 当容器中仅剩最后一个触摸点时讲移动flag还原
};
this.removeTouchFromContent(event, this.mapTouchList);
}, this);
this.map.on(cc.Node.EventType.TOUCH_CANCEL, (event) => {
if (this.operLock) return;
if (this.mapTouchList.length < 2) { // 当容器中仅剩最后一个触摸点时讲移动flag还原
this.isMoving = false;
};
this.removeTouchFromContent(event, this.mapTouchList);
}, this);
this.map.on(cc.Node.EventType.MOUSE_WHEEL, (event) => {
if (this.operLock) return;
cc.log('==== MOUSE WHEEL ===');
let location: any = event['getLocation']();
let worldPos: cc.Vec2 = cc.v2(location.x, location.y);
let scrollDelta: number = event['getScrollY']();
let scale: number = (this.map.scale - (scrollDelta / 10000 * -1));
let target: cc.Node = this.map;
let pos: cc.Vec2 = target.convertToNodeSpaceAR(worldPos);
this.smoothOperate(target, pos, scale);
}, this);
}
// 删除无用的触摸点
public removeTouchFromContent(event: any, content: any[]): void {
let eventToucheIDs: number[] = event['getTouches']().map(v => v.getID());
for (let len = content.length, i = len - 1; i >= 0; --i) {
if (eventToucheIDs.indexOf(content[i].id) > -1)
content.splice(i, 1); // 删除触摸
}
}
2.处理触摸过程中的缩放动作
private dealTouchData(touches: any[], target: cc.Node): void {
let touch1: any = touches[0].touch;
let touch2: any = touches[1].touch;
let delta1: any = touch1.getDelta();
let delta2: any = touch2.getDelta();
let touchPoint1: cc.Vec2 = target.convertToNodeSpaceAR(touch1.getLocation());
let touchPoint2: cc.Vec2 = target.convertToNodeSpaceAR(touch2.getLocation());
let distance: cc.Vec2 = touchPoint1.sub(touchPoint2);
let delta: cc.Vec2 = delta1.sub(cc.v2(delta2.x, delta2.y));
let scale: number = 1;
if (Math.abs(distance.x) > Math.abs(distance.y)) {
scale = (distance.x + delta.x) / distance.x * target.scaleX;
} else {
scale = (distance.y + delta.y) / distance.y * target.scaleY;
}
let pos: cc.Vec2 = touchPoint2.add(cc.v2(distance.x / 2, distance.y / 2));
this.smoothOperate(target, pos, scale);
}
private smoothOperate(target: cc.Node, pos: cc.Vec2, scale: number): void {
let scX: number = scale;
// 当前缩放值与原来缩放值之差
let disScale: number = scX - target.scaleX;
// 当前点击的坐标与缩放值差像乘
let gapPos: cc.Vec2 = pos.scale(cc.v2(disScale, disScale));
// 当前node坐标位置减去点击 点击坐标和缩放值的值
let mapPos: cc.Vec2 = target.getPosition().sub(cc.v2(gapPos.x, gapPos.y));
// 放大缩小
if (!this.isOutRangeScale(scale)) {
scale = (scale * 100 | 0) / 100;
target.scale = scale;
this.dealScalePos(mapPos, target);
}
// 更新 label 显示
scale = this.dealScaleRange(scale);
this.scaleTime.string = `${scale * 100 | 0}%`;
}
// 处理地图边缘缩放,移动地图
private dealScalePos(pos: cc.Vec2, target: cc.Node): void {
if (target.scale === 1) {
pos = cc.Vec2.ZERO;
}
else {
let worldPos: cc.Vec2 = this.node.convertToWorldSpaceAR(pos);
let nodePos: cc.Vec2 = this.node.convertToNodeSpaceAR(worldPos);
let edge: any = this.calculateEdge(target, this.node, nodePos);
if (edge.left > 0) {
pos.x -= edge.left;
}
if (edge.right > 0) {
pos.x += edge.right;
}
if (edge.top > 0) {
pos.y += edge.top;
}
if (edge.bottom > 0) {
pos.y -= edge.bottom;
}
}
target.position = pos;
}
3.处理玩家拖拽地图的操作
private dealMove(dir: cc.Vec2, map: cc.Node, container: cc.Node): void {
let worldPos: cc.Vec2 = map.convertToWorldSpaceAR(cc.Vec2.ZERO);
let nodePos: cc.Vec2 = container.convertToNodeSpaceAR(worldPos);
nodePos.x += dir.x;
nodePos.y += dir.y;
let edge: any = this.calculateEdge(map, container, nodePos);
if (edge.lBorderDelta <= 0 && edge.rBorderDelta <= 0) {
map.x += dir.x;
}
if (edge.uBorderDelta <= 0 && edge.dBorderDelta <= 0) {
map.y += dir.y;
}
}
4.处理玩家单击地图的操作,可以在组件外调用setSingleTouchCb来传入响应玩家点击操作的回调函数
public setSinglTouchCb(cb: Function): void {
this.singleTouchCb = cb;
}
private dealSelect(nodePos: cc.Vec2): void {
cc.log(`click map on cc.v2(${nodePos.x}, ${nodePos.y})`);
// do sth
if (this.singleTouchCb) this.singleTouchCb(nodePos);
}
5.处理移动和缩放用到的计算地图边距与容器距离的api
// 计算map的四条边距离容器的距离
public calculateEdge(target: cc.Node, container: cc.Node, nodePos: cc.Vec2): any {
let realWidth: number = target.width * target.scaleX;
let realHeight: number = target.height * target.scaleY;
let lBorderDelta: number = (nodePos.x - realWidth / 2) + container.width / 2;
let rBorderDelta: number = container.width / 2 - (realWidth / 2 + nodePos.x); // <= 0 safe
let uBorderDelta: number = container.height / 2 - (realHeight / 2 + nodePos.y);
let dBorderDelta: number = (nodePos.y - realHeight / 2) + container.height / 2;
return { lBorderDelta, rBorderDelta, uBorderDelta, dBorderDelta };
}
4.文章结尾
这也是我第一次尝试封装组件然后分享出来,文笔很差请大家见谅,希望能够帮助到大家,同时也是对自己平日工作里点点滴滴的一种记录以及对自身文笔一种锻炼。感谢大家能够阅读到最后,有什么不足之处或疑惑的地方欢迎大家留言讨论。
最后附上demo地址:GitHub - xshu1996/MapControl-master
// update 2020/04/02 新增鼠标滚轮放大缩小功能
在这里感谢 渡鸦 大佬的公众号,大家可以关注 “CocosCreator笔记” 微信公众号获取更多游戏开发过程中的一些知识分享,谢谢大家~