IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Développement de jeux avec Vala et SDL


précédentsommaire

III. Sprites

III-A. Introduction

Dans la première partie, nous avons passé du temps à tout configurer proprement avec notre projet et des outils automatiques. Maintenant que c'est fait, nous allons pouvoir faire des choses amusantes ! Dans cet article, nous allons créer une classe sprite, un gestionnaire de sprites ainsi que le début d'une application qui utilisera nos nouvelles classes pour faire une petite démonstration graphique. Je vais expliquer les nouvelles classes en détail donc vous pourrez trouver plus facile, afin de suivre ce tutoriel, de télécharger les sources pour cet article depuis ici.

III-B. src/Makefile.am

Oh non, pas encore des outils automatiques ! Malheureusement, si, encore des outils automatiques. Heureusement, c'est un changement très simple à faire. Nous avons besoin d'ajouter deux nouveaux fichiers à notre fichier src/Makefile.am afin de permettre à valac de les compiler :

 
Sélectionnez
...brickval_VALASOURCES = \
        brickval.vala \
        spritemanager.vala \
        sprite.vala
...

Ce n'était pas si terrible ! Comme je l'ai mentionné dans le précédent article, lorsque les choses sont mise en place avec des outils automatiques, la majeure partie du travail à faire est aussi simple que d'ajouter quelques noms de fichiers ici et là.

III-C. src/brickval.vala

C'est la classe principale de notre jeu. Commençons en changeant ça. Pour l'instant, effacez tout dans le fichier mis à part la ligne d'utilisation de Glib ainsi que l'information sur la licence. Ensuite, ajoutez les lignes suivantes après la ligne glib :

 
Sélectionnez
using SDL;
using SDLImage;
using Gee;

Vala utilise des paquets comme interface avec des bibliothèques existantes. Au lieu d'utiliser des #include, dans un projet Vala, vous utilisez un paquet. Dans ce cas, nous sommes intéressés par SDL, SDLImage et Gee. Gee est un package écris en Vala fournissant quelques types de structures de données qui utilisent les génériques (qui est une façon d'utiliser les même structures de données pour n'importe quel type de données). SDL est une bibliothèque très mature et très utilisée qui est une solution multi-plateforme pour générer des graphiques, du son ainsi que capturer les entrées / sorties. SDLImage est une bibliothèque SDL qui ajoute un support robuste dans le chargement des images que nous allons utiliser afin de charger les données de nos sprites.

Ensuite, nous allons définir notre classe et les champs pour la classe :

 
Sélectionnez
public class Brickval : Object {
    private const int SCREEN_WIDTH = 640;
    private const int SCREEN_HEIGHT = 480;
    private const int SCREEN_BPP = 32;
    private const int DELAY = 10;

    public weak SDL.Screen screen;
    private GLib.Rand rand;
    private bool done;

