Agrandissement multi modif repertoires

This commit is contained in:
Stephane MAURO 2026-04-18 08:50:08 +00:00
parent 39ffe368c9
commit 89079e554a
5 changed files with 3284 additions and 0 deletions

View File

@ -0,0 +1,192 @@
# Agrandissement de salles — Synchronisation multijoueur
## Ce que fait ce pack
Rend l'agrandissement de salle (clic [E] sur un panneau mural) **synchrone entre tous les joueurs** en multijoueur, tout en restant fonctionnel en solo.
## Architecture
```
Client joueur clique [E]
PlayerInteraction.Ramasser()
agr.DemanderAgrandir() ←─── [nouveau]
SynchronisationAgrandissement.DemanderAgrandissement(salleId, direction)
├── Mode solo : execution directe locale
└── Mode multi :
PlayerAgrandissementBridge.CmdDemanderAgrandissement (sur le joueur local)
Serveur : SynchronisationAgrandissement.TraiterDemandeServeur
├── Valide (pas deja agrandi dans cette direction)
├── Ajoute a la SyncList des agrandissements effectues
RpcLancerAgrandissement(salleId, direction)
Chaque client + serveur : AgrandirLocal(false)
Animation identique chez tous (meme parametres → meme geometrie)
```
## Script 1 — `SynchronisationAgrandissement.cs` (NOUVEAU)
Contient deux classes :
1. **`SynchronisationAgrandissement`** — singleton NetworkBehaviour, orchestre tout
2. **`PlayerAgrandissementBridge`** — NetworkBehaviour a ajouter au prefab du joueur pour porter la `[Command]`
## Script 2 — `AgrandissementSalle.cs` (MODIFIÉ)
Changements :
- Nouvelle méthode `DemanderAgrandir()` : point d'entrée multi
- Nouvelle méthode `AgrandirLocal(bool instantane)` : execute localement (avec ou sans anim)
- `Agrandir()` conservée pour compat : équivaut à `AgrandirLocal(false)`
- Toutes les coroutines d'animation (`ConstruireSol`, `ConstruireMurs`, `ConstruirePlafond`, `ConstruireEclairage`) acceptent un paramètre `skipVisuel` pour construire instantanément (rejeu client qui rejoint)
- Gardes `DedicatedServerMode.IsDedicatedServer` sur la création de `Material`/`TextMesh`/debris
- Layer `Emplacement` correctement appliqué aux emplacements de l'extension (il manquait dans l'original)
## Script 3 — `PlayerInteraction.cs` (MODIFIÉ)
Un seul changement : ligne ~933, `agr.Agrandir()` remplacé par `agr.DemanderAgrandir()`.
## Étapes dans Unity
### 1. Remplacer les 3 scripts
Copier dans `Assets/Scripts/` :
- `SynchronisationAgrandissement.cs` (nouveau fichier)
- `AgrandissementSalle.cs` (remplace l'existant)
- `PlayerInteraction.cs` (remplace l'existant)
### 2. Ajouter `SynchronisationAgrandissement` à la scène
Dans `Datacenter_01` :
1. Sélectionner le GameObject qui porte le `NetworkManager`
2. `Add Component``SynchronisationAgrandissement`
3. S'assurer que ce GameObject a un `NetworkIdentity` ; la plupart du temps le `NetworkManager` n'en a pas lui-même. Créer un enfant `SyncAgrandissement_Holder` avec un `NetworkIdentity` + le script si besoin.
**Important** : pour que Mirror synchronise le `SyncList`, ce GameObject doit être spawné sur le réseau. Deux options :
- **Option A (simple)** : le mettre dans `NetworkManager.spawnPrefabs` et le spawner au `OnStartServer` via un petit bootstrap
- **Option B (scène)** : garder le GameObject dans la scène. Mirror va automatiquement le traiter comme un objet de scène avec `NetworkIdentity`. Dans ce cas, s'assurer que l'objet porte **un `NetworkIdentity`** et qu'il est présent **dès l'ouverture de la scène**.
L'option B est ce que je recommande pour ton cas : Datacenter_01 est une scène fixe, tu ajoutes `SynchronisationAgrandissement` + `NetworkIdentity` sur un GameObject dédié, et Mirror le gère tout seul.
### 3. Ajouter `PlayerAgrandissementBridge` au prefab du joueur
1. Ouvrir le prefab du joueur (celui qui est dans `NetworkManager.playerPrefab`)
2. `Add Component``PlayerAgrandissementBridge`
3. Sauver le prefab
Sans ce composant sur le joueur, les clients ne pourront pas envoyer leur `CmdDemanderAgrandissement` vers le serveur.
### 4. Rebuild
- Client Windows (pour tester avec deux joueurs)
- Dedicated Server Linux (pour déployer sur la Debian)
## Test à faire
### Test 1 — Mode solo
1. Lancer en éditeur (play mode normal, pas host)
2. Cliquer [E] sur un panneau d'agrandissement
3. **Attendu** : l'animation se lance comme avant, salle agrandie à la fin
### Test 2 — Mode host + client
1. Lancer l'éditeur en Host
2. Lancer un build client et le connecter
3. **Host** clique [E] sur un panneau
4. **Attendu** : les deux voient la même animation en même temps, les nouveaux emplacements apparaissent des deux côtés
### Test 3 — Client déclencheur
1. Host + client
2. **Client** clique [E] sur un panneau
3. **Attendu** : les deux voient l'animation, la SyncList côté serveur contient l'entrée
### Test 4 — Dedicated + 2 clients
1. Lancer le dedicated sur la Debian
2. Connecter 2 clients Windows
3. Un des clients clique [E]
4. **Attendu** : les 2 clients voient l'animation synchrone, le serveur logue la création de l'extension
### Test 5 — Rejoin
1. Dedicated + 1 client
2. Le client déclenche l'agrandissement
3. Un **2e client** se connecte APRÈS
4. **Attendu** : le 2e client voit l'extension instantanément (pas d'animation), l'état est cohérent
## Points d'attention
### GameEconomy
Ton `GameEconomy` est en mode sandbox, donc les `TenterAchat` passent toujours. La validation économique serveur n'est pas implémentée dans ce pack, elle le sera quand tu sortiras du sandbox.
### Emplacements de baies
Les `EmplacementBaie` de l'extension sont créés **localement sur chaque instance** (pas via `NetworkServer.Spawn`). C'est cohérent avec le pattern actuel de `SalleDatacenter.GenererEmplacementsBaies()` qui fonctionne déjà en multi.
Les baies qui seront placées dessus continueront d'utiliser le flux `CmdPlacerBaie` qui spawne bien via `NetworkServer.Spawn` dans `BoutiqueReseau.TraiterPlacementBaieServeur`.
### Colliders sol + murs
Ils sont créés **partout (serveur et clients)** car ils sont nécessaires pour :
- Le CharacterController du joueur (marcher dans la nouvelle zone)
- La physique des objets posés
- Les raycasts d'interaction
Pas de risque de "tomber dans le vide" côté serveur.
### Néons
Sur le dedicated server, les `Light` ne sont pas créées (gain CPU + logs plus propres). Les clients les créent normalement. C'est sans impact gameplay.
## Workflow de debug
Côté serveur dedicated, les logs à surveiller :
```
[SyncAgrandissement] Agrandissement valide : 1_Nord (total : 1)
```
Côté client :
```
[SyncAgrandissement] Rpc recu, execution agrandissement : salle 1 direction Nord
AgrandissementSalle: PHASE 3 - Début construction sol
AgrandissementSalle: PHASE 7 - Construction complète !
```
Si tu vois :
```
[SyncAgrandissement] Mur introuvable : salle 1 direction Nord
```
Ça veut dire que le `salleId` ou la direction ne correspondent à aucun mur dans la scène. Vérifier que `SalleDatacenter.salleId` est bien = 1 (ou que le fallback "n'importe quel mur dans la bonne direction" trouve un match).
Si tu vois :
```
[PlayerAgrandissementBridge] Instance SynchronisationAgrandissement introuvable cote serveur !
```
Ça veut dire que le GameObject qui porte `SynchronisationAgrandissement` n'a pas bien été spawné côté serveur. Vérifier qu'il est présent dans la scène **au démarrage** (option B) ou dans `NetworkManager.spawnPrefabs` + spawné au `OnStartServer` (option A).
## Bugs connus limités
1. **Chat spam si plusieurs clients déclenchent en même temps** : si 2 clients cliquent [E] sur le même panneau dans la même frame, le serveur validera le premier et rejettera le second (log warning "deja agrandi"). Pas grave pour les joueurs.
2. **Anim qui démarre décalée** : la latence réseau entre Cmd et Rpc peut faire que les clients voient l'animation avec ~50-200ms de retard entre eux. Acceptable en coop standard.
3. **Animation coupée si un client disconnect pendant l'anim** : comme c'est local sur chaque client, ça ne casse rien pour les autres. Le joueur qui reconnect verra l'extension déjà construite via le rejeu.
## À implémenter plus tard (pas dans ce pack)
- Validation économique serveur-side (quand tu sortiras du sandbox)
- Persistence des agrandissements dans le SaveManager
- Support multi-salles pour l'agrandissement (actuellement `SynchronisationAgrandissement` trouve `FindObjectOfType<SalleDatacenter>()`, donc ça ciblera toujours la salle 1 ; à affiner quand tu auras plusieurs salles agrandissables)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,281 @@
// SynchronisationAgrandissement.cs
using UnityEngine;
using Mirror;
using System.Collections.Generic;
/// <summary>
/// Singleton NetworkBehaviour qui synchronise les agrandissements de salle entre
/// le serveur dedicated et tous les clients.
///
/// Pattern :
/// 1. Un client clique [E] sur un panneau d'agrandissement
/// -> AgrandissementSalle.DemanderAgrandir() trouve le singleton et appelle CmdAgrandir(id)
/// 2. Cote serveur, CmdAgrandir valide (pas deja agrandi) et diffuse RpcLancerAgrandissement(id)
/// 3. Chaque client (+ serveur) recoit le Rpc et execute l'animation locale d'Agrandir()
/// en parallele. Tous voient la meme animation car les parametres sont identiques.
/// 4. Un client qui rejoint en cours de partie recoit la SyncList des agrandissements deja
/// faits et les rejoue instantanement (sans animation).
///
/// A placer : en composant sur le GameObject du NetworkManager (ou tout objet avec
/// NetworkIdentity configure pour exister des le demarrage reseau).
///
/// L'identifiant d'un agrandissement est "salleId_direction", ex: "1_Nord".
/// </summary>
public class SynchronisationAgrandissement : NetworkBehaviour
{
public static SynchronisationAgrandissement Instance { get; private set; }
// Liste synchronisee des agrandissements deja effectues
// Format des entrees : "salleId_Direction" (ex: "1_Nord", "2_Est")
private readonly SyncList<string> _agrandissementsEffectues = new SyncList<string>();
private void Awake()
{
if (Instance != null && Instance != this)
{
Debug.LogWarning("[SyncAgrandissement] Instance deja existante, destruction de ce duplicata");
Destroy(this);
return;
}
Instance = this;
}
private void OnDestroy()
{
if (Instance == this) Instance = null;
}
// ============================================================
// CLIENT : appel par AgrandissementSalle.DemanderAgrandir()
// ============================================================
/// <summary>
/// Appele par un client qui veut agrandir. Envoie la demande au serveur.
/// En mode solo (pas de Mirror actif), execute directement localement.
/// </summary>
public void DemanderAgrandissement(int salleId, string direction)
{
string id = ConstruireId(salleId, direction);
// Mode solo (pas de serveur Mirror demarre) : execution directe locale
if (!NetworkServer.active && !NetworkClient.active)
{
Debug.Log($"[SyncAgrandissement] Mode solo, execution locale directe : {id}");
ExecuterAgrandissementLocal(salleId, direction, false);
return;
}
// Mode multi : Cmd vers serveur
// Il faut que le client ait authority. On passe par le player local qui
// porte ce role, ou directement si on est serveur nous-meme.
if (NetworkServer.active)
{
// Host ou dedicated : on est serveur, on peut traiter directement
TraiterDemandeServeur(salleId, direction);
}
else if (NetworkClient.active && NetworkClient.connection != null)
{
// Client pur : on doit passer par un PlayerBridge qui porte isLocalPlayer
PlayerAgrandissementBridge bridge = TrouverBridgeLocal();
if (bridge != null)
{
bridge.CmdDemanderAgrandissement(salleId, direction);
}
else
{
Debug.LogError("[SyncAgrandissement] Aucun PlayerAgrandissementBridge trouve sur le joueur local !");
}
}
}
// ============================================================
// SERVEUR : validation + broadcast
// ============================================================
/// <summary>
/// Appele cote serveur (soit par un host qui joue, soit par le bridge d'un client).
/// Valide et diffuse aux clients.
/// </summary>
[Server]
public void TraiterDemandeServeur(int salleId, string direction)
{
string id = ConstruireId(salleId, direction);
// Validation
if (_agrandissementsEffectues.Contains(id))
{
Debug.LogWarning($"[SyncAgrandissement] Demande refusee, deja agrandi : {id}");
return;
}
// Enregistrer dans la liste synchronisee (pour rejoin)
_agrandissementsEffectues.Add(id);
Debug.Log($"[SyncAgrandissement] Agrandissement valide : {id} (total : {_agrandissementsEffectues.Count})");
// Diffuser a tous les clients (le host le recoit aussi)
RpcLancerAgrandissement(salleId, direction);
}
// ============================================================
// CLIENTS : reception du Rpc
// ============================================================
[ClientRpc]
private void RpcLancerAgrandissement(int salleId, string direction)
{
Debug.Log($"[SyncAgrandissement] Rpc recu, execution agrandissement : salle {salleId} direction {direction}");
ExecuterAgrandissementLocal(salleId, direction, false);
}
// ============================================================
// REJOIN : un nouveau client rejoue l'etat actuel
// ============================================================
public override void OnStartClient()
{
base.OnStartClient();
// Ne pas rejouer sur le host (il est deja a jour, c'est lui le serveur)
if (NetworkServer.active) return;
// Le SyncList n'est pas forcement deja rempli a ce moment-la.
// On se branche sur ses callbacks et on rejoue les entrees existantes.
_agrandissementsEffectues.Callback += OnAgrandissementsListChanged;
// Rejouer immediatement les entrees deja presentes
if (_agrandissementsEffectues.Count > 0)
{
Debug.Log($"[SyncAgrandissement] Client rejoint : rejeu de {_agrandissementsEffectues.Count} agrandissement(s) passe(s)");
foreach (string id in _agrandissementsEffectues)
{
RejouerInstantane(id);
}
}
}
public override void OnStopClient()
{
base.OnStopClient();
_agrandissementsEffectues.Callback -= OnAgrandissementsListChanged;
}
private void OnAgrandissementsListChanged(SyncList<string>.Operation op, int index, string oldItem, string newItem)
{
// On ignore les changements qui arrivent par le flux normal (deja gere par Rpc)
// Ce callback ne sert qu'au cas ou la SyncList est modifiee avant que le client recoive
// le Rpc correspondant (race condition rare). Dans ce cas on rejoue aussi.
// En pratique on s'en remet surtout au rejeu initial dans OnStartClient().
}
private void RejouerInstantane(string id)
{
int salleId;
string direction;
if (!DecomposerId(id, out salleId, out direction))
{
Debug.LogWarning($"[SyncAgrandissement] Id mal forme : {id}");
return;
}
ExecuterAgrandissementLocal(salleId, direction, true);
}
// ============================================================
// EXECUTION LOCALE (cote serveur ET cote client)
// ============================================================
/// <summary>
/// Execute l'agrandissement sur l'instance locale : trouve le bon mur et lance Agrandir().
/// Si instantane=true, skip les animations (pour rejoin).
/// </summary>
private void ExecuterAgrandissementLocal(int salleId, string direction, bool instantane)
{
AgrandissementSalle mur = TrouverMurAgrandissement(salleId, direction);
if (mur == null)
{
Debug.LogWarning($"[SyncAgrandissement] Mur introuvable : salle {salleId} direction {direction}");
return;
}
if (mur.estAgrandi || mur.enAnimation)
{
Debug.Log($"[SyncAgrandissement] Agrandissement deja lance ou termine localement, skip");
return;
}
mur.AgrandirLocal(instantane);
}
private AgrandissementSalle TrouverMurAgrandissement(int salleId, string direction)
{
AgrandissementSalle.DirectionAgrandissement dirEnum;
if (!System.Enum.TryParse(direction, out dirEnum)) return null;
AgrandissementSalle[] tous = FindObjectsOfType<AgrandissementSalle>();
foreach (var a in tous)
{
if (a.direction != dirEnum) continue;
SalleDatacenter salle = FindObjectOfType<SalleDatacenter>();
if (salle == null) continue;
if (salle.salleId == salleId) return a;
}
// Fallback : si aucun salleId ne matche (salle mono, pas encore enregistree
// par le GestionnaireMultiSalles au moment de l'appel), on renvoie n'importe
// quel mur dans la bonne direction.
foreach (var a in tous)
{
if (a.direction == dirEnum) return a;
}
return null;
}
private PlayerAgrandissementBridge TrouverBridgeLocal()
{
PlayerAgrandissementBridge[] tous = FindObjectsOfType<PlayerAgrandissementBridge>();
foreach (var b in tous)
{
if (b.isLocalPlayer) return b;
}
return null;
}
// ============================================================
// HELPERS ID
// ============================================================
private static string ConstruireId(int salleId, string direction) => $"{salleId}_{direction}";
private static bool DecomposerId(string id, out int salleId, out string direction)
{
salleId = -1;
direction = "";
if (string.IsNullOrEmpty(id)) return false;
int sep = id.IndexOf('_');
if (sep <= 0 || sep >= id.Length - 1) return false;
if (!int.TryParse(id.Substring(0, sep), out salleId)) return false;
direction = id.Substring(sep + 1);
return true;
}
}
/// <summary>
/// Bridge Mirror : composant a ajouter au prefab du joueur pour qu'il puisse
/// envoyer une Cmd d'agrandissement vers le serveur (car les [Command] ne peuvent
/// etre appelees que depuis un NetworkBehaviour porte par le joueur local).
/// </summary>
public class PlayerAgrandissementBridge : NetworkBehaviour
{
[Command]
public void CmdDemanderAgrandissement(int salleId, string direction)
{
if (SynchronisationAgrandissement.Instance == null)
{
Debug.LogError("[PlayerAgrandissementBridge] Instance SynchronisationAgrandissement introuvable cote serveur !");
return;
}
SynchronisationAgrandissement.Instance.TraiterDemandeServeur(salleId, direction);
}
}