Passer au contenu principal

Composants personnalisés

Introduction

Problèmes courants et bonnes pratiques à suivre lors de la création de composants personnalisés.

Références périmées

Lorsque votre composant se voit passer (world, component) pour ajouter, cocher ou supprimer des callbacks, il n'est pas toujours sûr de référencer component dans une fonction imbriquée et de l'utiliser après le retour du callback.

Exemple incorrect

ecs.registerComponent({
name : 'age-counter',
data : {
age : ecs.i32,
interval : ecs.i32,
},
add : (world, component) => {
const interval = world.time.setInterval(() => {
// Ce n'est pas sûr parce que nous accédons aux données après un certain temps
// passé, il n'est pas garanti qu'elles soient toujours valides.
component.data.age += 1
}, 1000)

// C'est sûr parce que nous assignons des données dans la fonction add
component.data.interval = interval
},
tick : (world, component) => {
console.log('I am', component.data.age, 'seconds old')
},
remove : (world, component) => {
world.time.clearTimeout(component.data.interval)
}.
})

Exemple correct

ecs.registerComponent({
name : 'age-counter',
data : {
age: ecs.i32,
interval: ecs.i32,
},
add : (world, component) => {
const {eid, dataAttribute} = component
const interval = world.time.setInterval(() => {
// C'est sûr parce que nous ré-acquérons un curseur au moment où nous en avons besoin,
// au lieu d'utiliser un curseur périmé d'avant.
const data = dataAttribute.cursor(eid)
data.age += 1
}, 1000)

component.data.interval = interval
},
tick : (world, component) => {
console.log('I am', component.data.age, 'seconds old')
},
remove : (world, component) => {
// world.time.clearTimeout(component.data.interval)
}.
})

Dans l'exemple ci-dessus, dataAttribute est utilisé comme méthode stable pour accéder aux données du composant dans une fonction imbriquée. Cela garantit que les données restent valides et à jour même lorsque la fonction est appelée de manière asynchrone.

En outre, la variable eid est déstructurée plutôt que d'accéder directement à component.eid, car ce dernier peut changer en fonction de l'entité qui reçoit le rappel. L'utilisation d'une variable déstructurée permet d'éviter les références périmées.

Voici la validité de chacun des arguments transmis aux rappels de composants :

avertissement

Déstructurez toujours l'eid avant de l'utiliser au lieu d'accéder à component.eid, car l'accès direct à component.eid peut entraîner des références périmées.

contexteChangements après la sortie du callback ?Peut-on l'utiliser dans une fonction imbriquée ?Durée de vie
monde❌ Non✅ OuiExpérience à vie
eid✅ Oui✅ OuiDurée de vie de l'entité
schéma et données✅ Oui❌ NonNiveau supérieur de Callback
schemaAttribute & dataAttribute❌ Non✅ OuiDurée de vie de l'entité

Curseurs non validés

Les objets curseurs servent d'interfaces pour la lecture et l'écriture de données dans l'état ECS. Chaque fois qu'un curseur est demandé pour un composant, la même instance de curseur est réutilisée, mais elle pointe vers un emplacement différent dans la mémoire. Par conséquent, une référence de curseur peut devenir invalide, c'est-à-dire qu'elle peut ne plus pointer vers les données attendues.

Exemple incorrect

const cursor1 = MyComponent.get(world, entity1)
console.log(cursor1.name) // 'entity1'

const cursor2 = MyComponent.get(world, entity2)
console.log(cursor2.name) // 'entity2'

// Des bogues inattendus peuvent se produire si l'on utilise cursor1 après un autre accès au composant
console.log(cursor1.name) // 'entity2'
console.log(cursor1 === cursor2) // 'true' - il s'agit du même objet, juste initialisé différemment à chaque fois

Les auditeurs suspendus

Évitez de supposer qu'un objet ou un composant persistera indéfiniment. Au fur et à mesure que votre projet évolue ou que de nouvelles fonctionnalités sont introduites, il est important de s'assurer que la logique de votre composant est robuste, notamment en nettoyant correctement les récepteurs d'événements.

Les machines à états sont un excellent moyen de gérer et de nettoyer les écouteurs d'événements.

Exemple correct

ecs.registerComponent({
name : 'Game Manager',
schema : {
// Ajoute des données qui peuvent être configurées sur le composant.
scoreDisplay : ecs.eid, // combien de pièces vous avez collectées
},
schemaDefaults : {
// Ajoute des valeurs par défaut pour les champs du schéma.
},
data : {
// Ajoute des données qui ne peuvent pas être configurées en dehors du composant.
score : ecs.i32, // La valeur entière du score
},
stateMachine : ({world, eid, schemaAttribute, dataAttribute}) => {
// Ajoute un événement de score
const coinCollect = () => {
const data = dataAttribute.cursor(eid)
data.score += 1
ecs.Ui.set(world, schemaAttribute.get(eid).scoreDisplay, {
text : data.score.toString(),
})
}

ecs.defineState('default').initial().onEnter(() => {
world.events.addListener(world.events.globalId, 'coinCollect', coinCollect)
}).onExit(() => {
world.events.removeListener(world.events.globalId, 'coinCollect', coinCollect)
})
},
})