Chapitre 3 : Les métasphères
|
|
a) Le programme de base |
Bien, avant toute chose, nous savons
qu'il va nous falloir afficher plusieurs de ces sprites, donc
il est une bonne idée de prévoir une structure "tball" afin
que l'on s'y retrouve. Une telle structure doit contenir la
position, et la direction de la balle : |
type |
tball=record |
px,py:single; |
dx,dy:single; |
end; |
. |
A partir de maintenant, pensons à
rendre l'effet indépendant de la résolution, et mettons donc
les variables tx et ty en constantes au début du programme.
Nous mettrons également le rayon de la balle ainsi que le
nombre de balles affichées : |
const |
nballs=40; |
ray=50; |
tx=640; |
ty=480; |
. |
De ce fait, le nombre de nouvelles
variables à créer est faible : il nous faut l'ensemble des
balles, ainsi qu'un tableau contenant le graphisme de ces
balles : |
ball:array[0..nballs-1]
of tball; |
graph:array[0..4*ray*ray-1] of byte;
|
. |
Remarquez que je n'ai pas mis comme
on pouvait s'y attendre graph |
:array[0..2*ray-1,0..2*ray-1] |
Ce choix sera expliqué plus tard.
Sinon, en mémoire, cela revient exactement au même... |
. |
Bien, faisons les choses dans l'ordre,
tout d'abord il nous fait créer le graphisme des balles, et
donc remplir notre tableau graph. |
procedure createball; |
var x,y,c:integer; |
dist:single; |
begin |
for y:=0 to 2*ray-1 do
begin |
for x:=0 to 2*ray-1 do begin |
dist:=sqrt(sqr(x-ray)+sqr(y-ray));
|
if dist255 then c:=255; |
end else c:=0; |
graph[x+y*2*ray]:=c; |
end; |
end; |
end; |
. |
On comprend aisément la syntaxe de
la ligne graph[x+y*2*ray]:=c, c'est toujours le même système
du x+y*longueur_de_ligne avec évidement longueur=2*ray. |
Il nous faut à présent une routine
qui gère le déplacement des balles. J'ai personellement une
préférence "gravitationelle", chaque balle attire les autres
: |
procedure move; |
var i,j:integer; |
dist:single; |
begin |
for i:=0 to nballs-1 do begin |
for j:=0 to nballs-1 do begin |
if i<>j then begin |
dist:=sqrt(sqr(ball[i].px-ball[j].px)+sqr(ball[i].py-ball[j].py)); |
if dist>200 then begin |
ball[i].dx:=ball[i].dx+250*(ball[j].px-ball[i].px)/(dist*dist*dist);
|
ball[i].dy:=ball[i].dy+250*(ball[j].py-ball[i].py)/(dist*dist*dist);
|
end; |
end; |
end; |
ball[i].px:=ball[i].px+ball[i].dx; |
ball[i].py:=ball[i].py+ball[i].dy; |
if (ball[i].px<0) or (ball[i].px>=tx)
then ball[i].dx:=ball[i].dx*0.999; |
if (ball[i].py<0) or (ball[i].py>=ty)
then ball[i].dy:=ball[i].dy*0.999; |
end; |
end; |
. |
Bon, on se fiche des détails ici,
cet algo marche bien et puis on est contents, ça n'est pas
un tutoriel sur le déplacement de particules... |
Là où ça devient réellement interessant,
c'est quand il nous faut afficher les balles, créons donc
une routine pour ce faire : |
procedure showball(px,py:integer;desc:tddsurfacedesc);
|
Qui doit donc afficher une métaball
sur la surface précisée par le déscripteur de surface qu'on
lui donne, à la coordonée également donnée. Le déscripteur
est celui retourné par Lock. |
Nous devons prendre en compte le fait
que la balle pourrait sortir de l'écran, auquel cas nous ne
devons pas l'afficher. Nous devons également prévoir le cas
où la balle ne serait que partiellement visible. |
Calculons d'abord les coordonées des
coins du rectangle que nous devons modifier à l'écran : |
a:=px-ray; |
b:=px+ray-1; |
c:=py-ray; |
d:=py+ray-1; |
. |
Ensuite, nous pouvons directement
voir si la balle est totalement invisible : |
if (a>=tx)
or (c>=ty) or (b<0) or (d<0) then exit; |
. |
Le cas intermédiaire est plus délicat
à gérer, mais on comprendra facilement ces lignes : |
if a<0 then a:=0; |
if b>=tx then b:=tx-1; |
if c<0 then c:=0; |
if d>=ty then d:=ty-1; |
. |
A partir de cet instant, nous sommes
sûrs que notre zone se trouve totalement dans l'écran. |
Ensuite, comme nous avons affaire
à un pointeur vers l'écran, et que la situation est approximativement
la même pour la balle, nous devons connaître l'addresse du
premier pixel modifié à l'écran, ainsi que l'adresse (l'indice)
du premier pixel affiché de la balle (qui n'est pas forcément
nul, vu que la balle peut n'être que partiellement visible,
son coin supérieur gauche alors non dessiné). |
En ce qui concerne l'écran, c'est
chose facile, vu que nous savons que la première coordonées
est en (a,c) : |
osbuff:=c*desc.lpitch+a; |
. |
Pour la balle, le premier pixel affiché
est en réalité (a-px+ray,c-py+ray). En effet, il s'agit tout
simplement du décalage que nous avons créé entre la position
du coin inférieur gauche donné : (px-ray,py-ray) et le coin
inférieur gauche à partir du quel nous allons effectivement
commencer à tracer : (a,b). Faites un dessin, vous comprendrez
que c'est correct :) |
Nous avons donc : |
osball:=(a-px+ray)+(c-py+ray)*ray*2;
|
. |
Ensuite nous devons parcourir la première
ligne, et après chaque pixel de cette ligne, nous devons incrémenter
la valeur de osbuff et osball pour passer au pixel suivant.
Dans le cas présent il sera plus simple de faire une boucle
allant de 0 à b-a, ce qui fait qu'au lieu d'incrémenter la
valeur de osbuff et osball, nous utiliserons osbuff+x et osball+x.
A la fin de chaque ligne, il faudra évidement penser à incrémenter
osball et osbuff de la taille de leur longueurs de ligne respectives
afin de les voir pointer vers le début de la ligne suivante.
|
. |
Bref que au final, l'algo donne ceci
: |
for y:=0 to d-c do begin |
for x:=0 to b-a do begin |
lum:=tgraph(desc.lpSurface)[osbuff+x]+graph[osball+x]; |
if lum>255 then lum:=255; |
tgraph(desc.lPsurface)[osbuff+x]:=lum;
|
end; |
inc(osball,ray*2); |
inc(osbuff,desc.lpitch); |
end; |
Avec tgraph déclaré comme array of
byte. |
. |
A présent, l'utilité de déclarer graph
comme un tableau monodimensionnel au lieu de bidimensionnel
prend tout son sens. Il est en effet bien plus rapide de procéder
comme nous faisons ici que de passer par indices de tableau
bidimensionnels. |
Au final, la routine donne : |
procedure showball(px,py:integer;desc:tddsurfacedesc); |
type tgraph=array of byte; |
var osbuff,osball:integer; |
a,b,c,d,lum:integer; |
x,y:integer; |
begin |
a:=px-ray; |
b:=px+ray-1; |
c:=py-ray; |
d:=py+ray-1; |
begin a:=px-ray; b:=px+ray-1; c:=py-ray;
d:=py+ray-1; |
if a<0 then a:=0; |
if b>=tx then b:=tx-1; |
if c<0 then c:=0; |
if d>=ty then d:=ty-1; |
osbuff:=c*desc.lpitch+a; |
osball:=(a-px+ray)+(c-py+ray)*ray*2;
|
for y:=0 to d-c do begin |
for x:=0 to b-a do begin |
lum:=tgraph(desc.lpSurface)[osbuff+x]+graph[osball+x]; |
if lum>255 then lum:=255; |
tgraph(desc.lPsurface)[osbuff+x]:=lum;
|
end; |
inc(osball,ray*2); |
inc(osbuff,desc.lpitch); |
end; |
end; |
. |
Show, quand à lui, donne
d'une manière triviale : |
procedure show; |
var surfacedesc:tddsurfacedesc; |
i:integer; |
begin |
surfacedesc.dwSize:=sizeof(surfacedesc); |
if buffer.lock(nil,surfacedesc,0,0)<>dd_ok
then begin |
isterminated:=true; |
exit; |
end; |
fillchar(surfacedesc.lpsurface^,ty*surfacedesc.lPitch,0);
|
for i:=0 to nballs-1 do showball(trunc(ball[i].px),trunc(ball[i].py),surfacedesc);
|
buffer.Unlock(surfacedesc.lpsurface); |
lpdd.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0);
|
surface.Flip(nil,0); |
end; |
. |
Pour le programme principal, il faudra
penser à initialiser comme ceci : |
randomize; |
createball; |
for i:=0 to nballs-1 do begin |
ball[i].px:=random(tx); |
ball[i].py:=random(ty); |
ball[i].dx:=0; |
ball[i].dy:=0; |
end; |
. |
Tout cela mis ensemble
nous donne le programme complet : |
program balls; |
uses |
Forms, DDraw, Windows; |
const |
nballs=40; ray=50; tx=640; ty=480;
|
type |
tball=record |
px,py:single; |
dx,dy:single; |
end; |
var |
form:TForm; |
isterminated:boolean; |
lpdd:idirectdraw; |
surface,buffer:IDirectDrawSurface;
|
ddsd:TDDSURFACEDESC; |
palette:array[0..255] of paletteentry;
|
ddpalette:IDirectDrawPalette; |
i:integer; |
ball:array[0..nballs-1] of tball;
|
graph:array[0..4*ray*ray-1] of byte;
|
. |
procedure createball; |
var x,y,c:integer; |
dist:single; |
begin for y:=0 to 2*ray-1 do begin
|
for x:=0 to 2*ray-1 do begin |
dist:=sqrt(sqr(x-ray)+sqr(y-ray));
|
if dist255 then c:=255; |
end else c:=0; |
graph[x+y*2*ray]:=c; |
end; |
end; |
end; |
. |
procedure showball(px,py:integer;desc:tddsurfacedesc); |
type tgraph=array of byte; |
var osbuff,osball:integer; |
a,b,c,d,lum:integer; |
x,y:integer; |
begin |
a:=px-ray; |
b:=px+ray-1; |
c:=py-ray; |
d:=py+ray-1; |
if (a>=tx) or (c>=ty)
or (b<0) or (d<0) then exit; |
if a<0 then a:=0; |
if b>=tx then b:=tx-1; |
if c<0 then c:=0; |
if d>=ty then d:=ty-1; |
osbuff:=c*desc.lpitch+a;
|
osball:=(a-px+ray)+(c-py+ray)*ray*2;
|
for y:=0 to d-c do begin |
for x:=0 to b-a do begin |
lum:=tgraph(desc.lpSurface)[osbuff+x]+graph[osball+x];
|
if lum>255 then lum:=255; |
tgraph(desc.lPsurface)[osbuff+x]:=lum;
|
end; |
inc(osball,ray*2); |
inc(osbuff,desc.lpitch); |
end; |
end; |
. |
procedure key(Sender: TObject; var
Key: Char); |
begin |
isterminated:=true; |
end; |
. |
procedure show; |
var surfacedesc:tddsurfacedesc; |
i:integer; |
begin |
surfacedesc.dwSize:=sizeof(surfacedesc);
|
if buffer.lock(nil,surfacedesc,0,0)<>dd_ok
then begin isterminated:=true; |
exit; |
end; |
fillchar(surfacedesc.lpsurface^,ty*surfacedesc.lPitch,0);
|
for i:=0 to nballs-1 do showball(trunc(ball[i].px),trunc(ball[i].py),surfacedesc);
|
buffer.Unlock(surfacedesc.lpsurface);
|
lpdd.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0);
|
surface.Flip(nil,0); |
end; |
. |
procedure move; |
var i,j:integer; |
dist:single; |
begin |
for i:=0 to nballs-1 do begin |
for j:=0 to nballs-1 do begin |
if i<>j then begin |
dist:=sqrt(sqr(ball[i].px-ball[j].px)+sqr(ball[i].py-ball[j].py));
|
if dist>200 then begin |
ball[i].dx:=ball[i].dx+250*(ball[j].px-ball[i].px)/(dist*dist*dist); |
ball[i].dy:=ball[i].dy+250*(ball[j].py-ball[i].py)/(dist*dist*dist);
|
end; |
end; |
end; |
ball[i].px:=ball[i].px+ball[i].dx;
|
ball[i].py:=ball[i].py+ball[i].dy;
|
if (ball[i].px<0) or (ball[i].px>=tx)
then ball[i].dx:=ball[i].dx*0.999; |
if (ball[i].py<0) or (ball[i].py>=ty)
then ball[i].dy:=ball[i].dy*0.999; |
end; |
end; |
. |
begin |
form:=TForm.create(application); |
form.KeyPreview:=true; |
form.borderstyle:=bsnone; |
@form.onkeypress:=@key; |
form.show; |
if directdrawcreate(nil,lpdd,nil)<>dd_ok
then exit; |
if lpdd.SetCooperativeLevel(form.handle,DDSCL_EXCLUSIVE
OR DDSCL_FULLSCREEN OR DDSCL_ALLOWMODEX OR DDSCL_ALLOWREBOOT)<>dd_ok
then exit; |
if lpdd.SetDisplayMode(tx,ty,8)<>dd_ok
then exit; |
fillchar(ddsd,sizeof(ddsd),0); |
ddsd.dwSize:=sizeof(ddsd); |
ddsd.dwflags:=ddsd_caps or ddsd_backbuffercount; |
ddsd.ddscaps.dwcaps:=ddscaps_primarysurface
or ddscaps_systemmemory or ddscaps_complex or ddscaps_flip;
|
ddsd.dwbackbuffercount:=1; |
if lpdd.CreateSurface(ddsd,surface,nil)<>dd_ok
then exit; |
ddsd.ddscaps.dwCaps:=DDSCAPS_BACKBUFFER;
|
if surface.GetAttachedSurface(ddsd.ddscaps,buffer)<>dd_ok
then exit; |
for i:=0 to 255 do begin |
palette[i].peRed:=i; |
if i<192 then palette[i].peGreen:=0
else palette[i].peGreen:=(i-192)*4; |
palette[i].peBlue:=i; |
end; |
if lpdd.CreatePalette(DDPCAPS_8BIT,@Palette,ddpalette,nil)<>dd_ok
then exit; |
surface.SetPalette(ddpalette); |
form.WindowState:=wsmaximized; |
showcursor(false); |
randomize; |
createball; |
for i:=0 to nballs-1 do begin |
ball[i].px:=random(tx); |
ball[i].py:=random(ty); |
ball[i].dx:=0; |
ball[i].dy:=0; |
end; |
isterminated:=false; |
repeat |
move; |
show; |
application.ProcessMessages; |
until isterminated; |
form.hide; |
lpdd.restoredisplaymode; showcursor(true);
|
ddpalette:=nil; |
buffer:=nil; |
surface:=nil; |
lpdd:=nil; |
end. |
. |
b) Quelques optimisations |
Je ne vais pas parler ici des optimisations
des routines en elles-mêmes car l'algo que j'ai donné pour
l'affichage de la balle, bien que fortement optimisable, n'est
quand même pas trop mal. Il existe quelques subtiles optimisations
avec les instructions pentium pro pour éviter le saut conditionnel
(un MOVC al, 255 par exemple), mais cela c'est le problème
du compilateur, et même si je n'hésite souvent pas à passer
par l'assembleur pour optimiser les algos, ça va être un peu
compliqué dans ce tutoriel. |
Je vais plutôt vous expliquer comment
mieux utiliser les routines. |
Première constatation : on passe son
temps à effacer tout l'écran en mémoire, alors qu'une partie
de celui-ci n'a pas à être effacé. Il serait diablement plus
efficace (du moins si le nombre de balles n'atteint pas un
gigantisme) de n'effacer l'écran QUE là où des balles se trouvent
affichées. Voici une routine, directement issue de showball
mais en plus simple, qui efface le rectangle donné : |
procedure clear(x1,y1,x2,y2:integer;desc:tddsurfacedesc); |
type tgraph=array of byte; |
var osbuff:integer; |
x,y:integer; |
begin |
if (x1>=tx) or (y1>=ty) or (x2<0)
or (y2<0) then exit; |
if x1<0 then x1:=0; |
if x2>=tx then x2:=tx-1; |
if y1<0 then y1:=0; |
if y2>=ty then y2:=ty-1; |
osbuff:=y1*desc.lpitch+x1; |
for y:=0 to y2-y1 do begin |
for x:=0 to x2-x1 do tgraph(desc.lPsurface)[osbuff+x]:=0;
|
inc(osbuff,desc.lpitch); |
end; |
end; |
. |
Pour l'utiliser, il faut juste se
souvenir d'effacer l'écran, mais APRES que celui-ci ai été
copié à l'écran. Il faudra donc redemander un pointeur vers
cet écran, ce qui nous donne dans show : |
procedure show; |
var surfacedesc:tddsurfacedesc; |
i:integer; |
begin |
surfacedesc.dwSize:=sizeof(surfacedesc);
|
if buffer.lock(nil,surfacedesc,0,0)<>dd_ok
then begin |
isterminated:=true; |
exit; |
end; |
for i:=0 to nballs-1 do showball(trunc(ball[i].px),trunc(ball[i].py),surfacedesc);
|
buffer.Unlock(surfacedesc.lpsurface); |
lpdd.WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN,0);
|
surface.Flip(nil,0); |
if buffer.lock(nil,surfacedesc,0,0)<>dd_ok
then begin |
isterminated:=true; |
exit; |
end; |
for i:=0 to nballs-1 do clear(trunc(ball[i].px)-ray,trunc(ball[i].py)-ray,trunc(ball[i].px)+ray,trunc(ball[i].py)+ray,surfacedesc);
|
buffer.Unlock(surfacedesc.lpsurface);
|
end; |
. |
Ce qui est plus rapide. |
Mais on peut encore faire beaucoup
mieux : on se rend bien compte que quand toutes les balles
se superposent, il est totalement inutile d'effacer chaque
balle, car la première aussi effacé toutes les autres. Bien
sur la superposition n'est pas forcément parfaite. |
Mais on peu très bien imaginer de
n'effacer que le rectangle qui englobe toutes les balles,
ce qui règle le problème. Mais si ces balles sont assez éparpillées,
on en revient à effacer tout l'écran, et il est alors préférable
de n'effacer que les balles séparément. Il faut donc être
potentiellement capable d'effectuer ces deux méthodes, avec
un système de décision efficace. |
D'abord : le calcul du rectangle est
facile. On initialise ses coins à des valeurs bidons, genre
x1=tx, y1=ty, x2=-1, y2=-1. Ensuite on passe en revue les
balles, ce qui est déjà fait dans la boucle d'affichage et
on en profitera. On regardera avec ce système : |
x1:=tx;y1:=ty;x2:=-1;y2:=-1; |
for i:=0 to nballs-1 do
begin |
showball(trunc(ball[i].px),trunc(ball[i].py),surfacedesc); |
if trunc(ball[i].px)-ray < x1 then
x1:=trunc(ball[i].px)-ray; |
if trunc(ball[i].py)-ray < y1 then
y1:=trunc(ball[i].py)-ray; |
if trunc(ball[i].px)+ray>x2 then x2:=trunc(ball[i].px)+ray; |
if trunc(ball[i].py)+ray>y2 then y2:=trunc(ball[i].py)+ray; |
end; |
A la fin, on connaîtra le rectangle
qui englobe toutes les balles : (x1,y1)-(x2,y2). |
. |
Et pour décider quelle méthode d'affichage
utiliser, il suffit de regarder la surface de ce rectangle
par rapport à la somme des surfaces des balles à effacer :
|
if(x2-x1+1)*(y2-y1+1) < nballs*ray*ray*4
then begin |
clear(x1,y1,x2,y2,surfacedesc); |
end else begin |
for i:=0 to nballs-1 do clear(trunc(ball[i].px)-ray,trunc(ball[i].py)-ray,trunc(ball[i].px)+ray,trunc(ball[i].py)+ray,surfacedesc); |
end; |
. |
Ce qui est sacrément plus rapide.
|
Et je n'ai rien de mieux à vous proposer
pour le moment d'un point de vue optimisation. |
N'oubliez tout de même pas que même
si on n'efface que ce qui a effectivement changé, le Flip,
lui, copie l'intégralité de notre buffer vers l'écran. Une
optimisation supplémentaire sera de se passer de buffer secondaire
pour directement travailler à l'écran, mais comme cela est
lent et qu'on ne peut pas lire et écrire dessus en même temps
(ici on en a besoin pour notre métasphere), il faudra encore
passer par un buffer temporaire, encore des complications,
mais ici le jeu n'en vaut plus la chandelle. Si on passait
en 1024x768, il est clair qu'il faudrait adopter cette approche. |