Aller au contenu principal
Services autour du jeu

Cette page n’est pas encore traduite : la version anglaise est affichée.

Messaging

Salles, groupes, joueurs : un seul modèle d’adressage pour le chat et les notifications. Les messages arrivent dans une conversation ; une conversation est un canal auquel s’ajoutent l’historique, la modération et la livraison hors bande.

Quand l’utiliser

  • Les joueurs discutent (chat de salle, canaux de guilde, messages privés) en s’appuyant sur l’adressage que vous avez déjà : salle (room), groupe, joueur.
  • Les joueurs hors ligne doivent quand même être prévenus : des notifications construites à partir de modèles et planifiables sont livrées hors bande, en notifications push.
  • La modération doit intervenir avant la livraison : un hook avant envoi filtre ou rejette, et la plateforme fait respecter partout la mise en sourdine et le blocage.
  • Les joueurs qui reviennent doivent rattraper la conversation : History(take: 50) la parcourt page par page au lancement suivant.
  • Inutile quand la charge utile (payload) est de l’état de jeu et non une conversation. Les champs synchronisés de Data & Subscriptions et les canaux Core le diffusent déjà.

Qui fait quoi

ActeurCe qu’il fait sur cette page
playerenvoie et reçoit des messages ; lit l’historique ; met en sourdine ou bloque
moderatorfiltre et masque des messages, bannit des termes
backend-serviceenvoie ou planifie des notifications construites à partir de modèles

En bref

Un Send par cible d’adressage — salle, guilde, message direct — plus l’abonnement et l’historique
// conversations map to the addressing you already have
await playserv.Messaging.Send(Conversation.Room(roomId), "gg!");
await playserv.Messaging.Send(Conversation.Group(guildId), rally);
await playserv.Messaging.Send(Conversation.Direct(friendId), "re?");

playserv.Messaging.Subscribe(Conversation.Group(guildId), msg => Chat.Add(msg));
var history = await playserv.Messaging.History(Conversation.Room(roomId), take: 50);
// conversations map to the addressing you already have
await playserv.messaging.send(Conversation.room(roomId), 'gg!');
await playserv.messaging.send(Conversation.group(guildId), rally);
await playserv.messaging.send(Conversation.direct(friendId), 're?');

playserv.messaging.subscribe(Conversation.group(guildId), (msg) => chat.add(msg));
const history = await playserv.messaging.history(Conversation.room(roomId), { take: 50 });
# conversations map to the addressing you already have
await playserv.messaging.send(Conversation.room(room_id), "gg!")
await playserv.messaging.send(Conversation.group(guild_id), rally)
await playserv.messaging.send(Conversation.direct(friend_id), "re?")

playserv.messaging.subscribe(Conversation.group(guild_id), lambda msg: chat.add(msg))
history = await playserv.messaging.history(Conversation.room(room_id), take=50)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

// conversations map to the addressing you already have
Client->Messaging->Conversations->Get(FPSConversation::Room(RoomId),
    TPSOnResult<FPSConversation*>::CreateWeakLambda(this, [this](const TPSResult<FPSConversation*>& Result)
    {
        if (!Result.HasValue()) { return; }
        FPSConversation* RoomChat = Result.Value();
        RoomChat->Send->Text({ TEXT("gg!") });

        // history pages under the same node that carries the messages
        RoomChat->Messages->Select().Page(50).Then(
            TPSOnResult<TPSPage<FPSMessage>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<FPSMessage>>& History)
            {
                if (!History.HasValue()) { return; }
                Chat->Show(History.Value().Rows);
            }));
    }));

// group and direct targets resolve the same way
Client->Messaging->Conversations->Get(FPSConversation::Group(GuildId), OnConversation);
Client->Messaging->Conversations->Get(FPSConversation::Direct(FriendId), OnConversation);

// live messages: one handler, every target
TPSSubscription GuildFeed = Guild->Subscribe([this](const FPSMessage& Message) { Chat->Add(Message); });
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — s’exécute tel quel dans Unity avec les types générés
// conversations map to the addressing you already have
await playserv.Messaging.Send(Conversation.Room(roomId), "gg!");
await playserv.Messaging.Send(Conversation.Group(guildId), rally);
await playserv.Messaging.Send(Conversation.Direct(friendId), "re?");

playserv.Messaging.Subscribe(Conversation.Group(guildId), msg => Chat.Add(msg));
var history = await playserv.Messaging.History(Conversation.Room(roomId), take: 50);

Un message structuré est un événement déclaré, et la conversation le transporte par son nom, sans classe de charge utile à construire au moment de l’appel :

RallyCall déclaré une seule fois ; la conversation de guilde l’envoie par son nom
[Message("rallyCall")]
public class RallyCall
{
    public Vector3 At;
    public string Note = "";
}

