Sonolus Wiki

13. Note and Entity Data

In this chapter, we will setup a basic note script and look at how to integrate Entity Data into it.

Setup

Same as before, let's setup a note script, archetype, and entity:

export function note(): Script {
    return {}
}
export function note() {
    return {}
}
export const scripts = defineScripts({
    // ...
    note,
})
export const scripts = defineScripts({
    // ...
    note,
})
export const archetypes = defineArchetypes({
    // ...
    note: scripts.noteIndex,
})
export const archetypes = defineArchetypes({
    // ...
    note: scripts.noteIndex,
})
export const levelData: LevelData = {
    entities: [
        // ...
        {
            archetype: archetypes.noteIndex,
        },
    ],
}
export const levelData = {
    entities: [
        // ...
        {
            archetype: archetypes.noteIndex,
        },
    ],
}

Using Entity Data

Until now we only have initialization and stage, both should behave the same across all levels.

However that is not the case for notes: in one level maybe the first note at 5 second mark, while in another level it could be at 2; one level could have 200 notes, while another could have 30.

How would engine be able to handle various amount of notes with different information provided by the level? That's where Entity Data comes into play. Each level can specify all the entities and also inject data into them.

Let's define that the 0th value of Entity Data of note archetype stores the time of the note. If the level wants the note to have 2 as its time, it can simply inject it:

export const levelData: LevelData = {
    entities: [
        // ...
        {
            archetype: archetypes.noteIndex,
            data: {
                index: 0,
                values: [2],
            },
        },
    ],
}
export const levelData = {
    entities: [
        // ...
        {
            archetype: archetypes.noteIndex,
            data: {
                index: 0,
                values: [2],
            },
        },
    ],
}

In order for our note data script to read injected Entity Data, we can create a binding class for it with helpers to simplify our code, and pass it to createEntityData:

class EntityDataPointer extends Pointer {
    public get time() {
        return this.to<number>(0)
    }
}

const EntityData = createEntityData(EntityDataPointer)
class EntityDataPointer extends Pointer {
    get time() {
        return this.to(0)
    }
}

const EntityData = createEntityData(EntityDataPointer)

(Note: time is defined as 0th value of Entity Data just like we injected)

To test it out, let's simply DebugLog our time.

const updateParallel = DebugLog(EntityData.time)

return {
    updateParallel,
}
const updateParallel = DebugLog(EntityData.time)

return {
    updateParallel,
}

Spawning Logic

Now that we know how to access data such as note's time, we can implement spawning logic to our notes.

Let's say we want the note to appear 1 second before the time, falls from top to bottom, so player has time to react to it. Then, the spawning logic should be: if the current level time is larger than note time minus 1, we spawn.

However, there is no reason to keep calculating "note time minus 1" over and over: it's an intrinsic property of the note and unchanging, we should simply calculate it once and store it for use later.

Let's define spawnTime as the 0th value of Entity Memory

const spawnTime = EntityMemory.to<number>(0)
const spawnTime = EntityMemory.to(0)

In preprocess, let's calculate spawnTime and save it for later use:

const preprocess = spawnTime.set(Subtract(EntityData.time, 1))

return {
    preprocess,
}
const preprocess = spawnTime.set(Subtract(EntityData.time, 1))

return {
    preprocess,
}

For spawnOrder, we want our notes to spawn after stage, and should be ordered by their spawnTime: a note with spawn time at 2 second should spawn earlier than one at 5 second.

To achieve that, we can simply return spawnTime plus 1000:

const spawnOrder = Add(EntityData.spawnTime, 1000)

return {
    // ...
    spawnOrder,
}
const spawnOrder = Add(EntityData.spawnTime, 1000)

return {
    // ...
    spawnOrder,
}

For shouldSpawn, logic is as simply as check whether the current time is greater or equal to spawnTime:

const shouldSpawn = GreaterOr(Time, EntityData.spawnTime)

return {
    // ...
    shouldSpawn,
}
const shouldSpawn = GreaterOr(Time, EntityData.spawnTime)

return {
    // ...
    shouldSpawn,
}