Compare commits

16 Commits

Author SHA1 Message Date
Mathew 2c272c11b9 (Feat) Add Harvetable 2026-08-16 19:35:27 +02:00
Mathew d6f46f32af (Fix) Player Arms 2026-08-16 12:49:47 +02:00
Mathew cca54a5e0d (Feat) Add Animation Fps Charater 2026-08-13 22:07:08 +02:00
Mathew 176beab9fc (Feat) Add footsteps 2026-08-11 15:23:10 +02:00
Mathew e8e128457a (Fix) Vider les materiaux surcharges au changement de mesh
Passer du corps A au corps B donnait une silhouette feminine peinte avec la
peau et les abdominaux du modele masculin.

OverrideMaterials appartient au COMPOSANT et pas au mesh : SetSkeletalMeshAsset
remplace l'asset sans y toucher, donc le MID derive de MI_BodyA_a restait colle
a l'element 0. Le symptome trompe, parce que le mesh, lui, avait bien change --
seule sa peau ne suivait pas.

SetPartMesh() vide les overrides avant d'assigner, et rend la main sans rien
faire quand le mesh est deja le bon : un changement de couleur seul continue
ainsi de reutiliser les MID en place au lieu de tout reconstruire.

Vaut pour toutes les pieces, pas seulement le corps -- changer de tete ou de
coiffure donnait le meme symptome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 20:02:02 +02:00
Mathew 5970db0512 (Feat) Customisation -- replication et persistance
Lot 3 : l'apparence survit desormais au respawn, au changement de map et au
relancement du jeu, et les trois autres joueurs la voient.

Elle vit sur ASurvivalPlayerState et pas sur le pawn : c'est le JOUEUR
qu'elle decrit, pas le corps. Le pawn meurt et reapparait, le PlayerState
non -- reapparaitre avec le visage de quelqu'un d'autre n'aurait aucun sens,
alors que perdre son inventaire au sol est la regle du jeu.

SubmitCharacterAppearance() sur AFpsPlayerController est le point d'entree
UNIQUE : il persiste ET annonce au serveur, jamais l'un sans l'autre.
Persister sans annoncer laisserait le joueur seul a se voir change, annoncer
sans persister le ferait repartir au visage par defaut au lancement suivant.
C'est par la que passent les commandes console, et par la que passera
l'ecran du lot 4.

La course pawn / PlayerState est resolue des DEUX cotes. Chez un client ils
arrivent par deux chemins de replication independants et rien ne dit lequel
gagne : OnRep_CharacterAppearance monte le personnage si le pawn est deja la,
AFpsPlayer::OnRep_PlayerState va chercher l'apparence si c'est l'inverse. Les
deux appellent la meme fonction idempotente. Chez l'hote aucun OnRep ne se
declenche, d'ou l'appel local dans SetCharacterAppearance -- sans lui, l'hote
serait le seul a ne pas voir son propre personnage.

Rien n'est valide cote PlayerState, deliberement : borner un index demande le
catalogue, que seul UCharacterAppearanceComponent connait, et celui-ci passe
deja tout ce qu'il monte par Catalog->Sanitize() sur chaque machine.

Piege de test note dans CLAUDE.md : en PIE a deux joueurs, les deux fenetres
lisent le meme .ini, donc les deux personnages demarrent identiques.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 19:45:01 +02:00
Mathew 368f27a417 (Update) Rangement 2026-08-08 19:00:47 +02:00
Mathew 5ba5fbfebf (Feat) Customisation de personnage -- donnees et montage
Lot 2 du chantier : le corps du lot precedent devient assemblable. Tete,
coiffure, sourcils, barbe et moustache sont montes en leader pose sur le
mesh du Character, et les couleurs sont posees sur des MID.

FCharacterAppearance tient en 11 octets : des INDEX et jamais des chemins
d'assets. Un chemin couterait une chaine a chaque replication, alors qu'un
index tient dans un octet et se VALIDE contre le catalogue -- un client ne
peut donc pas demander un asset arbitraire.

Le corps se monte depuis une LISTE de meshes meme si elle n'a qu'une case.
Le pack livre exprès SKM_BodyA_torso, _arms, _legs_01/02/03 pour qu'une
armure puisse eteindre les segments qu'elle recouvre : le jour venu la case
0 devient SKM_BodyA_empty et les huit segments suivent, sans que ce code
change. Un pointeur unique imposerait de tout reprendre au premier plastron.

Trois pieges rencontres, aucun visible depuis le code :

- Les UFUNCTION(Exec) d'un UActorComponent ne sont JAMAIS appelees. La
  chaine de routage (UPlayer::Exec) interroge le monde, le PlayerInput, le
  controller, le pawn, le HUD, le GameMode, le CheatManager, le GameState et
  le camera manager -- jamais les composants, AActor ne surchargeant pas
  ProcessConsoleExec. Et rien ne proteste. D'ou EmberPart / EmberColor /
  EmberAppearanceDump posees sur AFpsPlayer, qui delegue.

- L'ordre des slots de materiau varie d'un mesh a l'autre : sur
  SKM_HeadA_01 l'Element 0 est celui des YEUX et le 1 celui de la peau. Les
  six tetes venant de deux packs, on cherche le slot par son NOM et on ne
  pose rien si aucun ne correspond -- ecraser le slot de la peau repeint le
  visage entier de la couleur des yeux.

- Un nom de parametre materiau se lit dans le Material INSTANCE, pas dans
  les chaines du master : M_Head_Base contient « Skin Base color » mais le
  parametre expose s'appelle « Skin ». D'ou des UPROPERTY de catalogue
  plutot que des constantes, pour corriger sans recompiler.

Body B reste a remplir dans DA_CharacterParts, avec son ABP_PlayerBody_B et
sans barbes ni moustaches -- le pack n'en livre pas, et une liste vide se
comporte deja comme NoPart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 18:48:37 +02:00
Mathew 6d22ae561e (Chore) Ranger les packs tiers dans Content/External
SoStylized, CustomizationPack01, StylizedCharacters, StylizedFood_JC,
Vefects et Wooden_Chest quittent la racine de Content pour Content/External.

Le but est de rendre visible d'un coup d'oeil ce qui nous appartient et ce
qui vient d'un pack : un asset de External ne se modifie pas, une mise a
jour du pack l'ecraserait. C'est ce qui a decide du deplacement des
animations retargetees vers Game/Animation/ThirdPerson au commit precedent
-- elles vivaient dans un dossier Demo_Levels qu'on voudra supprimer.

Deplacement pur : LFS retrouve les memes hashes, rien n'est reuploade.
Les .uasset modifies ne le sont que par la mise a jour de leurs references.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:54:31 +02:00
Mathew 0ffa79efc3 (Feat) Corps 3e personne anime
Le pawn n'avait aucun mesh 3e personne : en coop, les autres joueurs
voyaient un pseudo flotter au-dessus du vide. C'est aussi le prerequis de
la customisation de personnage, qu'on ne peut pas empiler sur un corps
inexistant.

UPlayerBodyAnimInstance publie l'etat du pawn pour ABP_PlayerBody_A, sur
le meme partage des roles que UFpsArmsAnimInstance : le C++ lit et
republie, le Blueprint choisit l'animation. Rien n'est replique -- tout
derive d'un etat que le moteur ou le projet synchronisent deja (Velocity,
bIsCrouched, bIsSprinting, RemoteViewPitch).

VerticalSpeed distingue le SAUT de la CHUTE : bIsFalling vaut vrai dans
les deux cas, le moteur appelant "falling" tout ce qui n'est pas au sol.
Sans son signe, sortir d'une falaise jouerait l'animation de detente.

Le Cast du pawn est relance dans NativeUpdateAnimation : chez un client
le mesh arrive par replication et peut s'animer avant que le pawn soit
la, ce qui figerait le corps de l'ami en pose de reference pour toute la
partie -- invisible en solo comme chez l'hote.

Dev_Scene rejoint MapsToCook : GameLevel peut pointer sur elle le temps
d'un test, et une reference soft n'est pas suivie par le cooker.

