[其他资源] 模式/关卡选择动画效果实现

[复制链接]
1562 |0
设计背景:
随着模式的增多,场景上的游戏入口对于玩家开局影响越来越大。这时,一个好的模式选择UI,既可以给到玩家一个便捷的游戏入口,也能带来一种良好的选择体验
明确目标:
1.模式item可以左右自由拖动
2.可以左右移动一格
3.拖动停止时随惯性继续移动
4.最终会停在固定的位置
5.未解锁的模式不可用
6.模式循环出现
7.从中间向两边缩放和zOrder层级逐渐变小
原型图:
关卡选择原型图.PNG
功能实现
容易想到的是将item放在scrollbox下面的canvas中,但是受限于左右自由拖动、item循环出现、层级缩放渐变的效果,所以这个方案搁置。如果要自己实现滚动功能的话,就需要一个能映射左右滚动距离的控件,进度条progressbar很契合这个条件。
我们可以在进度条的onPress记录初始值,用onChange的值与初始值比较印射出item向左或向右移动的距离并更新上一帧的值,在onRelease时判断item所具有的立和方向进行惯性移动。


    /** 进度条释放的回调 */
    private onRelease(value: number) {
        if (this.isReleaseLocked) { return; }

        // // 如果和按下时间戳相差不超过500ms,那么就是点击事件
        // if (Date.now() - this.pressTimeStamp < CLICK_TIME) { this.onClicked(value); return; }

        //惯性
        this.isPressed = false;
        // 计算当前时间戳
        let now = Date.now();
        // 计算惯性的值,默认是上一帧进度条的值,如果当前时间和上一帧的时间戳间隔小于计时器间隔那么取 上一帧的进度条值,否则取当前帧的进度条值
        let interValue = now - this.timerStamp < INTER_STAMP ? this.lastInterProBarValue : this.curInterProBarValue;
        // 惯性滑动距离 * 惯性系数
        let offset = (value - interValue) * INERTIA_COE;
        // 偏移量的绝对值
        let absOffset = Math.abs(offset);
        // 惯性滑动速度 - 带方向的
        let velocity = offset / STOP_INERTIA / INTER_STAMP2;
        // 绝对速度 - 无方向
        let absVelocity = Math.abs(velocity);
        this.shiftItem(absOffset, absVelocity, velocity);
    }

    // TODO: 保留滚动先
    // onClicked(value: number) {
    // 精确的点击X位置
    // const precisePosX = value * PRO_BAR_RATIO;
    // for (const levelItem of this.levelItemList) {
    //     if (levelItem.checkBeClicked(precisePosX)) { break; }
    // }
    // }

    /** 进度条按钮的回调 */
    private onPress(value: number) {
        // TODO: 024 bug,点击进度条马上就有onRelease回调,这里加个锁简单处理一下
        this.isReleaseLocked = true;
        setTimeout(() => {
            this.isReleaseLocked = false;
        }, INTER_STAMP2 * 10);

        // // 记录按下的时间戳
        // this.pressTimeStamp = Date.now();
        // 开启update
        this.levelItemList.forEach(item => { item.startUpdate(); });

        //记录拖动起始帧滚动条value值
        this.lastStampProBarValue = value;
        this.isPressed = true;

        /** 双指针取每帧进度条的值 */
        this.lastInterProBarValue = value;
        this.curInterProBarValue = value;

        // 还有惯性就清掉了
        if (this.inter2) {
            clearInterval(this.inter2);
            this.inter2 = null;
            this.levelItemList.forEach(item => { item.endUpdate(); });
        }

        this.timerStamp = null;
        if (this.inter) clearInterval(this.inter);
        this.inter = setInterval(() => {
            if (this.timerStamp != null) this.lastInterProBarValue = this.curInterProBarValue;
            this.curInterProBarValue = this.proBar.currentValue;
            this.timerStamp = Date.now();
        }, INTER_STAMP);
    }

    /** 进度条改变的回调 */
    private onChange(value: number) {
        // 避免先触发改变值的回调
        if (!this.isPressed) return;
        // // 避免影响模拟的onclick事件触发
        // if (Date.now() - this.pressTimeStamp < CLICK_TIME) { return; }
        //对比上一帧滚动条value比较值计算所有LevelItem的偏移值
        let offset = (value - this.lastStampProBarValue) * PRO_BAR_RATIO;
        // 偏差值大于0是向右滑动,小于0是向左滑动
        this.levelItemList.forEach((levelItem) => {
            levelItem.uiObject.position = new Vector2(levelItem.uiObject.position.x + offset, 0);
        })
        this.lastStampProBarValue = value;
    }

定义一个槽的类UISlot,定义属性zOrder、scale、pos,分别代表每个槽的z系数、缩放、位置,因为现在有三种模式并且界面上只需要展现5个item,所以我们再设计一个六个数量的槽类FiveSlot,在这个类中初始化好六个uislot。移动item时,比较每一个中间的item的位置与中间的slot的差距,如果贴合,就将每一个slot的属性应用到item,这样在惯性滑动结束时依然能确保每个item的位置、缩放和层级都能和每个槽的保持一致。


/** levelPanel 下 contentCanvas的 槽 */
class UISlot {

    /** z系数 */
    zOrder: number = 0;

    /** 缩放 */
    scale: Vector2 = new Vector2(0, 0);

    pos: Vector2 = new Vector2(0, 0);

    constructor(zOrder: number, scale: Vector2, pos: Vector2) {
        this.zOrder = zOrder;
        this.scale = scale;
        this.pos = pos;
    }
}

