Technical Deep-Dive

LootBox

Un système de lootbox animée avec ouverture en temps réel et effets visuels entièrement gérés côté serveur

C#
Architecture
Gameplay
Animation
3D
Art
Multijoueur
Scroll

Présentation

J'ai implémenté de bout en bout la LootBox de Paladium une feature centrale du modèle économique du serveur. Le joueur insère une clé, la boîte s'ouvre, ses récompenses apparaissent en 3D autour d'elle. Pas de menu, pas d'interface 2D : tout se passe dans le monde.

Des modèles et animations de base m'ont été fournis, mais pas tout. J'ai dû compléter, adapter et créer une partie significative des assets visuels, puis écrire tout le code serveur et le pilotage réseau.

L'objectif était de produire un moment fort dans la session, anticipé par le joueur, qui devait se ressentir comme un événement immersif, fluide et robuste en multijoueur.

Les contraintes

🎮

Immersion totale → pas d'interface 2D, tout doit se passer en 3D dans le monde

👥

Multijoueur simultané → plusieurs joueurs peuvent ouvrir des boîtes en même temps, sans interférence visuelle

🔌

Déconnexion propre → si le joueur part en pleine animation, aucune entité ne reste dans les limbes

📦

Assets partiels → modèles et animations reçus à l'état brut, à compléter et rendre opérationnels

🎨

Expérience visuelle forte → 4 ambiances par rareté, effet de glow, hologramme, sons spatialisés 3D

Récompenses fiables → jamais de loot perdu, même en cas de crash ou déconnexion

Résultat en jeu

Le lootbox en action

Avant de plonger dans l'architecture, voici la lootbox en jeu

Partie 1

⚙️ Backend C# · 🎨 3D

Trois entités, un seul spectacle

La LootBox n'est pas une seule entité : c'est un ensemble de 3 entités coordonnées, chacune avec un rôle précis. Aucune n'existe côté serveur elles sont envoyées directement au client du joueur concerné. Plusieurs joueurs peuvent ouvrir la même boîte en même temps : chacun voit sa propre session visuelle, sans jamais se mélanger.

Technique

Trois entités distinctes, chacune avec son identifiant Bedrock et son rôle :

plugin:lootbox

Le modèle principal tourne en idle, joue une animation calée sur la rareté à l'ouverture.

plugin:lootbox_panel

Le panneau holographique blend mode additif, lumineux sans ombre, maintenu en permanence face au joueur via une animation target.

plugin:lootbox_loot

Les entités loot jusqu'à 10 instances spawnées une par une, en éventail symétrique autour du panneau, chacune tenant l'item en main.

Partie 2

⚙️ Backend C# · 🎨 3D

Comment afficher des items en 3D ?

Pour que le joueur sache ce qu'il a gagné, il faut afficher les récompenses en 3D dans le monde. Problème : Bedrock ne propose aucune API pour spawner un objet flottant avec un visuel choisi côté serveur.

En cherchant un contournement, j'ai identifié le seul endroit où Bedrock affiche un item en 3D et où le serveur peut choisir lequel : la main d'une entité. C'est le seul vrai point de contrôle disponible dans le moteur.

La solution : créer des entités dédiées aux loots qui tiennent la récompense dans la main. En positionnant et animant la main précisément dans le modèle Blockbench, on obtient un item flottant, visible, magnifique sans jamais dépasser les limites du moteur.

Technique

Côté serveur, chaque entité loot est spawnée via un AddEntityPacket ciblant uniquement le client du joueur. L'item est ensuite équipé dans sa main principale via un MobEquipmentPacket. Le modèle Blockbench est conçu pour que le point d'affichage de la main soit exactement au centre visible de l'entité.

