setNeedsDisplay (Objective C) ne fonctionne pas

MortyMarsMortyMars Membre
10 mai modifié dans Objective-C, Swift, C, C++ #1

Bonjour à tous,

Je me heurte à un problème de mise à jour d'une NSView, un plateau d'échiquier, qui ne se fait pas comme je le souhaiterais.
Ainsi, dans le cas d'une partie opposant un Joueur à l'IA, le temps de réaction du Joueur permet que l'affichage de l'échiquier fonctionne correctement et l'on peut visualiser les coups successifs même si parfois deux coups peuvent s'afficher en même temps...
Le véritable problème survient lorsque je fais jouer l'IA contre elle-même : aucun des coups successifs de l'IA (visibles en console) n'est affiché "en direct" à l'écran et ce n'est qu'après le dernier coup (interruption par échec, mat, ou NSAlert) que la mise à jour de la Vue s'effectue, ce qui est juste frustrant...
J'ai intercalé dans mon code des [maVue setNeedsDisplay:YES] entre les coups joués par l'IA
J'ai forcé l'appel de ces setNeedsDisplay dans le tread principal et non éventuellement en arrière plan, par un :

dispatch_async(dispatch_get_main_queue(), ^{
      [maVue setNeedsDisplay:YES];
});
ou par un : 
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(void){
                  [self LancerCoupNoirs];
                  [viewEC setNeedsDisplay:YES];
               });

J'ai retardé l'exécution des coups pour donner à la vue le temps de se rafraichir, par un :

[NSTimer scheduledTimerWithTimeInterval:0.02
                                                           target:self
                                                        selector:@selector(LancerCoupNoirs)
                                                       userInfo:nil
                                                        repeats:NO];

ou par un :

[NSTimer performSelector:@selector(LancerCoupNoirs) withObject:self afterDelay:1];

voire un :

NSTimeInterval delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
                     [self LancerCoupNoirs];
                  });

J'ai parcouru le net à la recherche de solutions -ce qui m'a d'ailleurs permis de constater que le problème est bien (trop) connu- mais aucune des solutions que j'ai pu me voir proposer (dont celles ci-avant) n'a fonctionné dans mon cas.
Le Grand Central Dispatch (GCD) revient souvent parmi les propositions mais je ne suis souvent parvenu pour ma part qu'à désynchroniser le déroulement du jeu en exécutant par exemple, successivement plusieurs coups Blancs ou Noirs...
À ce stade donc, je sèche désespérément !
Merci de vos expériences éventuelles à ce sujet, ou d'idées nouvelles à explorer.

Mots clés:

