Sonolus Wiki

05. 音符显示

在本章中,我们将实现音符显示组件:

音符显示组件

让我们先设置一个音符显示组件:

export const noteDisplay = {}
export const noteDisplay = {}
// ...

const components = [initialization, stage, noteDisplay] as const

// ...
// ...

const components = [initialization, stage, noteDisplay]

// ...

声明

声明音符的皮肤精灵:

export const skin = defineSkin({
    sprites: {
        // ...
        note: SkinSpriteName.NoteHeadCyan,
    },
})
export const skin = defineSkin({
    sprites: {
        // ...
        note: SkinSpriteName.NoteHeadCyan,
    },
})

模式

音符显示组件将负责在多个片段中绘制音符,因此让我们声明一个模式枚举(enum)来代表它:

enum Mode {
    None,
    Overlay,
    Fall,
    Frozen,
}
const Mode = {
    None: 0,
    Overlay: 1,
    Fall: 2,
    Frozen: 3,
}

为了存储当前模式,我们可以使用 Tutorial Memory 区块中的一个值:

let mode = tutorialMemory(DataType<Mode>)
let mode = tutorialMemory(Number)

最后,组件可以暴露一系列方法来改变当前模式:

// ...

export const noteDisplay = {
    showOverlay() {
        mode = Mode.Overlay
    },

    showFall() {
        mode = Mode.Fall
    },

    showFrozen() {
        mode = Mode.Frozen
    },

    clear() {
        mode = Mode.None
    },
}
// ...

export const noteDisplay = {
    showOverlay() {
        mode = Mode.Overlay
    },

    showFall() {
        mode = Mode.Fall
    },

    showFrozen() {
        mode = Mode.Frozen
    },

    clear() {
        mode = Mode.None
    },
}

绘制

让我们实现音符显示组件的绘制功能。

当我们在 None 模式时,这里没有需要绘制的东西,我们就可以简单退出即可:

// ...

export const noteDisplay = {
    update() {
        if (!mode) return
    },

    // ...
}
// ...

export const noteDisplay = {
    update() {
        if (!mode) return
    },

    // ...
}

当我们在需要被 Intro 片段使用的 Overlay 模式时,我们应该在屏幕中心旁绘制一个放大的音符。

注意到 Intro 片段会持续 1 秒,因此我们可以让音符从 0.75 秒开始消失。

// ...

export const noteDisplay = {
    update() {
        // ...

        if (mode === Mode.Overlay) {
            const a = Math.unlerpClamped(1, 0.75, segment.time)

            const layout = Rect.one
                .mul(2 * note.radius)
                .scale(1, -1)
                .translate(0, 0.5)

            skin.sprites.note.draw(layout, 1000, a)
        }
    },

    // ...
}
// ...

export const noteDisplay = {
    update() {
        // ...

        if (mode === Mode.Overlay) {
            const a = Math.unlerpClamped(1, 0.75, segment.time)

            const layout = Rect.one
                .mul(2 * note.radius)
                .scale(1, -1)
                .translate(0, 0.5)

            skin.sprites.note.draw(layout, 1000, a)
        }
    },

    // ...
}

当我们在会被 Fall 片段使用的 Fall 模式时,我们应该绘制一个从顶部到判定线下落的音符。

注意到 Fall 片段会持续 2 秒,因此我们应该据此计算 y 坐标。

当我们在会被 Frozen 片段使用的 Frozen 模式时,我们应该在判定线绘制一个冻住的音符。我们可以通过将 y 设置为 1 来重复使用 Fall 模式的代码。

// ...

export const noteDisplay = {
    update() {
        // ...

        if (mode === Mode.Overlay) {
            // ...
        } else {
            const y = mode === Mode.Fall ? Math.unlerp(0, 2, segment.time) : 1

            const layout = Rect.one.mul(note.radius).scale(1, -1).translate(0, y)

            skin.sprites.note.draw(layout, 1000, 1)
        }
    },

    // ...
}
// ...

export const noteDisplay = {
    update() {
        // ...

        if (mode === Mode.Overlay) {
            // ...
        } else {
            const y = mode === Mode.Fall ? Math.unlerp(0, 2, segment.time) : 1

            const layout = Rect.one.mul(note.radius).scale(1, -1).translate(0, y)

            skin.sprites.note.draw(layout, 1000, 1)
        }
    },

    // ...
}