1var packet = new AddEntityPacket {
2    EntityUniqueId  = id,
3    EntityRuntimeId = id,
4    Identifier      = "plugin:lootbox_loot",
5    Position        = panelPosition + new Vector3(0, 2.5f, 0),
6    Metadata        = metadata,
7};
8player.SendPacket(packet);
9
10var equipPacket = new MobEquipmentPacket {
11    RuntimeEntityId = id,
12    Item            = new ItemStack(rewardIdentifier),
13    Slot            = 0,
14    SelectedSlot    = 0,
15    WindowId        = 0,
16};
17player.SendPacket(equipPacket);
18
19player.SendSound(panelPosition + new Vector3(0, 2.5f, 0), SoundEvent.Pop);
Contrainte → Créativité : la limitation de Bedrock a forcé une solution que je n'aurais pas explorée sur un moteur libre. La main de l'entité est le seul levier natif le comprendre et modéliser autour de lui, c'est l'essentiel du travail artistique de cette partie.

Partie 3

⚙️ Backend C# · 🎨 3D

Préparer les visuels

Les assets fournis étaient un point de départ, pas un livrable prêt à l'emploi. Avant d'écrire une ligne de code, il a fallu produire et adapter les éléments visuels : nettoyage des animations, ajustement des timings, création du panneau holographique de zéro, et surtout l'ajout d'un effet de glow émissif différent selon la rareté.

Le panneau holographique

Pour le panneau, on m'a fourni uniquement une texture. J'ai créé intégralement le modèle 3D, puis animé une séquence d'ouverture et de fermeture synchronisée avec la boîte. Le panneau utilise le blend mode additif (entity_beam_additive) : lumineux, sans ombre portée, semi-transparent. Une animation target le maintient en permanence face au joueur.

Panneau holographique  modèle et animation dans Blockbench
Blend mode additif · rotation face au joueur · entrée/sortie synchronisées avec la boîte

Les entités loot en éventail

Pour chaque entité loot, j'ai positionné la main exactement là où je voulais afficher l'item, puis animé les entités pour qu'elles se déploient en éventail symétrique. Les positions sont assignées en ordre entrecroisé (centre, droite extrême, gauche, droite…) pour créer l'effet de cartes que l'on étale progressivement.

Entités loot en éventail symétrique
Déploiement en éventail

Partie 4

⚙️ Backend C# · 🎨 3D

Les entités client-side

Ces entités n'existent pas côté serveur. Elles sont envoyées directement au client du joueur concerné via des packets réseau bas niveau, avec des IDs calculés par offset unique par joueur. Si deux joueurs ouvrent la même boîte en même temps, chacun voit sa propre session visuelle aucune interférence possible.

La séparation est nette : le serveur orchestre (il décide quand déclencher quelle animation et envoie les packets ciblés), le resource pack incarne (il exécute les courbes, timings et effets).

Technique

Chaque joueur a un offset d'ID fixe unique qui garantit que ses entités client-side n'entrent jamais en collision avec les entités monde du serveur.

1private long GetId(Player player) => BASE_ID_OFFSET + player.EntityId;
2
3var lootboxPacket = new AddEntityPacket {
4    EntityUniqueId  = GetId(player),
5    EntityRuntimeId = GetId(player),
6    Identifier      = "plugin:lootbox",
7    Position        = entity.Position,
8    Metadata        = rarityMetadata,
9};
10player.SendPacket(lootboxPacket);
11
12var panelPacket = new AddEntityPacket {
13    EntityUniqueId  = GetId(player) + 1,
14    EntityRuntimeId = GetId(player) + 1,
15    Identifier      = "plugin:lootbox_panel",
16    Position        = entity.Position + new Vector3(0, 1.5f, 0),
17};
18player.SendPacket(panelPacket);

Partie 5

⚙️ Backend C# · 🎨 3D

La machine à états par joueur

Chaque joueur en interaction avec une boîte dispose de sa propre machine à états finis, pilotée par des ticks serveur. L'idée : la boîte peut être en aperçu, en ouverture, en affichage des loots ou en fermeture. Quand un événement arrive (approche du joueur, clic, timer), l'état change proprement sans logique dispersée dans le code.

1
🧑 Joueur entre dans la zone de détection

LootBoxState.WaitingForOpen → ViewLoot

Spawn du panneau holographique + entités loot
Items déployés en éventail autour de la boîte
State → ViewLoot
2
🧑 Joueur clique sur la boîte avec une clé

