项目背景:
游戏背包 需要手动 拖拽游戏装备到 装备卡槽中,看了下网上资料很少。手搓了一个下午搞定,现在来记录下实现步骤;
功能拆分:
一个完整需求,我们一般会把它拆分成 几个小步骤分别造零件。等都造好了我们就能 装配到一起 形成一个完成功能。以下是对上面功能的 步骤拆分:
- 背包详情的展示,点击背包中的物品 展示一个详情页面(因为 是在 ScrollView 中拖拽 ScrollView 的元素 会造成 ScrollView 滚动)所有我们直接拖拽 详情页面中的元素就能 避免对 ScrollView 的影响。(如果没有详情可以 先生成一个一样的元素放置的 ScrollView 外层)
- 实现 拖拽元素,本身是对 拖拽事件的监听 来完成对元素位置的设置。这里有个知识点 触点坐标 到 世界坐标的转换
- 能拖拽元素了 那么我们就需要把 元素放在对应的位置, 实际上就是在拖拽的时候 判断两个元素的位置,根据需求我们这里是直接判断的 元素的 x坐标;来判断拖拽元素 是否到了 对应的卡槽;
- 既然能判断到那个具体卡槽了,我们停止拖拽,那就装备到对应的卡槽,要是没有到卡槽位置,我们隐藏拖拽元素 返回 (假装什么也没发生)
1.背包装备详情
点击装备展示,对应详情信息,应为装备位置不固定,
- 展示的时候就需要 通过 背包元素的 位置 来计算详情页面展示的位置
- 并且如果是 在边缘位置还要修改要展示的位置,然后修正位置信息后 在做展示
这里有一个技术点就是 坐标位置的转换:
想要对比两个节点元素 或者 参照当前节点 设置另一个节点位置信息(不在同一个父节点的时候) 需要把他们转换在 同一个坐标系及(参照同一个父节点)
问题: 由于图中的 粉色ScrollView item 和 详情不是在用一个坐标参照中, 如果我们直接获取 item 位置 赋值给 详情元素 那肯定无法让他们两位置一直;
解决问题思路:
先计 算出 item 世界坐标,然后再把这个坐标转化到 详情页面的局部坐标(他们世界坐标一直,局部坐标不一致)
同为 cocos creator 坐标转换需要使用 使用节点父元素计算:
https://docs.cocos.com/creator/3.8/api/zh/class/UITransform?id=convertToWorldSpaceARhttp://convertToWorldSpaceAR
ItemworldPositon = item.parent.getComponent(UITransform).convertToWorldSpaceAR(item.getPosition());详情面板位置 = 详情面板.parent.getComponent(UITransform).convertToNodeSpaceAR( ItemworldPositon)
- convertToNodeSpaceAR
/*** @en* Converts a Point to node (local) space coordinates.** @zh* 将一个 UI 节点世界坐标系下点转换到另一个 UI 节点 (局部) 空间坐标系,这个坐标系以锚点为原点。* 非 UI 节点转换到 UI 节点(局部) 空间坐标系,请走 Camera 的 `convertToUINode`。** @param worldPoint @en Point in world space.* @zh 世界坐标点。* @param out @en Point in local space.* @zh 转换后坐标。* @returns @en Return the relative position to the target node.* @zh 返回与目标节点的相对位置。* @example* ```ts* const newVec3 = uiTransform.convertToNodeSpaceAR(cc.v3(100, 100, 0));* ```*/ convertToNodeSpaceAR(worldPoint: math.Vec3, out?: math.Vec3): math.Vec3;
- convertToWorldSpaceAR
/*** @en* Converts a Point in node coordinates to world space coordinates.** @zh* 将距当前节点坐标系下的一个点转换到世界坐标系。** @param nodePoint @en Point in local space.* @zh 节点坐标。* @param out @en Point in world space.* @zh 转换后坐标。* @returns @en Returns the coordinates in the UI world coordinate system.* @zh 返回 UI 世界坐标系。* @example* ```ts* const newVec3 = uiTransform.convertToWorldSpaceAR(3(100, 100, 0));* ```*/ convertToWorldSpaceAR(nodePoint: math.Vec3, out?: math.Vec3): math.Vec3;
/*** 坐标计算* @param target ScrollView 中被点击的 item 元素 * @param item 数据*/ public setData(target: Node, item: CoachCharacterItem) {//console.log('target.getPosition():', target.getPosition());//当前节点本地坐标转世界坐标、//世界坐标转到 对应的 节点坐标let vec: Vec3 = this.rootParemt.getComponent(UITransform).convertToNodeSpaceAR(target.parent.getComponent(UITransform).convertToWorldSpaceAR(target.getPosition()));console.log(vec)//修正位置if (vec.x < 400) {vec.x += 250;} else {vec.x -= 250;}if (vec.y > 140) {vec.y = 140;} else if (vec.y < -100) {vec.y = -100;}this.node.setPosition(vec);this.node.active = true; }
2.拖拽元素
我们给详情页面添加 拖拽事件的监听:
这里有个技术点: tuochMove 中获取到的坐标实际上是 触点坐标, 我们需要将这个坐标 转化到 UI坐标才能对 UI元素赋值
//添加监听事件 onLoad() {this.node.on(Node.EventType.TOUCH_START, this.touchStart, this);this.node.on(Node.EventType.TOUCH_MOVE, this.touchMove, this);this.node.on(Node.EventType.TOUCH_END, this.touchEnd, this)this.node.on(Node.EventType.TOUCH_CANCEL, this.touchCancel, this) }//展示只有 在拖拽情况下才显示的 元素 equipmentFly touchStart(event: EventTouch) {//Log.trace('touchStart');//展示 拖拽 元素 equipmentFlythis.equipmentFly.node.active = true; }//获取 触点坐标位置 touchMove(event: EventTouch) {//更新 拖拽元素位置this.equipmentFly.updatePosition(event.getLocation()); }//结束拖拽 隐藏拖拽元素 touchCancel(event: EventTouch) {//console.log('touchCancel');//隐藏 拖拽 元素 equipmentFlythis.equipmentFly.node.active = false;//执行 拖拽完毕的判断逻辑this.equipmentFly.onConfirm();//重置拖拽元素的位置this.equipmentFly.resetPostion(new Vec3(0, 50));} //结束拖拽 touchEnd(event: EventTouch) {//console.log('touchEnd');//隐藏 拖拽 元素 equipmentFlythis.equipmentFly.node.active = false;//执行 拖拽完毕的判断逻辑this.equipmentFly.onConfirm();//重置拖拽元素的位置this.equipmentFly.resetPostion(new Vec3(0, 50)); } //隐藏详情面板 public hideNoticePanel() {this.equipmentFly.node.active = false;this.equipmentFly.resetPostion(new Vec3(0, 50));this.node.active = false; }
触点坐标转换UI坐标的实现:
触点坐标转 UI 坐标需要通过 camera.screenToWorld(触点坐标)
Cocos Creator APIDescriptionhttps://docs.cocos.com/creator/3.8/api/zh/class/renderer.scene.Camera?id=screenToWorld大概逻辑整理如下:
- 拿到 拖拽 事件传递的 触点坐标
- 触点坐标 通过 camera 转 世界坐标
- 世界坐标转 UI局部坐标赋值 拖拽元素坐标 这样就能实现 拖拽元素随着手指滑动而移动
//拿到摄像机 onLoad() {//获取摄像机this.camera = find('Canvas/Camera').getComponent(Camera);//获取卡槽1 的 世界坐标this.vec3WorldBattle = this.nodeWearBattle.parent.getComponent(UITransform).convertToWorldSpaceAR(this.nodeWearBattle.getPosition());//获取卡槽1 的 世界坐标this.vec3WorldReward = this.nodeWearReward.parent.getComponent(UITransform).convertToWorldSpaceAR(this.nodeWearReward.getPosition());}/*** 触点坐标转换 * 最后使用UI坐标赋值* @param vec */ updatePosition(vec: Vec2) {this.vec3Pos.x = vec.x;this.vec3Pos.y = vec.y;//触点坐标转 世界坐标this.vec3WorldNode = this.camera.screenToWorld(this.vec3Pos);// 世界坐标转 UI局部坐标this.vec3new = this.node.parent.getComponent(UITransform).convertToNodeSpaceAR(this.vec3WorldNode);this.node.position = this.vec3new;//计算 拖拽元素 和 两个卡槽的 x轴距离 这里 distance 为 50个单位if (Math.abs(this.vec3WorldNode.x - this.vec3WorldBattle.x) < this.distance) {//距离到了 通知界面展示 卡槽1 选中框this.coachCharacterEquipmentView.onPreview(this.equipment, 0);} else if (Math.abs(this.vec3WorldNode.x - this.vec3WorldReward.x) < this.distance) { //距离到了 通知界面展示 卡槽2 选中框this.coachCharacterEquipmentView.onPreview(this.equipment, 1);} else {//这里处理 距离都没到的逻辑 移除选中框 以及 从 已经选中状态到 没选中状态的切换this.coachCharacterEquipmentView.onPreview(null, -1);}}
3.卡槽判定
通过上面的代码我们已经可以实现,拖转元素到 卡槽函数的调用:
//计算 拖拽元素 和 两个卡槽的 x轴距离 这里 distance 为 50个单位 if (Math.abs(this.vec3WorldNode.x - this.vec3WorldBattle.x) < this.distance) {//距离到了 通知界面展示 卡槽1 选中框this.coachCharacterEquipmentView.onPreview(this.equipment, 0); } else if (Math.abs(this.vec3WorldNode.x - this.vec3WorldReward.x) < this.distance) {//距离到了 通知界面展示 卡槽2 选中框this.coachCharacterEquipmentView.onPreview(this.equipment, 1); } else {//这里处理 距离都没到的逻辑 移除选中框 以及 从 已经选中状态到 没选中状态的切换this.coachCharacterEquipmentView.onPreview(null, -1); }
因为我们一个卡槽有多个 装备位置,需要响应的装备展示 对应的卡槽;
this.coachCharacterEquipmentView.onPreview(data,soltIndex);
函数onPreview 当到对应的卡槽就把数据传递过去,具体不够就传 null,过去
卡槽现实的我们就略过去;
卡槽数据的确认:
这个环节就对应的是 到对应的卡槽,并且松手:
上面的代码中我们已经加过监听了;通过实践发现 即便我同时监听了 TOUCH_END 和 TOUCH_CANCEL 他也只有一个会执行;所有我两个都加了
所以一来逻辑就清晰了:
- 当拖拽元素到卡槽中的时候,我们已经传递参数过去了,并且 我们当拖出 卡槽的时候还把数据 赋值为 null,
- 当我们停止拖拽的时候 判断下 传递过去的 数据是否为 null,
- 如果为 null 则说明 没有在卡槽中停止 拖拽,不为空则为 在卡槽中停止的拖拽
touchCancel(event: EventTouch) {//console.log('touchCancel');this.equipmentFly.node.active = false;this.equipmentFly.onConfirm();this.equipmentFly.resetPostion(new Vec3(0, 50));} touchEnd(event: EventTouch) {//console.log('touchEnd');this.equipmentFly.node.active = false;this.equipmentFly.onConfirm();this.equipmentFly.resetPostion(new Vec3(0, 50)); }
到这里基本上级功能都实现了。而且我们的 距离判断逻辑只有在 拖拽时候才会执行,应该说性能还不错!