本帖最后由 六安🐟片 于 2024-11-7 14:41 编辑
在游戏的开发中,拖拽功能则是提升互动性的一个重要组成部分,无论是移动角色、调整物体位置,还是进行复杂的拼图游戏,拖拽操作都能极大地增强玩家的沉浸感。因此,对于一些模拟经营类游戏,策略类游戏都会涉及到拖动物体自定义摆放的功能。这篇帖子将探讨如何在3D游戏中实现这一功能的核心方法。
涉及核心功能及API
1. 触摸事件绑定
在场景中拖拽一个对象,需要监听玩家的触摸事件,例如onTouchBegin,onTouchMove,onTouchEnd,并处理相应的交互逻辑。
InputUtil(查看API文档)中提供了相应的接口,能够让开发者监听对应的触摸事件。
2. 通过屏幕坐标获取世界坐标及对象
我们需要将玩家在屏幕中触摸的位置转换成实际3D世界中的位置,以此来得知玩家点击实际命中了哪一个对象。
在ScreenUtil 中,提供了getGameObjectByScreenPosition() 方法来直接获取到二位屏幕位置对应的场景中的对象。
同时,在InputUtil 中提供了convertScreenLocationToWorldSpace() 将二维屏幕位置转换为世界空间三维位置和方向,开发者可以更灵活的来挑选使用适合的方法;
3. 后处理描边
我们需要标识一个物体处于被选中的状态,在这里,我们使用给物体添加描边的方式来进行标识。
对于一个有mesh的游戏对象,可以将它转换为Model类型,在Model类型中,提供了setPostProcessOutline() 方法来给一个模型添加描边。
描边的效果可以通过SelectionUtil 中的setGlobalOutlineParams() 来修改物体的描边及被遮挡效果。详细内容请参考V0.24.0.0 Release Note - TS新增框选/高亮接口。
物体拖动逻辑梳理
1. 定义拖动物体的逻辑
拖动物体有很多实现方式,我们可以选择一个最适合自己的,然后将交互方式确定下来。下面是我选择的交互方式。点击一个物体时,会选中这个物体。
选中物体后,在屏幕中拖动,物体会更新在指定的位置。
选中物体后,点击任意地方,会取消选中这个物体。
接着,我们可以确定一下什么是点击,什么是拖动。在0.2s内先后触发按下事件onTouchBegin 和抬起事件onTouchEnd 可以定义为点击。如果触发了onTouchBegin 事件,且0.2s之后没有触发onTouchEnd 事件,则可以视为玩家想要拖动这个对象。
此外,由于玩家在手机屏幕上不会像在PC上只有一个触控源(即鼠标点按),所以我们也需要考虑玩家多手指操控的情况,比如多个手指同时触控的情况。这时,触控事件中的index 参数可以让我们分辨当前的触控事件属于玩家同时触控中的哪一个。
2. 定义可拖动物体
在游戏中,应该不会让所有物体都可以被玩家拖动,所以我们需要对可拖动的物体进行标识,仅有这些物体检测到玩家点击时,才可以被拖动。可以采用tag的方式来进行标识。
3. 实机表现与代码分享
完成了上面的逻辑梳理,我们就可以着手使用代码进行实现了。
先上效果:
对于一个正式的游戏来说,往往还会对布局构建网格,从而让物体能够正确的摆放在网格中间。同时,也会管理玩家的摄像头视角,来提供良好的物品摆放体验。这个demo中仅实现了拖拽物体的方法,周边功能需要大家继续挖掘,后续也会通过帖子来和大家分享哒~~
再上代码:
每一步都有注释,欢迎大家在评论区讨论分享~
@Component
export default class DragScript extends Script {
/** 当前触摸屏幕的索引 */
inTouchIndex: number = null;
/** 当前触摸屏幕选定的物体 */
inTouchObject: GameObject = null;
/** 当前触摸屏幕的时间 */
inTouchTimer: number = 0;
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
if (SystemUtil.isServer()) {
return;
}
this.initTouch();
// 设置全局描边效果
SelectionUtil.setGlobalOutlineParams(4, 0.4, 0.8, 0, 1);
this.useUpdate = true;
}
/** 初始化绑定触摸事件 */
initTouch() {
InputUtil.onTouchBegin((index, location, touchType) => {
this.onTouchBegin(index, location, touchType);
})
InputUtil.onTouchMove((index, location, touchType) => {
this.onTouchMove(index, location, touchType);
})
InputUtil.onTouchEnd((index, location, touchType) => {
this.onTouchEnd(index, location, touchType);
})
}
onUpdate(dt: number) {
if (this.inTouchIndex !== null) {
this.inTouchTimer += dt;
}
}
/**
* 触摸开始
* @param index 触摸的索引
* @param location 触摸的位置
* @param touchType 触摸类型
* @returns
*/
onTouchBegin(index: number, location: Vector2, touchType: TouchInputType) {
console.log(`TouchBegin: ${index},${this.inTouchIndex}`);
if (this.inTouchIndex !== null) {
return;
}
this.inTouchIndex = index;
// 判断是否有触摸到物体,如果触摸到,则开始计时
const result = this.getTouchObjectAtPosition(location);
if (!result) return;
this.inTouchTimer = 0;
}
/**
* 触摸移动
* @param index 触摸的索引
* @param location 触摸的位置
* @param touchType 触摸类型
*/
onTouchMove(index: number, location: Vector2, touchType: TouchInputType) {
// 移动超过0.2秒,则认为是长按拖动
if (index == this.inTouchIndex && this.inTouchObject && this.inTouchTimer > 0.2) {
this.moveUpdate(location);
}
}
/**
* 触摸结束
* @param index 触摸的索引
* @param location 触摸的位置
* @param touchType 触摸类型
* @returns
*/
onTouchEnd(index: number, location: Vector2, touchType: TouchInputType) {
console.log(`TouchEnd: ${index},${this.inTouchIndex}`, this.inTouchTimer);
if (this.inTouchIndex !== index) {
return;
}
if (this.inTouchTimer < 0.2) {
this.trySelectObjAtPosition(location);
}
else {
this.inTouchIndex = null;
this.inTouchTimer = 0;
}
}
/**
* 尝试选择物体
* @param location 触摸屏幕的位置
* @returns
*/
trySelectObjAtPosition(location: Vector2) {
this.inTouchIndex = null;
this.inTouchTimer = 0;
if (this.inTouchObject) {
this.selectObject(this.inTouchObject, false);
this.inTouchObject = null;
return;
}
const result = this.getTouchObjectAtPosition(location);
if (!result) return;
this.inTouchObject = result.gameObject;
this.selectObject(result.gameObject, true);
}
/**
* 获取触摸屏幕的物体
* @param location 触摸屏幕的位置
* @returns
*/
getTouchObjectAtPosition(location: Vector2) {
const hitResults = ScreenUtil.getGameObjectByScreenPosition(location.x, location.y);
const result = hitResults.shift();
if (!result || result.gameObject.tag != "selectable") return null;
return result;
}
/**
* 选择物体,为物体添加描边
* @param obj 选择的物体
* @param select 是否选中
*/
selectObject(obj: GameObject, select: boolean) {
let mesh = obj as Model;
mesh.setPostProcessOutline(select, LinearColor.green, 4);
}
/**
* 拖动更新未知
* @param location 触摸屏幕的位置
*/
moveUpdate(location: Vector2) {
// 找到触摸屏幕的位置对应3d世界的地板的碰撞点
const hitResult = ScreenUtil.getGameObjectByScreenPosition(location.x, location.y, 10000, true);
const groundResult = hitResult.find(e => e.gameObject.name == "Ground");
if (groundResult) {
const pos = groundResult.impactPoint;
this.inTouchObject.worldTransform.position = pos;
}
}
}
|