LootBoxState.ViewLoot → OpenInProgress

Joueur possède une clé valide
Boîte non déjà en cours d'ouverture
Clé consommée · AnimateEntityPacket envoyé au client
Son d'ouverture spécifique à la raretéopen_legendary, open_epic, open_common…
State → OpenInProgress
3
Durée de l'animation écoulée (ticks serveur)

LootBoxState.OpenInProgress → Open

Entités loot spawnées une par une avec délai
Son pop à chaque apparition d'item
Récompenses envoyées en boîte mail (livraison async)
State → Open
4
Délai d'affichage expiré ou fermeture manuelle

LootBoxState.Open → CloseInProgress

Animation de fermeture déclenchée
Items « s'envolent » — entités despawn progressif
State → CloseInProgress
5
⚙️ Animation de fermeture terminée

LootBoxState.CloseInProgress → WaitingForOpen

Panneau holographique despawn
Toutes les entités client-side supprimées
State → WaitingForOpen

Technique

Chaque joueur a sa propre instance de LootBoxStateMachine, stockée dans un ConcurrentDictionary thread-safe. Quand SetState est appelé, il délègue à un handler d'entrée spécifique selon l'état cible, puis planifie le prochain tick via ScheduleUpdate() pas de polling, uniquement des events ciblés.

1public enum LootBoxState {
2    WaitingForOpen = 0,
3    OpenInProgress,
4    Open,
5    CloseInProgress,
6    ViewLoot,
7}
8
9private readonly ConcurrentDictionary<Uuid, LootBoxStateMachine> _stateMachines = new();
10
11private void SetState(Entity entity, Player player, LootBoxState state) {
12    long tick = player.GetLevel().GetCurrentTick();
13    var next = state switch {
14        LootBoxState.OpenInProgress  => HandleEnterOpenInProgress(tick, player, entity),
15        LootBoxState.Open            => HandleEnterOpen(player, tick, entity),
16        LootBoxState.CloseInProgress => HandleCloseInProgress(tick, player, entity),
17        LootBoxState.ViewLoot        => HandleEnterViewLoot(player, tick, entity),
18        _                            => null,
19    };
20    if (next != null) _stateMachines[player.Uuid] = next;
21    entity.ScheduleUpdate();
22}

Partie 6

⚙️ Backend C# · 🎨 3D

Piloter les animations à distance

Le serveur ne sait pas ce qu'est une keyframe. Il pilote uniquement quand déclencher quelle animation, via des AnimateEntityPacket ciblant les animation controllers définis dans le resource pack. C'est le resource pack qui exécute les courbes, les timings et les effets. Le serveur se contente de commander.

Pour l'ouverture, le packet est envoyé uniquement au joueur concerné. L'animation est sélectionnée selon la rareté (open_legendary, open_common…), et le son associé est joué côté client en simultané.

Technique

1var animPacket = new AnimateEntityPacket {
2    Animation        = openAnimationId,
3    NextState        = "default",
4    StopExpression   = "query.all_animations_finished",
5    Controller       = animationControllerId,
6    RuntimeEntityIds = [entity.RuntimeId],
7};
8player.SendPacket(animPacket);
9player.SendSound(entity.Position, openSoundId);
Séparation des responsabilités : tout le travail artistique courbes, timings, effets reste dans le pack client. Modifier une animation ne nécessite pas de toucher au code serveur. Cette séparation est un gain de temps énorme en production.

Partie 7

⚙️ Backend C# · 🎨 3D

Annulation propre le versioning atomique

Si un joueur se déconnecte en cours d'animation, les coroutines de spawn d'entités sont toujours en attente côté serveur. Sans mécanisme de nettoyage, ces entités seraient envoyées dans le vide ou pire, à un autre joueur qui a repris le même slot de connexion.

J'ai implémenté un système de versioning atomique : chaque ouverture incrémente un compteur par joueur. Toute coroutine dont la version ne correspond plus est ignorée proprement, sans laisser d'entités fantômes côté client.

Technique

