Sonolus Wiki

15. 输入桶

在本章中,我们将给音符添加输入桶。

输入桶

输入桶是一种有关音符的东西,音符可以将判定偏移值输入其中,在结果界面上将显示每个桶的分布图表。

虽然输入桶不是必要的,但这对于玩家校准输入偏移以及提高准确率非常有用。

创建桶

我们的引擎只有一种类型的音符,所以一个桶就可以了。

在结果屏幕上,每个桶将由一个由皮肤精灵组成的图形表示,因此我们应该使其尽可能接近音符的游戏视觉效果。这里我们设置成以毫秒为单位。

export const buckets = defineBuckets({
    note: {
        sprites: [
            {
                id: skin.sprites.note.id,
                x: 0,
                y: 0,
                w: 2,
                h: 2,
                rotation: 0,
            },
        ],
        unit: Text.MillisecondUnit,
    },
})
export const buckets = defineBuckets({
    note: {
        sprites: [
            {
                id: skin.sprites.note.id,
                x: 0,
                y: 0,
                w: 2,
                h: 2,
                rotation: 0,
            },
        ],
        unit: Text.MillisecondUnit,
    },
})

设置桶窗口

接下来,我们需要设置桶窗口。

从直觉上来看,我们会在 Note 的preprocess中编写代码。但是有一个问题:每个音符实体都会调用preprocess ,然而桶窗口只需要设置一次。

我们可以将我们的代码移至初始化的preprocess ,但这违反了单一责任原则(single responsibility principal)。

那我们应该如何清晰地构建这段代码呢?

全局预处理模式

对于任何的我们只想全局运行一次的预处理代码,我们需要把它们写在一个新的globalPreprocess类方法中。

对于音符而言,在这里可以设置桶窗口(记得将窗口的值转换为毫秒):

// ...

const toMs = ({ min, max }: Range) => new Range(Math.round(min * 1000), Math.round(max * 1000))

export const bucketWindows = {
    perfect: toMs(windows.perfect),
    great: toMs(windows.great),
    good: toMs(windows.good),
}
// ...

const toMs = ({ min, max }) => new Range(Math.round(min * 1000), Math.round(max * 1000))

export const bucketWindows = {
    perfect: toMs(windows.perfect),
    great: toMs(windows.great),
    good: toMs(windows.good),
}
export class Note extends Archetype {
    // ...

    globalPreprocess() {
        buckets.note.set(bucketWindows)
    }

    // ...
}
export class Note extends Archetype {
    // ...

    globalPreprocess() {
        buckets.note.set(bucketWindows)
    }

    // ...
}

目前, globalPreprocess没有在任何地方被调用。让我们在初始化里调用它:

export class Initialization extends Archetype {
    preprocess() {
        // ...

        archetypes.Note.globalPreprocess()
    }

    // ...
}
export class Initialization extends Archetype {
    preprocess() {
        // ...

        archetypes.Note.globalPreprocess()
    }

    // ...
}

现在音符的globalPreprocess只会在全局调用一次,因为初始化只有一次。

然而,每当新增一个需要调用globalPreprocess的新原型时,我们必须记得把它添加到初始化中。为了避免这种麻烦,我们可以创建原型迭代器,来调用所有具有globalPreprocess方法的原型:

export class Initialization extends Archetype {
    preprocess() {
        // ...

        for (const archetype of Object.values(archetypes)) {
            if (!('globalPreprocess' in archetype)) continue

            archetype.globalPreprocess()
        }
    }

    // ...
}
export class Initialization extends Archetype {
    preprocess() {
        // ...

        for (const archetype of Object.values(archetypes)) {
            if (!('globalPreprocess' in archetype)) continue

            archetype.globalPreprocess()
        }
    }

    // ...
}

这就是实现的全局预处理模式的方法。

输入结果

最后,让我们设置一下输入结果桶的索引和值:

export class Note extends Archetype {
    // ...
    touch() {
        // ...

        for (const touch of touches) {
            // ...

            this.result.bucket.index = buckets.note.index
            this.result.bucket.value = this.result.accuracy * 1000

            // ...
        }
    }

    // ...
}
export class Note extends Archetype {
    // ...
    touch() {
        // ...

        for (const touch of touches) {
            // ...

            this.result.bucket.index = buckets.note.index
            this.result.bucket.value = this.result.accuracy * 1000

            // ...
        }
    }

    // ...
}