/** 5个自定义槽 - 实际上放7个 - 避免留白的问题 */
class FiveSlot {

    slotList: UISlot[] = [];

    constructor() {
        // TODO: 搞6个,一个模式两个
        // this.slotList.push(new UISlot(1, new Vector2(1, 0.86), new Vector2(-470, 0)));
        this.slotList.push(new UISlot(1, new Vector2(1, 0.86), new Vector2(-170, 0)));
        this.slotList.push(new UISlot(2, new Vector2(1, 0.93), new Vector2(130, 0)));
        this.slotList.push(new UISlot(3, new Vector2(1, 1.00), new Vector2(430, 0)));
        this.slotList.push(new UISlot(2, new Vector2(1, 0.93), new Vector2(730, 0)));
        this.slotList.push(new UISlot(1, new Vector2(1, 0.86), new Vector2(1030, 0)));
        this.slotList.push(new UISlot(1, new Vector2(1, 0.86), new Vector2(1330, 0)));
    }
}

动态检测最中间的item的滑动方向和距离,以此来更新所有item的缩放、位置和层级


    /** 吸附 */
    private attach(): boolean {
        if (this.curSlotID != MIDDLE_ID) return;
        // 结束判断中间节点的位置,看是否需要吸附处理
        if (Math.abs(this.uiObject.position.x - this.slotList[this.nextSlotID].pos.x) <= ATTACH_DIS) {
            this.levelPanel.reqAllLevelItem().forEach((item) => { item.reBindSlot(true); });
            return true;
        }
        if (Math.abs(this.uiObject.position.x - this.slotList[this.lastSlotID].pos.x) <= ATTACH_DIS) {
            this.levelPanel.reqAllLevelItem().forEach((item) => { item.reBindSlot(false); });
            return true;
        }
        return false;
    }

    /**
    * 每一帧调用
    * 通过canUpdate可以开启关闭调用
    * dt 两帧调用的时间差,毫秒
    */
    protected onUpdate(dt: number) {
        // 中间的去影响所有的
        if (!this.isMiddleID) return;

        /** 缩放检测 */
        this.checkScaleChange();

        /** 右滑 */
        if (this.uiObject.position.x > this.slotList[this.curSlotID].pos.x) {
            this.checkScrollToRight();
        }

        /** 左滑 */
        if (this.uiObject.position.x < this.slotList[this.curSlotID].pos.x) {
            this.checkScrollToLeft();
        }
    }

    /** 缩放检测 */
    private checkScaleChange() {
        const twoSlotDis = this.uiObject.position.x - this.slotList[this.curSlotID].pos.x;
        const toScaleY = twoSlotDis * SCALE_RATIO;
        this.levelPanel.reqAllLevelItem().forEach((item) => { item.updateScale(toScaleY); });
    }

    /** 右滑检测 */
    private checkScrollToRight() {
        if (this.uiObject.position.x >= this.slotList[this.nextSlotID].pos.x) {
            this.levelPanel.reqAllLevelItem().forEach((item) => { item.reBindSlot(true); });
        }
    }

    /** 左滑检测 */
    private checkScrollToLeft() {
        if (this.uiObject.position.x <= this.slotList[this.lastSlotID].pos.x) {
            this.levelPanel.reqAllLevelItem().forEach((item) => { item.reBindSlot(false); });
        }
    }

    /** 应用槽信息 */
    private applySlotInfo() {
        this.uiObject.zOrder = this.slotList[this.curSlotID].zOrder;
        this.uiObject.position = this.slotList[this.curSlotID].pos;
        this.uiObject.renderScale = this.slotList[this.curSlotID].scale;
    }

    /**
     * 动态改变每个item的缩放
     * @paramtoScaleY toScaleY是一个矢量,正负表示方向
     */
    public updateScale(toScaleY: number) {
        if (this.curSlotID < MIDDLE_ID) {
            this.uiObject.renderScale = new Vector2(1, this.slotList[this.curSlotID].scale.y + toScaleY);
            this.checkOverScale(toScaleY >= 0, true);
        } else if (this.curSlotID > MIDDLE_ID) {
            this.uiObject.renderScale = new Vector2(1, this.slotList[this.curSlotID].scale.y - toScaleY);
            this.checkOverScale(toScaleY <= 0, false);
        } else {
            // 在中间的一定是减
            this.uiObject.renderScale = new Vector2(1, this.slotList[this.curSlotID].scale.y - Math.abs(toScaleY));
            // 这儿last 还是 next都可以
            this.checkOverScale(false);
        }
    }

    /**
     * 检查改变的scale是否超过了将要变成的scale
     * @paramisAddScale scale是否增长
     * @paramisLeft 是否是左边的 default true
     */
    private checkOverScale(isAddScale: boolean, isLeft: boolean = true) {
        if (isAddScale) {
            let slotID = isLeft ? this.nextSlotID : this.lastSlotID;
            const nextScaleY = this.slotList[slotID].scale.y;
            if (this.uiObject.renderScale.y >= nextScaleY) { this.uiObject.renderScale = this.slotList[slotID].scale; }
        } else {
            let slotID = isLeft ? this.lastSlotID : this.nextSlotID;
            const lastScaleY = this.slotList[slotID].scale.y;
            if (this.uiObject.renderScale.y <= lastScaleY) { this.uiObject.renderScale = this.slotList[slotID].scale; }
        }
    }

注:以上只是部分核心代码,主要代码请参考源文件
动态 Demo 演示

源码与UI文件
UILevel.ts (21.5 KB, 下载次数: 118)

回复

使用道具 举报

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