III. La 2D▲
III-A. Introduction▲
La 2D est l'aspect le plus accessible dans le développement de jeu. En revanche, cela ne veut pas dire que c'est facile !
Certains d'entre vous se souviendront d'une époque où le jeu de plateforme et le shoot-them-up étaient rois. Malgré leur âge, ces jeux utilisaient des effets parfois saisissants pour immerger encore plus le joueur. Afficher un personnage reste facile, l'intégrer dans un décor réactif et le faire bouger selon les actions du joueur est bien plus difficile ! Même si la méthode d'affichage reste la même.
C'est pour cette raison que nous verrons dans cette partie les différentes façons d'afficher des textes, des formes géométriques et des images (dit « sprites ») grâce à Asphyre.
Avant de commencer, il faut savoir que la version 4.1 (à la différence de la version Casual) nécessite une DLL pour fonctionner. Celle-ci est fournie dans le répertoire DLL et s'appelle « d3dx9_31.dll ».
Cette DLL est normalement fournie avec DirectX 9.0c (version 2006 donc incluse dans la dernière mise à jour mars 2008). Certains postes qui n'ont pas été mis à jour peuvent ne pas avoir cette DLL.
Comme pour chaque DLL indispensable à un programme, plusieurs choix s'offrent à vous :
- déplacer cette DLL une bonne fois pour toutes dans le répertoire System32 de Windows. Cela permettra à tous vos programmes de fonctionner sans vous en soucier, mais vous risquez de l'oublier lors du déploiement sur d'autres machines qui n'en sont pas forcément équipées ;
- placer cette DLL dans le répertoire de chacun de vos programmes. De cette façon, vous ne risquez pas de l'oublier. En revanche, il faut la placer dans le répertoire à chaque nouveau programme.
Les deux méthodes fonctionnent, choisissez celle qui vous convient le mieux. De toute façon, sans cette DLL, Asphyre refusera de démarrer dès l'initialisation.
III-B. Asphyre en 2D : Comment ça marche ?▲
Asphyre propose des méthodes à appeler pour initialiser, modifier toutes sortes de paramètres. En y regardant bien, le système n'est pas très différent d'Asphyre eXtreme sauf qu'aucun objet n'est visible, ce qui peut paraitre un peu déroutant au début.
En appelant la méthode Initialize, on initialise (oui le nom est bien choisi !) le moteur d'affichage et Asphyre se charge de créer tout ce dont on a besoin.
En fin de programme, il suffit d'appeler Finalize pour détruire tous les objets créés et libérer la surface utilisée.
III-B-1. Initialiser▲
Cette initialisation est indispensable pour tout projet utilisant Asphyre et DirectX en général. Il ne faut pas l'oublier !
L'initialisation se fait de la plus simple des façons. Il suffit en effet d'appeler :
Devices.Initialize(CfgEvent: TScreenConfigEvent; Tag: TObject): Boolean
;
Cette méthode Initialize accepte deux paramètres :
- CfgEvent : permet de spécifier une méthode qui sera exécutée au début de l'initialisation ;
- Tag : permet de conserver un lien vers le composant qui affiche (en général, la Form principale).
Le booléen renvoyé permet de vérifier que l'initialisation s'est correctement effectuée. Si ce n'est pas le cas, on peut refermer l'application puisque Asphyre n'affichera rien.
La méthode de type TScreenConfigEvent s'écrit de cette manière et devrait être déclarée en private puisque, théoriquement, seule la fiche d'affichage devrait y avoir accès :
type
TMainFrm = class
(TForm)
...
private
procedure
ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var
Config: TScreenConfig);
end
;
...
procedure
TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var
Config: TScreenConfig);
begin
end
;
Cette méthode ConfigureDevice aura pour but principal de paramétrer le composant qui affichera, son type d'affichage, sa taille, s'il est en mode fenêtré, etc.
Pour paramétrer votre affichage, il suffit, dans cette procédure, de modifier le paramètre Config passé en variable.
Typiquement, voici un exemple de ce que l'on peut trouver dans l'un des exemples fournis avec Asphyre (Extrait de BasicShdr) :
procedure
TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var
Config: TScreenConfig);
begin
Config.WindowHandle:= Self
.Handle; //Spécifie le handle de la Form pour l'affichage
Config.HardwareTL := True
; //Active le Transform and Lightning matériel
Config.Width := ClientWidth; //Taille de la largeur de l'affichage
Config.Height := ClientHeight; //Taille de la hauteur de l'affichage
Config.Windowed:= False
; //Définit le mode fenêtré. Ici à faux
Config.BitDepth:= bd24bit; //Définit la profondeur des couleurs. ici 24 bits
end
;
De cette façon, ces données seront utilisées lors de l'initialisation.
Il est également possible d'ajouter des méthodes lorsque certains événements inaccessibles autrement doivent être utilisés :
- EventDeviceCreate : se déclenche après que l'initialisation ait été effectuée.
Utilisé pour charger les ressources statiques ; - EventDeviceDestroy : se déclenche juste avant la finalisation
De la même façon, les ressources statiques doivent être libérées ici ; - EventDeviceReset : se déclenche quand le dispositif (Device) est placé en état fonctionnel ou qu'il est récupéré lors d'une perte.
Toutes les ressources non persistantes doivent être créées ici ; - EventDeviceLost : se déclenche lorsque le dispositif perd son état fonctionnel ou bien après qu'il ait été perdu.
Toutes les ressources créées dans DeviceReset doivent être libérées ici ; - EventDeviceFault : se déclenche lorsqu'une erreur fatale se produit et que l'application ne peut pas continuer. On peut l'utiliser par exemple, pour afficher un message d'erreur et clore l'application ;
- EventBeginScene : se déclenche après l'appel réussi de IDirect3DDevice9.BeginScene ;
- EventEndScene : se déclenche avant l'appel à IDirect3DDevice9.EndScene ;
- EventSymbolResolve : se déclenche avant quand une ressource (image par exemple) est en train d'être créée et chargée ;
- EventResolveFailed : se déclenche lorsque la création d'une ressource n'a pas fonctionné pour diverses raisons ;
Pour utiliser ces événements, il faut créer une procédure de type TAsphyreEventCallback dont voici sa syntaxe :
TAsphyreEventCallback = procedure
(Sender: TObject; EventParam: Pointer
;
var
Success: Boolean
) of
object
;
Une fois les procédures écrites, il faut appeler les méthodes Subscribe de chacun des événements que vous voulez utiliser.
Par exemple, si vous voulez modifier le Caption de la fiche principale lorsque le moteur est chargé, il faut faire comme ceci :
procedure
TMainFrm.OnDeviceResetCallback(Sender: TObject;
EventParam: Pointer
; var
Success: Boolean
);
begin
Caption := 'OK'
;
end
;
Écrivez ensuite, dans la procédure ConfigureDevice :
procedure
TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var
Config: TScreenConfig);
begin
Config.WindowHandle:= Self
.Handle; //Cette spécification est nécessaire sinon, l'initialisation ne réussit pas!
EventDeviceReset.Subscribe(OnDeviceResetCallback, Sender);
end
;
Ainsi, lorsque l'état du dispositif passera à l'état « En cours de fonctionnement » (fonctionnel), 'OK' sera affiché dans la barre de titre de la fiche.
Pour finir, il suffit de lancer la méthode Initalize de cette façon :
procedure
MainFrm.Create(Sender: TObject);
begin
if
(not
Devices.Initialize(ConfigureDevice, Self
)) then
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!'
, mtError, [mbOk], 0
);
Close();
Exit;
end
;
end
;
Ainsi, si Asphyre n'a pas réussi son activation pour une raison quelconque, la fiche principale se ferme.
III-B-2. Le Timer▲
Le timer est une notion importante en programmation DirectX.
En effet, lorsque vous développez une application qui doit dessiner par exemple quelque chose sur la fiche à l'aide de son Canvas (surface qui permet de dessiner du texte et des formes géométriques de type TCanvas), il convient d'écrire le code du dessin dans le OnPaint de la fiche. Lorsque la fiche aura besoin de se redessiner, elle appellera cette méthode. Il est néanmoins parfois nécessaire de forcer le dessin avec un appel à Invalidate, Repaint, etc.
Ici, rien de tout ça : un timer déclenche à chaque cycle la procédure de dessin. L'impact direct est que chacune de vos modifications (par exemple, la position d'une image) sont visibles quasiment en temps réel (Parler de temps réel est un abus de langage : en informatique, rien ne se fait en temps réel. En revanche, le temps entre la modification et la visualisation est tellement court que l'on parle de temps réel).
Une fois l'initialisation effectuée et réussie, il faut déclencher le timer :
Timer.OnTimer := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS := 100
;
Timer.Enabled := True
;
Comme vous pouvez le constater, ce Timer est déjà défini dans l'unité AsphyreTimer. Ceux qui ont déjà utilisé la version eXtreme d'Asphyre auront sans doute reconnu les événements OnTimer et OnProcess indispensables au fonctionnement d'Asphyre. En voici un descriptif :
- Enabled : permet exactement la même chose que le Enabled du TTimer de la VCL, c'est-à-dire, démarrer le Timer ;
- OnTimer : se déclenche à chaque cycle.
En règle générale, cet événement doit déclencher le rendu de la scène (2D ou 3D) et le Process du Timer ; - OnProcess : est directement déclenché dans l'événement OnTimer. Regroupe toutes les opérations effectuées sur les objets de la scène (déplacement, rotation, etc.) ;
- MaxFPS : permet de spécifier le nombre maximal de FPS (Frames Per Second : images par seconde). Si vous ne voulez pas le limiter, spécifiez un grand nombre.
Ainsi, il est nécessaire d'écrire deux méthodes réagissant à OnTimer et à OnProcess.
Vous me direz, pourquoi découper ce OnTimer en OnTimer et en OnProcess ? La raison principale est purement pratique : vous verrez qu'à la longue, le code permettant de gérer les différents sprites enfle rapidement. De cette façon, le code est rangé de façon à le retrouver plus facilement. Dans OnTimer, on déclenche le rendu et OnProcess. Dans OnProcess, on a tous les traitements à effectuer.
Ces méthodes sont de type TNotifyEvent et, donc leur écriture se fait comme ceci :
procedure
TimerEvent(Sender: TObject);
procedure
ProcessEvent(Sender: TObject);
On les mettra encore une fois dans la section private de la fiche pour les mêmes raisons que la méthode ConfigureDevice.
III-B-2-a. OnTimer▲
Cet événement se produit automatiquement à chaque cycle du Timer. Comme nous l'avons vu plus haut, c'est ici que l'on appelle le rendu (dessin) de la scène et qu'on appelle la méthode OnProcess à l'aide de la méthode Process de Timer.
procedure
TMainFrm.TimerEvent(Sender: TObject);
begin
DefDevice.Render(DrawView, Self
, $000000
);
Timer.Process;
end
;
Analysons plus en détail cette procédure.
DefDevice.Render(DrawView, Self
, $000000
);
- DrawView : méthode appelée pour dessiner des données complémentaires. on peut comparer cette procédure à la procédure OnPaint de TForm : c'est là-dedans qu'on dessine les formes, les textes, etc.
- Self : objet sur lequel dessiner. Ici, la fiche principale.
- $000000 : couleur de fond. Ici, du noir.
La méthode DrawView s'écrit de la façon suivante :
procedure
TMainFrm.DrawView(Sender: TAsphyreDevice; Tag: TObject);
begin
...
end
;
Ensuite, nous avons :
Timer.Process();
Cette ligne a pour unique fonction de déclencher OnProcess du Timer.
III-B-2-b. OnProcess▲
Cet événement regroupe toutes les opérations à effectuer sur les différents objets affichés (ou non). Nous n'en sommes qu'au début du tutoriel, nous verrons son utilisation plus loin. Néanmoins, nous le déclarerons même s'il est vide pour acquérir le bon réflexe.
III-B-3. Finaliser▲
Une fois le programme terminé, il faut libérer toutes les ressources allouées lors de l'initialisation.
Pour cela, rien de plus simple :
procedure
TMainFrm.FormDestroy(Sender: TObject);
begin
Devices.Finalize;
end
;
Et c'est tout ! Évidemment, si vous avez des ressources que vous avez créées au démarrage, il faut les libérer en choisissant l'événement adéquat comme vu dans la section III.B.1Initialiser.
III-C. Premier programme: « Hello World! »▲
Passons maintenant à la création d'un petit programme !
Pour respecter la tradition de la programmation, nous allons faire un simple programme qui affiche « Hello World » en blanc sur un fond noir, le tout en mode fenêtré.
Tout d'abord, créez une nouvelle application avec fiche et sauvegardez-la.
Appelez la fiche principale MainFrm.
Première chose à faire: initialiser Asphyre. Comme nous l'avons vu, voici ce dont nous avons besoin :
- OnCreate de la fiche afin d'initialiser Asphyre ;
- une procédure ConfigureDevice qui sera appelée lors de l'initialisation pour paramétrer l'affichage ;
- une procédure TimerEvent pour le OnTimer du Timer ;
- une procédure ProcessEvent pour le OnProcess du Timer ;
- une procédure RenderCallback pour dessiner le texte lors de l'appel à la méthode Render ;
- OnDestroy de la fiche afin de finaliser Asphyre.
procedure
TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var
Config: TScreenConfig);
begin
Config.Width := ClientWidth;
Config.Height := ClientHeight;
Config.Windowed:= True
;
Config.WindowHandle:= Self
.Handle;
Config.HardwareTL := true
;
end
;
Voilà pour le ConfigureDevice.
Pour le OnCreate, on aura donc l'initialisation d'Asphyre et du Timer, ainsi que la création de la police de caractères :
procedure
TMainFrm.FormCreate(Sender: TObject);
begin
if
(not
Devices.Initialize(ConfigureDevice, Self
)) then
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!'
, mtError, [mbOk], 0
);
Close();
Exit;
end
;
DefDevice.SysFonts.CreateFont('sys:tahoma'
, 'tahoma'
, 9
, False
, fwtHeavy,
fqtClearType, fctAnsi);
Timer.OnTimer := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS := 100
;
Timer.Enabled := True
;
end
;
Nous verrons plus en détail la façon d'écrire un texte plus loin dans le tutoriel.
Passons ensuite aux deux méthodes du Timer :
procedure
TMainFrm.ProcessEvent(Sender: TObject);
begin
//Rien
end
;
procedure
TMainFrm.TimerEvent(Sender: TObject);
begin
DefDevice.Render(RenderCallback, self
, $000000
);
end
;
Dans cette procédure RenderCallback, le texte sera écrit. Voici cette procédure :
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
begin
Sender.SysFonts.Font['sys:tahoma'
].TextOut('Hello world!'
,
4
, 10
, $FFFFFFFF
);
end
;
Comme dit plus haut, la méthode d'écriture sera détaillée plus loin.
Évidemment, il ne faut surtout pas oublier de libérer toute la mémoire que l'on a allouée :
procedure
TMainFrm.FormDestroy(Sender: TObject);
begin
Devices.Finalize;
end
;
Compilez et exécutez. Vous devriez voir ceci :
Voici le code complet :
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, AsphyreDevices;
type
TMainFrm = class
(TForm)
procedure
FormCreate(Sender: TObject);
procedure
FormDestroy(Sender: TObject);
private
{ Déclarations privées }
procedure
ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var
Config: TScreenConfig);
procedure
TimerEvent(Sender: TObject);
procedure
ProcessEvent(Sender: TObject);
procedure
RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
public
{ Déclarations publiques }
end
;
var
MainFrm: TMainFrm;
implémentation
uses
AsphyreEvents, AsphyreTimer, AsphyreSystemFonts;
{$R *.dfm}
procedure
TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var
Config: TScreenConfig);
begin
Config.Width := ClientWidth;
Config.Height := ClientHeight;
Config.Windowed:= True
;
Config.WindowHandle:= Self
.Handle;
Config.HardwareTL := true
;
end
;
procedure
TMainFrm.FormCreate(Sender: TObject);
begin
if
(not
Devices.Initialize(ConfigureDevice, Self
)) then
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!'
, mtError, [mbOk], 0
);
Close();
Exit;
end
;
DefDevice.SysFonts.CreateFont('sys:tahoma'
, 'tahoma'
, 9
, False
, fwtHeavy,
fqtClearType, fctAnsi);
Timer.Enabled := True
;
Timer.OnTimer := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS := 100
;
end
;
procedure
TMainFrm.FormDestroy(Sender: TObject);
begin
Devices.Finalize;
end
;
procedure
TMainFrm.ProcessEvent(Sender: TObject);
begin
//Rien
end
;
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
begin
Sender.SysFonts.Font['sys:tahoma'
].TextOut('Hello world!'
,
4
, 10
, $FFFFFFFF
);
end
;
procedure
TMainFrm.TimerEvent(Sender: TObject);
begin
DefDevice.Render(RenderCallback, self
, $000000
);
Timer.Process;
end
;
end
.
Comme vous pouvez le voir, aucun composant n'est nécessaire. Toutefois, il faut tout de même déclarer les unités qui contiennent les routines que l'on a utilisées. Puisque justement, il n'y a pas de composants, leur rajout ne se fait pas de façon automatique.
Seul AsphyreDevices nécessite d'être déclarée en interface puisque les procédures que l'on ajoute contiennent des types qui y sont déclarés. Toutes les autres peuvent être déclarées en implémentation.
III-C-1. Bug de librairie d3dx9d_31.dll▲
Si vous êtes sous Delphi 2007 ou RAD Studio 2007, il se peut que vous ayez une erreur telle que celle-ci :
C'est un bug connu d'Asphyre. La raison est simple: Yuriy Kotsarenko utilise le SDK de DirectX qui propose cette DLL pour le déboguage. Sa version Release est nommée « d3dx9_31.dll » (sans le dernier d). Delphi 2007, par défaut est en configuration de Debug. Dans le code de DXDX9.pas, si le mode debug est activé, c'est la DLL « d3dx9d_31.dll » qui sera utilisée, sinon, c'est l'autre.
Pour le résoudre, vous avez plusieurs solutions :
- récupérez cette DLL : allez sur MSDN , téléchargez le SDK et installez-le. Normalement, la librairie devrait s'ajouter dans System32 ;
- copiez « d3dx9_31.dll » et renommez-la en « d3dx9d_31.dll » ;
- passez votre projet en configuration Release (Projet\Options… section Compilation puis dans la boite déroulante « Config construction » en haut à droite, choisissez Release) puis reconstruisez (pas recompilez) le tout ;
- la méthode la plus simple : rendez-vous dans l'unité D3DX9.pas d'Asphyre et allez ligne 82. Vous y trouverez ce code :
d3dx9MicrosoftDebugDLL = 'd3dx9d_31.dll'
;
Vous l'avez compris : ce code définit la DLL lorsque le mode Debug est actif. Copiez cette ligne (ou remplacez-la) par ceci :
// On laisse la ligne commentée au cas où, mais ce n'est pas forcément
// nécessaire si vous ne comptez pas utiliser cette librairie de débogage
// d3dx9MicrosoftDebugDLL = 'd3dx9d_31.dll';
d3dx9MicrosoftDebugDLL = 'd3dx9_31.dll'
;
Cette DLL ne peut pas être distribuée. Pour pallier ce problème, vous pouvez télécharger les DLL libres de droits.
III-D. Méthodes de dessin▲
Dans cette section, nous allons voir les principales méthodes de dessin, à savoir écrire du texte, dessiner des formes et afficher une image.
III-D-1. Écrire du texte▲
Écrire du texte sur une surface DirectX reste assez simple grâce à Asphyre. Nous allons voir ici les deux méthodes qui permettent de dessiner du texte.
III-D-1-a. Écriture « simple » à partir d'une police système▲
La méthode ressemble à la méthode TextOut du TCanvas de la VCL. À ceci près que l'appel à ce TextOut se fait à un endroit bien précis.
Avant toute chose, il faut créer la police que l'on va utiliser. Pour cela, dans le OnCreate, il suffit d'appeler le constructeur de police. Dans notre exemple, on a cette ligne :
DefDevice.SysFonts.CreateFont('sys:tahoma'
, 'tahoma'
, 9
, False
, fwtHeavy,
fqtClearType, fctAnsi);
Voici la déclaration que l'on trouve dans AsphyreSystemFonts.pas :
function
CreateFont(const
Name, SystemFontName: string
; Size: Integer
;
Italic: Boolean
= False
; Weight: TFontWeightType = fwtNormal;
Quality: TFontQualityType = fqtClearType;
Charset: TFontCharsetType = fctAnsi): Integer
;
- Name : le nom de la police. C'est ce nom que l'on va utiliser pour appeler cette police de caractères.
- SystemFontName : le nom de la police telle qu'elle est définie dans Windows.
- Size : taille de la police.
- Italic : police en italique ou non.
- Weight : épaisseur de la police.
- Quality : qualité de la police.
- Charset : charset de la police.
Weight définit l'épaisseur de la police. Il propose neuf différentes valeurs :
fwtThin, fwtExtraLight, fwtLight, fwtNormal, fwtMedium, fwtSemiBold, fwtBold, fwtExtraBold, fwtHeavy
Voici à quoi ressemblent ces valeurs avec Quality et Charset à leur valeur par défaut (respectivement fqtClearType et fctAnsi) et police Technic :
La différence est subtile, mais assez visible sur cette police à cette taille.
Une fois la police créée, on peut écrire sur la surface. Pour cela, il suffit d'écrire, comme dans notre exemple, la procédure suivante dans le Callback de Rendu :
Sender.SysFonts.Font['sys:tahoma'
].TextOut('Hello world!'
,
4
, 10
, $FFFFFFFF
);
On identifie notre police grâce à son nom (ici 'sys:tahoma') puis on appelle sa méthode TextOut dont voici les déclarations :
procedure
TextOut(const
Text: WideString; const
Rect: TRect;
Format: TFontFormatType; Color: Cardinal
); overload
;
procedure
TextOut(const
Text: WideString; x, y: Integer
;
Color: Cardinal
); overload
;
La première permet de définir la position du texte dans un cadre (TRect) ce qui permet de spécifier des effets particuliers pour le texte que l'on veut inscrire dedans. Asphyre propose les dix valeurs différentes de TFontFormatType suivantes :
fftTop, fftBottom, fftLeft, fftRight, fftCenter, fftVCenter, fftNoClip, fftExpandTabs, fftSingleLine, fftWordBreak
Ces valeurs peuvent être combinées afin, par exemple d'obtenir un texte centré verticalement et horizontalement dans une case de cette façon :
var
UneCase: TRect;
begin
//Initialisation de UneCase
...
Sender.SysFonts.Font['MaPolice'
].TextOut('Mon texte'
, UneCase, [fftCenter, fftVCenter], $FFFFFFFF
);
end
;
La seconde permet de spécifier simplement la position du texte à l'aide de ses coordonnées en pixels sur la surface.
Le dernier paramètre est la couleur du texte. Il se décompose ainsi $AARRVVBB, AA étant la valeur de transparence, RR la valeur de rouge, VV, la valeur de vert et BB la valeur de bleu. On peut donc facilement définir la couleur ainsi que son opacité :
Sender.SysFonts.Font['sys:tahoma'
].TextOut('Opacité = $FF (255)'
,
4
, 10
, $FFFF0000
);
Sender.SysFonts.Font['sys:tahoma'
].TextOut('Opacité = $CC (204)'
,
4
, 30
, $CCFF0000
);
Sender.SysFonts.Font['sys:tahoma'
].TextOut('Opacité = $99 (153)'
,
4
, 50
, $99FF0000
);
Sender.SysFonts.Font['sys:tahoma'
].TextOut('Opacité = $66 (102)'
,
4
, 70
, $66FF0000
);
Sender.SysFonts.Font['sys:tahoma'
].TextOut('Opacité = $33 (51)'
,
4
, 90
, $33FF0000
);
III-D-1-a-i. Écrire le nombre d'images par seconde (FPS)▲
Une des utilités les plus intéressantes est de pouvoir afficher le nombre de FPS actuel. Cela peut nous informer de la qualité d'optimisation de notre programme.
Pour cela, il faut appeler la fonction FrameRate de Timer.
Le résultat de cette fonction est un entier. Il suffit donc de convertir la valeur et d'écrire, par exemple :
Sender.SysFonts.Font['sys:tahoma'
].TextOut('FPS: '
+ IntToStr(Timer.FrameRate),
4
, 10
, $FFFFFFFF
);
III-D-1-b. Écriture à partir d'une police « complexe »▲
Comme nous l'avons vu, il est très facile de créer des polices simples grâce à quelques lignes de code. Voyons maintenant comment créer des polices un peu plus complexes grâce à l'un des outils fournis avec Asphyre : FontTool.exe.
Cette méthode nécessite l'emploi de cet outil et l'intégration à votre programme est un peu plus complexe.
III-D-1-b-i. Description de l'outil FontTool.exe▲
Cet outil se trouve à la base du répertoire dans lequel vous avez décompressé Asphyre. Voici de quoi il a l'air :
Depuis ce programme, on va pouvoir créer une police personnalisée avec toute sorte d'effets (ombrage, antialiasing, etc.).
Chaque police sera représentée par une image (en général PNG) contenant tous les caractères de la police et d'un fichier XML contenant toutes les données relatives à cette police.
Voyons maintenant en détail à quoi servent tous ces panneaux.
Dans ce panneau, vous pouvez paramétrer la police, sa taille et ses caractéristiques (gras, italique) comme dans n'importe quel éditeur de texte. Le bouton Details ouvre la boite de dialogue de sélection de police de caractères que tout le monde connait.
Un aperçu de la police brute est affiché en dessous.
Ce panneau montre un aperçu de ce que donnera la police définitive. La mise à jour se fait au bout de 2-3 secondes après avoir modifié un paramètre.
Ce panneau vous permet de sélectionner les effets que vous voulez intégrer à votre police. Les effets disponibles sont l'anticrénelage (Antialias), le contour (Outline), l'ombrage (Shadow), l'exagération des contours (Sharpen). Ces effets peuvent être combinés. Pour les activer, il suffit de cliquer sur le bouton correspondant.
Les deux listes déroulantes du dessus permettent de définir l'étendue des caractères qui seront intégrés à la police. En effet, comme nous l'avons vu plus haut, une image de la police est générée. Si vous ne comptez utiliser que les chiffres par exemple, il est inutile de surcharger les ressources pour rien : intégrez uniquement les chiffres dans votre police. Vous pouvez néanmoins intégrer tous les caractères que vous voulez.
Ce panneau définit la luminosité (Brightness) et l'opacité (Opacity) des caractères. Si vous jouez sur la valeur de luminosité, vous verrez les couleurs s'assombrir ou s'éclaircir dans l'aperçu de gauche.
Le fond en carreau de cet aperçu permet de contrôler facilement l'opacité. Cette opacité vient en complément de celle que l'on peut définir dans TextOut. Ainsi, si on définit, dans FontTool, une opacité à 50 %, si dans TextOut, on la définit à 100 % ($FF), on aura tout de même une police transparente à 50 %.
Les deux panneaux suivants permettent de paramétrer les effets Border et Shadow.
Ce panneau paramètre l'effet de bordure. Vous pouvez spécifier la luminosité, l'opacité et l'épaisseur de cette bordure.
Ce panneau, quant à lui, permet de paramétrer l'effet d'ombrage. Vous pouvez ainsi modifier les valeurs d'adoucissement (les ombres seront moins marquées), de luminosité, d'opacité, et la distance de l'ombre par rapport à son objet en pixels. Si vous voulez donner l'effet d'un texte juste au-dessus de ce qu'il couvre, laissez une petite valeur. Comme pour tous les paramètres, vous pourrez vérifier le résultat dans l'aperçu de gauche.
Vous l'aurez compris, l'énorme avantage d'utiliser cette méthode pour créer une police – outre les effets disponibles – est que l'on ne dépend plus de la police elle-même : si vous utilisez une police non fournie avec Windows par exemple, vous n'avez plus à vous soucier de son déploiement puisque la police est intégrée à une image.
III-D-1-b-ii. Création d'une police▲
Voyons maintenant comment créer une police.
Exécutez FontTool.exe et paramétrez-le ainsi :
Panneau Windows Font Settings
Champ |
Valeur |
---|---|
Font |
Arial |
Size |
12 |
Italic |
Non |
Bold |
Oui |
Panneau Rendering Options
Champ |
Valeur |
---|---|
Etendu de caractères |
#032 à #126 |
Antialias |
Oui |
Outline |
Oui |
Shadow |
Non |
Sharpen |
Non |
Panneau Individual Letter Text
Champ |
Valeur |
---|---|
Brightness |
1.000 |
Opacity |
0.600 |
Panneau Outer Letter Border
Champ |
Valeur |
---|---|
Brightness |
0.700 |
Opacity |
1.000 |
Thickness |
1 |
Pour saisir une valeur exacte sur les gauges, on peut cliquer avec le bouton droit de la souris pour la saisir manuellement.
Panneau Letter Depth Shadow
Puisque l'effet Shadow n'est pas utilisé, il n'est pas nécessaire de paramétrer cette zone.
Une fois la police paramétrée, il faut la sauvegarder. Pour cela, cliquez sur le bouton en bas à gauche « Save Font As… » puis sélectionnez un chemin et un nom pour vos fichiers. Créez un répertoire « Fonts » dans le répertoire de votre projet puis enregistrez votre police dedans en la nommant « MaPolice.png ».
L'outil enregistrera automatiquement les deux fichiers dans le répertoire sélectionné. Ces deux fichiers auront le même nom.
Vous devez donc avoir deux fichiers :
-
MaPolice.png
- MaPolice.xml : ce fichier contient les données nécessaires à l'utilisation de la police.
III-D-1-b-iii. Créer le fichier XML de ressources▲
Auparavant, dans Asphyre eXtreme, on utilisait un composant, le TAsDb qui se chargeait d'ouvrir un fichier de type .asdb qui contenait toutes les ressources du programme. Ce fichier de ressources était créé à l'aide d'un outil proposé avec cette version d'Asphyre et permettait de rajouter toute sorte de fichiers.
Dans la version 4.1, tout se fait en XML. Le format .asdb n'existe plus (au grand dam de beaucoup de gens et au contraire de Asphyre Casual qui a repris ce système).
Vous devez donc créer un fichier XML qui fera appel au fichier XML de la police. Ce fichier XML peut contenir les données de polices et d'images que nous verrons plus loin.
Créez donc un nouveau fichier XML dans Delphi et appelez-le Ressources.xml.
Voici le squelette de ce fichier XML :
<unires>
</unires>
Il est inutile de spécifier la version d'encodage des caractères.
Chaque ressource (image, police) sera incluse dans un groupe que l'on peut nommer.
Comme notre police est contenue dans un fichier image, nous devons d'abord la répertorier dans un groupe d'image :
<image-group
name
=
"default group"
>
</image-group>
Entre ces balises, nous devons spécifier l'image. Les images sous Asphyre sont des textures et il convient de les déclarer comme tels :
<image
uid
=
"mapolicepicture"
type
=
"image"
>
<format
type
=
"a8r8g8b8"
miplevels
=
"auto"
/>
<textures
count
=
"1"
>
<texture
num
=
"0"
source
=
".\fonts\mapolice.png"
/>
</textures>
</image>
Regardons plus en détail ces lignes. La première ligne ouvre un nouveau nœud image. L'attribut uid est l'identifiant de l'image. L'attribut type définit lui le type d'image. Ce type peut être image, surface ou dynamic.
Le nœud format définit les caractéristiques de l'image. type définit comment sont encodées les couleurs et miplevels définit le niveau de mip-mapping.
Comme nous l'avons vu, il faut maintenant déclarer les textures qui composent notre image. Dans notre exemple, il n'y a qu'une seule texture : l'image MaPolice.png. Comme vous le voyez, il est tout à fait possible d'utiliser des chemins relatifs.
Une fois l'image créée, on peut déclarer une police dans un groupe de police de cette façon:
<font-group
name
=
"default"
>
</font-group>
Dans ce groupe, on répertorie une nouvelle police en utilisant l'image répertoriée plus haut et le fichier XML que l'on a créé avec FontTool.exe :
<font
uid
=
"mapolice"
type
=
"bitmap"
image
=
"mapolicepicture"
space
=
"0.5"
desc
=
".\fonts\mapolice.xml"
/>
Si on regarde cette ligne, excepté space (Sinon c'est trop facile!), on comprend rapidement à quoi correspondent les attributs :
- uid : identifiant de la police ;
- type : le type de ressource d'où elle est tirée ;
- image : l'uid de l'image qui contient la police créée précédemment ;
- space : facteur de multiplication pour la largeur du caractère espace (0.0 = pas d'espace ; 1.0 = un espace entier) ;
- desc : chemin du fichier XML qui définit la police.
Il est tout à fait possible de diviser les groupes en plusieurs fichiers XML. Par exemple, ici, on pourrait tout à fait avoir un fichier Images.xml et Fonts.xml, le premier contenant la déclaration du groupe d'images, le second du groupe de police.
En effet, une fois chargées, les ressources sont identifiées seulement par leur uid. Donc il importe peu qu'elles soient déclarées dans le même fichier !
Voilà le plus dur est fait ! À noter qu'il existe un utilitaire qui permet de faire ceci. Cet utilitaire, créé par le membre Ch@ser, est téléchargeable sur le site d'afterwarp ici. Les seuls inconvénients sont :
- quelle que soit l'image (texture complexe ou simple comme notre exemple) toutes les données sont ajoutées, même celles qui sont inutiles. Le XML en devient vite illisible ;
- l'interface pas franchement ergonomique à mon goût.
En revanche, on visualise plus facilement les données grâce à ce programme. L'autre avantage est que les possibilités du XML y sont – normalement – toutes. On peut donc utiliser ce programme pour savoir comment se constituent ces fichiers XML.
III-D-1-b-iv. Charger la police dans le projet▲
Une fois le fichier XML dûment créé, il faut maintenant charger dans le programme les données que l'on a définies dans ce fichier.
Comme toujours, Asphyre permet de faire assez simplement cette manœuvre. Contrairement à ce que l'on pourrait croire, le chargement de ces fichiers se fait avant l'initialisation et dans le OnCreate !
Le chargement se fait pour chacune des ressources de façon distincte :
procedure
TMainFrm.FormCreate(Sender: TObject);
begin
ImageGroups.ParseLink('Ressources.xml'
); //Chargement des images
FontGroups.ParseLink('Ressources.xml'
); //Chargement des polices
if
(not
Devices.Initialize(ConfigureDevice, Self
)) then
//Initialisation
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!'
, mtError, [mbOk], 0
);
Close();
Exit;
end
;
//Démarrage et paramétrage du Timer
Timer.Enabled := True
;
Timer.OnTimer := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS := 100
;
end
;
Notez bien l'ordre de chargement des ressources. Dans notre cas il est important !
En effet, dans le fichier XML, nous déclarons une image puis une police dépendante de cette image. Si l'on ne charge pas d'abord les images, le gestionnaire de polices ne va pas trouver l'image correspondante.
En général, puisque les images dépendent seulement de fichiers externes, on peut sans se soucier les charger en premier.
N'oubliez pas de déclarer les unités dans la clause uses :
- ImageGroups est déclaré dans MediaImages ;
- FontGroups est déclaré dans MediaFonts.
Une fois ces images chargées, on peut initialiser. En outre, il est également possible de vérifier que les ressources se soient chargées correctement.
Comme nous l'avons vu plus haut, il suffit de déclarer une méthode en cas d'erreur de chargement de cette façon :
procedure
TMainFrm.ConfigureDevice(Sender: TAsphyreDevice; Tag: TObject;
var
Config: TScreenConfig);
begin
Config.Width := ClientWidth;
Config.Height := ClientHeight;
Config.Windowed:= True
;
Config.WindowHandle:= Self
.Handle;
Config.HardwareTL := true
;
EventResolveFailed.Subscribe(OnResolveFailed, Sender); //Inscription de la méthode pour l'événement
end
;
Il faut alors créer une nouvelle procédure OnResolveFailed de cette façon :
procedure
TMainFrm.OnResolveFailed(Sender: TObject; EventParam: Pointer
;
var
Success: Boolean
);
begin
MessageDlg('Erreur de chargement de ressources.'
, mtError, [mbOk], 0
);
Devices.Finalize();
Timer.Enabled:= False
;
Application.Terminate();
end
;
Ainsi, si une erreur se produit, un message d'erreur apparait puis l'application se ferme.
Il ne faut pas libérer les images et polices créées: tout se fait dans des sections finalization dans leur unité respective.
III-D-1-b-v. Écrire avec la police▲
Nous voilà enfin parvenus à tout charger correctement! Il ne reste plus qu'à écrire avec cette police amoureusement créée.
De la même façon qu'avec l'écriture « simple », tout se passe encore dans le Callback de rendu. Par contre, cette fois-ci, ce n'est pas Sender.SysFonts, mais Sender.Fonts que l'on utilise :
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
begin
Sender.Fonts.Font['MaPolice'
].TextOut('Ma chaine de caracteres'
,
4
, 10
, $FFFF0000
);
end
;
procedure
TMainFrm.TimerEvent(Sender: TObject);
begin
DefDevice.Render(RenderCallback, self
, $FFFFFF
);
Timer.Process;
end
;
Pour des raisons esthétiques, j'ai simplement modifié la couleur de fond dans TimerEvent afin d'avoir un fond blanc.
Vous devriez avoir un résultat comme celui-ci :
Il existe également une autre procédure TextOut qui, à la place d'un Cardinal pour la couleur demande un type TColor2.
Mais un TColor2, c'est quoi donc ?? C'est évidemment un type d'Asphyre qui est une combinaison de deux couleurs. Mais pourquoi deux couleurs ? Que de questions !! Pour la réponse, je vous laisse découvrir ce que ça donne avec ce code :
Sender.Fonts.Font['MaPolice'
].TextOut('Ma chaine de caracteres'
,
4
, 10
, cColor2($FF00FF00
, $FFFF0000
) );
En effet, simplement en spécifiant deux couleurs et en utilisant la fonction cColor2, on obtient un dégradé le plus simplement du monde.
Cette fonction cColor2 est déclarée dans l'unité AsphyreTypes. Cette unité contient une quantité assez importante de constantes et autres routines de conversion.
Un bon conseil: si vous devez écrire une petite routine de conversion (de couleur par exemple) vérifiez d'abord que cette fonction n'existe pas déjà dans cette unité !
Par exemple, la fonction cColor2 utilisée ci-dessus renvoie un type TColor2 à partir de deux couleurs passées en paramètres.
On peut également modifier les options de dessin de la police. Par exemple, on voudrait modifier l'espace entre chaque caractère. Pour cela, la propriété Font propose une propriété Options qui permet ce genre de modifications.
Par exemple, ce code :
with
Sender.Fonts.Font['MaPolice'
] do
begin
Options.Kerning := -2
;
TextOut('Ma chaine de caracteres'
,
4
, 10
, cColor2($FF00FF00
, $FFFF0000
) );
end
;
va diminuer l'espace entre chaque caractère. D'autres paramètres sont disponibles comme la taille (attention: ce n'est qu'un facteur d'échelle : l'image d'origine reste la même !), afficher l'ombrage ou non, etc.
III-D-2. Dessiner des formes▲
Le dessin de forme, comme on le fait sur un TCanvas à l'aide de MoveTo, LineTo, Rectangle, FillRect, etc. dans la VCL, se fait de manière assez similaire puisque le type TAsphyreDevice propose une propriété Canvas de type TAsphyreCanvas qui permet toute sorte de dessin.
Toutes les fonctions de dessin se font dans la procédure de Callback du rendu que l'on a appelé jusque là RenderCallback. Dans les codes qui suivront, Sender sera donc du type TAsphyreDevice.
À l'instar du TCanvas de la VCL, le TAsphyreCanvas permet de dessiner une grande variété de figures géométriques. Nous ne verrons ici que le point, la ligne, le rectangle et l'ellipse afin de bien s'imprégner de la méthode de dessin d'Asphyre.
III-D-2-a. Paramétrer le dessin▲
Avant de dessiner quoi que ce soit, on peut paramétrer des valeurs telles que la largeur des lignes, le type d'anticrénelage, etc.
Pour cela, à l'instar du TCanvas de la VCL, le TAsphyreCanvas offre des propriétés. Voyons les plus utiles :
- LineAntialias : permet de spécifier si on veut utiliser l'anticrénelage ou non ;
- LineWidth : épaisseur de la ligne.
Nous allons le voir plus loin, mais plusieurs procédures de dessin se déclinent en deux versions : Ex et Hw (par exemple LineEx et LineHw). La fonction type Hw est plus rapide puisqu'elle utilise directement le matériel (la carte graphique). En revanche, Hw n'est pas impacté par LineWidth et l'anticrénelage n'est pas paramétrable.
III-D-2-b. Dessiner un point▲
La méthode de dessin de base est de dessiner un pixel. En fait, on ne dessine pas un pixel à proprement dit, on lui définit une couleur. Cette opération se fait en utilisant la procédure PutPixel du TAsphyreCanvas. Deux méthodes surchargées sont disponibles et ont uniquement pour différence le type de donnée pour les coordonnées du pixel. L'une demande X et Y, l'autre demande un type TPoint2.
procedure
PutPixel(const
Point: TPoint2; Color: Cardinal
;
DrawFx: Integer
); overload
;
procedure
PutPixel(x, y: Single
; Color: Cardinal
; DrawFx: Integer
); overload
;
Les paramètres suivants sont la couleur du point et sa méthode de dessin que nous détaillerons plus bas.
III-D-2-c. Dessiner une ligne▲
Il existe plusieurs façons de dessiner une ligne. Nous allons voir les deux plus caractéristiques.
Le première utilise LineEx. Cette fonction accepte un point de départ, un point d'arrivée et une couleur :
Sender.Canvas.LineEx(4
, 30
, 200
, 30
, $FFFF0000
);
Sender.Canvas.LineEx(Point(4
, 30
), Point(200
, 30
), $FFFF0000
);
Sender.Canvas.LineEx(Point2(4
, 30
), Point2(200
, 30
), $FFFF0000
);
Ces trois méthodes donnent rigoureusement le même résultat. À noter que la fonction Point2 ainsi que le type TPoint2 se trouve dans l'unité Vectors2.
La seconde méthode permet un peu plus de possibilités. On pourra en effet définir un dégradé et une transparence.
Sender.Canvas.LineHw(4
, 35
, 200
, 35
, $FFFF0000
, $FF00FF00
, fxuBlend);
Cette ligne dessine une ligne partant du rouge opaque ($FFFF0000) vers un vert opaque ($FF00FF00). Le dernier paramètre définit la façon dont va se dessiner la ligne. fxuBlend (unité AsphyreEffects.pas) spécifie d'utiliser les channels alpha (de transparence) des couleurs utilisées.
De la même façon que LineEx, LineHw propose plusieurs versions surchargées qui pourront être utiles suivant le type de données que vous avez au moment de l'appel (TPoint ou TPoint2).
Il est à noter que LineHw est plus rapide que LineEx.
III-D-2-d. Dessiner un rectangle▲
À l'instar du dessin de ligne, on retrouve deux types de procédures de TAsphyreCanvas: WireQuadEx et WireQuadHw. Les différences sont les mêmes : la première dessine un rectangle simple à l'aide de ses quatre côtés et d'une couleur.
La seconde dessine un rectangle avec les quatre côtés, un type TColor4 (combinaison de quatre couleurs pour définir un dégradé entre chaque « branche » du rectangle) et une méthode de dessin.
Exemple d'utilisation :
Sender.Canvas.WireQuadEx(Point4(Point2(4
, 10
), Point2(200
, 10
), Point2(200
, 30
), Point2(4
, 30
)), $FF00FF00
);
Sender.Canvas.WireQuadHw(
Point4(Point2(4
, 50
), Point2(200
, 50
), Point2(200
, 70
), Point2(4
, 70
)),
cColor4($FFFFFFFF
, $FFFF0000
, $FF00FF00
, $FF0000FF
),
fxuBlend);
La fonction Point4 est déclarée dans l'unité AsphyreTypes.
Voici ce que donne ce code :
Les quatre points donnés doivent être dans l'ordre afin d'avoir un rectangle. Il est tout à fait possible d'obtenir ceci :
avec ce code :
Sender.Canvas.WireQuadHw(
Point4(Point2(4
, 50
), Point2(200
, 70
), Point2(200
, 50
), Point2(4
, 70
)),
cColor4($FFFFFFFF
, $FFFF0000
, $FF00FF00
, $FF0000FF
),
fxuBlend);
En fait, les procédures WireQuadEx et WireQuadHw permettent de dessiner des quadrilatères, quels qu'ils soient.
À noter qu'il existe également des procédures permettant de dessiner des rectangles pleins. En particulier, FillQuad qui s'approche beaucoup de WireQuad :
Sender.Canvas.FillQuad(
Point4(Point2(4
, 50
), Point2(200
, 50
), Point2(200
, 70
), Point2(4
, 70
)),
cColor4($FFFFFFFF
, $FFFF0000
, $FF00FF00
, $FF0000FF
),
fxuBlend);
Donne un rectangle rempli de beaux dégradés :
III-D-2-e. Dessiner un cercle et une ellipse▲
Deux procédures sont fournies pour cette opération : Circle et Ellipse (oui les noms sont bien choisis…). Voici les paramètres demandés par cette procédure :
- x et y : la position du centre du cercle ;
- Radius : le rayon du cercle (type single) ;
- Steps : précision de l'arrondi. Plus la valeur sera grande, mieux l'arrondi sera dessiné. En revanche, le dessin sera plus lourd ;
- Color : la couleur de type cardinal ($AARRGGBB).
Exemple d'utilisation :
Sender.Canvas.Circle(50
, 50
, 40
, 20
, $FF0000FF
);
Comme toujours, voici le résultat :
La procédure Ellipse marche rigoureusement de la même façon sauf qu'évidemment, au lieu du rayon seulement, on définit le grand axe et le petit axe.
À noter que la procédure Circle ne fait rien d'autre qu'appeler la procédure Ellipse avec Radius en petit axe et grand axe !
De la même façon que pour le quadrilatère, il existe des procédures équivalentes pour dessiner un cercle et une ellipse remplis. Ces fonctions sont FillCircle et FillEllipse et s'utilisent de la même façon que FillQuad avec les paramètres de Circle et Ellipse à savoir un type TColor4 (pour faire des dégradés) et une méthode de dessin.
Pour illustrer ceci, voici le résultat que donne ce code :
with
Sender.Canvas do
begin
FillEllipse(50
, 50
, 40
, 20
, 20
,
cColor4($FFFFFFFF
, $FFFF0000
, $FF00FF00
, $FF0000FF
),
fxuBlend);
FillCircle(150
, 50
, 40
, 20
,
cColor4($FFFF00FF
, $FFFF00FF
, $FFFFFF00
, $FF00FFFF
),
fxuBlend);
end
;
III-D-3. Afficher une image▲
Passons maintenant à l'affichage des images.
La première chose à prendre en compte est la taille de l'image. En effet, Asphyre n'affichera que des images dont la taille est une puissance de 2 (2, 4, 8, 16, 32, 64, 128, 256, etc.).
Donc si vous avez par exemple une image de 300x100, il vous faudra l'inscrire dans une image dont la taille est une puissance de 2 supérieure. Dans cet exemple, une image de 512x128 convient. Évidemment, il faut choisir correctement la taille de son image originale pour éviter d'avoir une image dont une bonne partie n'affiche rien. Par exemple, si une image a un de ses côtés à 513 pixels, l'image affichée dans Asphyre devra être de 1024 pixels puisque dans 512, cela ne rentre pas.
Si vous utilisez une image dont la taille ne correspond pas à cette règle, votre image sera déformée afin de correspondre à sa taille puissance de 2 minimale.
Heureusement, un outil est fourni pour modifier les images. Il s'agit de TextureMaker.exe. Cet outil va permettre de créer une image qui respecte cette règle depuis une autre image.
Si nous utilisons ce programme, cela est uniquement dû à la taille de l'image non conforme. Il va sans dire que cette manipulation peut être faite depuis n'importe quel logiciel de dessin en copiant l'image dans une autre de taille conforme.
Si l'image est déjà à la bonne taille, il est inutile de faire cette opération.
III-D-3-a. Utilisation de TextureMaker▲
Ce programme sert à modifier les images de façon à ce qu'elles soient de taille d'une puissance de 2. Il peut également découper une image en plusieurs pour optimiser la place de vos sprites sur l'image.
Vous pouvez aussi extraire des images depuis une image source suivant un schéma défini.
III-D-3-a-i. Qu'est-ce qu'un pattern ?▲
Dans l'animation, le pattern est une notion très importante. En effet, lorsque vous animez un personnage par exemple, vous ne faites « que » succéder des images donnant l'illusion du mouvement. Donc deux options s'offrent à nous pour arriver à ce résultat :
- utiliser une image par mouvement ;
- utiliser une image qui regroupe tous les mouvements.
Il est évident qu'en termes d'utilisation de ressources, la seconde méthode est à privilégier !
Ce genre d'image s'appelle une feuille de sprites.
Voici donc à quoi pourrait ressembler une telle image :
Cette image est extraite de l'excellent tutoriel de Loka que vous trouverez ici.
Comme vous le voyez, chaque décomposition du mouvement est sur la même image. Chacune de ses décompositions est un pattern ! Par exemple, dans notre exemple la taille du pattern à utiliser est de la taille de l'image puisqu'il n'y a pas d'animation. En revanche, sur une feuille de sprites, la taille du pattern est celle de chacun des éléments.
III-D-3-a-ii. Paramétrage de TextureMaker▲
Comme d'habitude, analysons chaque partie du programme séparément.
Première chose à faire : charger l'image. Cela se fait à l'aide du bouton Browse. Une fois l'image chargée, il faut spécifier la taille du pattern.
Donc, si vous avez bien suivi, vous aurez compris que dans l'exemple qui nous intéresse, la taille du pattern est la taille de l'image. Vous remarquerez le « Number of patterns » grisé en dessous qui indique le nombre de patterns d'après la taille que vous avez spécifiée au-dessus.
Cette partie peut être intéressante si vous n'utilisez pas de format d'image transparent. Vous pouvez en effet choisir de garder les informations de transparence de l'image utilisée (channel Alpha) ou bien spécifier une couleur qui va servir de transparence avec une tolérance.
En règle générale, je vous conseille d'utiliser un format contenant des informations de transparence comme le PNG par exemple.
Cette partie permet de spécifier la taille de sortie de l'image. Comme nous l'avons vu, la taille doit être une puissance de 2. En cliquant sur le bouton , le programme calculera lui-même la taille la plus adaptée.
Ce paramètre correspond à l'écart que vous voulez donner entre vos patterns. Si vous voulez qu'ils soient accolés, laissez à 0, sinon, vous pouvez spécifier une autre valeur. Dans notre cas, laissons 0 puisque nous n'avons qu'un seul pattern sur la texture.
Cette partie permet de visualiser l'organisation finale et les pixels inutilisés d'après les paramètres que l'on a fournis.
III-D-3-b. Déclarer l'image dans le XML▲
Comme nous avons vu dans le chapitre III.D.1.b.iiiCréer le fichier XML de ressources, il faut déclarer l'image dans le fichier XML de ressources.
<image
uid
=
"MonImage"
type
=
"image"
>
<format
type
=
"a8r8g8b8"
miplevels
=
"auto"
/>
<textures
count
=
"1"
>
<texture
num
=
"0"
source
=
".\Images\MonImage.png"
/>
</textures>
</image>
Si l'image utilise des patterns, on peut les spécifier en les déclarant juste après le nœud format :
<pattern
width
=
"300"
height
=
"100"
count
=
"1"
padx
=
"0"
pady
=
"0"
/>
Dans notre cas, c'est évidemment inutile.
III-D-3-c. Charger l'image dans le programme▲
Nous l'avons déjà vu, le chargement des images depuis le fichier ressources se fait dans le OnCreate de la fiche avant l'initialisation d'Asphyre.
procedure
TMainFrm.FormCreate(Sender: TObject);
begin
ImageGroups.ParseLink('Ressources.xml'
);
if
(not
Devices.Initialize(ConfigureDevice, Self
)) then
begin
MessageDlg('Asphyre n''a pas pu s''initialiser!'
, mtError, [mbOk], 0
);
Close();
Exit;
end
;
Timer.Enabled := True
;
Timer.OnTimer := TimerEvent;
Timer.OnProcess:= ProcessEvent;
Timer.MaxFPS := 100
;
end
;
III-D-3-d. Dessiner l'image▲
Nous voilà enfin à l'étape du dessin. De la même façon que toutes les opérations de dessin sur le TAsphyreCanvas, cela se fait dans la procédure de Callback du rendu.
L'affichage d'une image se fait en deux temps :
- UseMap : on identifie d'abord l'image à dessiner ;
- TexMap : on l'affiche sur le TasphyreCanvas.
Voilà ce que donne le code :
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
//Constante pour donner la position de l'image
X = 10
;
Y = 10
;
var
ImageIndex: integer
;
p4: TPoint4;
begin
//On retrouve l'index de l'image que l'on veut utiliser
ImageIndex := Sender.Images.Image['MonImage'
].ImageIndex;
//On retrouve le rectangle d'affichage de l'image à l'aide des constantes
//de position et de la taille de la texture.
//PpBounds4 convertit 4 entiers (gauche, droite, largeur, hauteur) en un type TPoint4
p4 := pBounds4(X, Y,
Sender.Images[ImageIndex].Texture[0
].Size.X,
Sender.Images[ImageIndex].Texture[0
].Size.Y);
with
Sender.Canvas do
begin
//Pour vérifier que l'image est correcte, on peut désactiver l'anticrénelage
Antialias := atNone;
//On définit l'image à utiliser et son pattern (0)
UseImage(Sender.Images[ImageIndex], 0
);
//On dessine l'image sélectionnée
TexMap(p4, clWhite4, fxuBlend);
end
;
end
;
Regardons un peu plus en détail ce code.
UseImage sert donc à sélectionner l'image à utiliser. Il existe quatre versions surchargées de cette procédure suivant les données que vous avez. Celle que j'ai choisie est celle faisant appel au pattern. Il n'y en a qu'un, c'est le premier, son index est donc 0.
Il est aussi possible de définir ce que l'on veut afficher en utilisant des coordonnées de texture :
procedure
UseImage(Image: TAsphyreCustomImage;
const
Mapping: TPoint4; TexNum: Integer
= 0
); overload
;
Cette version accepte un paramètre de type TPoint4 qui représente les coordonnées de texture. Ici, puisque l'on veut afficher la totalité de l'image, on peut définir des coordonnées de textures qui utilisent cette totalité. Comme je vous le disais, vérifiez d'abord que ce que vous voulez faire n'existe pas déjà dans AsphyreTypes ! On utilisera donc
TexFull4: TPoint4 = ((x: 0
.0
; y: 0
.0
), (x: 1
.0
; y: 0
.0
), (x: 1
.0
; y: 1
.0
),
(x: 0
.0
; y: 1
.0
));
Le code sera donc :
UseImage(Sender.Images[ImageIndex], TexFull4);
Passons ensuite à TexMap.
procedure
TexMap(const
Points: TPoint4; const
Colors: TColor4;
DrawFx: Integer
);
Le premier paramètre est de type TPoint4 (un tableau de quatre TPoint) qui donne le cadre dans lequel dessiner l'image. Vous l'aurez compris, la fonction pRect4 de l'unité AsphyreTypes (encore elle) convertit un type TRect en type TPoint4.
Le paramètre suivant est un type TColor4 (un tableau de quatre couleurs en cardinal). clWhite4 est encore une constante de l'unité AsphyreTypes qui représente 4 blancs.
Le dernier paramètre de type Integer, nous le connaissons maintenant, il s'agit de la méthode de dessin.
Voici ce que peut donner ce code :
Il est évidemment possible de faire varier le paramètre Colors de façon à obtenir des effets intéressants sur l'image. Par exemple, voyons ce que donne ce code :
TexMap(pRect4(aRect), cColor4($66FFFFFF
, $99FF0000
, $CC00FF00
, $FF0000FF
), fxuMultiBlend);
La fonction cColor4 construit un type TColor4 à l'aide de une ou de quatre couleurs.
Le paramètre fxuMultiBlend permet de combiner les transparences entre celles de l'image et celles des couleurs définies ainsi que les couleurs elles-mêmes.
Nous obtenons alors cette image :
III-D-3-e. Les effets de déformations▲
Comme nous l'avons vu, la procédure TexMap utilise un type TPoint4 qui est simplement un tableau de quatre TPoint2 qui définissent chacun un angle de l'image. Il est donc tout à fait possible de modifier un ou plusieurs angles afin de déformer l'image. Ce code par exemple modifie le premier point :
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
X = 10
;
Y = 10
;
var
t: TAsphyreCustomImage;
p: TPoint4;
begin
with
Sender.Canvas do
begin
t := Sender.Images.Image['ImageDVP'
];
UseImage(t, TexFull4);
p := pBounds4(X, Y,
t.Texture[0
].Size.X,
t.Texture[0
].Size.Y);
p[0
].y := p[0
].y - 50
;
TexMap(p, clWhite4, fxuBlend);
end
;
end
;
Voci le résultat d'une telle modification :
On peut également effectuer une rotation. À l'aide des fonctions Rotate de AsphyreTypes : pRotate4, pRotate4se, pRotate4c.
Voici par exemple ce code :
procedure
TMainFrm.RenderCallback(Sender: TAsphyreDevice; Tag: TObject);
const
X = 100
;
Y = 50
;
var
t: TAsphyreCustomImage;
p: TPoint4;
begin
with
Sender.Canvas do
begin
t := Sender.Images.Image['ImageDVP'
];
UseImage(t, TexFull4);
p := pBounds4(X, Y,
t.Texture[0
].Size.X,
t.Texture[0
].Size.Y);
p := pRotate4(
Point2(X, Y),
Point2(t.Texture[0
].Size.X, t.Texture[0
].Size.Y),
Point2(0
, 0
),
45
);
TexMap(p, clWhite4, fxuBlend);
end
;
end
;
donne ce résultat :
Il est aussi possible de modifier l'échelle, de faire un miroir horizontal et vertical (respectivement pMirror4 et pFlip4). Leur utilisation ressemble à celle que l'on vient de voir, je ne reviendrai donc pas dessus.
III-E. Téléchargement du programme d'exemple▲
Vous pouvez télécharger le programme d'exemple ici : Exemple2D.zip
III-F. Conclusion▲
Nous venons donc de voir les principales méthodes de dessin en 2D. Ce sont les bases qui vous permettront de créer votre premier jeu en 2D avec Asphyre. Pourquoi par un shoot-them-up ou un jeu de plateformes ? Voir un casse-briques ! Bref, une fois que vous avez assimilé ce tutoriel, seule votre imagination sera votre limite !
Éclatez-vous et n'hésitez pas à me proposer vos créations !