Limites connues : pas d'animation accroupie (absente du pack) et le
BlendSpace s'arrete a 500 alors que le sprint est a 650.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:53:54 +02:00
Mathew c69e39e3c1 Feat Add Charater 2026-08-07 10:02:56 +02:00
Mathew 72a34f6e40 (Feat) Add Animation 2026-08-06 13:27:21 +02:00
Mathew fca1b271f1 (Feat) Add Slasp Screen 2026-08-04 09:49:07 +02:00
Mathew 1456ed91c7 (Feat) Fix Fade Loading 2026-08-03 21:10:45 +02:00
Mathew 7595df6862 (Feat) Player name tag 2026-08-03 11:44:01 +02:00
Mathew a4cc59a411 (Feat) Add mulitplayer 2026-08-03 10:30:47 +02:00
11326 changed files with 45124 additions and 13744 deletions
+629 -29
View File
@@ -1,6 +1,6 @@
# Survival_projet — Unreal Engine 5.8 # Survival_projet — Unreal Engine 5.8
Jeu de survie solo à la première personne, gameplay écrit en **C++**, Blueprints réservés au Jeu de survie coop en ligne via steam p2p à la première personne, gameplay écrit en **C++**, Blueprints réservés au
câblage d'assets. Inspirations assumées : Raft, Aloft, Valheim, esthétique solarpunk. câblage d'assets. Inspirations assumées : Raft, Aloft, Valheim, esthétique solarpunk.
Assets d'environnement : pack **SoStylized** (sa végétation n'est pas à l'échelle humaine). Assets d'environnement : pack **SoStylized** (sa végétation n'est pas à l'échelle humaine).
@@ -51,10 +51,13 @@ règles de mort), demander avant de coder plutôt que de deviner.
## Compiler ## Compiler
```powershell ```powershell
& "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\Build.bat" Survival_projetEditor Win64 Development -Project="d:\Projet\Perso\Unreal\Survival_projet\Survival_projet.uproject" -WaitMutex & "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\Build.bat" EmberWildEditor Win64 Development -Project="d:\Projet\Perso\Unreal\Survival_projet\EmberWild.uproject" -WaitMutex
``` ```
- Un DLL nommé `UnrealEditor-Survival_projet-000N.dll` signifie que **l'éditeur était ouvert** : *(Le module et le `.uproject` s'appellent désormais **EmberWild** ; seul le dossier sur le disque
est resté `Survival_projet`.)*
- Un DLL nommé `UnrealEditor-EmberWild-000N.dll` signifie que **l'éditeur était ouvert** :
c'est un hot reload. Prévenir l'auteur de fermer et relancer l'éditeur. c'est un hot reload. Prévenir l'auteur de fermer et relancer l'éditeur.
- `-Rebuild` échoue tant que l'éditeur tourne (DLL verrouillés). - `-Rebuild` échoue tant que l'éditeur tourne (DLL verrouillés).
- **Ne jamais réécrire en masse les timestamps** des fichiers source pour forcer un build. - **Ne jamais réécrire en masse les timestamps** des fichiers source pour forcer un build.
@@ -100,6 +103,9 @@ l'oublie, en Standalone seulement : la caméra ne répond qu'au clic droit maint
**Enhanced Input en pause** : un jeu en pause ne fait qu'un tick d'input réduit, où seules les **Enhanced Input en pause** : un jeu en pause ne fait qu'un tick d'input réduit, où seules les
actions cochées **« Trigger When Paused »** sont évaluées. C'est une case sur l'asset `IA_`, actions cochées **« Trigger When Paused »** sont évaluées. C'est une case sur l'asset `IA_`,
pas du C++ — et son corollaire est qu'aucune action du pawn ne se déclenche en pause. pas du C++ — et son corollaire est qu'aucune action du pawn ne se déclenche en pause.
*Le projet ne met plus jamais le jeu en pause (voir le menu de pause plus bas), donc la case sur
`IA_Pause` ne sert plus à rien. La laisser cochée est sans effet ; la note reste ici parce que le
piège ressortira au premier `SetPause` réintroduit.*
**Le `namespace` anonyme n'isole rien.** Le build unity concatène les `.cpp` du module : deux **Le `namespace` anonyme n'isole rien.** Le build unity concatène les `.cpp` du module : deux
constantes de même nom dans deux fichiers différents deviennent une redéfinition (C2374). Les constantes de même nom dans deux fichiers différents deviennent une redéfinition (C2374). Les
@@ -112,6 +118,7 @@ qu'une compilation propre l'est vraiment.
| Système | Fichiers | | Système | Fichiers |
|---|---| |---|---|
| Personnage, caméra, game feel | `FpsPlayer.h/.cpp` | | Personnage, caméra, game feel | `FpsPlayer.h/.cpp` |
| Balancement de tête procédural | `HeadBobComponent` (profils par état, additionné dans `ApplyCameraTransform`) |
| HUD, input d'interface, fondus, respawn | `FpsPlayerController.h/.cpp` | | HUD, input d'interface, fondus, respawn | `FpsPlayerController.h/.cpp` |
| Interaction (trace + interface) | `Interactable.h`, `InteractionComponent`, `InteractionPromptWidget` | | Interaction (trace + interface) | `Interactable.h`, `InteractionComponent`, `InteractionPromptWidget` |
| Objets | `ItemDataAsset`, `PickupItem` | | Objets | `ItemDataAsset`, `PickupItem` |
@@ -120,11 +127,429 @@ qu'une compilation propre l'est vraiment.
| Menu principal | `MainMenuGameMode`, `MainMenuPlayerController`, `MainMenuWidget`, `MenuCameraSpot` | | Menu principal | `MainMenuGameMode`, `MainMenuPlayerController`, `MainMenuWidget`, `MenuCameraSpot` |
| Fabrication | `CraftingRecipeDataAsset`, `CraftingRecipeBook`, `CraftingComponent`, `CraftingWidget`, `CraftingRecipeSlotWidget`, `CraftingIngredientRowWidget` | | Fabrication | `CraftingRecipeDataAsset`, `CraftingRecipeBook`, `CraftingComponent`, `CraftingWidget`, `CraftingRecipeSlotWidget`, `CraftingIngredientRowWidget` |
| Cuisson | `CookingStation` + bloc *Cooking* / *Fuel* de `ItemDataAsset`**doc complète : [Docs/CookingSystem.md](../Docs/CookingSystem.md)** | | Cuisson | `CookingStation` + bloc *Cooking* / *Fuel* de `ItemDataAsset`**doc complète : [Docs/CookingSystem.md](../Docs/CookingSystem.md)** |
| Coffres | `StorageContainer`, `StorageScreenWidget` + `UInventoryComponent::TransferSlot` / `TransferAllTo` | | Coffres | `StorageContainer` + le panneau de coffre d'`InventoryScreenWidget` et `UInventoryComponent::TransferSlot` / `TransferAllTo`**doc complète : [Docs/Storage.md](../Docs/Storage.md)** |
| Écran à onglets (inventaire / craft) | `InventoryScreenWidget` | | Écran à onglets (inventaire / craft) | `InventoryScreenWidget` |
| Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` | | Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` |
| Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` | | Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` |
| Musique | `MusicDirector`, `MusicSubsystem`, `MusicPlaylistDataAsset` |
| Audio (volumes) | `SurvivalAudioSettings`, `SettingsAudioWidget` + `USurvivalUserSettings::ApplyAudioSettings` |
| Bruits de pas | `FootstepComponent`, `FootstepSoundsDataAsset` + les `PhysicalSurfaces` de `DefaultEngine.ini` |
| Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` | | Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` |
| Coop Steam (sessions) | `SessionSubsystem`, `SurvivalGameMode`, `SurvivalGameState`, `SurvivalPlayerState` |
| Pseudo au-dessus des joueurs | `PlayerNameplateComponent`, `PlayerNameplateWidget` |
| Corps 3e personne | `PlayerBodyAnimInstance` + `ABP_PlayerBody_A`, le `Mesh` de `BP_FpsPlayer` |
### Le corps vu par les autres — pièges du personnage modulaire
Le pack **Stylized Characters** (+ *CustomizationPack01*, tous deux dans `Content/External/`)
livre un personnage **en morceaux** : `SKM_BodyA` s'arrête au cou, la tête, les sourcils, les
cheveux, la barbe et la moustache sont des meshes séparés.
- **Un seul mesh porte l'animation, les autres recopient sa pose.** C'est le
**Leader Pose Component** : le corps est le leader avec son AnimBP, tous les autres sont des
`USkeletalMeshComponent` enfants **sans animation à eux**. La correspondance se fait **par nom
d'os**, donc elle marche même entre squelettes distincts — les têtes du pack de customisation
sont sur `SK_BodyA_empty` et se posent sans problème sur un corps `SK_BodyA`.
- **`LeaderPoseComponent` est `BlueprintReadOnly`, PAS `EditAnywhere`.** Elle n'apparaît donc
**jamais** dans le Details panel, et on la cherche longtemps : la section « Leader Pose
Component » qu'on y voit ne contient que des cases de bornes et de LOD. Elle ne s'assigne que
par `SetLeaderPoseComponent()` — Construction Script en Blueprint, ou C++.
- **La physique de cheveux est perdue en leader pose.** Coiffures, barbes et moustaches ont leur
propre squelette et leur physics asset, mais l'AnimBP post-process d'un *follower* ne tourne
pas. La démo du pack accepte déjà ce compromis.
- **Le squelette est celui du Mannequin UE5** (`ik_foot_root`, `ik_hand_gun`, `spine_01..05`,
`index_metacarpal_l` — les métacarpiens n'existaient pas sur le squelette UE4). Conséquence
précieuse : **toute animation vendue « UE5 Mannequin compatible » se branche sans retarget.**
C'est par là qu'arriveront l'accroupissement et les gestes d'outil, absents du pack.
- **`Approx Size` d'un Skeletal Mesh n'est pas sa hauteur visible** : les bounds incluent une
marge et celles du Physics Asset. `SKM_BodyA` affiche 184 alors que le personnage entier,
tête comprise, mesure à peu près autant. Pour juger une taille, passer le viewport en vue
**Front** et se servir de la ligne verte (`Z = 0`, centre de la capsule) : le bas est à
`-HalfHeight`, le haut à `+HalfHeight`. Le personnage fait ~184 cm pour une capsule de 176,
et on **ne scale pas** — la capsule porte la collision et le game feel déjà calibré.
**Côté animation**, `UPlayerBodyAnimInstance` suit le partage des rôles de
`UFpsArmsAnimInstance` : le C++ lit l'état du pawn et le republie, le Blueprint choisit
l'animation. Deux points valent d'être retenus :
- **Rien n'y est répliqué, et c'est voulu.** Le corps est vu par tout le monde, mais chaque
variable **dérive** d'un état déjà synchronisé : `Velocity` par le CharacterMovement,
`bIsCrouched` par `ACharacter`, `bIsSprinting` par `AFpsPlayer`, et le tangage du regard par
`RemoteViewPitch` que `APawn` réplique tout seul — d'où `GetBaseAimRotation()` et surtout pas
`GetControlRotation()`, qui n'existe que sur la machine propriétaire.
- **`bIsFalling` ne distingue pas un saut d'une chute** : le moteur appelle « falling » tout ce
qui n'est pas au sol. C'est le **signe de `VerticalSpeed`** qui tranche, sans quoi sortir d'un
rebord jouerait l'animation de détente. Et la condition `Ground → Jump` a besoin du `AND` avec
`bIsFalling` : gravir une pente donne aussi une vitesse verticale positive.
- **Le `Cast` du pawn est relancé à chaque frame tant qu'il est nul.** Chez un client le mesh
arrive par réplication et peut s'animer **avant** le pawn : un `Cast` fait une seule fois dans
`NativeInitializeAnimation` laisserait le corps de l'ami figé en pose de référence pour toute
la partie. Invisible en solo comme chez l'hôte.
Enfin, le BlendSpace du pack posait `Walk_InPlace` à la vitesse 0 — un personnage qui piétine
sur place à l'arrêt. On y a mis l'idle à la place, plutôt qu'un état `Idle` séparé : un état,
c'est un **seuil** qui claque au démarrage et au freinage, là où le BlendSpace interpole en
continu. Ses points restent calés sur les **vitesses réelles mesurées des animations**
(225.6 et 500), ce qui est précisément ce qui évite le glissement de pieds — on ne les déplace
pas. Il s'arrête en revanche à 500 alors que le sprint est à 650 : un second échantillon de
course posé à 650 avec un **Rate Scale** de 1.3 le couvrirait.
### Coop Steam — décisions figées
- **Cible : 4 joueurs, serveur d'écoute, P2P par le relais Valve.** Plugins `OnlineSubsystemSteam`
et `SteamSockets`, AppID **4282850** (Emberwild). Le C++ ne parle **jamais** de Steam : tout passe
par `IOnlineSubsystem`, dont l'implémentation est choisie dans `DefaultEngine.ini`. Corollaire
précieux : sans Steam le moteur retombe sur `OnlineSubsystemNULL` et tout le code bascule en LAN
(`USessionSubsystem::IsLanMode`), ce qui rend le test à deux instances possible sur une machine.
- **L'OSS Steam est désactivé de force dans l'éditeur** (`IsEnabled()` teste `IsRunningGame()`).
Steam ne s'initialise donc **jamais en PIE** — c'est voulu par Epic, pas un bug de config. Pour
le tester : **Play > Standalone Game**, ou un build packagé.
- **Toute partie est une partie en ligne.** Un seul bouton *Jouer*, qui crée la session puis ouvre
la map en `?listen`. Pas de « Héberger » vs « Rejoindre » : le joueur devrait choisir avant de
savoir si un ami le rejoindra, et devrait relancer sa partie pour changer d'avis.
- **On rejoint UNIQUEMENT par invitation Steam**, depuis le bouton *Inviter* du menu de pause.
L'acceptation est écoutée par `USessionSubsystem` dès son `Initialize` et jamais coupée : une
invitation tombe à n'importe quel instant, y compris en pleine partie, et le sous-système est le
seul objet dont on soit certain qu'il vive à ce moment-là. La commande console **`EmberJoin`**
existe pour tester à deux instances, sans aucun bouton pour y mener.
- **Un échec de session ne bloque pas le lancement** : la map s'ouvre quand même en serveur
d'écoute, seul *Inviter* est grisé. Un joueur qui clique Jouer veut jouer ; le renvoyer à son
menu pour une panne de Steam serait une punition qui n'est pas la sienne.
- **`bUseLobbiesIfAvailable = true` est LE drapeau à ne pas oublier** : Steam ne route le P2P que
par un lobby. Sans lui la session se crée, apparaît même dans une recherche, mais aucune
connexion n'aboutit hors du LAN.
- **`AGameMode` n'existe que sur le serveur.** `GetGameMode()` rend `nullptr` sur un client,
toujours. Tout appel écrit côté client doit devenir un RPC serveur ou une lecture du GameState —
c'est notamment le cas du respawn dans `AFpsPlayerController`, **encore à faire**.
- **Autorité : le serveur, pour tout ce qui touche au monde.** Pickups, coffres, feux, établis et
inventaires sont modifiés **uniquement** côté serveur ; les clients demandent par RPC et
reçoivent le résultat par réplication. Coop entre amis, donc aucune anti-triche : les
validations serveur sont des garde-fous contre la désynchronisation, pas contre le joueur.
- **`Slots` de `UInventoryComponent` est répliqué à TOUT LE MONDE**, pas au seul propriétaire.
Ce n'est pas un raccourci : les conditions de `GetLifetimeReplicatedProps` sont mises en cache
**par classe**, pas par instance. Un `COND_OwnerOnly` frapperait donc aussi le composant d'un
coffre — or un coffre n'appartient à aucune connexion, et sa grille n'arriverait chez personne.
Le même composant sert le sac et le contenant, et les coffres sont à accès simultané. Coût réel :
4 × 30 slots de trois champs, envoyés seulement quand ils changent.
- **Coffres : accès simultané**, pas de verrou. `TransferSlot` diffuse déjà `OnInventoryChanged`
des deux côtés, l'ossature est là.
### Tester la coop — ça ne se teste QUE en build Steam
**PIE ne teste pas la coop de ce projet.** L'OSS Steam y est désactivé de force par Epic
(`IsEnabled()` teste `IsRunningGame()`), donc pas de session, pas d'invitation, pas de relais.
PIE en *Play As Listen Server* reste utile pour la réplication pure (voir l'autre joueur bouger,
un pickup disparaître), mais **tout ce qui touche à la session se teste en build packagé**,
distribué par le dépôt Steam. C'est la procédure de référence.
**1. Packager** — éditeur fermé :
```powershell
& "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\RunUAT.bat" BuildCookRun `
-project="D:\Projet\Perso\Unreal\Survival_projet\EmberWild.uproject" `
-noP4 -platform=Win64 -clientconfig=Development `
-build -cook -stage -pak -compressed -prereqs `
-archive -archivedirectory="D:\Projet\Perso\Unreal\Survival_projet\Build\Package" `
-utf8output -nocompileeditor
```
`Development` et pas `Shipping` : on garde la console (`~`), les logs dans `Saved/Logs` et la
commande `EmberJoin`. Sans eux, un échec chez le testeur ne dit rien. Premier cook ≈ 18 min,
les suivants sont bien plus courts (DDC chaud).
**2. Copier et uploader — c'est le MÊME script, il n'y a rien à copier à la main.**
`C:\sdk\tools\ContentBuilder\upload_emberwild.bat` (App **4282850**, dépôt **4282851**) fait les
trois étapes : `rmdir /s /q content` puis `mkdir`, robocopy depuis `Build\Package\Windows`
en `/XF steam_appid.txt`, puis `steamcmd`. Le `.vdf` n'a **pas** de `SetLive`, donc le build monte
sans devenir live ; `upload_emberwild.bat live` utilise l'autre `.vdf` et publie.
Deux choses à savoir sur ce script :
- **Il repart d'un `content\` vide à chaque fois, et c'est indispensable** : le staging d'UAT ne
supprime rien, donc un fichier disparu du jeu — ou renommé, comme au passage
`Survival_projet``EmberWild` — resterait dans le dépôt indéfiniment et les testeurs
traîneraient des assets fantômes.
- **Il garde en dur le nom de l'exe** (`EmberWild.exe`) dans ses trois garde-fous : présence du
build, date de l'exe, vérification d'après-copie. Un renommage de projet le casse avec un
« aucun build trouve » trompeur, alors que le build est bien là.
**3. Publier sur la branche `develop`**, jamais sur `default`. Et vérifier que *Launch Options*
pointe sur `EmberWild.exe` — puis **publier les changements**, sinon rien n'est visible
côté client.
**4. Tester à deux** : les deux lancent le jeu **depuis Steam**, l'hôte clique *Jouer*, puis
Échap → *Inviter des amis*. Les logs à lire sont dans `EmberWild/Saved/Logs/` de chaque
côté — `ShowInviteUI` chez l'hôte, `invitation acceptee` puis `voyage vers [...]` chez l'invité.
**Deux réglages qui cassent tout si on les oublie** : la *List of Maps to Include* dans les
Packaging Settings doit contenir `MenuScene` **et** `GameScene` — cette dernière n'est référencée
que par un `TSoftObjectPtr`, et une référence soft n'est pas suivie par le cooker ; sans elle le
build démarre sur le menu et *Jouer* ne charge rien. Et sans liste du tout, le cook embarque les
~25 maps de démo SoStylized.
**Se tester seul** reste possible malgré l'invitation obligatoire : deux instances lancées avec
`-nosteam` retombent sur `OnlineSubsystemNULL`, donc en LAN, et la commande console `EmberJoin`
fait recherche + connexion sur la seconde.
### Réplication — la règle, et où on en est
**La règle : rien de ce qui touche au monde ne s'exécute sur un client.** Un client trace, vise,
affiche, glisse — mais dès qu'il faut *changer* quelque chose, il envoie un RPC serveur et attend
la réplication. Le symptôme quand on l'oublie est toujours le même et il est fourbe : ça marche
parfaitement en solo et chez l'hôte, et l'objet réapparaît chez le client à la réplication suivante.
**Le point de passage unique : `IInteractable::Execute_Interact`.** Les quatre acteurs du monde
(`PickupItem`, `CookingStation`, `CraftingStation`, `StorageContainer`) passent tous par là.
`UInteractionComponent::TryInteract()` route donc vers `Server_Interact` quand on n'a pas
l'autorité — une seule RPC rend les quatre autoritaires. Ne jamais appeler `Execute_Interact`
ailleurs sans se poser la question de qui exécute.
**`SetIsReplicatedByDefault(true)` est obligatoire sur un composant qui porte un RPC serveur**,
même s'il ne réplique aucune propriété : sans lui le moteur **jette silencieusement** l'appel.
**`OnConstruction` s'exécute avant l'arrivée des propriétés répliquées.** Un acteur dont le
visuel dépend d'une `UPROPERTY` répliquée doit la déclarer `ReplicatedUsing` et reposer son
visuel dans l'`OnRep` — sinon l'objet est correct chez l'hôte et nu chez les clients. C'est
exactement le cas d'`APickupItem::OnRep_ItemData`.
**L'UI se recâble toute seule.** Tous les widgets sont abonnés à `OnInventoryChanged` et aucun
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` |
| Sprint (`bIsSprinting` + RPC vitesse) | ✅ fait |
| Interaction (RPC serveur + validation de distance) | ✅ fait |
| `PickupItem` (ramassage, visuel, quantité) | ✅ fait |
| `InventoryComponent` — lecture (`Slots` répliqué) | ✅ fait |
| `InventoryComponent` — écriture (transferts, consommation) | ✅ fait |
| Jeter au sol (`DropFromInventory`, `DropAllItems`) | ✅ fait |
| `SurvivalStatsComponent` (simulation serveur, mort par `OnRep_Dead`) | ✅ fait |
| Respawn (`Server_RequestRespawn`) | ✅ fait |
| `StorageContainer` (contenu, couvercle) | ✅ fait |
| `CraftingComponent` (`Server_Craft`) | ✅ fait |
| `CookingStation` (grill, bûches, flamme) | ✅ fait |
| Corps 3e personne animé (rien à répliquer, tout dérivé) | ✅ fait |
**Un acteur du monde qui ouvre une interface doit RENVOYER l'ordre au client.**
`Interact_Implementation` s'exécute sur le serveur : `PlayerController->OpenStorage(...)` y créerait
un widget dans le vide et l'écran du joueur ne s'ouvrirait jamais. D'où `Client_OpenStorage` et
`Client_OpenCraftingAtStation`. C'est le piège le plus contre-intuitif du portage, parce que le
code *semble* correct et fonctionne parfaitement chez l'hôte.
**L'établi n'a rien à répliquer.** `ACraftingStation` est un meuble : un mesh, un prompt, aucun
état. Ce qui devait changer, c'est le sens de l'appel (ci-dessus) et `Craft()`, qui touche
l'inventaire autoritaire. Le poste accessible est tenu **des deux côtés** — le client en a besoin
pour filtrer sa grille de recettes, le serveur pour valider `Server_Craft`. N'en tenir qu'un
donnerait soit un onglet vide devant un établi, soit un établi utilisable depuis la forêt.
**La cuisson ne se compte que sur le serveur.** Les `FTimerHandle` ne sont pas des `UPROPERTY` et
ne partent jamais sur le réseau : les clients ne reçoivent que le *résultat* — l'objet devenu
cuit, la bûche disparue, la flamme allumée. Quatre clients qui compteraient chacun leur cuisson
divergeraient, et le steak serait brûlé chez l'un et cru chez l'autre.
**Le sprint est le piège qui ne se voit pas chez l'hôte.** `MaxWalkSpeed` posé en local
seulement laisse le serveur croire le client à `WalkSpeed` : il corrige alors sa position à
chaque pas, et c'est le rubber-banding. D'où `Server_SetSprinting`, appelée **après** avoir
appliqué en local — l'inverse donnerait un sprint qui démarre en retard.
**Pas de prédiction sur l'inventaire.** Le client envoie sa RPC et attend `OnRep_Slots`.
Quelques dizaines de millisecondes sur un relais Steam, invisibles pour un glisser-déposer,
alors qu'une prédiction fausse donnerait une pile qui saute.
**`FindNetProxy` est le passage obligé des transferts vers un coffre.** Un `UInventoryComponent`
de conteneur n'appartient à aucune connexion : une RPC émise depuis lui est **silencieusement
jetée**. On route donc toujours par l'inventaire du pawn local, et la source comme la destination
sont passées en paramètres — jamais déduites du récepteur, sinon « Tout prendre » viderait le sac
du joueur dans lui-même.
### Audio — le mixage passe par les SoundClass, et par rien d'autre
- **Une hiérarchie de `SoundClass`, un `SoundMix`, quatre curseurs.** `SC_Master` est la
racine ; `SC_Music`, `SC_SFX` et `SC_UI` en descendent. Le moteur multiplie déjà le volume
du parent dans celui de l'enfant, donc le volume général est gratuit — pas de produit à
calculer en C++.
- **`SM_Settings` est déclaré comme *Default Base Sound Mix*** dans Project Settings > Audio,
et le C++ le relit à cet endroit au lieu d'en garder une référence à lui. Deux raisons : un
mix de base est **rejoué à plein régime en permanence** (un mix poussé à la main s'éteindrait
au bout de sa durée), et le moteur le **repose à chaque chargement de map** — donc les
volumes traversent les transitions sans une ligne de code. Une seconde référence côté projet
n'apporterait qu'un risque de divergence.
- **`bApplyToChildren = true` sur le maître, `false` sur les trois autres.** C'est le piège de
ce système : le moteur propage les propriétés parent → enfant (`ParseSoundClasses`) **avant**
d'appliquer les ajusteurs du mix (`ApplyClassAdjusters`). Un ajusteur posé sur `SC_Master`
arrive donc trop tard pour redescendre tout seul. Sans ce drapeau, le volume général n'agit
que sur les sons rangés *directement* dans `SC_Master` — c'est-à-dire quasiment rien, et le
symptôme se lit comme « le curseur maître ne fait rien ».
- **Aucun fondu sur `SetSoundMixClassOverride``FadeInTime` vaut 0.** Ce n'est pas une
simplification : toute durée non nulle rend le curseur inutilisable. Reposer un override
avant la fin du précédent relance l'interpolation (`FDynamicParameter::Set` remet
`CurrTimeSec` à zéro) **sans** appeler `Update(DeltaTime)` la même frame. Un curseur qu'on
glisse repose l'override à chaque image, donc la rampe repart indéfiniment de zéro et le
volume ne rattrape son retard qu'une fois le geste **arrêté**. Le mixeur lisse déjà le gain
de chaque source dans son buffer de rendu : à zéro il n'y a pas de clic pour autant.
- **`SC_SFX` est la *Default Sound Class* du projet.** Un son qui n'annonce pas sa classe
atterrit donc dans les effets plutôt que nulle part : oublier de taguer un bruitage le laisse
quand même sous le curseur Effets. Seuls les sons d'interface se taguent à la main.
- **La musique impose sa classe depuis le code** (`SoundClassOverride` posé par
`UMusicSubsystem`), pas depuis les assets : ajouter une piste à une playlist ne doit pas
demander de penser à la taguer. Corollaire, `UMusicSubsystem::SetMusicVolume` **n'est pas** le
réglage du joueur — deux chemins vers le même volume finiraient par se multiplier.
- **Les volumes sont des pourcentages, élevés au carré avant d'être envoyés.** L'oreille répond
au logarithme de l'amplitude : un curseur envoyé tel quel donne un réglage où tout se joue
dans les vingt derniers pourcents. À 50 %, l'amplitude vaut 0,25, soit environ la moitié du
volume *perçu*.
- **`ApplyAudioSettings()` est séparée d'`ApplySurvivalSettings()`** : cinq commandes vers le
thread audio à chaque cran du champ de vision seraient du bruit pur. `ApplyNonResolutionSettings`
appelle les deux, donc le démarrage n'en oublie aucune.
- **Le jeu est muet en arrière-plan par défaut, et c'est le moteur qui le décide**
(`FApp::UnfocusedVolumeMultiplier`, dont la valeur d'usine est 0). Le réglage ne fait que
l'exposer ; il ne passe pas par le mix, puisqu'il baisse l'application entière bien en amont.
### Bruits de pas — la distance, pas les AnimNotify
- **Le pas est déclenché par la DISTANCE parcourue**, jamais par un notify d'animation.
`UFootstepComponent` accumule le déplacement horizontal et sonne tous les `StrideLength` cm.
Trois raisons : le corps 3e personne du joueur local est en `bOwnerNoSee`, les notifies posés
dans un **BlendSpace** se déclenchent selon l'échantillon dominant et sautent ou doublent un pas
au passage marche → course, et les bras FPS sont sur `BasePose_Skeleton`, qui n'a rien à voir
avec `SK_BodyA` — aucun notify ne pourrait servir aux deux.
- **`StrideLength` se calcule, il ne se devine pas** : `Vitesse / (2 × StrideFrequency)` du profil
correspondant de `UHeadBobComponent`, le facteur 2 parce qu'une foulée compte deux pas. D'où
143 / 182 / 210 cm pour accroupi / marche / sprint. Changer `WalkSpeed` ou `SprintSpeed` sans
retoucher ces valeurs désynchronise le son de la tête, et **ça s'entend avant de se voir**.
- **Rien n'est répliqué**, exactement comme `UPlayerBodyAnimInstance` : chaque machine calcule les
pas de tous les pawns qu'elle connaît, à partir de la vitesse et du mode de déplacement déjà
synchronisés. Un multicast par pas coûterait trois messages par seconde et par joueur pour un
résultat identique.
- **Saut et réception passent par `MovementModeChangedDelegate`, surtout pas par
`ACharacter::Landed` / `OnJumped`.** `Landed` vient de `ProcessLanded`, qui ne tourne que là où
le mouvement est réellement simulé, et `OnJumped` que là où l'input est lu : sur le pawn d'un ami
vu depuis notre machine, ni l'un ni l'autre n'existe. Le **mode de déplacement**, lui, est
répliqué, donc son délégué est diffusé partout. Et c'est le **signe de `Velocity.Z`** qui
distingue un saut d'une sortie de rebord, comme dans l'AnimBP du corps.
- **La vitesse de chute s'échantillonne PENDANT la chute** : au moment où le mode repasse à
`Walking`, le moteur a déjà remis `Velocity.Z` à zéro. Le cas où le tick à 20 Hz n'a pas eu le
temps de tourner est précisément celui d'une chute négligeable, qu'on veut de toute façon muette.
- **`bTraceComplex = true` n'est pas négociable.** Un `PhysicalMaterial` posé sur un **matériau**
n'est rendu que par un trace per-poly ; le trace simple remonte celui du `BodySetup` du mesh,
presque toujours vide. Symptôme d'un décochage : tout le décor sonne sur `DefaultSurface`, ce
qu'on lit comme « la table n'est pas remplie ». Coût réel : deux à trois traces par seconde et
par personnage, jamais un par frame — le tick tourne à **20 Hz** et ne fait qu'une soustraction
de vecteurs, le trace n'a lieu qu'au moment du pas.
- **Sur un Material INSTANCE, le champ *Phys Material* a une case à cocher devant lui, et elle
compte.** `UMaterialInstance::GetPhysicalMaterial()` teste `bOverridePhysMaterial` **avant** de
regarder le pointeur : décochée, l'instance hérite de son parent quoi qu'on ait glissé dans le
champ ; cochée mais vide, elle retombe sur `DefaultPhysMaterial` et **coupe** l'héritage.
Corollaire précieux pour SoStylized, dont les 398 instances descendent d'une poignée de parents :
taguer `MI_Rock`, `MI_Grass`, `MI_Snow`… suffit à couvrir toute leur descendance, et l'on ne
redescend dans un enfant que pour l'exception.
- **L'ordre des `PhysicalSurfaces` dans `DefaultEngine.ini` EST le contrat.** Le moteur ne stocke
que le numéro, jamais le nom : insérer une matière au milieu renumérote tout ce qui suit et les
`PM_` existants pointent silencieusement ailleurs. **Toute nouvelle surface s'ajoute à la fin.**
- Le pas du joueur local part en **2D** (`bPlayLocalStepsIn2D`), celui des autres en 3D à leurs
pieds : joué en 3D depuis ses propres chevilles, le pas local passe par l'atténuation et sonne
creux alors qu'il devrait être le son le plus présent du mixage. Les cues du pack n'ont pas de
`SoundClass`, donc les deux chemins tombent sur `SC_SFX` — le curseur *Effets* les couvre sans
qu'on ait rien à taguer.
- Les noms des surfaces décrivent le **monde**, pas le pack : `Grass` existe alors que le pack
livre *Leaves*, `Stone` alors qu'il livre *Concrete*. C'est `DA_Footsteps` qui fait la
traduction, et c'est sa raison d'être — changer de pack de sons ne doit toucher ni la config ni
les `PM_`.
### Décisions de gameplay figées ### Décisions de gameplay figées
@@ -133,11 +558,52 @@ qu'une compilation propre l'est vraiment.
tableau uniquement pour que le glisser-déposer n'ait aucune conversion d'index à faire. tableau uniquement pour que le glisser-déposer n'ait aucune conversion d'index à faire.
- **Ramassage** : complète les piles existantes partout, puis ouvre un slot dans la **barre en - **Ramassage** : complète les piles existantes partout, puis ouvre un slot dans la **barre en
priorité**, puis déborde dans la grille. priorité**, puis déborde dans la grille.
- **Le volume visable d'un `APickupItem` est une BOÎTE ajustée au mesh, jamais une sphère.** Un
seul `BP_Pickup` sert des centaines d'objets : sa taille se calcule à partir de
`UStaticMesh::GetBoundingBox()` dans `FitInteractionBox()`, appelée par `ApplyItemVisuals()`
donc par `OnConstruction` **et** `OnRep_ItemData`. Trois raisons de ne pas revenir à la sphère :
son rayon englobant vaut la **demi-diagonale**, ce qui est le pire cas exact pour les objets
allongés du jeu (une branche de 120×20×15 donnait 62 cm de rayon, vingt-sept fois son volume) ;
elle ne peut pas exprimer une orientation, là où la boîte tourne avec l'acteur ; et le symptôme
n'est pas cosmétique — deux pickups côte à côte voyaient leurs volumes se chevaucher, et le
trace retenait le **premier touché** plutôt que l'objet visé. La marge (`InteractionBoxPadding`)
est **additive en cm et jamais multiplicative** : un facteur appliqué à la demi-épaisseur d'une
branche n'ajoute rien tout en rallongeant son grand axe. Deux pièges partagés avec
`AStorageContainer::FitInteractionBox` : lire les bounds de l'**asset** et pas `Mesh->Bounds`,
déjà multipliées par l'échelle que `SetBoxExtent` remultiplie ; et recentrer sur
`GetCenter()`, le pivot d'un mesh de pack étant presque toujours au sol.
- **Glisser** : normal = pile entière, **Maj** = moitié arrondie au supérieur, **Ctrl** = un seul. - **Glisser** : normal = pile entière, **Maj** = moitié arrondie au supérieur, **Ctrl** = un seul.
Lâcher hors de la grille jette au sol ; lâcher entre deux cases est absorbé (jamais de perte Lâcher hors de la grille jette au sol ; lâcher entre deux cases est absorbé (jamais de perte
accidentelle). accidentelle).
- **Clic gauche** = utiliser l'objet actif de la barre. Bindé sur le **pawn**, donc muet quand - **Clic gauche** = utiliser l'objet actif de la barre. Bindé sur le **pawn**, donc muet quand
l'inventaire est ouvert — le conflit avec le drag & drop se règle tout seul. l'inventaire est ouvert — le conflit avec le drag & drop se règle tout seul.
- **L'effet du clic gauche part d'un AnimNotify, jamais de l'appui.** Le clic ne fait que lancer
le montage ; c'est `UAnimNotify_ItemAction`, posé dans la timeline au moment de l'impact, qui
appelle `AFpsPlayer::TriggerHeldItemAction()`. Sinon la hache abat l'arbre *avant* de l'avoir
touché — le résultat précède le geste qui aurait dû le produire. L'instant de l'impact
appartient à l'**animation** : un délai en secondes sur le DataAsset serait à re-régler à
chaque animation livrée, et faux dès qu'on touche au Play Rate. Trois points à tenir :
le montage d'usage ne tourne **que sur les bras du joueur local**, donc le notify tombe sur une
seule machine et le routage réseau ne change pas ; un objet **sans `UseMontage` retombe sur
l'effet immédiat** (aucun marqueur ne viendra jamais, le priver d'effet le casserait) ; et la
case visée est **figée à l'appui** (`PendingUseSlotIndex`), la molette pouvant changer d'objet
pendant les trois dixièmes de seconde du geste.
- **Saut des bras FPS : une machine à états, pas des montages.** `JumpStart``JumpFallLoop`
`JumpLanding` / `HardLanding`. Le milieu de la chaîne est une **boucle**, et une boucle ne se
joue pas en montage — c'est ce qui a fait retirer `LandMontage` / `HardLandMontage`, dont le
geste se serait de toute façon superposé à l'état de chute qui venait de démarrer. La sortie de
l'état de chute est pilotée par `ELandingImpact` (`None` / `Soft` / `Hard`), produit par le pawn.
**`Hard` signifie exactement « cette chute a coûté de la vie »** : son seuil est
`FallDamageMinSpeed` et rien d'autre — il n'existe aucun réglage d'animation à côté, qui
finirait par diverger et ferait encaisser le personnage sans qu'il perde un point de vie. Le
seuil bas, lui, reste celui de la secousse de caméra (`LandPunchMinFallSpeed`). Un enum et pas
deux booléens : les trois valeurs mènent aux trois
sorties et s'excluent par construction, donc aucune combinaison ne rend deux transitions valides
en même temps. Il est **remis à `None` dès la première image de vol** (dans `Tick`), ce qui
dispense chaque transition de vérifier `bIsFalling` par-dessus. Deux états de réception distincts
et pas un seul avec un `Blend Poses by bool` : c'est le piège déjà noté pour l'accroupissement,
« Automatic Rule Based on Sequence Player in State » a besoin d'**un** Sequence Player pour lire
sa durée.
- **Objets à charges** (`MaxUses > 1`) ne s'empilent jamais. Les valeurs de restauration - **Objets à charges** (`MaxUses > 1`) ne s'empilent jamais. Les valeurs de restauration
s'appliquent **par utilisation**. L'usure suit l'objet jusqu'au sol et revient avec lui. s'appliquent **par utilisation**. L'usure suit l'objet jusqu'au sol et revient avec lui.
- **Mort** : tout tombe au sol autour du corps, respawn au `PlayerStart`, jauges pleines. - **Mort** : tout tombe au sol autour du corps, respawn au `PlayerStart`, jauges pleines.
@@ -146,11 +612,34 @@ qu'une compilation propre l'est vraiment.
- Après un respawn, le pawn est un **nouvel acteur** : les widgets doivent se rebrancher via - Après un respawn, le pawn est un **nouvel acteur** : les widgets doivent se rebrancher via
`BindToOwningPawn()`, appelé depuis `OnPossess`. Même raison côté réglages : le pawn les `BindToOwningPawn()`, appelé depuis `OnPossess`. Même raison côté réglages : le pawn les
**relit** à son `BeginPlay` et s'abonne à `OnSurvivalSettingsApplied`. **relit** à son `BeginPlay` et s'abonne à `OnSurvivalSettingsApplied`.
- **Échap** a une priorité explicite dans `HandlePauseInput()` : boîte de confirmation → - **Échap** a une priorité explicite dans `HandleEscapeInput()` : boîte de confirmation →
réglages → inventaire → pause. Sans cet ordre, deux écrans se retrouvent ouverts en même temps. réglages → coffre → inventaire → pause. Sans cet ordre, deux écrans se retrouvent ouverts en
- **Menu de pause** : `SetPause(true)` fige le monde, mais **pas Slate** — les animations UMG et même temps.
`ScreenFadeComponent` continuent (d'où son `bTickEvenWhenPaused`). Pas de bouton « Quitter le - **Un widget UMG qui a le focus clavier COUPE la route des touches vers Enhanced Input.** Les
jeu » : la sortie se fait depuis le menu principal. widgets vivent dans le `SGameLayerManager`, **frère** du `SViewport` et pas son enfant : une
touche remonte les parents du widget focalisé et n'atteint jamais le viewport. Or un `UButton`
prend le focus au clic. Symptôme exact rencontré : Échap refermait l'inventaire (que des cases,
rien de focusable) mais plus le menu de pause dès qu'on avait cliqué dedans — et jamais après un
aller-retour dans les réglages, qui prennent le focus (`SetKeyboardFocus`) sans le rendre.
La parade est donc double, et les deux moitiés comptent : tout écran à boutons est `Focusable`
et relaie Échap par `NativeOnKeyDown` vers `HandleEscapeInput()` — un seul point d'entrée, donc
la priorité ci-dessus reste vraie quel que soit le chemin — **et** l'écran qui redevient la
couche active **reprend** le focus (`UPauseMenuWidget::SetInteractive(true)`). Corollaire
assumé : menu de pause ouvert, Tab et les touches de barre rapide ne répondent plus.
- **Dans la map du menu, `AMainMenuPlayerController` ne binde AUCUNE action d'input.** Tout Échap
qui y fonctionne passe donc par le `NativeOnKeyDown` d'un widget — il n'existe pas d'autre
chemin, et un écran qui l'oublie n'a simplement pas de touche Retour. C'était le cas de
`UCharacterCustomizationWidget`. Son `SetWidgetToFocus()` côté contrôleur ne suffisait pas non
plus : **Slate refuse de focaliser un widget qui ne déclare pas supporter le focus clavier**,
donc l'appel ne faisait rien tant que `SetIsFocusable(true)` manquait.
- **Menu de pause : il ne met plus rien en pause.** `SetPause()` a disparu du projet. En coop la
pause est impossible par nature — côté client l'appel est ignoré (elle appartient à l'autorité),
côté hôte elle figerait le monde des quatre joueurs parce qu'un seul a ouvert son menu. Le monde
continue donc de tourner, comme dans Valheim et Raft : ouvrir son menu ne met pas à l'abri.
Corollaire **non négociable** : c'est la pause qui coupait gratuitement l'input du pawn, il faut
donc passer par `SetUiInputMode(true)` — le même que l'inventaire et le coffre. L'oublier laisse
la caméra tourner derrière le menu. Pas de bouton « Quitter le jeu » : la sortie se fait depuis
le menu principal.
- **Réglages** : `USurvivalUserSettings` fait autorité, jamais le CDO du pawn. Application - **Réglages** : `USurvivalUserSettings` fait autorité, jamais le CDO du pawn. Application
immédiate, aucun bouton « Appliquer » ; la sauvegarde a lieu à la fermeture de l'écran. immédiate, aucun bouton « Appliquer » ; la sauvegarde a lieu à la fermeture de l'écran.
L'écran ignore qui l'ouvre (`OnCloseRequested`), d'où un seul `WBP_Settings` pour le menu L'écran ignore qui l'ouvre (`OnCloseRequested`), d'où un seul `WBP_Settings` pour le menu
@@ -210,9 +699,14 @@ qu'une compilation propre l'est vraiment.
referme le couvercle — par Échap, par la mort, ou parce que le joueur s'est éloigné referme le couvercle — par Échap, par la mort, ou parce que le joueur s'est éloigné
(`MaxStorageDistance`, surveillé par un timer à 0,25 s, jamais un Tick). Un coffre qui (`MaxStorageDistance`, surveillé par un timer à 0,25 s, jamais un Tick). Un coffre qui
déciderait seul finirait ouvert dans la moitié de ces cas. déciderait seul finirait ouvert dans la moitié de ces cas.
- **`LidPivot`**, un `SceneComponent` vide entre la caisse et le couvercle : le pivot de - **Le couvercle tourne sur le pivot de son propre mesh.** `LidMesh` est enfant direct de la
`SM_Chest_Top` est celui du pack, presque jamais la charnière. Sans lui le couvercle tourne caisse et c'est lui qu'on fait tourner : un `UStaticMeshComponent` pivote toujours autour de
autour de son centre et traverse la caisse. Le coffre ne tick **que** pendant l'animation. l'origine de son mesh, donc la charnière est celle que le pack a donnée à `SM_Chest_Top`, sans
composant intermédiaire à placer à la main. *(Un `SceneComponent` `LidPivot` a existé pour
découpler les deux ; il a été retiré — un pivot vide laissé à `(0,0,0)` fait tourner le
couvercle autour du centre de la caisse, et c'était le symptôme.)* Si un jour un pack pose ce
pivot au centre du couvercle, on le corrige **dans l'asset** (Modeling Mode > XForm > Edit
Pivot), pas dans le code. Le coffre ne tick **que** pendant l'animation.
- **Toute la collision du coffre tient dans une `UBoxComponent`, les meshes n'en portent - **Toute la collision du coffre tient dans une `UBoxComponent`, les meshes n'en portent
aucune.** C'est la seule exception à la règle « le mesh EST la surface d'interaction » posée aucune.** C'est la seule exception à la règle « le mesh EST la surface d'interaction » posée
par `ACraftingStation`, et elle vient de ce qu'un coffre est en **deux morceaux dont un par `ACraftingStation`, et elle vient de ce qu'un coffre est en **deux morceaux dont un
@@ -295,26 +789,132 @@ Le détail du système de craft — API, câblage éditeur, dépannage — est d
## État actuel et suite ## État actuel et suite
Boucle complète : récolter → **fabriquer****cuire** → ranger → consommer → survivre Boucle complète **en coop jusqu'à 4 joueurs** : récolter → fabriquer → cuire → ranger
mourir → réapparaître. Autour : menu principal, menu de pause, et un écran de réglages dont **seul consommer → survivre → mourir → réapparaître. Autour : menu principal, menu de pause, écran de
l'onglet « Jeu & Contrôles » est rempli**. réglages, étiquettes de pseudo Steam.
Le craft est en place de bout en bout : `DA_RecipeBook`, onglets Inventaire / Fabrication, Le portage réseau est terminé pour tout le gameplay existant — voir *Réplication* plus haut pour
grille de recettes filtrée par poste accessible, panneau de détail avec compteurs colorés. le détail et les pièges. Les transitions de map sont propres de bout en bout : plus de flash, plus
Restent à faire quand le besoin viendra : l'**acteur établi** qui appellera de pop-in, départ groupé quand l'hôte quitte.
`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`.
Suite immédiate sur les réglages, dans l'ordre : onglet **Affichage** (le moteur fait tout, Les vraies recettes restent à écrire : celles en place sont deux `DA_Craft_Test_*` bâties sur
prévoir la confirmation à rebours de 10 s sur la résolution), onglet **Graphismes** (les dix `DA_Branch` et `DA_Rocher`. Les **boutons de filtre** par poste ne sont pas faits non plus
curseurs de `UGameUserSettings` ; végétation et distance d'affichage comptent double avec (`SetStationFilter` existe déjà, `ECraftingStation::Count` sert de sentinelle « tout »).
SoStylized), onglet **Audio** quand des `SoundClass` existeront, et enfin le **remappage des
touches** via `UEnhancedInputUserSettings` — le seul morceau réellement coûteux. ### Chantier en cours : customisation de personnage
**Doc complète : [Docs/CharacterCustomization.md](../Docs/CharacterCustomization.md)** — deux
moitiés séparées, une pour l'artiste (ajouter une coiffure, une couleur, un corps, sans code) et
une pour le développeur (architecture, réseau, pièges du moteur).
Découpé en quatre lots. **Le lot 1 est fait** : le pawn a enfin un corps visible et animé (voir
*Le corps vu par les autres* plus haut) — sans lui il n'y avait rien à customiser, et les autres
joueurs voyaient un pseudo flotter au-dessus du vide.
**Le lot 2 est fait aussi** : `FCharacterAppearance` (11 octets d'index), `UCharacterPartsDataAsset`
(le catalogue) et `UCharacterAppearanceComponent` (monte les pièces en leader pose, pose les
couleurs sur des MID). `DA_CharacterParts` couvre Body A ; **Body B reste à remplir**, avec son
`ABP_PlayerBody_B` et sans barbes ni moustaches — le pack n'en livre pas, et une liste vide se
comporte déjà comme `NoPart`.
**Le lot 3 est fait** : l'apparence vit sur `ASurvivalPlayerState` en `ReplicatedUsing` et se
persiste dans `USurvivalUserSettings`. Elle survit donc au respawn — le pawn est un nouvel acteur,
le PlayerState non — et au relancement du jeu.
- **`SubmitCharacterAppearance()` sur `AFpsPlayerController` est le point d'entrée UNIQUE.** Il
persiste *et* annonce au serveur, jamais l'un sans l'autre : persister sans annoncer laisserait
le joueur seul à se voir changé, annoncer sans persister le ferait repartir au visage par défaut
au lancement suivant. C'est par là que passent les commandes console, et par là que passera
l'écran du lot 4.
- **La course pawn / PlayerState se résout des deux côtés.** Chez un client ils arrivent par deux
chemins de réplication indépendants et rien ne dit lequel gagne : `OnRep_CharacterAppearance`
monte le personnage si le pawn est déjà là, `AFpsPlayer::OnRep_PlayerState` va chercher
l'apparence si c'est le pawn qui est arrivé le premier. Les deux appellent la même fonction
idempotente. Chez l'hôte, aucun `OnRep` ne se déclenche — c'est le `BeginPlay` du pawn qui monte,
et `SetCharacterAppearance` qui applique en local juste après avoir écrit la propriété.
- **Rien n'est validé côté PlayerState, et c'est délibéré** : borner un index demande le catalogue,
que seul `UCharacterAppearanceComponent` connaît — or il passe déjà tout ce qu'il monte par
`Catalog->Sanitize()`, sur chaque machine. Dupliquer la validation demanderait de donner le
catalogue au PlayerState pour un résultat identique.
- Piège de test : **en PIE à deux joueurs, les deux fenêtres lisent le même `.ini`**, donc les deux
personnages démarrent identiques. Il faut changer l'un des deux à la console pour vérifier que la
réplication marche.
Reste le **lot 4** — l'écran : `ACharacterPreviewActor` dans `MenuScene`, caméra à cadrages
interpolés, UI à gauche. Et le remplissage de **Body B** dans `DA_CharacterParts`.
#### Trois pièges du lot 2, aucun visible depuis le code
- **Les `UFUNCTION(Exec)` d'un `UActorComponent` ne sont JAMAIS appelées.** La chaîne de routage
(`UPlayer::Exec`, dans `Player.cpp`) interroge le monde, le `PlayerInput`, le contrôleur, le
**pawn**, le HUD, le GameMode, le `CheatManager`, le GameState et le camera manager — jamais les
composants, `AActor` ne surchargeant pas `ProcessConsoleExec` pour les parcourir. Et rien ne
proteste : la commande est simplement introuvable. D'où `EmberPart` / `EmberColor` /
`EmberAppearanceDump` posées sur `AFpsPlayer`, qui délègue au composant. *(`EmberJoin` marche
parce que `UGameInstance`, lui, route vers ses subsystems.)*
- **L'ordre des slots de matériau varie d'un mesh à l'autre.** Sur `SKM_HeadA_01`, l'Element 0
est celui des **yeux** et l'Element 1 celui de la peau — l'inverse de l'intuition. Les six têtes
viennent de deux packs différents, rien ne garantit qu'elles s'accordent. On cherche donc le slot
**par son nom** (`EyeMaterialSlotHint`, « Eye »), et si rien ne correspond on ne pose rien et on
le dit dans le log : écraser le slot de la peau repeint le visage entier de la couleur des yeux,
symptôme qu'on met bien plus longtemps à diagnostiquer qu'une ligne de log.
- **Un nom de paramètre matériau se lit dans l'éditeur du Material INSTANCE, jamais dans les
chaînes du master.** Le binaire de `M_Head_Base` contient « Skin Base color », mais le paramètre
réellement exposé s'appelle **`Skin`** — le chercher sous l'autre nom dans `MI_HeadA_01_a` ne
rend aucun résultat. Corollaire : les noms de paramètres sont des `UPROPERTY` du catalogue et
pas des constantes C++, pour qu'une erreur se corrige sans recompiler.
Le montage lui-même n'a rien de surprenant : le composant crée un `USkeletalMeshComponent` par
catégorie, le met en leader pose du `Mesh` du Character, et recopie sur chacun le `bOwnerNoSee` et
le `bCastHiddenShadow` du porteur — c'est ce qui fera que le mannequin du menu, lui, sera visible
sans une ligne de plus. Les couleurs sont posées sur **tous** les éléments de chaque mesh sans
chercher lequel les porte : un `SetVectorParameterValue` dont le paramètre n'existe pas est
ignoré, ce qui évite une table d'index de slots qui casserait au premier mesh mal rangé.
**Décisions figées** :
- **Couleurs = palette pure, un bouton par couleur.** Une `TArray<FLinearColor>` par catégorie
dans le catalogue, un index d'un octet dans l'apparence. Cliquer une pastille pose la couleur,
sans réglage intermédiaire. Le curseur de nuance envisagé au départ a été retiré : l'auteur veut
des boutons.
- **Pas de `DisplayName` sur les entrées du catalogue** — la vignette (`Icon`, un `UTexture2D`
comme celle d'`UItemDataAsset`) identifie l'option à elle seule.
- **Écran imposé sur le chemin *Jouer*.** Corollaire assumé : l'arrivée par invitation Steam ne
passe pas par là, l'invité garde donc son apparence persistée du dernier passage.
- **Étendue : silhouette + couleurs.** Tatouages et maquillage reportés — ce ne sont que des
paramètres texture de plus (`M_Head_Base` et `M_Body_Base` les exposent déjà), aucune refonte.
- **Les index et jamais les chemins d'assets** dans la struct répliquée : quelques octets au lieu
d'une chaîne, et un index se valide contre le catalogue.
- **Le corps se monte depuis une LISTE de meshes, jamais un pointeur unique**, même quand la liste
n'a qu'une case. C'est ce qui permettra aux armures de cacher des segments (`SKM_BodyA_torso`,
`_arms`, `_legs_01/02/03`…) sans refonte — le pack les fournit exprès. Contrainte qui en
découle : **tout futur pack d'armure devra être skinné sur `SK_BodyA` / `SK_BodyB`**, à vérifier
avant achat. Le jour venu, `SKM_BodyA_empty` deviendra le porteur d'animation invisible — il
faudra lui réassigner `SK_BodyA`, son squelette d'origine étant `SK_BodyA_empty`.
- Limite acceptée : **les bras vus en 1re personne ne sont pas customisés.** Leur squelette
(`BasePose_Skeleton`) n'a rien à voir avec `SK_BodyA` ; les rendre customisables imposerait de
retargeter toutes les animations FPS. Raft assume le même compromis.
**Les matériaux du pack sont pilotables par MID**, ce qui rend les variantes `_a` / `_b` / `_c`
inutiles — ce ne sont que des presets de couleur figés. Une seule instance par pièce suffit :
| Master | Paramètres utiles |
|---|---|
| `M_Body_Base` | `Skin color`, `Underware color`, `Tattoo 01..05 color` + masques |
| `M_Head_Base` | `Skin Base color`, `Skin roughness`, `Blush` / `Lipstick` / `Eyeliner` / `Eyeshadow` / `Eyelashes` / `Tattoo` (couleur + masque chacun) |
| `M_Hairstyle_Base` | `Color` — parent des **cheveux, barbes, moustaches ET sourcils** |
| `M_Eyes_Base` | `Eye color`, groupes Iris / Pupil / Sclera |
### Après la customisation
Prochaine étape gameplay recommandée : **spawner de ressources avec repousse** 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 (`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, Ensuite, par ordre de valeur : objet visible en main, sauvegarde, cycle jour/nuit.
cycle jour/nuit.
> **Systèmes présents dans `Source/` mais pas encore décrits ici** :
> `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.
+87 -3
View File
@@ -86,11 +86,62 @@ FontDPIPreset=Standard
FontDPI=72 FontDPI=72
[/Script/Engine.Engine] [/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/Survival_projet") ; Le nom du MODULE est grave dans chaque .uasset qui reference une classe C++ :
+ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/Survival_projet") ; un Blueprint derive d'AFpsPlayer stocke "/Script/<module>.FpsPlayer". Renommer
; le module sans redirection casserait donc la classe parente de tous les BP,
; widgets et DataAssets d'un coup. Ces lignes sont lues AVANT la resolution des
; references, donc le moteur charge l'ancien nom et trouve le nouveau.
;
; Elles restent valables tant que des assets non re-sauvegardes portent l'ancien
; nom -- c'est-a-dire indefiniment en pratique, un asset n'etant reecrit que
; quand on y touche. Ne pas les supprimer.
+ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/EmberWild")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/EmberWild")
+ActiveGameNameRedirects=(OldGameName="Survival_projet",NewGameName="/Script/EmberWild")
+ActiveGameNameRedirects=(OldGameName="/Script/Survival_projet",NewGameName="/Script/EmberWild")
; Sans cette ligne le moteur instancie UGameUserSettings et nos reglages de jeu ; Sans cette ligne le moteur instancie UGameUserSettings et nos reglages de jeu
; n'existent tout simplement pas au runtime. ; n'existent tout simplement pas au runtime.
GameUserSettingsClassName=/Script/Survival_projet.SurvivalUserSettings GameUserSettingsClassName=/Script/EmberWild.SurvivalUserSettings
; ==================================================================
; Coop Steam (4 joueurs, listen server, P2P par le relais Valve)
; ==================================================================
; Le NetDriver par defaut ouvre un socket UDP et attend une IP : inutilisable
; derriere un NAT. SteamSocketsNetDriver fait transiter la connexion par le
; relais de Valve -- aucun port a ouvrir, aucune IP echangee entre joueurs.
; Le ClearArray est obligatoire : sans lui on empile une seconde definition
; "GameNetDriver" et c'est celle du moteur qui gagne.
; Le Fallback sert au PIE et aux tests hors Steam : sans client Steam lance,
; le moteur retombe sur l'IpNetDriver classique au lieu de refuser de demarrer.
[/Script/Engine.GameEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="/Script/SteamSockets.SteamSocketsNetDriver",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
[/Script/SteamSockets.SteamSocketsNetDriver]
NetConnectionClassName="/Script/SteamSockets.SteamSocketsNetConnection"
[OnlineSubsystem]
DefaultPlatformService=Steam
; SteamDevAppId = notre AppID reel (Emberwild). Le classique 480 (Spacewar)
; ne sert que si des testeurs ne possedent pas encore le jeu sur leur compte.
;
; PAS de bInitServerOnClient : il force l'initialisation de l'API GameServer
; de Steam en plus de l'API client, dans le meme process. C'est fait pour
; tester du code de serveur dedie sur une machine de dev. Un listen server
; n'en a pas besoin -- il cree un LOBBY Steam via l'API client. Pire, si
; l'init serveur echoue alors que le client a reussi, OnlineSubsystemSteam
; detruit TOUT (voir le Shutdown() de son Init) : on ajouterait un mode
; d'echec sans rien gagner.
;
; Rappel qui fait perdre du temps : dans l'EDITEUR, IsEnabled() rend toujours
; faux (IsRunningGame() est faux), donc Steam ne s'initialise jamais en PIE et
; le moteur retombe sur OnlineSubsystemNULL. C'est voulu par Epic. Pour tester
; Steam il faut Play > Standalone Game, ou un build package.
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=4282850
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] [/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
bEnablePlugin=True bEnablePlugin=True
@@ -106,3 +157,36 @@ ConnectionType=USBOnly
bUseManualIPAddress=False bUseManualIPAddress=False
ManualIPAddress= ManualIPAddress=
[/Script/Engine.AudioSettings]
DefaultSoundClassName=/Game/Game/Sounds/Mix/SC_SFX.SC_SFX
DefaultBaseSoundMix=/Game/Game/Sounds/Mix/SM_Settings.SM_Settings
; ==================================================================
; Surfaces physiques -- bruits de pas
; ==================================================================
;
; Une entree par matiere du pack Essential_Foosteps_SK. C'est cette liste que
; l'on retrouve dans le menu deroulant "Surface Type" d'un PhysicalMaterial, et
; c'est la CLE de la table de UFootstepSoundsDataAsset.
;
; L'ORDRE EST LE CONTRAT : SurfaceType1 est le premier de cette liste, et le
; moteur ne stocke que ce numero, jamais le nom. Inserer une matiere au milieu
; renumerote donc tout ce qui suit, et les PhysicalMaterial deja crees pointent
; silencieusement sur la mauvaise matiere -- le decor se met a sonner en verre.
; Toute nouvelle surface s'AJOUTE A LA FIN, sans exception.
;
; SurfaceType_Default (l'entree 0, non listee ici) reste ce que porte tout mesh
; non tague : c'est lui qui tombe sur DefaultSurface dans la banque de sons.
[/Script/Engine.PhysicsSettings]
+PhysicalSurfaces=(Type=SurfaceType1,Name="Dirt")
+PhysicalSurfaces=(Type=SurfaceType2,Name="Grass")
+PhysicalSurfaces=(Type=SurfaceType3,Name="Wood")
+PhysicalSurfaces=(Type=SurfaceType4,Name="Stone")
+PhysicalSurfaces=(Type=SurfaceType5,Name="Gravel")
+PhysicalSurfaces=(Type=SurfaceType6,Name="Sand")
+PhysicalSurfaces=(Type=SurfaceType7,Name="Metal")
+PhysicalSurfaces=(Type=SurfaceType8,Name="Glass")
+PhysicalSurfaces=(Type=SurfaceType9,Name="Snow")
+PhysicalSurfaces=(Type=SurfaceType10,Name="Slush")
+PhysicalSurfaces=(Type=SurfaceType11,Name="Water")
+60
View File
@@ -9,7 +9,67 @@ CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
[/Script/EngineSettings.GeneralProjectSettings] [/Script/EngineSettings.GeneralProjectSettings]
ProjectID=4774BE68499C24D0A70C6E87B49DA419 ProjectID=4774BE68499C24D0A70C6E87B49DA419
; Le titre affiche est ce que porte la barre de fenetre du jeu packagee. Il est
; independant du nom du module et de l'exe : on peut donc ecrire le nom commercial
; ("Emberwild", comme sur la fiche Steam) sans toucher au code.
ProjectName=Emberwild
ProjectDisplayedTitle=NSLOCTEXT("[/Script/EngineSettings]", "ProjectDisplayedTitle", "Emberwild")
CompanyName=Highland Game Studio
CompanyDistinguishedName=Highland Game Studio
Homepage="https://highlandgamesstudio.com/"
SupportContact="https://highlandgamesstudio.com/"
[/Script/DiscordRichPresence.DiscordRichPresenceSettings] [/Script/DiscordRichPresence.DiscordRichPresenceSettings]
ClientId=1432045921620725933 ClientId=1432045921620725933
; Sans cette liste le cooker embarque TOUTES les maps du dossier Content, donc
; les ~25 maps de demonstration de SoStylized : build enorme et cook interminable.
;
; MenuScene doit y figurer meme si c'est deja la GameDefaultMap, et GameScene
; SURTOUT : elle n'est referencee que par un TSoftObjectPtr (GameLevel sur
; BP_MainMenuPlayerController). Une reference soft n'est pas suivie par le
; cooker comme une reference dure -- sans cette ligne le build demarre sur le
; menu, on clique Jouer, et rien ne se charge.
[/Script/UnrealEd.ProjectPackagingSettings]
+MapsToCook=(FilePath="/Game/Game/Maps/MenuScene")
+MapsToCook=(FilePath="/Game/Game/Maps/GameScene")
; Dev_Scene y figure parce que GameLevel peut pointer sur elle le temps d'un
; test : la map visee se choisit dans BP_MainMenuPlayerController, et toute map
; susceptible d'y atterrir doit etre listee ici. Les garder toutes les trois
; coute un peu de cook mais permet de basculer sans retoucher cette liste --
; l'oubli, lui, ne se voit qu'une fois le build chez le testeur.
+MapsToCook=(FilePath="/Game/Game/Maps/Dev/Dev_Scene")
; ==================================================================
; Videos de demarrage (logo moteur puis logo studio)
; ==================================================================
;
; Les entrees sont des noms de fichiers SANS extension, cherches dans
; Content/Movies/. Ce ne sont PAS des assets : le cooker ne les importe pas,
; il copie le dossier tel quel au staging (automatique, aucun reglage de
; packaging a ajouter -- sauf a cocher "Skip Movies", qui les supprimerait).
;
; L'ordre de cette liste EST l'ordre de lecture.
;
; bWaitForMoviesToComplete : sans lui, la video est coupee net des que la
; premiere map est prete. MenuScene se charge vite, donc le logo du studio
; serait ampute -- ou jamais vu.
;
; bMoviesAreSkippable : le passage se fait au CLIC SOURIS, pas au clavier
; (voir le commentaire d'UMoviePlayerSettings). Inutile de chercher pourquoi
; Echap ne fait rien.
;
; Piege qui coute une soiree : ces videos ne jouent JAMAIS en PIE. Il faut
; Play > Standalone Game, ou un build packagee -- meme logique que Steam.
[/Script/MoviePlayer.MoviePlayerSettings]
bWaitForMoviesToComplete=True
bMoviesAreSkippable=True
+StartupMovies=UnrealLogo
+StartupMovies=StudioLogo
[/Script/EmberWild.SurvivalAudioSettings]
MasterSoundClass=/Game/Game/Sounds/Mix/SC_Master.SC_Master
MusicSoundClass=/Game/Game/Sounds/Mix/SC_Music.SC_Music
EffectsSoundClass=/Game/Game/Sounds/Mix/SC_SFX.SC_SFX
UiSoundClass=/Game/Game/Sounds/Mix/SC_UI.SC_UI
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More