Conception de système de messagerie de jeu

8

Je fais un jeu simple et j'ai décidé d'essayer d'implémenter un système de messagerie.

Le système ressemble essentiellement à ceci:

L'entité génère un message -> le message est publié dans la file d'attente de messages globale -> messageManager notifie chaque objet du nouveau message via onMessageReceived (Message msg) -> si l'objet le souhaite, il agit sur le message.

La façon dont je crée des objets de message est la suivante:

//base message class, never actually instantiated
abstract class Message{
  Entity sender;
}

PlayerDiedMessage extends Message{
  int livesLeft;
}

Maintenant, mon SoundManagerEntity peut faire quelque chose comme ça dans sa méthode onMessageReceived ()

public void messageReceived(Message msg){
  if(msg instanceof PlayerDiedMessage){
     PlayerDiedMessage diedMessage = (PlayerDiedMessage) msg;
     if(diedMessage.livesLeft == 0)
       playSound(SOUND_DEATH);
  }
}

Les avantages de cette approche:

  1. Très simple et facile à mettre en œuvre
  2. Le message peut contenir autant d'informations que vous le souhaitez, car vous pouvez simplement créer une nouvelle sous-classe Message contenant les informations nécessaires.

Les inconvénients:

  1. Je ne peux pas comprendre comment je peux recycler des objets Message dans un pool d'objets , sauf si j'ai un pool différent pour chaque sous-classe de Message. J'ai donc beaucoup, beaucoup de création d'objets / d'allocation de mémoire au fil du temps.
  2. Impossible d'envoyer un message à un destinataire spécifique, mais je n'en ai pas encore eu besoin dans mon jeu, donc cela ne me dérange pas trop.

Qu'est-ce que j'oublie ici? Il doit y avoir une meilleure implémentation ou une idée que je manque.

you786
la source
1) Pourquoi avez-vous besoin d'utiliser ces pools d'objets, quels qu'ils soient? (En passant, quels sont-ils et à quoi servent-ils?) Il n'est pas nécessaire de concevoir au-delà de ce qui fonctionne. Et ce que vous avez actuellement semble n'avoir aucun problème avec cela. 2) Si vous devez envoyer un message à un destinataire spécifique, vous le trouvez et vous appelez son onMessageReceived, aucun système sophistiqué n'est requis.
snake5
Eh bien, je voulais dire que je ne peux pas actuellement recycler les objets dans un pool d'objets à usage général. Donc, si jamais je veux réduire les allocations de mémoire nécessaires, je ne peux pas. Pour le moment, ce n'est pas vraiment un problème car mon jeu est si simple qu'il fonctionne parfaitement bien.
you786
Je suppose que c'est Java? On dirait que vous essayez de développer un système d'événements.
Justin Skiles
@JustinSkiles Correct, c'est Java. Je suppose que vous avez raison, il sert vraiment de système d'événements. Quelles autres façons pouvez-vous utiliser un système de messagerie?
you786

Réponses:

11

