(Feat) Fix Fade Loading
This commit is contained in:
+111
-17
@@ -129,6 +129,7 @@ qu'une compilation propre l'est vraiment.
|
||||
| Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` |
|
||||
| Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` |
|
||||
| Coop Steam (sessions) | `SessionSubsystem`, `SurvivalGameMode`, `SurvivalGameState`, `SurvivalPlayerState` |
|
||||
| Pseudo au-dessus des joueurs | `PlayerNameplateComponent`, `PlayerNameplateWidget` |
|
||||
|
||||
### Coop Steam — décisions figées
|
||||
|
||||
@@ -242,6 +243,99 @@ exactement le cas d'`APickupItem::OnRep_ItemData`.
|
||||
ne fait de Tick : il suffit qu'`OnRep_Slots()` rediffuse ce délégué. C'est le dividende de la
|
||||
règle « les widgets s'abonnent à des délégués », et il faut la tenir pour les systèmes à venir.
|
||||
|
||||
**Un client arrive en SPECTATEUR, et la caméra y reste un instant.** C'est la cause du « on voit
|
||||
sous la carte » à la connexion, et elle est contre-intuitive : le pawn est bien créé **et bien
|
||||
placé**, mais `GetViewTarget()` vaut encore `SpectatorPawn_0` — né au centre du monde, donc sous
|
||||
le terrain. Mesuré sur une vraie connexion :
|
||||
|
||||
```
|
||||
FadeIn démarre ViewTarget=SpectatorPawn_0 PawnLoc=(11319, -16530, 1805)
|
||||
AcknowledgePossession Pawn=BP_FpsPlayer_C_0 ViewTarget=SpectatorPawn_0
|
||||
```
|
||||
|
||||
La caméra bascule sur le corps **après** l'acquittement de la possession. La bonne condition pour
|
||||
éclaircir n'est donc pas « ai-je un pawn » mais **`GetViewTarget() == GetPawn()`** — d'où la
|
||||
boucle de réessai de `TryStartupFadeIn()`, avec `CompleteStartupFadeIn()` comme filet au bout de
|
||||
`StartupFadeTimeout` (un écran noir définitif se lit comme un plantage).
|
||||
|
||||
**Et rien ne doit être rendu avant.** Entre le chargement de la map et ce moment-là, aucun acteur
|
||||
n'existe à qui accrocher un voile — le composant de fondu vit justement sur le contrôleur absent.
|
||||
`USurvivalGameInstance` coupe donc `bDisableWorldRendering` sur le viewport, seul objet qui survive
|
||||
au changement de map sans appartenir à personne, et c'est le contrôleur qui le rend une fois son
|
||||
voile noir en place.
|
||||
|
||||
**`PreLoadMap` et surtout PAS `PostLoadMapWithWorld`** pour cette coupure. Le second est diffusé
|
||||
**après** le `BeginPlay` des acteurs : sur une map chargée localement — le menu, ou la partie chez
|
||||
l'hôte — le contrôleur rendait la main au rendu, et on la lui reprenait juste après. Écran noir
|
||||
définitif jusqu'au filet de sécurité. Chez un client ça passait *par accident*, son contrôleur
|
||||
arrivant plus tard — donc invisible si l'on ne teste que ce cas-là.
|
||||
|
||||
*Note : `ReceivedPlayer()` arrive AVANT `BeginPlay()` chez un client, et `IsLocalController()` y est
|
||||
déjà vrai — ce n'est donc pas là qu'est le problème, contrairement à ce qu'on pourrait croire.*
|
||||
|
||||
**`IsLocalController()` est indispensable sur tout ce qui touche à l'écran.** Sur la machine de
|
||||
l'hôte, chaque joueur distant a AUSSI un `AFpsPlayerController` côté serveur. Sans cette garde, le
|
||||
contrôleur d'un ami qui se connecte va poser un fondu et rallumer le rendu **du viewport de
|
||||
l'hôte**, à un instant totalement arbitraire.
|
||||
|
||||
### Étiquettes de pseudo
|
||||
|
||||
- **Le serveur ne peut PAS deviner le pseudo Steam d'un client** : l'API d'identité ne connaît que
|
||||
l'utilisateur local de chaque machine. C'est donc le client qui l'annonce
|
||||
(`Server_SetPlayerNickname`), et le serveur l'écrit sur le `PlayerState`, déjà répliqué par le
|
||||
moteur. Sans ça on lit « Player » au-dessus des quatre joueurs — c'est le nom par défaut que le
|
||||
moteur met dans l'URL de voyage.
|
||||
- **Espace ÉCRAN, pas Monde** : l'étiquette fait face à la caméra *par construction*, sans calcul
|
||||
d'orientation, et n'est jamais occultée par le décor. Mais `SWorldWidgetScreenLayer::Tick`
|
||||
n'applique **aucune échelle de distance** — le pseudo garderait une taille en pixels constante
|
||||
pendant que le personnage rétrécit, ce que l'œil lit comme une étiquette qui *grossit* en
|
||||
s'éloignant. D'où l'échelle calculée à la main en 1/d, et l'opacité en fondu sur la dernière
|
||||
bande avant `MaxDrawDistance`.
|
||||
- Le composant ne réplique **rien** : le pseudo vit déjà sur le `PlayerState`.
|
||||
|
||||
### Le pop-in au chargement — deux causes, deux remèdes
|
||||
|
||||
**Les requêtes** — textures et LOD de meshes. `IStreamingManager::StreamAllResources()` les traite
|
||||
toutes (`UpdateResourceStreaming(0, true)` n'a pas besoin de point de vue, ce qui tombe bien : le
|
||||
rendu est encore coupé) puis bloque jusqu'à ce qu'elles aboutissent. Appelé dans
|
||||
`ResumeWorldRendering()`, donc **écran noir** : c'est du temps de chargement, pas une saccade.
|
||||
Mesuré : ~80 ms, 0 requête restante.
|
||||
|
||||
**Ce qui converge par IMAGE** — Nanite, ombres virtuelles, Lumen. Aucun flush ne peut les forcer :
|
||||
il leur faut des images rendues. D'où `BeginSceneWarmup()`, qui laisse le monde rendre derrière le
|
||||
voile encore noir avant de l'éclaircir.
|
||||
|
||||
**Il compte des IMAGES, pas des secondes**, et c'est tout l'intérêt : une durée fixe donnerait
|
||||
quarante images sur une machine rapide et cinq sur une machine lente — c'est-à-dire le moins de
|
||||
chauffe à celle qui en a le plus besoin. `SetTimerForNextTick` = un tick, une image.
|
||||
`SceneWarmupMaxSeconds` n'est qu'un **plafond**, pas la mesure ; le log dit « plafond atteint »
|
||||
quand il s'applique, pour savoir lequel des deux réglages ajuster.
|
||||
|
||||
Ordre non négociable dans le menu : `SetViewTarget` **puis** `ResumeWorldRendering()`. L'inverse
|
||||
ferait chauffer le décor vu depuis l'angle par défaut, c'est-à-dire pas celui qu'on va montrer.
|
||||
|
||||
**`OnPossess` n'existe QUE sur le serveur.** Chez un client, le pawn arrive par réplication et
|
||||
`OnPossess` n'est jamais appelée — son pendant est **`AcknowledgePossession`**. Tout ce qui doit
|
||||
se produire à la prise de contrôle (création du HUD, rebranchement des widgets, abonnement à
|
||||
`OnDied`) passe donc par `SetupForPossessedPawn()`, appelée des deux côtés et **idempotente** :
|
||||
chez l'hôte, qui est serveur *et* client local, les deux chemins l'exécutent. D'où
|
||||
`AddUniqueDynamic` sur `OnDied`, sinon la mort déclencherait deux fondus et deux respawns.
|
||||
Symptôme quand on l'oublie : un joueur qui rejoint n'a aucune interface, alors que l'hôte a la
|
||||
sienne — donc invisible en solo comme en test chez soi.
|
||||
|
||||
**Une connexion qui tombe ne ramène personne nulle part.** Le moteur coupe le lien et s'arrête là.
|
||||
`USurvivalGameInstance` s'abonne donc à `GEngine->OnNetworkFailure()` et `OnTravelFailure()` pour
|
||||
détruire la session et rouvrir le menu. C'est sur le GameInstance et pas sur le PlayerController
|
||||
parce que celui-ci est justement détruit par l'échec.
|
||||
|
||||
**Quand l'hôte quitte, tout le monde quitte — et on le dit explicitement.** Le moteur ne prévient
|
||||
personne : sans RPC, les clients restent dans un monde mort, avec le **pawn fantôme de l'hôte**
|
||||
encore planté devant eux, jusqu'à un timeout qui peut ne jamais venir. `HandleQuitToMenuConfirmed`
|
||||
appelle donc `KickRemoteClientsToMainMenu()` **avant** de lancer son propre fondu, pour que les
|
||||
quatre fondus tournent ensemble. Le filet `OnNetworkFailure` reste pour ce qui n'est pas annoncé —
|
||||
hôte qui plante, câble arraché — et se tait pendant un départ voulu grâce à
|
||||
`BeginReturnToMainMenu()`, sinon il chargerait le menu en plein milieu du fondu.
|
||||
|
||||
| Système | État |
|
||||
|---|---|
|
||||
| Déplacement, crouch | ✅ gratuit via `ACharacter` / `CharacterMovement` |
|
||||
@@ -463,26 +557,26 @@ Le détail du système de craft — API, câblage éditeur, dépannage — est d
|
||||
|
||||
## État actuel et suite
|
||||
|
||||
Boucle complète : récolter → **fabriquer** → **cuire** → ranger → consommer → survivre →
|
||||
mourir → réapparaître. Autour : menu principal, menu de pause, et un écran de réglages dont **seul
|
||||
l'onglet « Jeu & Contrôles » est rempli**.
|
||||
Boucle complète **en coop jusqu'à 4 joueurs** : récolter → fabriquer → cuire → ranger →
|
||||
consommer → survivre → mourir → réapparaître. Autour : menu principal, menu de pause, écran de
|
||||
réglages, étiquettes de pseudo Steam.
|
||||
|
||||
Le craft est en place de bout en bout : `DA_RecipeBook`, onglets Inventaire / Fabrication,
|
||||
grille de recettes filtrée par poste accessible, panneau de détail avec compteurs colorés.
|
||||
Restent à faire quand le besoin viendra : l'**acteur établi** qui appellera
|
||||
`AddAvailableStation(Workbench)`, les **boutons de filtre** par poste (`SetStationFilter` existe
|
||||
déjà et `ECraftingStation::Count` sert de sentinelle « tout »), et les vraies recettes — celles
|
||||
en place sont deux `DA_Craft_Test_*` bâties sur `DA_Branch` et `DA_Rocher`.
|
||||
Le portage réseau est terminé pour tout le gameplay existant — voir *Réplication* plus haut pour
|
||||
le détail et les pièges. Les transitions de map sont propres de bout en bout : plus de flash, plus
|
||||
de pop-in, départ groupé quand l'hôte quitte.
|
||||
|
||||
Suite immédiate sur les réglages, dans l'ordre : onglet **Affichage** (le moteur fait tout,
|
||||
prévoir la confirmation à rebours de 10 s sur la résolution), onglet **Graphismes** (les dix
|
||||
curseurs de `UGameUserSettings` ; végétation et distance d'affichage comptent double avec
|
||||
SoStylized), onglet **Audio** quand des `SoundClass` existeront, et enfin le **remappage des
|
||||
touches** via `UEnhancedInputUserSettings` — le seul morceau réellement coûteux.
|
||||
Les vraies recettes restent à écrire : celles en place sont deux `DA_Craft_Test_*` bâties sur
|
||||
`DA_Branch` et `DA_Rocher`. Les **boutons de filtre** par poste ne sont pas faits non plus
|
||||
(`SetStationFilter` existe déjà, `ECraftingStation::Count` sert de sentinelle « tout »).
|
||||
|
||||
Prochaine étape gameplay recommandée : **spawner de ressources avec repousse**
|
||||
(`AResourceSpawner`, liste pondérée de DataAssets, rayon, densité, délai). Sans lui la carte se
|
||||
vide en dix minutes.
|
||||
vide en dix minutes. À écrire **serveur-autoritaire d'emblée** — c'est un acteur du monde.
|
||||
|
||||
Ensuite, par ordre de valeur : objet visible en main, établi, coffres, sauvegarde,
|
||||
cycle jour/nuit.
|
||||
Ensuite, par ordre de valeur : objet visible en main, sauvegarde, cycle jour/nuit.
|
||||
|
||||
> **Systèmes présents dans `Source/` mais pas encore décrits ici** : `MusicDirector`,
|
||||
> `MusicSubsystem`, `MusicPlaylistDataAsset`, `SettingsDisplayWidget`, `SettingsGraphicsWidget`,
|
||||
> `SettingsControlsWidget`, `SettingKeyRowWidget`, `KeyCaptureWidget`. Ils sont arrivés en dehors
|
||||
> des sessions qui ont produit ce document — à documenter quand on y touchera, plutôt que de
|
||||
> décrire de mémoire un code qu'on n'a pas lu.
|
||||
|
||||
Reference in New Issue
Block a user