Sonolus Wiki

10. 音符绘制

本章我们将实现音符的绘制逻辑。

可视时间

让我们计算一个音符的可视时间,它由最小和最大可视时间组成,并重构生成时间,让它使用最小可视时间:

export class Note extends Archetype {
    // ...

    visualTime = this.entityMemory({
        min: Number,
        max: Number,
    })

    // ...

    preprocess() {
        // ...

        this.visualTime.max = this.targetTime
        this.visualTime.min = this.visualTime.max - 1

        this.spawnTime = this.visualTime.min
    }

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

    visualTime = this.entityMemory({
        min: Number,
        max: Number,
    })

    // ...

    preprocess() {
        // ...

        this.visualTime.max = this.targetTime
        this.visualTime.min = this.visualTime.max - 1

        this.spawnTime = this.visualTime.min
    }

    // ...
}

声明

就像舞台原型一样,为简单起见,我们为音符使用标准化的精灵:

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

绘制

通过最小和最大可视时间以及当前时间,我们可以计算音符的y位置。因为我们将屏幕坐标系变成了从y = 0 (音符顶部生成)到y = 1 (判定线),这大大简化了我们的计算:

export class Note extends Archetype {
    // ...

    updateParallel() {
        const y = Math.unlerp(this.visualTime.min, this.visualTime.max, time.now)
    }
}
export class Note extends Archetype {
    // ...

    updateParallel() {
        const y = Math.unlerp(this.visualTime.min, this.visualTime.max, time.now)
    }
}

计算出y后,我们就可以绘制音符了:

export class Note extends Archetype {
    // ...

    updateParallel() {
        // ...

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

        skin.sprites.note.draw(layout, 0, 1)
    }
}
export class Note extends Archetype {
    // ...

    updateParallel() {
        // ...

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

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

Z 轴堆叠冲突

虽然目前我们已经成功运行了,但还有一个隐藏的问题我们还没有解决:Z 轴堆叠冲突。

它指的是当使用相同的 Z 值渲染多个对象时,它们的顺序可能在帧与帧之间不一致,并且如果它们重叠则可能会闪烁。

为了解决这个问题,让我们将 Z 轴堆叠顺序设置为1000减去目标时间,这样较早的音符将始终位于较晚的音符之上。

这也是音符的一个不变的属性,所以我们应该计算一次并存储在 Entity Memory 中以供重复使用。然而这次我们将在initialize而不是preprocess中计算它,因为它不会在生成逻辑中使用,所以我们可以推迟计算来缩短关卡加载时间:

export class Note extends Archetype {
    // ...

    z = this.entityMemory(Number)

    // ...

    initialize() {
        this.z = 1000 - this.targetTime
    }

    updateParallel() {
        // ...

        skin.sprites.note.draw(layout, this.z, 1)
    }
}
export class Note extends Archetype {
    // ...

    z = this.entityMemory(Number)

    // ...

    initialize() {
        this.z = 1000 - this.targetTime
    }

    updateParallel() {
        // ...

        skin.sprites.note.draw(layout, this.z, 1)
    }
}