Jeu de la vie

Soyez le premier à donner votre avis sur cette source.

Vue 11 903 fois - Téléchargée 1 338 fois

Description

Une implémentation en C# du jeu de la vie, illustrant certaines méthodes permettant de gagner en rapidité d'exécution :
- Utilisation du ThreadPooling pour traiter en parallèle de grandes quantités de données.
- Création de graphisme directement à partir d'un tampon d'octets.
Cette implémentation permet aussi de charger ses propres "formes" et d'observer leur comportement.

Divers :
- La vitesse est limité à 100 génération / secondes.
- Si une cellule vivante est entourée par 2 ou 3 cellules vivantes elle survie, sinon elle meurt.
- Si une cellule morte est entourée par 2 cellules vivantes, elle revit.
- Le tiers central est laissé vide pour laisser place à une image Bitmap choisie par l'utilisateur.

Source / Exemple :


//-----------------------
        // Prend le tampon pour appliquer les règles de développement, Paramètre : l'indice du thread
        //-----------------------
        static private void Génération(object Numéro)
        {
            //Détermine sur quelle partie du buffer on travail
            int NuméroThread = (int)Numéro;
            int CellulesTotales = Programme.Fenetre.Hauteur * Programme.Fenetre.Largeur;
            int NombreDeCellules = CellulesTotales / Programme.Threads;
            int OffsetDépart = NuméroThread * NombreDeCellules;
            int TailleLigne = Programme.Fenetre.Largeur;
            byte[] Tampon = Programme.TamponModèle;

            //Remplissage
            for (int Indice = 0; Indice < NombreDeCellules; Indice++)
            {
                //Offset des cellules voisines
                int HautGauche = OffsetDépart - TailleLigne - 1;
                int HautMilieu = OffsetDépart - TailleLigne;
                int HautDroite = OffsetDépart - TailleLigne + 1;

                int MilieuGauche = OffsetDépart - 1;
                int MilieuMilieu = OffsetDépart;
                int MilieuDroite = OffsetDépart + 1;

                int BasGauche = OffsetDépart + TailleLigne - 1;
                int BasMilieu = OffsetDépart + TailleLigne;
                int BasDroite = OffsetDépart + TailleLigne + 1;

                //Valeur des cellules voisines à partir du tampon modèle
                //Système d'élimination des bords approximatif, mais plus rapide
                byte CelulleHautGauche;
                byte CelulleHautMilieu;
                byte CelulleHautDroite;
                byte CelulleMilieuGauche;

                if (HautGauche > 0)
                {
                    CelulleHautGauche = Tampon[HautGauche];
                    CelulleHautMilieu = Tampon[HautMilieu];
                    CelulleHautDroite = Tampon[HautDroite];
                    CelulleMilieuGauche = Tampon[MilieuGauche];                    
                }
                else
                {
                    CelulleHautGauche = 255;
                    CelulleHautMilieu = 255;
                    CelulleHautDroite = 255;
                    CelulleMilieuGauche = 255;
                }

                byte CelulleMilieuMilieu = Tampon[MilieuMilieu];

                byte CelulleMilieuDroite;
                byte CelulleBasGauche;
                byte CelulleBasMilieu;
                byte CelulleBasDroite;
                if (BasDroite < Programme.TamponTravail.Length)
                {
                    CelulleMilieuDroite = Tampon[MilieuDroite];
                    CelulleBasGauche = Tampon[BasGauche];
                    CelulleBasMilieu = Tampon[BasMilieu];
                    CelulleBasDroite = Tampon[BasDroite];
                }
                else
                {
                    CelulleMilieuDroite = 255;
                    CelulleBasGauche = 255;
                    CelulleBasMilieu = 255;
                    CelulleBasDroite = 255;
                }

                int Total = 0;
                if (CelulleHautGauche == 0) Total++;
                if (CelulleHautMilieu == 0) Total++;
                if (CelulleHautDroite == 0) Total++;
                if (CelulleMilieuGauche == 0) Total++;
                if (CelulleMilieuDroite == 0) Total++;
                if (CelulleBasGauche == 0) Total++;
                if (CelulleBasMilieu == 0) Total++;
                if (CelulleBasDroite == 0) Total++;

                //Case actuelle noire : prend la 'bonne' couleur si il y en 3 autres à coté
                if (CelulleMilieuMilieu != 0)
                {
                    //Résultat :
                    if (Total == 3) Programme.TamponTravail[MilieuMilieu] = 0;
                    else Programme.TamponTravail[MilieuMilieu] = 255;
                }                
                //Case actuelle pleine : reste en vie seulement si 2 ou 3 voisines
                else
                {
                    //Résultat :
                    if (Total == 3 || Total == 2) Programme.TamponTravail[MilieuMilieu] = 0;
                    else Programme.TamponTravail[MilieuMilieu] = 255;
                }

                OffsetDépart++;
            }
            
            //Indique qu'une opération est fini
            lock (Programme.Verrou) { Programme.Complétion++; }
        }

Conclusion :


// Mise à jour 06/11/2011 :

- Modification de la classe 'Pixel' en classe 'Couleur', plus succincte.
- La création de l'image est désormais elle-aussi accélérée par ThreadPooling.
- Factorisation du code déterminant la vie ou la mort d'une cellule.
- Utilisation d'une référence directe au tampon contenant l'image précédente pour éviter l'appel (lent) à l'accesseur.

