Compare commits

..

6 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
4909 changed files with 21774 additions and 346 deletions
+175 -24
View File
@@ -1,6 +1,6 @@
# 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.
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
```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.
- `-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.
@@ -124,12 +127,13 @@ qu'une compilation propre l'est vraiment.
| Menu principal | `MainMenuGameMode`, `MainMenuPlayerController`, `MainMenuWidget`, `MenuCameraSpot` |
| Fabrication | `CraftingRecipeDataAsset`, `CraftingRecipeBook`, `CraftingComponent`, `CraftingWidget`, `CraftingRecipeSlotWidget`, `CraftingIngredientRowWidget` |
| 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` |
| Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` |
| 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` |
| Coop Steam (sessions) | `SessionSubsystem`, `SurvivalGameMode`, `SurvivalGameState`, `SurvivalPlayerState` |
| Pseudo au-dessus des joueurs | `PlayerNameplateComponent`, `PlayerNameplateWidget` |
@@ -242,7 +246,7 @@ distribué par le dépôt Steam. C'est la procédure de référence.
```powershell
& "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\RunUAT.bat" BuildCookRun `
-project="D:\Projet\Perso\Unreal\Survival_projet\Survival_projet.uproject" `
-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" `
@@ -253,19 +257,28 @@ distribué par le dépôt Steam. C'est la procédure de référence.
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 dans le dépôt** — le contenu de `Build\Package\Windows\` va **à la racine** de
`C:\sdk\tools\ContentBuilder\content\`, en excluant `steam_appid.txt` : Steam fournit lui-même
l'AppID au lancement, et le fichier l'écraserait.
**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.
**3. Uploader**`C:\sdk\tools\ContentBuilder\upload_emberwild.bat` (App **4282850**, dépôt
**4282851**). Le `.vdf` n'a **pas** de `SetLive`, donc le build monte sans devenir live.
Deux choses à savoir sur ce script :
**4. Publier sur la branche `develop`**, jamais sur `default`. Et vérifier que *Launch Options*
pointe sur `Survival_projet.exe` — puis **publier les changements**, sinon rien n'est visible
- **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.
**5. Tester à deux** : les deux lancent le jeu **depuis Steam**, l'hôte clique *Jouer*, puis
Échap → *Inviter des amis*. Les logs à lire sont dans `Survival_projet/Saved/Logs/` de chaque
**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
@@ -487,6 +500,57 @@ du joueur dans lui-même.
(`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
- **Inventaire** : un seul `TArray` de 30 slots. Index **0-9 = barre rapide**, **10-29 = grille**.
@@ -494,11 +558,52 @@ du joueur dans lui-même.
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
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.
Lâcher hors de la grille jette au sol ; lâcher entre deux cases est absorbé (jamais de perte
accidentelle).
- **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'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
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.
@@ -507,8 +612,26 @@ du joueur dans lui-même.
- 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
**relit** à son `BeginPlay` et s'abonne à `OnSurvivalSettingsApplied`.
- **Échap** a une priorité explicite dans `HandlePauseInput()` : boîte de confirmation →
réglages → inventaire → pause. Sans cet ordre, deux écrans se retrouvent ouverts en même temps.
- **Échap** a une priorité explicite dans `HandleEscapeInput()` : boîte de confirmation →
réglages → coffre → inventaire → pause. Sans cet ordre, deux écrans se retrouvent ouverts en
même temps.
- **Un widget UMG qui a le focus clavier COUPE la route des touches vers Enhanced Input.** Les
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
@@ -576,9 +699,14 @@ du joueur dans lui-même.
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
déciderait seul finirait ouvert dans la moitié de ces cas.
- **`LidPivot`**, un `SceneComponent` vide entre la caisse et le couvercle : le pivot de
`SM_Chest_Top` est celui du pack, presque jamais la charnière. Sans lui le couvercle tourne
autour de son centre et traverse la caisse. Le coffre ne tick **que** pendant l'animation.
- **Le couvercle tourne sur le pivot de son propre mesh.** `LidMesh` est enfant direct de la
caisse et c'est lui qu'on fait tourner : un `UStaticMeshComponent` pivote toujours autour de
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
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
@@ -675,6 +803,10 @@ Les vraies recettes restent à écrire : celles en place sont deux `DA_Craft_Tes
### 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.
@@ -685,12 +817,31 @@ couleurs sur des MID). `DA_CharacterParts` couvre Body A ; **Body B reste à rem
`ABP_PlayerBody_B` et sans barbes ni moustaches — le pack n'en livre pas, et une liste vide se
comporte déjà comme `NoPart`.
Restent les lots 3 et 4 :
**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.
- **Lot 3** — réplication par `ASurvivalPlayerState` en `ReplicatedUsing`, persistance dans
`USurvivalUserSettings`
- **Lot 4** — l'écran : `ACharacterPreviewActor` dans `MenuScene`, caméra à cadrages interpolés,
UI à gauche
- **`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
+43 -3
View File
@@ -86,11 +86,22 @@ FontDPIPreset=Standard
FontDPI=72
[/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/Survival_projet")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/Survival_projet")
; Le nom du MODULE est grave dans chaque .uasset qui reference une classe C++ :
; 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
; 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)
@@ -150,3 +161,32 @@ ManualIPAddress=
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")
+10 -1
View File
@@ -9,6 +9,15 @@ CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
[/Script/EngineSettings.GeneralProjectSettings]
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]
ClientId=1432045921620725933
@@ -58,7 +67,7 @@ bMoviesAreSkippable=True
+StartupMovies=UnrealLogo
+StartupMovies=StudioLogo
[/Script/Survival_projet.SurvivalAudioSettings]
[/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
Binary file not shown.

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