    private SDL.Surface bg;
    private SpriteManager spriteManager;
    private ArrayList<Sprite> sprites;

Vala ne compile pas directement vers du C, il compile vers du 'C/Gobject'. Afin de bénéficier de beaucoup de caractéristiques de Vala, votre classe devra hériter de la classe Object. Cela nous garantie que lorsque votre classe sera compilée, elle devient à part entière un GObject et sera capable de s'intégrer proprement avec ce système. Vala n'intègre pas de préprocesseur pour gérer les définitions mais elle permet un comportement similaire avec les variables const. Ce sont des variables qui une fois définies ne peuvent pas être changées. L'avantage de l'utilisation des variables constantes par rapport aux définitions du préprocesseur est que, comme elles sont en fait des variables réelles, elles n'évitent pas les mécanismes de vérification de type par le compilateur et évite une partie des erreurs possibles.

Une autre chose à noter est que la syntaxe utilisée pour les génériques avec Vala est quasi identique au C# ou à Java et les utilisateurs de ces langages ne seront donc pas dépaysés. Il en est de même pour le modificateur de porté weak utilisé. Comme Vala fournit une gestion de la mémoire via un système de comptage de références quand une classe arrive à expiration ou est explicitement libérée, Vala essaye de désallouer la mémoire utilisée par cette classe. Comme, à l'origine, Vala est une façon d'utiliser un langage moderne, orienté objet, sans casser la compatibilité ABI(1) du C, il y a des fois où quand vous interfacez avec des bibliothèques qui n'utilisent pas GObject. Comme le comptage de références ne fonctionnera pas sur ces champs non-GObject, nous avons besoin de les marquer explicitement afin que valac sache qu'il n'a pas à les traiter. Ceci est accompli en déclarant le champ comme une référence faible. Cette référence faible existe en dehors du système de gestion de la mémoire de Vala et doit être traité par le programmeur.

Maintenant que nous avons défini notre classe et ses champs, passons en revu chaque méthode dans la classe.

III-C-1. construct

construct
Sélectionnez
construct {
    this.rand = new GLib.Rand ();
}

Comme Vala compile vers du 'C/GObject', les constructeurs sont dans une zone qui peut être légèrement non familier par rapport à ceux habitués à d'autres langages qui arrivent à ressembler un peu à Vala. Un constructeur standard ressemble à :

constructeur standard
Sélectionnez
public MyClass(int foo, bool bar) {
...
}

... ne peut être utilisé dans Vala pour faire autre chose que définir des propriétés. Ceci est dû à la façon dont GObject lui même instancie les classes. Pour permettre aux constructeurs de faire plus que définir des propriétés, Vala fournit un nombre différent de types de constructeurs anonymes. Ceux-ci incluent construct (qui est celui que vous utiliserez le plus), qui est lancé à chaque instanciation d'une classe, class construct qui sera lancé seulement à la première instanciation d'une classe, et static construct qui est lancé une fois à la définition de la classe et plus jamais ensuite.

Dans notre cas, nous créons simplement une instance de GLib.Rand.

III-C-2. main

main
Sélectionnez
public static int main (string[] args) {
    SDL.init(InitFlag.EVERYTHING);

    var app = new Brickval();
    app.run();

    SDL.quit();

    return 0;
}

C'est notre fonction main et la fonction est actuellement exécutée lorsque nous lançons notre programme vala. La définition de la fonction static semblera très familière aux développeurs C# et Java. Chaque argument du programme est passé par un tableau de chaines de caractère args. Actuellement, nous ne faisons rien avec les arguments passés. Pour démarrer, nous initialisons SDL avec le InitFlag EVERYTHING qui dit à SDL de démarrer tous ses sous-systèmes. Ensuite, nous créons une instance de notre classe Brickval et nous appelons sa méthode run. La méthode run est la boucle principale de notre jeu et nous la verrons en détail par la suite. Lorsque run se termine, cela veut dire qu'il est temps de tout éteindre donc nous appelons simplement SDL.quit() pour quitter SDL proprement.

Regardons maintenant à quoi ressemble notre boucle principale.

III-C-3. run

run
Sélectionnez
public void run () {
    this.init_video ();
    spriteManager = new SpriteManager();
    sprites = new ArrayList<Sprite>(direct_equal);

    InitSprites();
    while (!done) {
        this.update();
        this.draw ();
        this.process_events ();
        SDL.Timer.delay (DELAY);
    }
}

Comme vous pouvez le voir, notre boucle principale est très compacte. Nous faisons quelques initialisations et ensuite, tant que la variable done est à faux, nous bouclons sur les différents systèmes. A noter qu'actuellement, nous faisons une pause un temps constant avant de continuer notre boucle. Dans un futur article, nous changerons ça par une autre méthode qui nous assurera un nombre fixe de frames par boucle, ce qui nous apportera une mise à jour constante indépendamment de la vitesse de l'ordinateur sur lequel notre programme tournera.

En regardant brièvement l'initialisation des fonctions, nous voyons que nous initialisons la vidéo en premier. Ensuite, nous instancions deux classes : SpriteManager et ArrayList. La classe SpriteManager gère tous les sprites étant affichés n'importe où et vous permet des opérations variées. Nous explorerons cette classe un peu plus tard. L'instanciation d'ArrayList créé une liste où nous stockons toutes les instances du sprite courant. La raison de ce mécanisme de liste de sprites sera plus claire plus tard mais généralement, nous faisons ça car le gestionnaire de sprite contient l'ensemble des sprites et si nous voulons ajouter une granularité plus fine à cet ensemble, nous avons besoin d'un mécanisme afin de le faire séparément. Ceci est une faiblesse dans la classe courante SpriteManager que nous corrigerons dans un futur article.

InitSprites fais ce que vous pensez qu'il fait, comme son nom l'indique, il initialise les sprites pour permettre leur utilisation. La partie restante est simplement la boucle et fait ce qu'on attend d'elle. Nous allons voir en détail chacune de ces fonctions plus tard. Commençons par la première : init_video.

III-C-4. init_video

init_video
Sélectionnez
private void init_video () {
    uint32 video_flags = SurfaceFlag.DOUBLEBUF
            | SurfaceFlag.HWACCEL
            | SurfaceFlag.HWSURFACE;

    this.screen = Screen.set_video_mode (SCREEN_WIDTH, SCREEN_HEIGHT,
        SCREEN_BPP, video_flags);
    if (this.screen == null) {
        GLib.error ("Could not set video mode.");
    }

        this.bg = SDLImage.load(Config.BACKGROUND_DIR + "/bg.png");

        SDL.WindowManager.set_caption ("Brickval " + Config.VERSION, "");
}

C'est une fonction intéressante car elle montre beaucoup de choses que nous avons mis en place précédemment. Premièrement, nous définissons plusieurs options que nous assemblons ensemble avec l'opérateur OR afin de former au final une variable de 32 bits. Cela a pour but d'activer le double buffering, l'accélération matérielle et l'usage de la vram matérielle respectivement. Ensuite, nous créons notre surface de sortie en appelant la méthode de classe set_video_mode depuis la classe SDL.Screen. Si cette fonction retourne null alors quelque chose ne s'est pas bien passé. Nous vérifions donc cette condition et retournons un message d'erreur si cela arrive. L'erreur fera quitter le programme après l'affichage du message d'erreur.

Nous utilisons le paquet SDLImage afin de charger toutes les images. C'est une très bonne bibliothèque qui nous permet de charger beaucoup de format différent d'image. Le paquet inclut une méthode load qui utilise quelques heuristiques afin de déterminer quel type d'image nous sommes en train de charger. Elle retourne un objet SDL.Surface initialisé que nous assignons à notre champ bg.

Le dernier appel à la méthode set_caption de SDL.WindowManager change simplement le titre de la fenêtre.

Notre appel à Config.<FOO> utilise le paquet config.vapi que nous avons créé plus tôt pour mettre les informations des définitions générées par les outils automatiques. Ceci est spécialement utile pour le chargement d'image comme nous n'avons aucune garantie sur l'endroit où l'utilisateur installera les données. Cela pourrait, par exemple, être dans /usr/share/brickval/backgrounds ou /usr/local/share..., /opt/usr/share, etc. En utilisant cette méthode, nous n'avons pas besoin de savoir en tant que développeur où les données seront installées précisément.

III-C-5. InitSprites

InitSprites
Sélectionnez
private void InitSprites() {
        for(int i = 0; i < 100; ++i)
        {
                Sprite s = spriteManager.Get(spriteManager.Add(Config.SPRITE_DIR + "/bricks.png"));
                s.InitSpriteSheet(40, 20, 21, true);

                s.X = rand.int_range(0, screen.w);
                s.Y = rand.int_range(0, screen.h);
                s.XV = (rand.next_int() % 2 == 0) ? -1 : 1;
                s.YV = (rand.next_int() % 2 == 0) ? -1 : 1;

                s.SetCurrentCell(rand.int_range(0, s.CellCount));
                if(s.CurrentCell == 6) s.SetCurrentCell(7);

                sprites.add(s);
        }
}

Notre fonction InitSprites utilise notre classe SpriteManager pour créer un nouvel objet sprite. Dans ce cas, nous créons 100 sprites, les mettons à une position aléatoire sur l'écran, leur donnons une direction de départ aléatoire, et définissons l'image qui sera utilisée. nous n'allons pas expliquer en détail ce que font chacune des méthodes Sprite dans cette section, nous allons garder ça pour plus tard dans l'article.

III-C-6. update

update
Sélectionnez
private void update() {
        UpdateSprites();

        spriteManager.Update();
}

C'est la méthode update qui est appelée dans la boucle principale. Vous pouvez voir que nous appelons une méthode nommée UpdateSprites et ensuite la méthode Update() du SpriteManager. La raison de cette double mise à jour vient de la particularité de la classe SpriteManager. Nous allons couvrir ça plus en détail plus tard dans l'article, cependant, vous devez juste être conscient que UpdateSprites() contient la logique pour le comportement des sprites (ie, ce qu'ils font), et le SpriteManager quand à lui gère la mise à jour des coordonnées des sprites, leur affichage, etc.

III-C-7. UpdateSprites

UpdateSprites
Sélectionnez
private void UpdateSprites() {
        Iterator<Sprite> iter = sprites.iterator();
        while(iter.next())
        {
                Sprite s = iter.get();
                if(s.X < 0)
                        s.XV = 1;

                if( (s.X + s.Width) > screen.w)
                        s.XV = -1;

                if(s.Y < 0)
                        s.YV = 1;

                if( (s.Y + s.Height) > screen.h)
                   s.YV = -1;
        }
}

C'est la logique qui conduit le comportement de nos sprites. Nous utilisons un Gee.Iterator pour parcourir notre ArrayList de sprites que nous avons créé dans InitSprites. Nous vérifions si un bout du sprite est en dehors de l'écran et si c'est le cas, on inverse la vitesse de ce sprite, donc il bougera dans la direction opposée.

Il serait mieux, on pourrait l'exposer, si le sprite contenait le comportement qu'il devrait avoir sans avoir à gérer une liste séparée. Dans un futur article, nous ajouterons cette capacité à nos sprites lorsque nous explorerons l'utilisation des méthodes déléguée de vala.

III-C-8. draw

draw
Sélectionnez
private void draw () {
        Rect sr, dr;

        sr.x = 0;
        sr.y = 0;
        sr.w = (uint16)this.bg.w;
        sr.h = (uint16)this.bg.h;

        dr.x = 0;
        dr.y = 0;
        dr.w = sr.w;
        dr.h = sr.h;

        this.bg.blit(sr, screen, dr);

        spriteManager.Render(screen);

    this.screen.flip ();
}

C'est la méthode draw() qui est appelée dans la boucle principale. Après que la logique pour les sprites soit gérée, nous pouvons les mettre dans une nouvelle frame. Lorsque nous utilisons le double buffering dans SDL, chaque blit que vous faites est fait dans un buffer temporaire non affiché. Ensuite, une fois que tout le blitting est fini, on peut appeler flip() sur l'écran qui remplacera le buffer d'affichage par le nouveau buffer nouvellement dessiné. Cela permet que tout le processus de dessin soit terminé avant que l'utilisateur puisse voir la frame mise à jour ce qui fais une animation plus fluide.

Pour afficher la nouvelle frame, nous remplissons deux Rects avec des données. Le premier, sr, est le rect source et est le rectangle utilisé pour définir où et combien de la donnée graphique dans la surface source à utiliser. Le rect dr, Rect de destination, est le même, mais définit où et combien à blitter sur la surface de destination. Nous voulons blitter l'image de fond entière à l'écran donc nous définissons le début du rectangle source aux cordonnées 0,0 (le coin haut gauche de la texture de fond) et on met la largeur et hauteur entière du fond. Ensuite, nous définissons le rect source aux mêmes valeurs donc l'image de fond remplie entièrement l'écran. Après que les rects soient mis en place, nous pouvons ensuite blitter la texture de fond sur l'écran.

Après que l'image de fond soit dessinée, nous demandons ensuite au SpriteManager de dire à chaque sprite de se rendre eux même sur l'écran.

III-C-9. process_events

process_events
Sélectionnez
private void process_events () {
    Event event;
    while (Event.poll (event) == 1) {
        switch (event.type) {
        case EventType.QUIT:
            this.done = false;
            break;
        case EventType.KEYDOWN:
            this.on_keyboard_event (event.key);
            break;
        }
    }
}

Finalement, nous arrivons à process_events. C'est la fonction finale appelée, que nous n'avons pas encore abordé, dans la boucle principale. Pour l'instant, c'est une fonction très simple qui vérifie seulement deux évènements : QUIT et KEYDOWN. Si l'évènement QUIT est reçu (lorsque la fenêtre est fermée), alors nous mettons la variable done à false ce qui aura pour effet de quitter la boucle principale et de fermer le programme. Si une touche est pressée, nous passons cette touche au gestionnaire on_keyboard_event qui s'occupera des touches pressées.

III-C-10. on_keyboard_event

on_keyboard_event
Sélectionnez
private void on_keyboard_event (KeyboardEvent event) {
        if(event.keysym.sym == KeySymbol.ESCAPE)
                this.done = false;
}

Comme nous mettons juste les choses en place, c'est un gestionnaire très simple. Nous vérifions simplement si la touche Escape a été pressée et nous mettons done à false si c'est le cas. Encore une fois, cela aura pour effet de casser la boucle principale et de quitter le programme.

Avec ça, nous en avons fini avec le main de la classe Brickval. Cela a été très simple jusqu'à maintenant, mais c'est une bonne base sur laquelle construire. C'est plus que ce dont on a besoin pour démarrer le travail basic avec les images et les sprites ce qui sera notre travaille par la suite. La suite de cet article se concentrera sur les classes Sprite et SpriteManager en détail.

III-D. src/spritemanager.vala

La classe SpriteManager est une classe très simple pour le moment (vous remarquez une tendance ?). Son but est de conserver un tableau de tous les sprites et d'd'exécuter les fonctions sur eux comme demandé. Ceci nous laisse le choix sur un nombre arbitraire de sprites sans avoir à s'embêter à appeler manuellement la mise à jour et le rendu de chaque sprite. De plus, plus tard dans la série d'articles, la classe SpriteManager aura de nouvelles fonctionnalités qui permettra aux sprite d'avoir un ordre de rendu et une détection des collisions entre groupe de sprites sur d'autres choses.

Avec ce qui a été dit, commençons à regarder la classe du gestionnaire de sprite.

 
Sélectionnez
using GLib;
using SDL;
using Gee;

public class SpriteManager : Object {

        private ArrayList<Sprite> sprites;

        construct {
                sprites = new ArrayList<Sprite>(direct_equal);
        }

Ceci devrait maintenant vous sembler familier. Nous déclarons les paquets que nous allons utiliser pour cette classe et ensuite la classe elle même. Encore, nous dérivons de Object ainsi nous pouvons conserver les avantages des objets Vala. Nous avons seulement un simple champ qui est un ArrayList qui conserve les sprites à gérer et nous initialisons ce tableau dans notre méthode construct.

III-D-1. Add

Add
Sélectionnez
public int Add(string file) {
        Sprite newSprite = new Sprite(file);

        if(!sprites.add(newSprite))
        {
                GLib.warning("Could not add new sprite %s", file);
                return -1;
        }

        return sprites.index_of(newSprite);
}

La méthode Add() prend une chaine de caractère en entrée, qui représente le chemin vers le fichier image que nous allons utiliser pour le sprite. Nous allons ensuite instancier un nouvel objet Sprite et essayons de l'ajouter à notre ArrayList. Si cela échoue, nous lançons un warning et retournons -1. Sur une adjonction réussie d'un nouveau sprite à l'ArrayList, nous retournons l'index de ce sprite.

III-D-2. Remove

Remove
Sélectionnez
        public void Remove(int index) {
                if(index < 0 || index > sprites.size - 1)
                {
                        GLib.warning("SpriteManager: Index out of bounds (Index %d)", index);
                        return;
                }

                sprites.remove_at(index);
        }

La méthode Remove() fais l'inverse de la méthode Add() et enlève le sprite de l'ArrayList. Nous vérifions que l'index passé est bon et si c'est le cas, on supprime le sprite. Cette méthode devrait appeler une méthode Dispose() sur le Sprite ainsi le sprite peut faire le nettoyage nécessaire, cependant, comme la classe Sprite ne possède pas encore une méthode telle que celle-ci, nous ne pouvons l'appeler. A ce point, dans la série d'article, nous ne supprimons pas les sprites donc ce n'est pas vraiment un problème mais l'implémentation de ces classes dans un vrai projet ne serait pas encore utilisable à cause de limitations comme celle-ci.

III-D-3. Get

Get
Sélectionnez
public Sprite Get(int index) {
        if(index < 0 || index > sprites.size - 1)
        {
                GLib.warning("SpriteManager: Index out of bounds (Index: %d)", index);
                return (Sprite)null;
        }

        return sprites.get(index);
}

Get prend simplement un index, vérifie s'il existe et retourne l'instance du sprite stocké dans le tableau si c'est bon. Si l'index n'existe pas, nous lançons un warning et retournons null.

Notez que nous avons à caster null vers le type Sprite. Vala est plus fortement typé que le C dans plusieurs cas et c'est l'un d'entre eux.

III-D-4. Update

Update
Sélectionnez
public void Update() {
        Iterator<Sprite> iter = sprites.iterator();

        while(iter.next())
                iter.get().Update();
}

La méthode Update() itère simplement à travers l'ArrayList de Sprites et appelle la méthode update sur chaque sprite. Comme nous utilisons Gee, pour accéder à notre liste de sprite avec ArrayList, nous disposons d'une classe Iterator générique. Comme l'itérateur 'sait' quel type il contient, nous pouvons faire certaines choses comme appeler directement les instance de méthode depuis get() sans avoir à faire un cast vers un autre type avant.

III-D-5. Render

Render
Sélectionnez
public void Render(SDL.Surface render_surface) {
        Iterator<Sprite> iter = sprites.iterator();

        while(iter.next())
                iter.get().Render(render_surface);
}

La méthode Render() fait essentiellement la même chose que la méthode Update() mais à la place appelle Render() sur chaque sprite. C'est la méthode sur les sprites qui leur dit de se dessiner. Nous passons notre écran à cette méthode qui est ensuite passé à tous les sprites et chaque sprite se rend lui même directement sur l'écran.

Ainsi se termine la classe SpriteManager. Je vous avais dit que ce serais simple ! Ceci nous donne pas mal de pouvoir même dans sa forme actuelle : par exemple nous pouvons avoir un nombre arbitrait de sprites et pas avoir besoin de les gérer, les mettre à jour et les rendre chacun individuellement. Pourtant, comme cela a été mentionné plus tôt, cette classe grandira en même temps que la série d'articles progressera.

III-E. src/sprite.vala

La classe Sprite est le coeur de notre application de démonstration. cette classe encapsule toutes les informations nécessaires que les sprites ont besoin pour fonctionner et s'afficher eux-mêmes. Beaucoup de cette classe permet simplement de récupérer et mettre à jour les informations et Vala permet une utilisation très facile et puissante pour définir des propriétés comme celle-ci.

Comme nous l'avons fais pour le reste de l'article, nous allons commencer avec les paquets inclus, les champs et les constructeurs.

Sprite
Sélectionnez
using GLib;
using SDL;
using SDLImage;

public class Sprite : Object {
        /* Private Fields */
        private SDL.Surface Image;
        private int Delay;

        /* Properties */

        public Rect SourceRect { public get; private set; }
        public Rect DestRect { public get; private set; }

        public bool HasSheet { public get; private set; }
        public bool IsHorizLayout { public get; private set; }
        public bool Visible { public get; public set; }
        public bool Animated { public get; private set; }

        public int X { public get; public set; }
        public int Y { public get; public set; }
        public float XV { public get; public set; }
        public float YV { public get; public set; }
        public float XVMax { public get; public set; }
        public float YVMax { public get; public set; }
        public float XVMin { public get; public set; }
        public float YVMin { public get; public set; }
        public float XAccel { public get; public set; }
        public float YAccel { public get; public set; }
        public float XAccelMax { public get; public set; }
        public float YAccelMax { public get; public set; }
        public float XAccelMin { public get; public set; }
        public float YAccelMin { public get; public set; }

        public int Width { public get; private set; }
        public int Height { public get; private set; }
        public int CellCount { public get; private set; }
        public int CurrentCell { public get; private set; }

        public uint AnimateDelay { public get; public set; }
        public uint AnimateStartCell { public get; public set; }
        public uint AnimateEndCell { public get; public set; }

        public string GraphicsFile { public get; private set construct; }

        /* Constructor */
        construct {
                X = Y = XV = YV = 0;
                XVMin = YVMin = -10;
                XVMax = YVMax = 10;
                XAccel = YAccel = 0;
                XAccelMax = YAccelMax = 5;
                XAccelMin = XAccelMin = -5;

                Delay = 0;
                Visible = true;
                HasSheet = false;
                IsHorizLayout = true;

                this.Image = SDLImage.load(GraphicsFile);

                if(Image == null)
                        GLib.warning("Could not load image data for sprite.");
                else {
                        Height = Image.h;
                        Width = Image.w;
                        CellCount = 1;
                        CurrentCell = 0;
                }
        }

        public Sprite(string file) {
                this.GraphicsFile = file;
        }

Whoa ! ça fait beaucoup ! Oui, mais la classe Sprite fais un peu plus que l'instanciation que nos autres classes font à ce point de la série d'articles. Premièrement, nous créons deux champs privés, un conserve les données de l'image pour le sprite, l'autre est utilisé par le système d'animation.

Ensuite, nous arrivons à une série de définition qui démontre une caractéristique puissante de Vala. Ce sont des générateurs de propriétés automatique qui créé une propriété GObject complète pour nous sans avoir à définir un champ pour elle.

La suite est une façon 'standard' de construire une propriété :

 
Sélectionnez
private int mInt;

public int MyProperty {
    get { return mInt; }
    set { mInt = value; }
}

Dans ce cas, nous avons un champ int privé, mInt, et une propriété publique qui couvre ce champ privé. Vous pouvez utiliser cette syntaxe dans Vala, cependant Vala permet la façon suivante pour faire ça automatiquement :

 
Sélectionnez
public int MyProperty{ get; set; }

Cela produit le même code que la première version mais en plus compacte et vous permet de ne pas avoir à maintenir séparément les champs privés et les champs de propriétés. De plus, vala permet aux getters et setters d'avoir accès aux modificateurs. Que se passe t-il si vous voulez qu'une propriété soit publiquement récupérable mais seulement mise à jour par la classe elle même ? Vous devrez déclarer la propriété comme ceci :

 
Sélectionnez
public bool Animated { public get; private set; }

Cela permet à toutes les classes externes de récupérer la valeur de Animated mais seulement la classe Sprite peut mettre à jour la valeur.

Une autre chose à noter dans la définition des propriétés est la définition de la propriété GraphicsFile :

 
Sélectionnez
public string GraphicsFile { public get; private set construct; }

Le setter a un modificateur additionnel construct. Comme le type string de Vala est un type de référence (il pointe vers une référence d'un objet string), il est traité légèrement différemment que les autres types quand on arrive aux propriétés. Normalement, construct est appelé après que toutes les propriétés dans le constructeur de la classe sont construites. Pourtant, avec les types de référence, ce n'est pas le cas. Les propriétés des types de référence, par défaut, ne sont pas construites tant que construct n'est pas appelé. Comme nous voulons construire la propriété GraphicsFile dans le constructeur de l'instance de la classe et que nous utilisons cette valeur dans construct nous utilisons ce modificateur pour empêcher ce comportement et permettre à GraphicsFile d'être construit durant l'invocation de construct.

La méthode construct s'explique par elle même. Nous mettons les valeurs par défaut pour différentes propriétés et essayons de charger les données des images. Si les données ne peuvent être chargées, nous lançons un warning, sinon nous construisons plusieurs nouvelles propriétés qui dépendent du chargement nécessaire des données des images.

Les propriétés de la classe Sprite définissent le comportement du sprite et sont décrite ci-dessous :

SourceRect Un rectangle utilisé pour définir quelle partie du sprite à afficher
DestRect La position de destination du sprite
HasSheet True si la donnée du sprite est une feuille de sprite
IsHorizLayout True si le layout de la feuille de sprite est disposée de façon horizontal
Visible Rend les sprites visible
Animated Détermine si le sprite utilise de multiples frames pour l'animation
X La coordonnée X courante du sprite
Y La coordonnée Y courante du sprite
XV La vitesse X courante du sprite
YV La vitesse X courante du sprite
XVMax La valeur max permise pour XV
YVMax La valeur max permise pour YV
XVMin La valeur min permise pour XV
YVMin La valeur min permise pour YV
XAccel Le facteur d'accélération (appliqué à chaque frame à XV)
YAccel Le facteur d'accélération (appliqué à chaque frame à YV)
XAccelMax Le facteur d'accélération maximum permis pour XAccel
YAccelMax Le facteur d'accélération maximum permis pour YAccel
XAccelMin Le facteur d'accélération minimum permis pour XAccel
YAccelMin Le facteur d'accélération minimum permis pour YAccel
Width La largeur d'une simple frame d'un sprite (la largeur est égale à la largeur du fichier si ce n'est pas une feuille de sprite)
Height La hauteur d'une simple frame d'un sprite (la hauteur est égale à la hauteur du fichier si ce n'est pas une feuille de sprite)
CellCount Le nombre de frame dans une feuille de sprite
CurrentCell la frame courante selectionnée dans la feuille de sprite
AnimateDelay Le nombre de frame qui doivent passer avant que la prochaine frame dans le cycle de l'animation soit sélectionnée
AnimateStartCell La frame dans la feuille de sprite où la boucle d'animation débute
AnimateEndCell La frame dans la feuille de sprite où la boucle d'animation fini
GraphicsFile Une chaine de caractère contenant le chemin vers les données de l'image

Les méthodes dans la classe Sprite opèrent sur ces propriétés afin de compléter leurs fonctions. Commençons à regarder quelques unes de ces méthodes maintenant.

III-E-1. InitSpriteSheet

InitSpriteSheet
Sélectionnez
public void InitSpriteSheet(int cell_width, int cell_height,
     int cell_count, bool horizontal) {
        Width = cell_width;
        Height = cell_height;
        CellCount = cell_count;
        IsHorizLayout = horizontal;
        HasSheet = true;
}

InitSpriteSheet construit simplement quelques propriétés et met HasSheet à vrai. Mais qu'est ce que cela signifie ? Que faisons-nous actuellement ? Pour répondre à cette question, nous avons besoin de discuter de comment les sprites sont disposés dans un fichier image.

Ce qui suit est un fragment de data/sprites/bricks.png :

fragment_sheet

Comme vous pouvez le voir, le fichier contient, dans une longue bande, tous les types de briques possibles. Pas seulement, elles sont aussi disposés de manière horizontale et n'ont pas d'espace entre elles. Lorsque les données des sprites sont disposées ainsi dans un simple fichier, ceci est appelé une feuille de sprite. C'est ainsi que beaucoup de jeux stockent les informations de leurs sprites. La feuille de sprite pourrait aussi facilement être disposée de façon verticale, chaque sprite étant empilé l'un sur l'autre. Et c'est ce que IsHorizLayout vérifie : sont-elle stockée côte à côte ou l'une sur l'autre.

L'autre information que cette méthode construit est le CellCount, le Width (largeur) et le Height (hauteur) d'une simple frame. Le CellCount est simplement le total de frame dans la feuille de sprite (dans le cas de data/sprites/bricks.png, la valeur est de 21). Width et Height décrivent la taille de chaque frame. En utilisant ces nombre, on peut, en effet, décrire une 'fenêtre' sur la feuille de sprite donc nous pouvons sortir individuellement les frames et seulement afficher ces dernières. La façon de faire exactement cette chose est décrite dans la partie sur la méthode de rendu plus tard.

Finalement, nous mettons HasSheet à vrai pour dire à la méthode Render que les données de l'image est une feuille de sprite. Si HasSheet est à faux, Render rendra simplement la totalité des données su sprite sur l'écran.

III-E-2. SetCurrentCell

SetCurrentCell
Sélectionnez
public void SetCurrentCell(int cell) {
        if(cell < 0)
        {
                GLib.warning("Tried to set a negative cell.");
                return;
        }

        CurrentCell = cell % CellCount;
}

C'est une méthode simple qui défini quel frame de sprite dans la feuille de sprite est actuellement affiché. Nous faisons quelques vérifications sur la valeur passée en argument afin de s'assurer qu'elle a un sens. On prend le modulo de la valeur passée par le CellCount afin de s'assurer que la valeur passée est dans l'ordre de grandeur du nombre de frame de sprite dans la feuille. Modulo (%) est une opération mathématique qui travaille sur des entiers, il nous donne le reste de la division. Si le reste est 0, alors la division est tombe juste, sinon il retourne le reste.

III-E-3. IsCollidingWith

IsCollidingWith
Sélectionnez
public bool IsCollidingWith(Sprite s) {
        return !(s.DestRect.x > DestRect.x + DestRect.w
                        || s.DestRect.x + s.DestRect.w < DestRect.x
                        || s.DestRect.y > DestRect.y + DestRect.h
                        || s.DestRect.y + s.DestRect.h < DestRect.h);
}

C'est une méthode très basique de vérification d'intersection de rectangle. Cela permet de vérifier si le Sprite passé est en train de toucher le sprite courant. C'est à peu près aussi simple que ce que nous donne la réponse de collision et ce n'est pas la façon la plus fiable de faire les choses. Mais c'est bon de l'avoir dans une classe afin de permettre quelques capacités rudimentaires. La réponse de collision est vraiment un gros sujet et aura son propre article plus tard dans cette série.

III-E-4. StartAnimate StopAnimate

StartAnimate et StopAnimate
Sélectionnez
public void StartAnimate(uint start_cell, uint end_cell, uint delay) {
        AnimateStartCell = start_cell;
        AnimateEndCell = end_cell;
        AnimateDelay = delay;

        Animated = true;
}

public void StopAnimate() {
        Animated = false;
}

Je regroupe ces deux méthodes ensemble car StopAnimate() est trop simple. StartAnimate prend une zone (depuis start_cell jusqu'à end_cell) et une valeur d'attente. L'attente est un nombre de frames et contrôle combien de frame doivent passer avant que le sprite change vers la prochaine cellule. Notez que jusqu'à maintenant, le support d'animation est assez simple. Nous supportons seulement les animations qui bouclent d'une frame de sprite contigu. Aussi, il n'y a pas de construction logique pour les 'groupe' d'animations dans des arrangements utiles (comme WalkingUp, WalkingDown, etc.). Nous allons étendre ces capacités dans de futurs articles.

III-E-5. Update

Update
Sélectionnez
public void Update() {
        if(XAccel > XAccelMax)
                XAccel = XAccelMax;

        if(YAccel > YAccelMax)
                YAccel = YAccelMax;

        if(XAccel < XAccelMin)
                XAccel = XAccelMin;

        if(YAccel < YAccelMin)
                YAccel = YAccelMin;

        XV += XAccel;
        YV += YAccel;

        if(XV > XVMax)
                XV = XVMax;

        if(YV > YVMax)
                YV = YVMax;

        if(XV < XVMin)
                XV = XVMin;

        if(XV < YVMin)
                YV = YVMin;

        X += XV;
        Y += YV;
}

Maintenant, nous allons voir une des principales méthodes de la classe sprite : Update. Update met à jour les propriétés X et Y des sprites pour chaque frame basé sur les propriétés courantes de vitesse XV et YV. Pour déplacer un sprite en utilisant ce système, vous définissez simplement les valeurs de vitesse que vous voulez et ils vont bouger d'eux même. Nous avons aussi le concept d'accélération. Cela ajoute une valeur à chaque vitesse par frame et vous permet doucement d'aller plus vite ou de ralentir le mouvement d'une animation de sprite par une quantité constante.

A ce jour, X et Y sont des valeurs entières car c'est ce que SDL a besoin pour blitter. C'est une grande limitation car cela limite l'ordre de grandeur de nos vitesses et accélérations. Il n'y a pas aussi de façon simple pour faire une rotation ou mélanger un sprite en utilisant le mode graphique de SDL. Ces limitations seront dépassées lorsqu'on utilisera OpenGL avec Vala et SDL.

III-E-6. Render

Render
Sélectionnez
public void Render(SDL.Surface render_surface) {
        if(Image == null) {
                GLib.warning("Tried to render NULL sprite data.");
                return;
        }

        if(Animated)
                if(++Delay > AnimateDelay) {
                        Delay = 0;

                        if(++CurrentCell > (AnimateEndCell - 1) % CellCount)
                                CurrentCell = AnimateStartCell % CellCount;
                }

        if(IsHorizLayout) {
                SourceRect.x = (int16) (CurrentCell * Width);
                SourceRect.y = 0;
        } else {
                SourceRect.x = 0;
                SourceRect.y = (int16) (CurrentCell * Height);
        }

        SourceRect.w = (uint16) Width;
        SourceRect.h = (uint16) Height;

        DestRect.x = (int16) X;
        DestRect.y = (int16) Y;
        DestRect.w = SourceRect.w;
        DestRect.h = SourceRect.h;

        Image.blit(SourceRect, render_surface, DestRect);
}

C'est la fonction primaire de sprite. Elle prend une surface SDL sur laquelle blitter (dans notre cas, on passe notre surface d'écran) et ensuite trouve quel frame de sprite à blitter. Si les données de l'image sont null (car elle n'a pas été chargée correctement par exemple), alors nous lançons un warning et arrêtons tout. Ensuite vous pouvez voir toute la discussion sur l'animation réduite en cinq lignes de code. nous utilisons notre champ privé Delay comme un compteur et le vérifions avec AnimateDelay. Une fois qu'il est plus grand que AnimateDelay, nous incrementons CurrentCell. Si CurrentCell est plus grand que AnimateEndCell, nous revenons à AnimateStartCell et la boucle d'animation recommence encore.

Vous noterez que nous prenons le modulo de ces valeurs par CellCount encore. Cela nous assure que même si on obtient des valeurs non entières pour initialiser AnimateStartCell et AnimateEndCell, le programme continuera toujours à animer le sprite.

Ensuite, nous vérifions si la disposition est horizontale ou verticale. Si le sprite n'est pas une feuille de sprite alors il est traité ici comme une feuille de sprite disposée horizontalement avec une frame. La seule différence entre une feuille de sprite horizontale et verticale sont les coordonnées que nous utilisons afin de bouger la 'fenêtre' dans la bonne frame. Les feuilles horizontales utilisent la coordonnée X et les verticales la coordonnée Y.

Après que les valeurs X et Y de notre SourceRect sont bien mise en place, nous utilisons nos propriétés Width et Height afin de finir de décrire la frame de sprite que nous souhaitons depuis les données de l'image. Ensuite, nous créons un DestRect qui décrit un rectangle avec les coordonnées courantes X/Y du sprite ainsi que ses dimensions. Finalement, nous blittons sur l'écran et notre sprite est affiché.

III-F. Lancer le programme

Si vous n'avez toujours pas essayé de compiler les sources, vous devez vous demander comment faire. La première chose que nous avons besoin de faire est de lancer le script autogen.sh dans le répertoire racine de notre projet :

 
Sélectionnez
cd ~/dev/brickval
./autogen.sh

Vous pouvez passer des paramètres à autogen.sh, notamment --prefix. --prefix vous laisse spécifier où vous souhaitez que les données du programme soient installées. La valeur par défaut de prefix est /usr/local. Si vous utilisez la valeur par défaut, alors l'exécutable sera stocké dans /usr/local/bin et les données dans /usr/share/brickbal/* où * représente les sprites ou le backgrounds respectivement. Vous n'avez besoin de lancer ce script une seule fois, il fait une installation des outils automatiques et permet au projet d'être prêt à l'emploi.

Une fois que le script à fini, vous tapez simplement :

 
Sélectionnez
make

... et ensuite

 
Sélectionnez
make install

... pour l'installer. Vous devez lancer make install seulement pour récupérer les images stockées dans le bon chemin. Mais si vous n'avez pas ajouté d'images durant le développement, vous pouvez vous sentir libre de lancer l'exécutable directement depuis le dossier src. La raison du besoin d'installer au moins une fois est du à la façon dont nous regardons où nos données sont dans notre application (config.vapi !). Donc, s'il n'y a pas d'installation initiale, alors les données n'existent pas du point de vue du programme.

Une fois installé, vous pouvez lancer le programme en tapant :

 
Sélectionnez
brickval

... Si vous l'avez installé dans votre PATH. Ou sinon :

 
Sélectionnez
./src/brickval

Vous devriez voir une démo apparaitre comme l'image qui suit :

demo

Maintenant, imaginez toutes ces briques bouger...

III-G. Conclusion

Nous sommes arrivés assez loin et rapidement. Nous pouvons afficher, bouger, et animer un nombre arbitraire de sprites et en plus on a mit en place un environnement de travail qui nous permettra une manipulation des sprites encore plus avancé dans le futur. Jusqu'à maintenant, nous avons été limités à ce que nous pouvions faire avec le mode graphique par défaut de SDL, donc, dans le prochain article, nous allons changer ça en utilisant OpenGL comme notre moteur de rendu. Cela nous permettra de faire une tonne de choses en plus avec le bonus d'avoir une accélération matérielle (aussi longtemps que vous aurez une carte graphique le permettant).

En attendant, jouez un peu avec la classe sprite ! Regardez ce qu'il se passe si vous ajoutez, par exemple, cette ligne :

 
Sélectionnez
s.StartAnimate(0, 6, rand.int_range(1,10));

dans la méthode InitSprites() dans src/brickval.vala pour commencer.

A la prochaine !


précédentsommaire
ndt : ABI pour Application Binary Interface