La réponse simple est que vous ne voulez pas recycler les messages. On recycle des objets qui sont soit créés (et supprimés) par milliers à chaque image, comme des particules, soit très lourds (dizaines de propriétés, long processus d'initialisation).

Si vous avez une particule de neige avec un bitmap, des vitesses, des attributs physiques associés qui vient de descendre sous votre vue, vous pouvez la recycler au lieu de créer une nouvelle particule et d'initialiser à nouveau le bitmap et de randomiser les attributs. Dans votre exemple, il n'y a rien d'utile dans le Message pour le conserver, et vous gaspillerez plus de ressources pour supprimer ses propriétés spécifiques à l'instance que vous ne le feriez pour créer un nouvel objet Message.

Markus von Broady
la source
3
Bonne réponse. J'utilise un pool pour mes messages, et je n'ai jamais cherché à savoir s'ils étaient utiles ou non. Temps (passé) pour profiler!
Raptormeat
Hmm. Que faire si j'ai un message qui est émis, peut-être à chaque image? Je suppose que la réponse serait d'avoir un pool spécifique pour ce type d'objet, ce qui est logique.
you786
@ you786 Je viens de faire un rapide test sale dans Actionscript 3. Créer un tableau et le remplir avec 1000 messages prend 1 ms dans un environnement de débogage lent. J'espère que cela clarifie les choses.
Markus von Broady
@ you786 Il a raison, identifiez d'abord cela comme un goulot d'étranglement, si vous voulez être un développeur de jeux indépendants, vous devez apprendre à faire les choses rapidement et efficacement. Investir du temps dans le réglage fin de votre code n'en vaut la peine que si ce code ralentit actuellement votre jeu et que ces modifications peuvent augmenter considérablement les performances. Par exemple, la création d'instances Sprtie dans AS3 prendrait beaucoup de temps, il est préférable de les recycler. La création d'objets Point ne l'est pas, leur recyclage serait une perte de temps de codage et de lisibilité.
AturSams
Eh bien, j'aurais dû clarifier cela, mais je ne m'inquiète pas de l'allocation de mémoire. Je m'inquiète que le garbage collector de Java fonctionne au milieu du jeu et provoque des ralentissements. Mais votre point sur l'optimisation prématurée est toujours valable.
you786
7

Dans mon jeu basé sur Java, j'ai trouvé une solution très similaire, sauf que mon code de gestion des messages ressemble à ceci:

@EventHandler
public void messageReceived(PlayerDiedMessage msg){
  if(diedMessage.livesLeft == 0)
     playSound(SOUND_DEATH);
}

J'utilise des annotations pour marquer une méthode comme gestionnaire d'événements. Ensuite, au début du jeu, j'utilise la réflexion pour calculer un mappage (ou, comme je l'appelle, "piping") de messages - quelle méthode invoquer sur quel objet lorsqu'un événement d'une classe spécifique est envoyé. Cela fonctionne ... génial.

Le calcul de la tuyauterie peut devenir un peu compliqué si vous souhaitez ajouter des interfaces et des sous-classes au mélange, mais c'est néanmoins assez simple. Il y a un léger ralentissement pendant le chargement du jeu lorsque toutes les classes doivent être analysées, mais cela n'est fait qu'une seule fois (et peut probablement être déplacé vers un thread séparé). Ce qui est gagné à la place, c'est que l'envoi réel de messages est moins cher - je n'ai pas à invoquer toutes les méthodes "messageReceived" dans la base de code juste pour qu'il vérifie si l'événement est de bonne classe.

Liosan
la source
3
Vous avez donc trouvé un moyen de ralentir votre jeu? : D
Markus von Broady
3
@MarkusvonBroady Si c'est une fois en charge, ça vaut vraiment le coup. Comparez cela à la monstruosité de ma réponse.
michael.bartnett
1
@MarkusvonBroady L'invocation par réflexion n'est que légèrement plus lente que l'invocation normale, si vous avez toutes les pièces en place (au moins mon profilage n'y montre aucun problème). La découverte coûte cher, c'est sûr.
Liosan
@ michael.bartnett Je code pour un utilisateur, pas pour moi. Je peux vivre avec mon code plus gros pour une exécution plus rapide. Mais c'était juste ma remarque subjective.
Markus von Broady
+1, le chemin des annotations est quelque chose auquel je n'ai jamais pensé. Donc, juste pour clarifier, vous auriez essentiellement un tas de méthodes surchargées appelées messageReceived avec différentes sous-classes de Message. Ensuite, lors de l'exécution, vous utilisez la réflexion pour créer une sorte de carte qui calcule MessageSubclass => OverloadedMethod?
you786
5

EDIT La réponse de Liosan est plus sexy. Regardez-y.