var guild = PlayServ.Group(guildId).Conversation;
await guild.Send.RallyCall(at: northGate, note: "push now");
@Message('rallyCall')
export class RallyCall {
  at!: Vector3;
  note = '';
}

const guild = playserv.group(guildId).conversation;
await guild.send.rallyCall({ at: northGate, note: 'push now' });
@message("rallyCall")
class RallyCall:
    at: Vector3
    note: str = ""

guild = playserv.group(guild_id).conversation
await guild.send.rally_call(at=north_gate, note="push now")
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

USTRUCT(PSMessage = (Name = "rallyCall"))
struct FRallyCall
{
    GENERATED_BODY()

    UPROPERTY() FVector At;
    UPROPERTY() FString Note;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI

// the group's conversation is an address you resolve, then send into
Client->Messaging->Conversations->Get(FPSConversation::Group(GuildId),
    TPSOnResult<FPSConversation*>::CreateWeakLambda(this, [this](const TPSResult<FPSConversation*>& Result)
    {
        if (!Result.HasValue()) { return; }
        Result.Value()->Send->RallyCall({ NorthGate, TEXT("push now") });
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — la même déclaration et le même appel compilent dans Unity, sur la base 2021.3
[Message("rallyCall")]
public class RallyCall
{
    public Vector3 At;
    public string Note = "";
}

var guild = PlayServ.Group(guildId).Conversation;
await guild.Send.RallyCall(at: northGate, note: "push now");

Le message déclaré arrive typé dans le même abonnement : un client qui connaît RallyCall reçoit des champs, pas un blob.

Les notifications sont hors bande, construites à partir de modèles et planifiables ; elles s’envoient avec l’autorité fn ou adm, jamais depuis une session joueur :

Une notification raid-starts construite à partir d’un modèle, envoyée depuis une fonction cloud et livrée hors bande
// cloud function — Notify needs fn/adm authority
await PlayServ.Messaging.Notify(playerId, Template.Named("raid-starts"),
                                args: new { at = start });
// cloud function — notify needs fn/adm authority
await playserv.messaging.notify(playerId, Template.named('raid-starts'),
  { args: { at: start } });
# cloud function — notify needs fn/adm authority
await playserv.messaging.notify(player_id, Template.named("raid-starts"),
                                args={"at": start})
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

Un autre acteur fait cet appel

L’appel existe dans Unreal. Envoyer une notification exige l’autorité fn/adm : la plateforme le refuse donc sur une session joueur, quel que soit le binding qui passe l’appel ; Unreal reçoit la notification livrée. Voir Access & Roles.

Un autre acteur fait cet appel

L’appel existe dans Unity. Envoyer une notification exige l’autorité fn/adm : la plateforme le refuse donc sur une session joueur, quel que soit le binding qui passe l’appel ; Unity reçoit la notification livrée. Voir Access & Roles.

La modération passe par des hooks, avec le même contrat que partout ailleurs :

Un hook avant envoi : les grossièretés sont rejetées avant même d’arriver
[Before(Messaging.Send)]
public static Verdict Filter(OutgoingMessage m) =>
    Profanity.Hits(m.Text) ? Hook.Reject("filtered") : Hook.Continue(m);
export const filter = before(Messaging.send, (m: OutgoingMessage) =>
  Profanity.hits(m.text) ? Hook.reject('filtered') : Hook.continue(m));
@before(messaging.send)
def filter_message(m: OutgoingMessage) -> Verdict:
    return Hook.reject("filtered") if profanity.hits(m.text) else Hook.continue_(m)
Bientôt — Go

La déclaration par attributs en Go n’est pas encore tranchée (O-1). Les exemples arriveront quand la forme d’écriture sera choisie.

S’exécute hors du moteur

Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — Unreal s’abonne aux événements qu’il produit. Des fonctions écrites côté moteur sont prévues : graphes Blueprint et C++ (traduit vers un langage de script validé par la plateforme). Les unes comme les autres seront analysées et publiées comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.

S’exécute hors du moteur

Un hook est une fonction cloud : il s’exécute sur la plateforme, pas dans le moteur. Écrivez-le en C#, TypeScript ou Python — Unity s’abonne aux événements qu’il produit.

Le modèle

Ce que déclare un type de conversation.

DéclarationCe que c’est
le groupesa composition — un participant est un acteur, exactement comme dans Groups
rattachement à une durée de viefacultativement celle d’une autre entité, pour que le chat d’une salle disparaisse avec sa salle

Où passe la frontière entre l’enveloppe et la charge utile.

PartieÀ qui elle appartient
enveloppeà la plateforme : l’auteur, la conversation, l’instant selon l’horloge déclarée
charge utileau studio : déclarée comme un type de message aux champs typés, elle apparaît comme sa propre interface d’envoi plutôt que comme un sac non typé

Ce module possède trois choses qu’un simple événement n’a pas : l’ordre au sein d’une conversation, la durée de rétention et l’objet de la modération. Aucune ne figure dans les événements, et c’est pourquoi le chat n’est pas « un événement avec un historique ».

Les trois machines à états.

MachineÉtats
une conversationcreated → active → closed
un messagesent → published | rejected by the filter, puis modifié ou supprimé, de façon observable
une notificationcreated → queued → delivered | expired

Ce qui vaut pour tout message.

ToujoursCe que c’est
ordre au sein d'une conversationest stable et déclaré. L’ordre entre conversations n’est pas garanti
modification et suppressionsont observables : un message ne disparaît jamais en silence, si bien que l’historique d’un client ne peut pas diverger de celui du serveur sans qu’on s’en aperçoive
historiquece sont les messages eux-mêmes, avec une durée de rétention déclarée, lus par pages, avec un curseur, à partir d’une position
la rétention dure plus longtemps que le délai de signalementla durée n’est jamais inférieure au temps prévu pour traiter un signalement : le signalement arrive après le message, et un message qui n’existe plus ne laisse rien à traiter
état de lecturec’est une position, pas un indicateur : une position par acteur et par conversation, et le marquage comme lu est monotone — la position ne recule jamais, si bien qu’un appel répété ne peut pas annuler la progression. Le nombre de non-lus est dérivé de cette position, et non un compteur à part
envoiest idempotent par clé : deux appels, ce sont deux répliques du dialogue, et c’est donc la clé qui rend une nouvelle tentative sûre
blocageest un prédicat de livraison, pas un refus d’envoi : l’expéditeur n’est pas prévenu, car un refus révélerait le blocage. L’état lui-même vit dans Social
l'expéditeur compose la charge utilela plateforme ne lit pas les données du destinataire pour compléter votre texte. La langue du destinataire peut être une claim de contexte déclarée qui parvient jusqu’au point d’extension : la substitution et la traduction sont donc le travail du hook — le seul endroit qui connaît à la fois le destinataire et sa langue
la voie de livraisonne fait pas partie du contrat : push, notification dans l’application ou autre chose, c’est une décision de routage, pas une promesse
livraisonest observable dans des limites déclarées : « en file d’attente » toujours ; au-delà, dans la mesure où la voie de livraison peut le signaler

Chaque point d’extension nomme le type qu’il transmet au hook : le message sortant avant publication, le message publié après. Un filtre peut corriger le contenu qu’on lui a transmis — masquer un mot est une correction — mais jamais l’expéditeur ni la conversation.

Erreurs

  • Une conversation inexistante ou masquée, et un acteur qui n’y participe pas, reçoivent tous deux not found : un refus ne révèle ainsi jamais une conversation dont vous ne faites pas partie.
  • Une conversation fermée est un conflit.
  • L’absence de permission d’écrire dans ce type répond forbidden, et il est inutile de réessayer.
  • Un rejet par le filtre est un verdict, pas un refus. L’appel a été exécuté, le contenu a été examiné, la décision est négative et le motif est une valeur déclarée — c’est pourquoi on le distingue d’un refus pour cause de permissions, et pourquoi la suite à donner dépend du motif.
  • Un filtre indisponible répond unavailable, et il vaut la peine de réessayer avec un délai croissant (backoff) — mais rien n’a été publié entre-temps.
  • Un type de message non déclaré pour cette conversation, et un message trop volumineux, sont des refus de validation ; le contenu n’est jamais tronqué en silence.
  • Un dépassement de la fréquence d’envoi reçoit une réponse de la catégorie limitation de débit (rate limit), avec un délai.
  • Modifier le message de quelqu’un d’autre répond forbidden.
  • Une notification expirée est un conflit : envoyez-en une nouvelle.

Limites

Les valeurs chiffrées arriveront avec le chapitre sur les limites de la plateforme ; ce qui se passe à chaque plafond est déjà fixé.

  • Taille des messages, et pièces jointes avec leur taille : l’envoi est refusé comme échec de validation, jamais tronqué. Les fichiers eux-mêmes relèvent de Files & UGC.
  • Fréquence d’envoi par acteur : une limitation de débit avec un délai.
  • Profondeur de l’historique : passé la durée prévue, un message est évincé de la rétention avec un événement, au lieu de disparaître discrètement.
  • Conversations par acteur : en rejoindre une de plus est refusé comme conflit.
  • Notifications en file d’attente par acteur : une nouvelle notification est refusée, et l’éviction est interdite : une notification abandonnée en silence ressemblerait à une notification jamais envoyée.
  • Expiration d’une notification : expired, avec un événement.
  • Participants à une conversation : c’est la limite de Groups, et blocages par acteur, celle de Social ; aucune n’est répétée ici.