- L'IHM affiche désormais le temps de conversion 'Cellules -> Image' en plus du temps de génération.
- L'IHM utilise désormais des timers pour l'affichage CPU/Mémoire au lieu de l'afficher à chaque génération.
- Le chargement d'image s'adapte désormais aux dimensions (levée de la contrainte de 64x64 pixels).
- Changement du système qui vérifiait la fin des ThreadPool : chaque thread a désormais sa propre 'case', ce qui évite les problèmes de concurrence.

-----------------------------------------------------------------------------

N'hésitez pas à poser vos questions / remarques / conseils / etc ...

Merci ;-)

Codes Sources

A voir également

Ajouter un commentaire

Commentaires

Messages postés
83
Date d'inscription
samedi 21 mai 2005
Statut
Membre
Dernière intervention
22 mars 2011

Pour Color je ne pense pas que ça vienne d'un problème de mémoire / collection, mais plutôt du fait que les valeurs RVB y sont stockées sous formes de propriétés et non d'attributs. Du coup on récupère la valeur via un accesseur (qui est une fonction) ce qui est sensiblement plus long que de lire directement un attribut, comme dans le cas de ma classe 'Couleur'.

Pour la classe Task je l'avais survolée un moment mais apparemment il s'agit juste d'un 'emballage' autour des ThreadPool, et les objets 'Task' semblent à usage unique. Je vais quand même faire un essai au cas où.

Quand à l'assembleur c'est le langage que j’utilisai (x86) avant de me mettre au C#. Ça laisse des habitudes reconnaissables on dirait ;-)
Messages postés
2
Date d'inscription
lundi 26 novembre 2007
Statut
Membre
Dernière intervention
7 novembre 2011

C'est intéressant, même sans P.O.O !
En tout cas, j'ai appris quelque chose, c'est l'essentiel.

Ca me rapelle un jeu de la vie écrit en assembleur Z80, écrit en 1986 ... j'avais touché 300F(et oui, pas de problèmes avec l'euro en ce temps là...) de la revue "Amstrad CPC"!
Messages postés
234
Date d'inscription
jeudi 18 janvier 2007
Statut
Membre
Dernière intervention
3 novembre 2011

Si ton objectif était d'obtenir les meilleurs performances possibles alors c'est assez réussi. Au passage, je salue le soin que tu as apporté à ton code (d'où ma note assez positive). Je trouve juste un peu frustrant que ce code soit si fastidieux à comprendre et à s’approprier pour d'éventuelles modifications. Mais si selon toi cette approche est la plus approprié pour obtenir des performances robustes alors je te fais confiance ;)

Concernant System.Drawing.Color, c'est assez surprenant que cela entraîne une chute des performances. System.Drawing.Color étant une structure elle devrait justement optimiser les performances du fait qu'elle ne laisse aucun déchet (pas de pointeur) et du coup, ne fait jamais intervenir le Garbage Collector.

Un petit conseil : As-tu déjà exploré les solutions du namespace System.Threading.Task ? Il fournit des méthodes pour le traitement parallèle sur plusieurs cores. Cela pourrait éventuellement accroître d'avantage les perf de ton logiciel.

Simon
Messages postés
83
Date d'inscription
samedi 21 mai 2005
Statut
Membre
Dernière intervention
22 mars 2011

J'ai l'impression que tu te fait une fausse idée de l'objectif de cette source ... J'ai clarifié la présentation, qui n'était peut être pas assez explicite. Le but n'est pas de publier un FrameWork d'automate cellulaire, ou de montrer l'application de la POO sur une problématique qui ne s'y prête pas, mais de montrer comment gagner en vitesse d’exécution en C#.

Concernant ma méthode 'obèse', le contenu de cette boucle est appelé 480.000 fois toutes les 10 millisecondes ... vouloir insérer des appels de fonction ou d'interface dedans pour le seul plaisir d'avoir découpé le code reviendrait à se tirer une balle dans le pied quand on connait le coût réel d'un appel de fonction.

De même, j'ai testé l'utilisation de la classe 'Color' : l'utilisation d'accesseurs (donc de fonctions) pour récupérer les valeurs RVB rallonge de plus de 20% le temps de création total de l'image ... donc je vais rester sur ma version 'Maison'.

Concernant le CheckForIllegalCrossThreads, oui c'était nécessaire dans la mesure où l'architecture du logiciel fait qu'un seul thread à la fois accède à un élément donné de la WinForm. On s'épargne ainsi de devoir accéder à chaque label à travers des 'InvokeRequired + Delegate', qui est une approche relativement lourde.
Messages postés
234
Date d'inscription
jeudi 18 janvier 2007
Statut
Membre
Dernière intervention
3 novembre 2011

Une approche objet aurait pu simplement optimiser la qualité de ton code. Répartir le travail à travers plusieurs entités t'aurait éviter d'avoir des méthodes obèses telle que celle que tu as publié dans la présentation. La majorité des lecteurs ne s'aventureront pas à décrypter une pareille méthode. Tu aurais pu gagner en extensibilité aussi. Définir des interfaces définissants les règles de vie ou de mort d'une cellule par exemple. N'importe qui pourrait ainsi définir ses propres règles sans avoir à lire et comprendre la totalité du code. Certes pour un code personnel je pense que l'approche que tu utilises et la POO sont assez équivalentes dans la mesure ou tu connais sans doute ton code par coeur. Mais lorsqu'il doit être publié et compris par d'autres personnes, l’efficacité de la POO n'est plus à démontrer...

Autre chose, désactiver CheckForIllegalCrossThreads était vraiment nécessaire ?

Simon
Afficher les 7 commentaires

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.