Cette page n’est pas encore traduite : la version anglaise est affichée.
Extensibility
Chaque scénario de la plateforme est une chaîne de fonctions enregistrées : remplacez un maillon ou enveloppez-le. Vous modifiez le comportement de la plateforme en remplaçant ses étapes, pas en forkant son code source.
Quand l’utiliser
- Une étape de la plateforme doit exécuter votre logique : déclarez le remplaçant d’un maillon nommé avec
[Override(…)]. - Il vous faut des vérifications ou des effets de bord autour d’une étape : du middleware
Before/Afterordonné, qui peut opposer un veto ou notifier. - Du code doit s’exécuter selon un planning, sur un événement ou sur un webhook : les déclencheurs (trigger) vous fournissent un contexte typé, déjà analysé.
- Vous devez savoir ce qui s’exécutera réellement avant de déployer : faites une exécution à blanc d’une chaîne et lisez l’ordre résolu.
- Inutile quand la règle porte sur les écritures d’une seule entité. Un hook de Data & Subscriptions est la forme la plus légère.
Qui fait quoi
| Acteur | Ce qu’il fait sur cette page |
|---|---|
backend-service | surcharge des maillons, enveloppe des étapes avec du middleware, écrit des handlers de déclencheurs |
operator | inspecte les chaînes, fixe l’ordre, lit les secrets, résout les chaînes à blanc |
En bref
SignIn, envelopper grant avec du middleware, exécuter du code sur un cron// gate one named step of the auth scenario — a before hook may refuse, fail-closed
[Before(Auth.SignIn)]
public static Task<Verdict> GateRegion(SignInAttempt a) =>
a.Region == "sanctioned"
? Hook.Reject(Problem.Forbidden, "region not served")
: Hook.Continue(a);
// wrap a step with ordered middleware
PlayServ.Extend.Scenario("commerce.purchase")
.Before("grant", LogPurchaseIntent)
.After("grant", NotifySquad, order: 10);
// customer code on a trigger
[OnSchedule("0 4 * * *")]
public static async Task NightlyCleanup() { ... }// gate one named step of the auth scenario — a before hook may refuse, fail-closed
export const gateRegion = before(Auth.signIn, (a: SignInAttempt) =>
a.region === 'sanctioned'
? Hook.reject(Problem.forbidden, 'region not served')
: Hook.continue(a));
// wrap a step with ordered middleware
playserv.extend.scenario('commerce.purchase')
.before('grant', logPurchaseIntent)
.after('grant', notifySquad, { order: 10 });
// customer code on a trigger
export const nightlyCleanup = onSchedule('0 4 * * *', async () => { /* ... */ });# gate one named step of the auth scenario — a before hook may refuse, fail-closed
@before(auth.sign_in)
async def gate_region(a):
if a.region == "sanctioned":
return hook.reject(problem.FORBIDDEN, "region not served")
return hook.cont(a)
# wrap a step with ordered middleware
playserv.extend.scenario("commerce.purchase") \
.before("grant", log_purchase_intent) \
.after("grant", notify_squad, order=10)
# customer code on a trigger
@on_schedule("0 4 * * *")
async def nightly_cleanup(): ...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.
Surcharges, middleware et déclencheurs s’exécutent tous comme des fonctions cloud sur la plateforme, pas dans le moteur. Écrivez-les en C#, TypeScript ou Python — Unreal s’abonne aux événements qui en résultent. Des fonctions écrites côté moteur sont prévues : des graphes Blueprint, et du C++ (traduit vers une cible de script validée par la plateforme). Les deux sont analysés et publiés comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.
Surcharges, middleware et déclencheurs s’exécutent tous comme des fonctions cloud sur la plateforme, pas dans le moteur. Écrivez-les en C#, TypeScript ou Python — Unity s’abonne aux événements qui en résultent.
Le modèle
Ce à quoi la plateforme vous permet de vous brancher.
| Terme | Ce que c’est |
|---|---|
fonction enregistrée | une étape de la plateforme qu’on peut surcharger — « créer le profil », « calculer le prix » |
scenario | la chaîne ordonnée qu’exécute un flux de la plateforme : authentification, entrée en salle, achat |
overridability | indique si un maillon peut être remplacé, seulement enveloppé, ou s’il est figé |
middleware | un handler ordonné qui s’exécute avant ou après un maillon |
trigger | ce qui lance votre code : un événement, un planning, un webhook |
secret | une valeur que votre handler peut lire |
invocation | une exécution, avec sa trace |
Ce que déclare un hook.
| Déclare | Ce que c’est |
|---|---|
position | l’étape nommée à laquelle il se rattache |
kind | gatekeeper — un contrôle d’admission ou une validation, qui bloque en cas d’échec : l’étape ne s’exécute pas quand le hook lui-même plante ; ou observer — un journal, une notification, un compteur, qui laisse passer en cas d’échec : l’étape s’exécute, et l’échec est tout de même signalé au lieu d’être passé sous silence. Il n’y a pas de valeur par défaut |
moment | before — avant la validation, il reçoit le payload typé et peut le modifier ou le rejeter ; ou after — une fois l’étape validée, il reçoit la requête et le résultat, pour des effets de bord uniquement, et il ne peut jamais faire échouer l’opération ni modifier la réponse |
effect | ce que le hook fait, pas seulement l’endroit où il se trouve. L’ordre s’exprime alors en termes de travail plutôt que de numéros, et il tient bon quand quelqu’un d’autre ajoute un hook à côté du vôtre |
version et condition | un handler qui s’applique à un seul environnement ou à une seule audience est une version déclarée de ce hook, et non une branche dans son corps ; c’est ce que bascule l’interrupteur du panneau |
Trois façons de brancher du code.
| Forme | À utiliser pour |
|---|---|
attribut [Before(Step)] / [After(Step)] | une règle sur une étape nommée — la plupart des hooks |
attribut [Override(Link)] | remplacer purement et simplement l’implémentation d’un maillon |
Extend.Scenario("…").Before("link", fn, order: n) | envelopper un maillon à l’intérieur d’une chaîne, quand l’ordre par rapport aux autres middlewares compte |
| Toujours | Ce que c’est |
|---|---|
les trois | se déploient avec playserv push |
les deux formes par attribut | sont celles qu’affiche le panneau, car la déclaration transporte le nom de l’étape ou du maillon dans le modèle publié |
la forme middleware | transporte un ordre à la place, et c’est ce dont une chaîne a besoin |
l'affectation au démarrage | (Scenario.OnX = fn) reste disponible pour un handler qui n’a pas besoin d’apparaître dans l’arborescence d’administration |
remplacer un maillon | laisse les maillons voisins intacts, et aucun d’eux ne sait quelle implémentation a répondu — l’étape de la plateforme ou la vôtre |
Où va un appel, et ce qui vaut pour toute route.
| Toujours | Ce que c’est |
|---|---|
quatre directions | une fonction cloud · le backend externe du consommateur · le serveur de jeu · une autre fonction déclarée |
le routeur | fonctionne par messages et signaux ; request-response n’est qu’un adaptateur posé dessus, pas sa nature |
correspondance | se fait sur le nom déclaré de l’opération ou du signal, et sur rien d’autre : ni la forme du payload, ni l’appelant, ni la charge |
la direction | ne fait pas partie du contrat de l’opération : déplacer un handler d’une direction à une autre n’est pas un changement cassant |
« le serveur de jeu » | se définit par ce qu’il est, pas par qui l’héberge — notre flotte et l’hébergement propre d’un studio sont une seule direction, et la déclaration ne porte aucune marque indiquant à qui appartient l’infrastructure |
RPC du serveur de jeu | s’enregistrent dans le même routeur : en déclarer un, c’est l’enregistrer, et il n’existe pas d’autre moyen |
ordre | le middleware s’exécute de haut en bas, et quand une étape a plusieurs implémentations, le routeur choisit de gauche à droite selon la condition ; la version marquée par défaut répond quand rien ne correspond |
Une chaîne avec surcharges et middleware peut être résolue et lue avant toute exécution. L’ordre résolu s’inspecte dans le panneau et depuis le code.
Ce qu’est une contrainte entre hooks, et quand elle est vérifiée.
| Ce que c’est | |
|---|---|
une contrainte nommée | un point d’extension peut nommer les effets qu’il contraint — un contrôle anti-triche doit précéder un placement, un reçu exige un débit à cet endroit — et ne contraindre rien d’autre |
un effet non nommé | n’est pas contraint, mais n’est pas refusé non plus |
une violation | est un défaut de configuration, et le refus nomme les deux hooks et la contrainte qu’ils ont enfreinte — pas un avertissement, ni un réordonnancement silencieux |
moment de la vérification | à chaque acte qui peut changer ce qui s’exécute à un point donné : enregistrement, déploiement, modification de l’agencement. Un agencement qui atteint l’exécution a donc déjà été admis |
jamais revérifiée à l'exécution | un agencement admis n’est pas revérifié quand il s’exécute |
| Toujours | Ce que c’est |
|---|---|
handlers | typés en entrée et en sortie : pas de dynamic, pas de sacs de contexte. Le handle de la plateforme est ambiant, et le contexte d’invocation — appelant, déclencheur, trace — arrive déjà analysé |
ce que voit un build du moteur | les événements que le scénario émet ensuite, car une surcharge ou un middleware s’exécute sur la plateforme, et un runtime de moteur n’est pas fait pour en héberger. C’est ce que montrent les onglets moteur des exemples de cette page |
[Rpc("resolve_price", Default = true)]
public static Price ResolvePrice(Sku sku) => Pricing.Base(sku);
[Rpc("resolve_price", When = "env == 'staging'")]
public static Price ResolvePriceStaging(Sku sku) => Pricing.WithDiscount(sku, 0.5f);
// a hook can carry a version too, gated by its own condition
[After("grant", When = "audience == 'beta'")]
public static void NotifySquadBeta(GrantResult r) => Messaging.PingBeta(r.Squad);export const resolvePrice = rpc('resolve_price', { default: true },
(sku: Sku) => Pricing.base(sku));
export const resolvePriceStaging = rpc('resolve_price', { when: "env == 'staging'" },
(sku: Sku) => Pricing.withDiscount(sku, 0.5));
// a hook can carry a version too, gated by its own condition
export const notifySquadBeta = after('grant', { when: "audience == 'beta'" },
(r: GrantResult) => Messaging.pingBeta(r.squad));@rpc("resolve_price", default=True)
def resolve_price(sku: Sku) -> Price:
return pricing.base(sku)
@rpc("resolve_price", when="env == 'staging'")
def resolve_price_staging(sku: Sku) -> Price:
return pricing.with_discount(sku, 0.5)
# a hook can carry a version too, gated by its own condition
@after("grant", when="audience == 'beta'")
def notify_squad_beta(r: GrantResult):
messaging.ping_beta(r.squad)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.
Surcharges, middleware et déclencheurs s’exécutent tous comme des fonctions cloud sur la plateforme, pas dans le moteur. Écrivez-les en C#, TypeScript ou Python — Unreal s’abonne aux événements qui en résultent. Des fonctions écrites côté moteur sont prévues : des graphes Blueprint, et du C++ (traduit vers une cible de script validée par la plateforme). Les deux sont analysés et publiés comme n’importe quelle déclaration ; l’exécution reste sur la plateforme.
Surcharges, middleware et déclencheurs s’exécutent tous comme des fonctions cloud sur la plateforme, pas dans le moteur. Écrivez-les en C#, TypeScript ou Python — Unity s’abonne aux événements qui en résultent.
La logique propre de la plateforme peut être récupérée, modifiée et redéployée. Ce sont des fonctions, comme les vôtres :
playserv functions pull matchmaking.match # the platform's implementation, as source
# edit: widen the skill window for the weekend event
playserv push # your version registers; the default stays as fallback
Ce que vous personnalisez ici, c’est de la logique : les surcharges, le middleware et les versions dont traite cette page.
Vous ne pouvez pas ajouter de champs aux entités de la plateforme
Un joueur, une salle, un enregistrement de classement et une commande sont de l’état système avec leurs propres machines à états, pas le point de départ de votre modèle de données. Vos données forment votre propre entité, déclarée dans Le schéma dans le code et rattachée à l’état de la plateforme par un prédicat : le propriétaire est ce joueur, la portée est cette salle. Elles restent ainsi les vôtres quand le modèle propre de la plateforme change.
Erreurs
- Un nom qui ne correspond à aucun handler enregistré répond
not found— un appel n’est jamais abandonné en douce parce que personne n’écoutait. - Un nom enregistré deux fois, ou un ensemble où deux implémentations revendiquent la même condition, est refusé dès la déclaration de l’ensemble : au déploiement, pas au moment de l’appel.
- Un refus de hook porte le code et le motif propres au hook : « une règle du jeu a dit non » n’arrive donc jamais sous l’apparence d’une panne de transport.
- Un hook qui échoue se comporte selon le type déclaré —
fail-openoufail-closed— et ce type a été déclaré, pas déduit de ce qui s’est passé. - Un hook ne peut pas modifier ce qui est déjà établi : ni le propriétaire, ni la cible, ni le classement ou la conversation auxquels l’appel était adressé. Il corrige des entrées et rend un verdict.
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é.
- Délai d’exécution d’un hook : au-delà, le comportement en cas d’échec que déclare son type.
- Hooks à une même position et implémentations d’une même méthode : l’enregistrement d’un élément supplémentaire est refusé.
- Profondeur d’imbrication de « un hook invoque une opération qui a des hooks » : un refus déclaré, jamais un épuisement des ressources.
- Taille du contexte passé à un hook : la troncature est interdite ; l’enregistrement est donc refusé plutôt que de laisser un handler recevoir un contexte à moitié.
Leçons et recettes : un classement dans Tanks utilise les hooks de ce module ; un tournoi quotidien passe par ce module.