[开发者心得] 想要游戏内再做个编辑器?来看看拖拽物体的简单逻辑

[复制链接]
100 |1
六安🐟片 发表于 2024-11-7 14:20:54 | 显示全部楼层 |阅读模式
本帖最后由 六安🐟片 于 2024-11-7 14:41 编辑

image.png
在游戏的开发中,拖拽功能则是提升互动性的一个重要组成部分,无论是移动角色、调整物体位置,还是进行复杂的拼图游戏,拖拽操作都能极大地增强玩家的沉浸感。因此,对于一些模拟经营类游戏,策略类游戏都会涉及到拖动物体自定义摆放的功能。这篇帖子将探讨如何在3D游戏中实现这一功能的核心方法。

涉及核心功能及API
1. 触摸事件绑定
在场景中拖拽一个对象,需要监听玩家的触摸事件,例如onTouchBeginonTouchMoveonTouchEnd,并处理相应的交互逻辑。
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的方式来进行标识。
image.png

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;
        }
    }
}



回复

使用道具 举报

丸子 发表于 2024-11-7 14:29:35 | 显示全部楼层
大佬,牛!
回复

使用道具 举报

热门版块
快速回复 返回顶部 返回列表