Comme l'a dit Markus, les messages ne sont pas un candidat courant pour les pools d'objets. Ne vous embêtez pas pour les raisons qu'il a mentionnées. Si votre jeu envoie des tonnes de messages de types spécifiques, alors cela en vaudrait peut-être la peine. Mais à ce stade, je vous suggère de passer aux appels de méthode directs.

En parlant de ça,

Impossible d'envoyer un message à un destinataire spécifique, mais je n'en ai pas encore eu besoin dans mon jeu, donc cela ne me dérange pas trop.

Si vous connaissez un destinataire spécifique auquel vous souhaitez l'envoyer, ne serait-il pas assez facile d'obtenir une référence à cet objet et de faire un appel direct à la méthode? Vous pouvez également masquer des objets spécifiques derrière les classes de gestionnaire pour un accès plus facile entre les systèmes.

Il y a un inconvénient à votre implémentation, et c'est qu'il y a le potentiel pour beaucoup d'objets d'obtenir des messages dont ils ne se soucient pas vraiment. Idéalement, vos classes ne recevraient que des messages qui leur tiennent à cœur.

Vous pouvez utiliser un HashMap<Class, LinkedList<Messagable>>pour associer des types de message à une liste d'objets qui souhaitent recevoir ce type de message.

L'abonnement à un type de message ressemblerait à ceci:

globalMessageQueue.subscribe(PlayerDiedMessage.getClass(), this);

Vous pouvez implémenter subscribecomme ça (pardonnez-moi quelques erreurs, je n'ai pas écrit java depuis quelques années):

private HashMap<Class, LinkedList<? extends Messagable>> subscriberMap;

void subscribe(Class messageType, Messagable receiver) {
    LinkedList<Messagable> subscriberList = subscriberMap.get(messageType);
    if (subscriberList == null) {
        subscriberList = new LinkedList<? extends Messagable>();
        subscriberMap.put(messageType, subscriberList);
    }

    subscriberList.add(receiver);
}

Et puis vous pouvez diffuser un message comme celui-ci:

globalMessageQueue.broadcastMessage(new PlayerDiedMessage(42));

Et broadcastMessagepourrait être implémenté comme ceci:

void broadcastMessage(Message message) {
    Class messageType = message.getClass();
    LinkedList<? extends Messagable> subscribers = subscriberMap.get(messageType);

    for (Messagable receiver : subscribers) {
        receiver.onMessage(message);
    }
}

Vous pouvez également vous abonner à un message de telle sorte que vous puissiez diviser votre gestion des messages en différentes fonctions anonymes:

void setupMessageSubscribers() {
    globalMessageQueue.subscribe(PlayerDiedMessage.getClass(), new Messagable() {
        public void onMessage(Message message) {
            PlayerDiedMessage msg = (PlayerDiedMessage)message;
            if (msg.livesLeft <= 0) {
                doSomething();
            }
        }
    });

    globalMessageQueue.subscriber(EnemySpawnedMessage.getClass(), new Messagable() {
        public void onMessage(Message message) {
            EnemySpawnedMessage msg = (EnemySpawnedMessage)message;
            if (msg.isBoss) {
                freakOut();
            }
        }
    });
}

C'est un peu bavard, mais aide à l'organisation. Vous pouvez également implémenter une deuxième fonction, subscribeAllqui gardera une liste séparée de Messagables qui veulent entendre tout.

michael.bartnett
la source
1

1 . Non.

Les messages sont peu coûteux. Je suis d'accord avec Markus von Broady. GC les récupérera rapidement. Essayez de ne pas joindre beaucoup d'informations supplémentaires pour les messages. Ce ne sont que des messages. La recherche d'une instance de est une info en soi. Utilisez-le (comme vous l'avez fait dans votre exemple).

2 . Vous pouvez essayer d'utiliser MessageDispatcher .

Contrairement à la première solution, vous pourriez avoir un répartiteur de messages centralisé qui traite les messages et les transmet aux objets voulus.

edin-m
la source