296 lines
8.8 KiB
Markdown
296 lines
8.8 KiB
Markdown
---
|
|
title: Opérations Async
|
|
type: docs
|
|
weight: 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()` :
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
// 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 :
|
|
|
|
```java
|
|
getEventRegistry().registerAsync(AsyncEvent.class, event -> {
|
|
return CompletableFuture.supplyAsync(() -> {
|
|
// Traitement async
|
|
return processEvent(event);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Patterns Courants
|
|
|
|
### Opérations de Base de Données
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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 :
|
|
|
|
```java
|
|
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 :**
|
|
1. Ne jamais accéder à l'état du jeu directement depuis des threads async
|
|
2. Toujours utiliser `PlayerRef`, ne jamais stocker `Player` ou `Ref<EntityStore>`
|
|
3. Capturer les données nécessaires avant de passer en async
|
|
4. Utiliser `world.execute()` pour retourner au thread du monde
|
|
5. Gérer les exceptions correctement
|
|
6. Utiliser des collections concurrentes pour les données partagées
|
|
{{< /callout >}}
|
|
|
|
```java
|
|
// 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()` ou `runLater()`
|
|
- Utilisez `CompletableFuture.runAsync()` au lieu des méthodes async spécifiques à la plateforme
|
|
- Utilisez `world.execute()` au lieu de `runSync()` pour retourner au thread du monde
|
|
- Utilisez `CompletableFuture.delayedExecutor()` pour les tâches différées
|
|
- Utilisez `ScheduledExecutorService` pour les tâches répétitives
|
|
{{< /callout >}}
|