Réponses

  • CéroceCéroce Membre, Modérateur
    10 mai modifié #2

    Effectivement c'est un problème classique. Tout ce qui est interface utilisateur (IHM) doit être exécuté sur le thread principal. Si les calculs sont effectués sur ce même thread, alors ils bloquent l'affichage, qui ne sera rafraîchi qu'à la fin des calculs.

    La solution est d'effectuer les calculs sur un thread secondaire. Pour cela, on peut utiliser GCD, mais je te conseille l'utilisation de NSOperationQueue et de NSOperation, qui sont de plus haut niveau, donc plus faciles à utiliser et conviennent tout à fait pour ce que tu veux faire.

    L'idée est de créer une NSOperationQueue série (.maxConcurrentOperations = 1) et ensuite d'y ajouter le calcul de chaque "coup" sous la forme d'une NSOperation (vois NSBlockOperation, qui est assez pratique).

    La vraie difficulté sera de communiquer au thread principal que le calcul est terminé. La solution habituelle est d'utiliser une callback: il s'agit d'une closure (= un "block" en ObjC) passée en paramètre, qui sera appelée à l'issue du calcul.
    Enfin, attention! L'appel de la callback sera effectué dans le thread courant, donc celui de l'opération. Cela veut dire que ça ne peut pas directement effectuer des opérations d'interface utilisateur (qui doivent s'exécuter sur le thread principal — bis). Il faudra donc appeler les méthodes d'IHM sur le thread principal:

    dispatch_async(dispatch_get_main_queue(), ^{
        [self.view setNeedsDisplay];
    });
    

    Je ne donne pas beaucoup de code, réclame si besoin.

  • PyrohPyroh Membre

    Bah sinon un tick avec un NSTimer qui se répète et qu'on annule dès que la partie est finie.
    Tu pars avec ça :
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

    Avec un block du genre :

    ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            // Définir quelle couleur doit jouer
            // Faire jouer la couleur
            [self.view setNeedsDisplay];
            // Définir sur la partie est terminée et annuler le timer le cas échéant.
        });
    }
    

    Ça t'évite tout un tas de soucis avec un code toujours synchrone qui manipule le modèle et la mise à jour de la vue.
    Un tick par seconde et tu peux regarder les parties. Tu peux même t'autoriser une petite coquetterie et mettre un des contrôles de vitesse, un bouton pause, etc...

    Tu ferais du Swift je t'aurai conseillé Combine mais bon...

  • Merci à vous deux, Céroce et Pyroh, vous me redonnez l'espoir qui commençait à manquer :)
    Je me plonge vers ces contrées encore inexplorées et vous tiens au courant des avancées ;)

  • CéroceCéroce Membre, Modérateur

    @Pyroh a dit :
    Bah sinon un tick avec un NSTimer qui se répète et qu'on annule dès que la partie est finie.

    Même si ça ne coûte pas grand chose d'essayer, je doute que ça fonctionne. Les NSTimers sont exécutés par la Runloop, et si le thread principal est bloqué, alors les itérations de la runloop sont bloquées.

  • @Cérore et @Pyroh
    Des nouvelles pistes vers lesquelles vous m'avez redirigé, j'ai compris qu'il ne s'agissait pas seulement d'exécuter les méthodes d'IHM dans le thread principal, mais tout aussi essentiellement d'exécuter les méthodes de calcul dans un thread secondaire.
    Le code auquel je suis parvenu est le suivant :

        // Insertion de code dans un thread secondaire 
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            int compteur = 0;
            // Tant qu'aucun des camps n'est Pat ou Mat 
            // et que le compteur n'a pas atteint 20
            while (([Minimax PossibleMovesForSide:sideWhite board:boardEC].count!=0) &&
                   ([Minimax PossibleMovesForSide:sideBlack board:boardEC].count!=0) &&
                   (compteur < 20)) {
    
               // L'IA joue pour les Blancs (qui ne sont ni Pat ni Mat) en AR plan
               [viewEC MakeIAMoveForSide:sideWhite Board:boardEC];
    
               // Mise à jour du ChessView après incursion dans le thread principal
               dispatch_async(dispatch_get_main_queue(), ^{
                  [viewEC setNeedsDisplay:YES];
               });
    
               // Si les Noirs ne se retrouvent pas Pat ou Mat après le coup Blancs...
               if ([Minimax PossibleMovesForSide:sideBlack board:boardEC].count!=0) {
                  // ... alors l'IA joue pour les Noirs en AR plan
                  [viewEC MakeIAMoveForSide:sideBlack Board:boardEC];
    
                  // Mise à jour du ChessView après nouvelle incursion 
                  // dans le thread principal
                  dispatch_async(dispatch_get_main_queue(), ^{
                     [viewEC setNeedsDisplay:YES];
                  });
               }
            }
            compteur++;
         });
    

    Dans le principe ça fonctionne plutôt bien, même si 'esthétiquement' il y aurait lieu de ralentir la succession des coups. Mais malheureusement d'autres soucis apparaissent désormais : une interruption brutale du programme après plusieurs coups, accompagnée d'un message d'erreur :
    "NSWindow drag regions should only be invalidated on the Main Thread!"
    Je suis persuadé que c'est dû à diverses NSAlert présentes dans la méthode gérant les coups de l'IA pour signaler une mise en échec ou un mat, ...NSAlert qui ne supportent pas d'être appelées d'un thread secondaire.
    Je suis donc un peu empêtré dans mon code, hésitant à réécrire certaines méthodes pour gérer ces fameuses NSAlert et/ou les threads, ou à créer des versions 'silencieuses' des méthodes impliquées

    Je n'abandonne pas pour autant vos propositions de code, mais j'avoue ne pas encore être parvenu à les assimiler complètement, puisque j'ai quelques difficultés pour l'instant à comprendre les NSOperation et à gérer efficacement les blocks...

    J'ai donc encore pas mal de choses à régler avant de voir le bout de cette affaire ;)

  • klogklog Membre
    15 mai modifié #7

    Tu dois encapsuler tous les appels à l'interface qui auraient lieu dans ton thread secondaire, dans des :

    dispatch_async(dispatch_get_main_queue(), ^{
    ...
    });

  • Merci Klog 😉
    Ça confirme une des solutions envisagées, mais j'ai certaines NSAlert qui utilisent des variables qui elles aussi doivent être encapsulées sans être isolées du reste du code. Ça ne fonctionnera donc pas partout.
    D'où ma réflexion plus globale pour une solution standard
  • PyrohPyroh Membre

    @Céroce a dit :

    @Pyroh a dit :
    Bah sinon un tick avec un NSTimer qui se répète et qu'on annule dès que la partie est finie.

    Même si ça ne coûte pas grand chose d'essayer, je doute que ça fonctionne. Les NSTimers sont exécutés par la Runloop, et si le thread principal est bloqué, alors les itérations de la runloop sont bloquées.

    Tu as totalement raison sur le principe. Mais comme là le soucis était que les actions s'enchainaient sans qu'on ait le temps de les apprécier je suis parti du principe que le main thread n'était pas trop encombré 😉

  • CéroceCéroce Membre, Modérateur

    @MortyMars a dit :smile:
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    Je ne ferais pas ça: cet appel renvoie une queue système partagée, dont tu connais pas vraiment les caractéristiques.
    Je réitère que tu devrais créer ta propre queue. Tu peux utiliser GCD, mais NSOperationQueue est plus pratique et repose sur GCD de toute façon.
    Ensuite utilise bien une queue série, autrement les coups seront calculés en parallèle, je te laisse imaginer le désastre.

    Dans le principe ça fonctionne plutôt bien, même si 'esthétiquement' il y aurait lieu de ralentir la succession des coups.

    C'était prévisible.

    Je suis persuadé que c'est dû à diverses NSAlert présentes dans la méthode gérant les coups de l'IA pour signaler une mise en échec ou un mat

    Oui c'est le cas. De toute façon, mélanger IHM et code métier est une mauvaise pratique. Mais les séparer est forcément plus difficile car tu vas devoir trouver un moyen de faire communique ces deux domaines. Typiquement tu peux utiliser la délégation, même si dans le code moderne ou utiliserait peut-être des callbacks sous forme de closures ("blocks").

    Je n'abandonne pas pour autant vos propositions de code, mais j'avoue ne pas encore être parvenu à les assimiler complètement, puisque j'ai quelques difficultés pour l'instant à comprendre les NSOperation et à gérer efficacement les blocks...

    Je te montre un exemple très simple:

    @property NSOperationQueue *gameQueue;
    
    -(instancetype) init {
        self = [super init];
    
        _gameQueue = [[NSOperationQueue alloc] init];
        _gameQueue.maxConcurrentOperation = 1; // serial queue
    
        return self;
    }
    
    -(void) queueComputationOfNextMove:(void (^)(void))moveComputation {
        [self.gameQueue addOperation:[NSBlockOperation blockOperationWithBloc: moveComputation]];
    }
    

    J'ai donc encore pas mal de choses à régler avant de voir le bout de cette affaire ;)

    Les closures ne sont pas si difficiles à utiliser mais tu entres dans la programmation concurrente, ce qui n'est jamais simple. L'autre difficulté est la communication des classes.

    P.S.: Très utile:
    http://fuckingblocksyntax.com

Connectez-vous ou Inscrivez-vous pour répondre.