_renderVersions est un ConcurrentDictionary global thread-safe. CancelLootRender incrémente le compteur atomiquement via AddOrUpdate. Dans chaque coroutine, la version est vérifiée avant et après chaque attente si elle ne correspond plus, la coroutine s'arrête immédiatement via yield break, zéro entité orpheline.

1private static readonly ConcurrentDictionary<Uuid, int> _renderVersions = new();
2
3public void CancelLootRender(Player player) =>
4    _renderVersions.AddOrUpdate(player.Uuid, 1, (_, v) => v + 1);
5
6private IEnumerator SpawnLootItemsCoroutine(Player player, int version) {
7    for (int i = 0; i < itemCount; i++) {
8        if (!IsVersionCurrent(player, version)) yield break;
9        SpawnLootEntity(player, i);
10        yield return new WaitForSeconds(spawnDelay);
11        if (!IsVersionCurrent(player, version)) yield break;
12    }
13}
14
15private static bool IsVersionCurrent(Player player, int version) =>
16    _renderVersions.TryGetValue(player.Uuid, out int v) && v == version;

Les feedbacks joueur

La boîte communique en permanence avec le joueur. À chaque étape, un signal différent : nametag à distance, hologramme à l'approche, animation et son à l'ouverture, items qui apparaissent un par un avec un son pop, récompenses dans le chat colorées par rareté.

Les récompenses arrivent dans la boîte mail du joueur de manière asynchrone même si le joueur se déconnecte au moment de l'ouverture, rien n'est perdu. L'animation et la livraison sont deux systèmes découplés : l'un peut être retardé ou annulé sans impacter l'autre.

ContexteSignalDétail
À distanceNametag (visible à travers les blocs)Indique si le joueur a une clé ou non, mis à jour en temps réel
En zone procheHologramme + itemsDéploiement automatique, sans interaction une invitation
À l'ouvertureAnimation + sonSpécifiques à la rareté 4 ambiances distinctes
Pendant l'ouvertureItems un par unChaque apparition accompagnée d'un son pop
À la fermetureItems qui s'envolentRécompenses livrées en boîte mail (livraison async)
Dans le chatCouleur par raretéChaque récompense affichée avec sa couleur de rareté

Récapitulatif technique

DéfiSolutionRésultat
Effets visuels riches sur Bedrock3 entités client-side coordonnéesAucun impact sur le monde serveur
Afficher des items en 3DEntités tenant l'item en mainLevier natif du moteur détourné créativement
Assets partiels non opérationnelsModélisation et animation maisonVisuels complets et cohérents
Plusieurs joueurs simultanésIDs par offset unique par joueurSessions visuelles indépendantes
Déconnexion en cours d'animationVersioning atomique des coroutinesZéro entité fantôme
Animations par raretéAnimateEntityPacket ciblés4 ambiances, extensibles sans recompile
Récompenses non perduesLivraison async en boîte mailDécouplée de l'animation

Ce que ce projet m'a appris

  • 🎮

    Penser côté client

    Les animations ne sont pas jouées elles sont commandées à distance. Cette frontière serveur/client, une fois bien comprise, ouvre des possibilités créatives très larges.

  • 🎨

    Code et art ne sont pas séparés

    Sur ce projet, modéliser le panneau holographique et en écrire le code réseau sont des tâches complémentaires. Comprendre les deux permet de prendre de meilleures décisions sur chacun.

  • 🧩

    Orchestrer vs incarner

    Le serveur orchestre, le resource pack incarne. Modifier une animation ne nécessite pas de recompiler le serveur cette séparation est un gain de temps énorme en production.

  • 🔒

    Le versioning comme garde-fou

    Un simple compteur atomique suffit à invalider toutes les coroutines en cours lors d'une déconnexion. Propre, sans race condition, sans fuite mémoire.

  • La contrainte comme moteur créatif

    Construire une expérience visuelle riche sur un moteur contraint oblige à penser différemment. Les limitations de Bedrock ont forcé des solutions que je n'aurais pas trouvées sur un moteur libre.

© 2026 Nathanaël Rallo. All rights reserved.