8.8 KiB
8.8 KiB
title, type, weight
| title | type | weight |
|---|---|---|
| Opérations Async | docs | 2 |
Les opérations asynchrones permettent aux plugins d'effectuer des tâches lourdes sans bloquer le thread de tick du monde.
Pourquoi Async ?
{{< callout type="info" >}} Chaque World a un thread de tick dédié qui gère la logique du jeu, les interactions des joueurs et les mises à jour de tick. Le bloquer cause du lag et une mauvaise expérience joueur. Utilisez async pour :
- Les requêtes de base de données
- Les I/O de fichiers
- Les requêtes réseau
- Les calculs complexes {{< /callout >}}
Pattern Async de Base
Utilisez CompletableFuture.runAsync() et world.execute() :
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// Ceci s'exécute sur un thread d'arrière-plan
// Faire le travail lourd ici
Data result = computeData();
// Retourner au thread du monde pour les changements d'état du jeu
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat : " + result));
});
});
Sécurité des Threads
Utiliser PlayerRef
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.runAsync(() -> {
// Charger les données de manière asynchrone
PlayerData data = loadFromDatabase(playerRef.getUuid());
// Retourner au thread du monde
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
}
});
});
});
Collections Thread-Safe
// Utiliser des collections concurrentes pour les données partagées
private final Map<UUID, PlayerData> playerData = new ConcurrentHashMap<>();
private final Set<UUID> processing = ConcurrentHashMap.newKeySet();
public void processPlayer(PlayerRef playerRef, World world) {
UUID uuid = playerRef.getUuid();
// Empêcher le traitement en double
if (!processing.add(uuid)) {
return; // Déjà en traitement
}
CompletableFuture.runAsync(() -> {
try {
PlayerData data = compute(uuid);
playerData.put(uuid, data);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Traitement terminé !"));
});
} finally {
processing.remove(uuid);
}
});
}
Événements Async
Gérer les événements async avec CompletableFuture :
getEventRegistry().registerAsync(AsyncEvent.class, event -> {
return CompletableFuture.supplyAsync(() -> {
// Traitement async
return processEvent(event);
});
});
Patterns Courants
Opérations de Base de Données
public void savePlayerData(PlayerRef playerRef, World world) {
// Capturer les données sur le thread du monde avant de passer en async
PlayerData data = captureData(playerRef);
CompletableFuture.runAsync(() -> {
try {
database.save(playerRef.getUuid(), data);
getLogger().at(Level.INFO).log("Données sauvegardées pour " + playerRef.getUsername());
} catch (Exception e) {
getLogger().error("Échec de la sauvegarde des données", e);
}
});
}
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
PlayerData data = database.load(playerRef.getUuid());
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && data != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Données chargées !"));
}
});
});
}
Requêtes HTTP
public void fetchPlayerStats(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
String response = httpClient.get("https://api.example.com/stats/" + playerRef.getUuid());
Stats stats = parseStats(response);
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
displayStats(ref, stats);
}
});
} catch (Exception e) {
world.execute(() -> {
playerRef.sendMessage(Message.raw("Échec de la récupération des stats."));
});
}
});
}
Traitement par Lots
public void processAllPlayers(World world) {
// Capturer les refs de joueurs sur le thread du monde
List<PlayerRef> players = new ArrayList<>();
world.getEntityStore().forEach((ref, store) -> {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
players.add(playerRef);
}
});
CompletableFuture.runAsync(() -> {
Map<UUID, Result> results = new HashMap<>();
for (PlayerRef playerRef : players) {
results.put(playerRef.getUuid(), computeResult(playerRef));
}
world.execute(() -> {
for (PlayerRef playerRef : players) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
Result result = results.get(playerRef.getUuid());
applyResult(ref, result);
}
}
});
});
}
Gestion des Erreurs
public void safeAsyncOperation(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
riskyOperation(playerRef.getUuid());
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération réussie !"));
});
} catch (Exception e) {
getLogger().error("L'opération async a échoué", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération échouée. Veuillez réessayer."));
});
}
});
}
Chaînage des Opérations Async
Utilisez le chaînage CompletableFuture pour les opérations async séquentielles :
public void chainedOperations(PlayerRef playerRef, World world) {
CompletableFuture
.supplyAsync(() -> {
// Première opération async
return fetchFromDatabase(playerRef.getUuid());
})
.thenApplyAsync(data -> {
// Deuxième opération async
return processData(data);
})
.thenAccept(result -> {
// Retourner au thread du monde avec le résultat final
world.execute(() -> {
playerRef.sendMessage(Message.raw("Résultat final : " + result));
});
})
.exceptionally(e -> {
getLogger().error("La chaîne a échoué", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Opération échouée."));
});
return null;
});
}
Bonnes Pratiques
{{< callout type="warning" >}} Règles Async :
- Ne jamais accéder à l'état du jeu directement depuis des threads async
- Toujours utiliser
PlayerRef, ne jamais stockerPlayerouRef<EntityStore> - Capturer les données nécessaires avant de passer en async
- Utiliser
world.execute()pour retourner au thread du monde - Gérer les exceptions correctement
- Utiliser des collections concurrentes pour les données partagées {{< /callout >}}
// CORRECT : Capturer les données, traiter en async, appliquer sur le thread du monde
public void correctPattern(PlayerRef playerRef, World world) {
String username = playerRef.getUsername(); // Capturer sur le thread du monde
CompletableFuture.runAsync(() -> {
String result = process(username);
world.execute(() -> {
playerRef.sendMessage(Message.raw(result));
});
});
}
// MAUVAIS : Utiliser les objets monde directement en async
// public void wrongPattern(Player player, Ref<EntityStore> ref) {
// CompletableFuture.runAsync(() -> {
// // NE FAITES PAS ÇA - ceux-ci peuvent être invalides depuis le contexte async
// String name = player.getDisplayName();
// player.sendMessage("Bonjour");
// });
// }
Comparaison avec d'Autres Plateformes
{{< callout type="info" >}} Note pour les développeurs d'autres plateformes :
- Contrairement à Bukkit/Spigot, Hytale n'a PAS de méthodes
runSync()ourunLater() - Utilisez
CompletableFuture.runAsync()au lieu des méthodes async spécifiques à la plateforme - Utilisez
world.execute()au lieu derunSync()pour retourner au thread du monde - Utilisez
CompletableFuture.delayedExecutor()pour les tâches différées - Utilisez
ScheduledExecutorServicepour les tâches répétitives {{< /callout >}}