[开发者心得] 单脚本,运行时四边形框选器

[复制链接]
241 |1
天晴 发表于 2024-7-22 10:54:26 | 显示全部楼层 |阅读模式
注意,这个功能是基于035的绘制画布UI控件而实现的!

具体效果: 20240722104921_rec_.gif


export class QuadrilateralSelector {
    private _enable: boolean = false;
    private _clickPointBegin: mw.Vector2 = null;
    private _clickPointEnd: mw.Vector2 = null;
    private _befDrawID: number = null;
    private readonly _vertex: mw.UIDrawCustomVertex[] = [];
    private readonly _evs: mw.EventListener[] = [];
    private readonly viewToScreenRate: { x: number, y: number } = { x: 1, y: 1 };
    private _tempcanvas: mw.DrawCanvas = null;
    private _isInnerCnvas: boolean = false;

    /**绘制颜色 */
    public readonly drawColor = new LinearColor(0, 1, 0, 0.5);
    /**绘制画布 */
    public getDrawCanvas: () => mw.DrawCanvas = null;
    private createTempCanvas(): mw.DrawCanvas {
        // if (!this._tempcanvas) {
        //     this._tempcanvas = new DrawCanvas();
        //     this._tempcanvas.setSize(WindowUtil.getViewportSize());
        //     this._tempcanvas.setPosition(new Vector2(0, 0));
        //     this._tempcanvas.setAnchor(new Vector2(0, 0));
        //     this._tempcanvas.setPivot(new Vector2(0, 0));
        // }
        const rootui = UIService.canvas;
        this._tempcanvas = DrawCanvas.newObject(rootui);
        this._tempcanvas.position = new Vector2(0, 0);
        this._tempcanvas.size = rootui.size;
        this._isInnerCnvas = true;
        return this._tempcanvas;
    }

    /**选择回调 会触发4个视口坐标作为参数 */
    public readonly selectCallback: Action1<mw.Vector2[]> = new Action1();
    public readonly selectBeginCallback: Action = new Action();
    public readonly selectEndCallback: Action = new Action();

    /**执行转世界坐标时,用于射线检查,哪些物体算是可命中的点 */
    public checkWorldObject: (g: mw.GameObject) => boolean = null;

    constructor() {
        const screenSize = WindowUtil.getViewportSize();
        const viewSize = mw.getViewportSize();
        // const [rateX, rateY] = [viewSize.x / screenSize.x, viewSize.y / screenSize.y]
        this.viewToScreenRate.x = viewSize.x / screenSize.x;
        this.viewToScreenRate.y = viewSize.y / screenSize.y;
    }

    /**视口坐标转屏幕坐标 */
    public viewPointToScreen(v: Vector2): Vector2 {
        return new Vector2(v.x * this.viewToScreenRate.x, v.y * this.viewToScreenRate.y);
    }





    public get enable() { return this._enable; }
    public set enable(v: boolean) {
        if (this._enable != v) {
            this._enable = v;
            if (v) {
                this._evs.push(
                    InputUtil.onTouchBegin(this.touchBegin),
                    InputUtil.onTouchMove(this.touchMove),
                    InputUtil.onTouchEnd(this.touchEnd)
                )
            } else {
                this._evs.forEach(ev => { ev.disconnect(); });
                this._evs.length = 0;
            }
        }
    }

    private touchBegin = () => {
        this.selectBeginCallback.call();
        this._clickPointBegin = mw.getMousePositionOnViewport();
        this._isInnerCnvas = false;
        this._tempcanvas = this.getDrawCanvas ? this.getDrawCanvas() ?? this.createTempCanvas() : this.createTempCanvas();
    }


    private checkSimplePointDistance(p1: Vector2, p2: Vector2, checkLength: number = 3): boolean {
        const { x: x1, y: y1 } = p1;
        const { x: x2, y: y2 } = p2;

        return Math.abs(x1 - x2) > checkLength && Math.abs(y1 - y2) > checkLength;
    }

    private touchMove = () => {


        if (!this._tempcanvas) { return; }
        if (this._befDrawID != null) {
            this._tempcanvas.removeDrawById(this._befDrawID);
        }
        this._clickPointEnd = mw.getMousePositionOnViewport();
        if (!this.checkSimplePointDistance(this._clickPointBegin, this._clickPointEnd)) {
            return;
        }

        const [xmin, ymin, xmax, ymax] = [
            Math.min(this._clickPointBegin.x, this._clickPointEnd.x),
            Math.min(this._clickPointBegin.y, this._clickPointEnd.y),
            Math.max(this._clickPointBegin.x, this._clickPointEnd.x),
            Math.max(this._clickPointBegin.y, this._clickPointEnd.y)
        ]
        const begin = new Vector2(xmin, ymin);
        const end = new Vector2(xmax, ymax);

        this._vertex.length = 0;
        let v1 = new UIDrawCustomVertex();
        v1.color = this.drawColor;
        v1.position = begin;
        v1.texCoord = new Vector2(1, 1);
        this._vertex.push(v1);

        let v2 = new UIDrawCustomVertex();
        v2.color = this.drawColor;
        v2.position = new Vector2(end.x, begin.y);
        v2.texCoord = new Vector2(1, 1);
        this._vertex.push(v2);

        let v3 = new UIDrawCustomVertex();
        v3.color = this.drawColor;
        v3.position = end;
        v3.texCoord = new Vector2(1, 1);
        this._vertex.push(v3);

        let v4 = new UIDrawCustomVertex();
        v4.color = this.drawColor;
        v4.position = new Vector2(begin.x, end.y);
        v4.texCoord = new Vector2(1, 1);
        this._vertex.push(v4);

        this._befDrawID = this._tempcanvas.drawCustom(this._vertex, [0, 1, 3, 3, 1, 2]);
        this.selectCallback.call(this._vertex.map(v => { return v.position; }));
    }

