- Introduction ===============
Le Megacity Demo, lancé vers 2018, est une démonstration de taille réduite comparée à Book of the Dead, se concentrant sur l'illustration des fonctionnalités DOTS. Ayant du temps disponible récemment, j'ai entrepris de combler cette lacune. Ce projet est disponible sur Baidu Netdisk avec le lien et code d'accès fournis. Notez qu'il nécessite Unity 2019.1.0b7, et certains packages pourraient ne pas se télécharger correctement en raison de leur ancienneté, des ajustements manuels pouvant être requis.
- Composants Hybrides ECS ==========================
2.1 HLOD (Hierarchical Level of Detail)
Dans les sous-scènes du Megacity, plusieurs modèles intègrent des composants HLOD. Cette technique fuisonne les objets distants en un seul maillage lorsque le dernier niveau LOD est atteint, comme des bâtiments ou des poteaux électriques éloignés. La fusion peut être effectuée hors ligne pour générer des modèles combinés au préalable. L'inconvénient réside dans l'utilisation mémoire supplémentaire et les risques d'optimisation négative selon les projets. Dans Megacity, le script CombineMeshFromLOD.cs permet la création hors ligne de ces modèles. La logique HLOD est implémentée via ECS pour gérer l'affichage et le remplacement, nécessitant une correspondance entre le nombre de niveaux LOD dans le composant LOD Group et les LodParentTransforms du HLOD, comme illustré avec deux GameObjects de bas niveau représentant des niveaux HLOD multiples.
2.2 SubScene
Les SubScènes dans Unity DOTS permettent l'intégration de scènes secondaires, avec un noyau basé sur des interfaces de chargement en flux. Voici un exemple de code illustrant cette opération asynchrone :
operations[i] = new OperationChargementSceneAsynchrone(cheminBinaireEntites, tailleFichier, nombreComposantsPartages, cheminRessources, gestionnaireEntites);
scenes[i].EntiteScene = entite;
Cette fonctionnalité convertit également le contenu de la scène en un format binaire adapté au chargement en flux.
- Concepts préalables d'ECS ============================
3.1 Mécanisme de filtrage
Contrairement aux managers tradisionnels utilisant l'inscription/désinscription, ECS repose sur le filtrage des composants. Par exemple, dans le SystemeDetectionCollisions de Megacity, les boîtes de collision sont enregistrées via des composants de conversion Mono. Le code de filtrage dans le System ECS ressemble à :
groupeCollisions = ObtenirGroupeComposants(
new RequeteArchetypeEntite
{
Tous = new TypeComposant[] { typeof(ZoneEnglobante) },
Aucun = new TypeComposant[] { typeof(ConditionActivee) },
Nul = Tableau.Vide<typecomposant>(),
});</typecomposant>
Ici, les entités avec le composant ZoneEnglobante sont traitées, et la désinscription se fait en supprimant le composant pour les exclure du filtrage suivant.
3.2 Gestion des CommandBuffers dans les Jobs
Dans les Jobs ECS, les modifications directes des composants ne sont pas permises. On utilise des CommandBuffers pour les files d'attente opértaionnelles, similaires à ceux dans les pipelines de rendu. Un exemple de Job qui ajoute un composant via CommandBuffer :
public struct TacheDetection : IJobChunk
{
public EntityCommandBuffer.Concurrent tamponCommandesEntite;
public void Execute(ArchetypeChunk morceau, int indexMorceau, int premierIndexEntite)
{
tamponCommandesEntite.AjouterComposant(indexMorceau, nouvelleZone, new ConditionActivee());
}
}
3.3 Logique de marquage
Pour marquer les entités en collision, on ajoute un composant comme ConditionActivee via CommandBuffer. Le filtrage ultérieur exclut ces entités marquées, comme montré :
groupeCollisions = ObtenirGroupeComposants(
new RequeteArchetypeEntite
{
Tous = new TypeComposant[] { typeof(ZoneEnglobante) },
Aucun = new TypeComposant[] { typeof(ConditionActivee) },
Nul = Tableau.Vide<typecomposant>(),
});</typecomposant>
Dans d'autres systèmes, comme celui de l'audio, on récupère les entités marquées avec ConditionActivee et d'autres composants spécifiques.
- Corps du Megacity Demo =========================
4.1 Structure de la scène
La scène statique Megacity contient plusieurs éléments : Audio pour les configurations sonores utilisant le module ECS DSPGraph, Pathing pour les trajectoires des vaisseaux (un point de démonstration), et la logique du vaisseau joueur sans DOTS.
4.2 SystemePoolLumieres
Ce système gère les lumières dans le champ de vision de la caméra et des vaisseaux, avec réutilisation via un pool d'objets. Il exploite le HDRP pour intégrer les lumières avec les effets de brouillard volumétrique. Le script ReferenceLumiere.cs convertit les lumières en composants ECS. Dans la méthode OnUpdate, la logique se déroule en étapes : initialisation des nouvelles lumières sans balise TagCreationPool, filtrage par frustum et distance, allocation d'instances, et mise à jour de l'intensité avec extinction progressive.
4.3 Logique de diffusion en flux
La logique de chargement en flux est encapsulée car Unity SubScene ne fournit pas de gestion complète. Un composant de configuration ComposantConfigFlux est attaché au joueur, et le système utilise des CommandBuffers pour ajouter ou supprimer des balises SceneChargee aux entités, comme illustré :
struct TacheTamponCommandes : IJob
{
public EntityCommandBuffer tamponCommandes;
public NativeArray<entity> tableauAjouts;
public NativeArray<entity> tableauSuppressions;
public void Execute()
{
foreach (var entite in tableauAjouts)
{
tamponCommandes.AjouterComposant(entite, default(SceneChargee));
}
foreach (var entite in tableauSuppressions)
{
tamponCommandes.SupprimerComposant<scenechargee>(entite);
}
}
}</scenechargee></entity></entity>
4.4 SystemeAudioMegacity
Ce système utilise le module DSPGraph d'Unity ECS pour gérer l'audio, avec un débogage activé via la macro ENABLE_DSPGRAPH_INTERCEPTOR. Les sons sont gérés via des composants comme ComposantEmetteurECS, et les clips audio sont ajoutés au système de lecture :
var systemeLecture = Monde.Actif.ObtenirOuCreerManager<systemelectureechantillon>();
systemeLecture.AjouterClip(clip);</systemelectureechantillon>
Les entités audio sont créées avec des composants spécifiques pour le volume, la boucle et le pitch, identifiées par l'ID d'instance du clip.
4.5 Logique du trafic
4.5.1 Traitement des routes
Les chemins des vaisseaux NPC utilisent un système personnalisé avec des options d'édition comme l'affichage des poignées et des routes colorées, et des paramètres pour les voies d'accès.
4.5.2 Recherche de chemin des vaisseaux NPC
Les vaisseaux NPC utilisent l'interpolation CatmullRom pour calculer les trajectoires, avec une dérivée pour une vitesse constante :
public void Execute(ref CheminVehicule p, ref PositionCible pos, [ReadOnly] ref EtatPhysiqueVehicule etatPhysique)
{
var sectionRoute = sectionsRoute[p.IndexRoute];
float3 position = CatmullRom.ObtenirPosition(sectionRoute.p0, sectionRoute.p1, sectionRoute.p2, sectionRoute.p3, p.positionCourbe);
float3 tangente = CatmullRom.ObtenirTangente(sectionRoute.p0, sectionRoute.p1, sectionRoute.p2, sectionRoute.p3, p.positionCourbe);
float vitesseCourbe = longueur(tangente);
pos.PositionIdeale = position;
pos.VitesseIdeale = p.vitesse;
if (longueurCarree(etatPhysique.Position - position) < kCarreAttacheMax)
{
p.positionCourbe += Constantes.AjustementVitesseVehicule / sectionRoute.longueurArc * p.vitesse / vitesseCourbe * DeltaTempsSecondes;
}
}
4.5.3 Génération des vaisseaux NPC
Le Job de génération utilise des opérations atomiques pour les UID des vaisseaux, comme Interlocked.Increment, et des masques de bits pour sélectionner les types de vaisseaux via des calculs de bits.
4.5.5 Destruction des vaisseaux
Les vaisseaux sont détruits lorsqu'ils atteignent la fin de leur trajectoire :
public struct TacheDestructionVehicule : IJobProcessComponentDataWithEntity<cheminvehicule>
{
public EntityCommandBuffer.Concurrent tamponCommandesEntite;
public void Execute(Entity entite, int index, [ReadOnly] ref CheminVehicule vehicule)
{
if (vehicule.positionCourbe >= 1.0f)
{
tamponCommandesEntite.DetruireEntite(index, entite);
}
}
}</cheminvehicule>
4.5.6 Gestion des voies et occupation
Les vaisseaux NPC changent de voie si la leur est occupée, et accélèrent si la voie est dégagée devant.
4.5.7 Évitement d'obstacles
Un algorithme de hachage quantifie l'espace en cellules, avec une table de hachage multi-valeurs pour stocker les données des véhicules par cellule. L'évitement se fait via des calculs vectoriels pour détourner les vaisseaux des obstacles.
- Divers =========
5.1 ComponentDataFromEntity<T> pour l'accès rapide
Cette classe permet d'obtenir des composants par entité, maintenant appelée ComponentLookup<T> dans les versions récentes d'ECS.
5.2 DelayLineDopplerHack
Ce script tente d'implémenter l'effet Haas avec un traitement audio direct, comme montré :
var delaiHaas = (int)((s.attenuation[0] - s.attenuation[1]) * m_Haas * (c * 2 - 1));
var echantillonsDelai = Mathf.Clamp(echantillonsDelaiBase + delaiHaas, 0, longueurMax);
L'effet Haas influence la perception stéréo lorsque des sons arrivent avec un léger retard.
5.3 ChunkEntityEnumerable
Cet utilitaire simplifie l'itération dans les Jobs en gérant la pagination des chunks :
public bool Suivant()
{
if (++indexElement >= longueurMorceauActuel)
{
if (++indexMorceau >= morceaux.Length)
{
return false;
}
indexElement = 0;
morceauActuel = morceaux[indexMorceau].ObtenirTableauNatif(typeEntite);
longueurMorceauActuel = morceauActuel.Length;
}
return true;
}
Pour les démos Unity récentes, consultez les liens vers les versions 2022 et multijoueur de Megacity, ainsi que la rétrospective de Book of the Dead.