    /**视口坐标转世界坐标 */
    public getWorldPoint(viewPoint: mw.Vector2 | number[]): mw.Vector2 {
        if (viewPoint instanceof Array) {
            viewPoint = new Vector2(viewPoint[0], viewPoint[1]);
        }

        const screenPoint = this.viewPointToScreen(viewPoint);
        const worldinfo = InputUtil.convertScreenLocationToWorldSpace(screenPoint.x, screenPoint.y);
        if (worldinfo.result) {
            const startLoc = worldinfo.worldPosition;
            const dir = worldinfo.worldDirection;
            const endLoc = Vector.add(startLoc, dir.multiply(99999));
            let hits = mw.QueryUtil.lineTrace(startLoc, endLoc, true, false).filter(hit => {
                return this.checkWorldObject ? this.checkWorldObject(hit.gameObject) : true;
            });
            let { x, y } = hits[0] ? hits[0].impactPoint : endLoc;
            return new Vector2(x, y);
        }
        return null;
    }

    private touchEnd = () => {
        if (this._befDrawID != null) {
            this._tempcanvas.removeDrawById(this._befDrawID);
        }
        if (this._isInnerCnvas) {
            this._tempcanvas.destroyObject();
            this._tempcanvas = null;
        }

        this.selectEndCallback.call();
    }

    /**基于2维平面下,指定点 p 是否在 abcd 所成的四边形内 */
    public isPointInsideQuadrilateral(a: Vector2, b: Vector2, c: Vector2, d: Vector2, p: Vector2): boolean {
        // Calculate vectors for each side of the quadrilateral
        const ab = Vector2.subtract(b, a);
        const bc = Vector2.subtract(c, b);
        const cd = Vector2.subtract(d, c);
        const da = Vector2.subtract(a, d);

        // Calculate vectors from each corner to point p
        const ap = Vector2.subtract(p, a);
        const bp = Vector2.subtract(p, b);
        const cp = Vector2.subtract(p, c);
        const dp = Vector2.subtract(p, d);

        // Check if the cross product of each side vector with the corresponding corner-to-p vector is non-negative
        const flag1 = Vector2.cross(ab, ap) >= 0;
        const flag2 = Vector2.cross(bc, bp) >= 0;
        const flag3 = Vector2.cross(cd, cp) >= 0;
        const flag4 = Vector2.cross(da, dp) >= 0;

        // If all flags are true, then point p is inside the quadrilateral
        return flag1 && flag2 && flag3 && flag4;
    }
}



上面动图的实现示例代码:
其中,这里使用的是tag作为检查标准,需要地面设置tag为 "2" ,可选的小方块tag设置为 "1"
具体实际逻辑代码可以自行定义,本质上是设置 SelectorTest.checkWorldObject 的值作为检查条件

            let gs1: mw.GameObject[] = [];
            const SelectorTest: QuadrilateralSelector = new QuadrilateralSelector();
            //指示标签为2的物体作为辅助转世界坐标的射线命中物体
            SelectorTest.checkWorldObject = g => { return g.tag === "2"; }
            //逻辑代码,在进行拖拽开始和结束时,分别锁定和解锁相机
            SelectorTest.selectBeginCallback.add(() => { cameraController.scrollCameraEnable = false; });
            SelectorTest.selectEndCallback.add(() => { cameraController.scrollCameraEnable = true; });

            SelectorTest.selectCallback.add((viewPoints) => {
                //视口坐标转世界坐标
                const [a, b, c, d] = viewPoints.map(v => { return SelectorTest.getWorldPoint(v); });
                //检查所有感兴趣的物体
                let finds = GameObject.findGameObjectsByTag("1");
                finds = finds.filter(g => {
                    //目标物体所在世界坐标
                    const p = new Vector2(g.worldTransform.position.x, g.worldTransform.position.y);
                    //使用选择器辅助函数,判断目标物体是否在四边形内
                    return SelectorTest.isPointInsideQuadrilateral(a, b, c, d, p);
                });

                gs1?.forEach(g => {//取消上一次框选的描边
                    (g as Model).setPostProcessOutline(false);
                })
                gs1 = finds;
                finds.forEach(g => {//描边
                    (g as Model).setPostProcessOutline(true, LinearColor.red, 3);
                })
            });
            SelectorTest.enable = true;//启动选择器


回复

使用道具 举报

叽里咕噜小胡桃 发表于 2024-7-22 10:55:55 | 显示全部楼层
帅啊,有种在玩RTS的感觉了 "yes commander"
回复

使用道具 举报

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