dans un jeu de plateforme, ou n'importe quel autre type de jeu 2D ( rpg par exemple )
le système présenté ici sera simple, pas besoin d'avoir fait math sup pour y arriver.
Avant de commencer , il nous faut une base de travail , voici un squelette d'application simple avec lequel
nous allons travailler :
Code : Tout sélectionner
; Initialisation de l'application
;
;
deltatime.f = 1/60
oldtime.f = 0
accumulator.f = 0
running.a = #True
InitSprite() ; si la commande foire, change de pc...
OpenWindow(0,0,0,1024,768,"",#PB_Window_SystemMenu | #PB_Window_SizeGadget | #PB_Window_MaximizeGadget)
OpenWindowedScreen(WindowID(0),0,0,1024,768,1,0,0)
While( running )
; gestion du deltatime
;
deltatime = (ElapsedMilliseconds() - oldtime) / 1000
oldtime = ElapsedMilliseconds()
accumulator + deltatime
; gestion des évenement PureBasic
;
Repeat
event = WindowEvent()
If event = #PB_Event_CloseWindow
running = #False
EndIf
Until event = 0
; ON GERERA LES INPUTS ICI
; gestion de la physique
;
While( accumulator > 1/60 )
; ON GERERA LA PHYSIQUE ICI
accumulator - (1/60)
Wend
; on efface l'écran
;
ClearScreen($101010)
; ON FERA LE RENDU ICI
; on fait le rendu dans un sprite
;
rendering_output.i = GrabSprite(#PB_Any,0,0,320,240)
; on re efface l'écran
;
ClearScreen(0)
ZoomSprite(rendering_output,800,600) ; on se fiche du ratio, ce n'est pas le sujet
DisplaySprite(rendering_output,512-400,384-300)
; rendu final
;
FlipBuffers()
Wend
CloseScreen()
CloseWindow(0)
End
Rien de bien compliqué ici , on ouvre un screen ( j'aime bien les modes fenetré , on peu facilement passer en plein écran en "resizant"
la fenêtre et en cachant la barre de titre. Le rendu interne sera en 320x240 et rescalé en 800x600 et centré sur la fenêtre ( qui peu aussi être rescalé par l'utilisateur ), oui, j'aime bien le style rétro , autant en profité.
Ici, la gestion du deltatime m'assure que la physique tournera à 60hz (1/60) par seconde , pour le reste , le rendu peut être à 1000fps , le jeu n'ira pas plus vite et cela simplifira
le calcul de la "physique" du jeu.
Maintenant , nous allons représenté le niveau avec une simple datasection , notre écran fait 320x240 pixels , nos tuilles
qui composerons le niveau feront elles , 16x16 pixels, soit un niveau composé de 20x15 tuilles
Code : Tout sélectionner
DataSection
level: ; 20x15
Data.l 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1
Data.l 1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,0,0,2,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1
Data.l 1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,0,0,0,2,2,2,2,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,1,1,0,2,2,2,2,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,2,2,0,2,2,2,2,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
EndDataSection
Dans notre datasection, chaque numéro correspond à une tuille que nous allons définir dans une petite structure
Code : Tout sélectionner
Structure Tile
spriteID.l ; identifiant sprite purebasic
collide.b ; flag si la tuile est bloquante ou non
EndStructure
Pour le stockage de notre niveau dans un tableau , 2 lignes de code suffisent :
Code : Tout sélectionner
Global Dim Level.l(20*15)
CopyMemory(?Level,@Level(),(20*15)*4)
Bien faire attention au type de Data dans la datasection et au type du tableau , sinon crash...
Quand à nos tuilles , elles seront définie et stocké de cette manière :
Code : Tout sélectionner
Structure Tile
spriteID.l ; identifiant sprite purebasic
collide.b ; flag si la tuile est bloquante ou non
EndStructure
Global NewList Tiles.Tile()
ce qui veut dire que notre liste de tuilles contiendra 2 éléments dans cet exemple.
et pour créer une tuille :
Code : Tout sélectionner
AddElement(Tiles())
Tiles()\collide = #True
Tiles()\spriteID = CreateSprite(#PB_Any,16,16)
StartDrawing(SpriteOutput(Tiles()\spriteID))
Box(0,0,16,16,RGB(0,192,64))
StopDrawing()
Code : Tout sélectionner
Procedure.l GetLevelSprite(x,y)
index = x + 20 * y
If index >= 0 And index < (20*15)
tile_id = Level(index)
If tile_id > 0
SelectElement(Tiles(),tile_id-1) ; -1 car le 1° élément est à l'index zero
ProcedureReturn Tiles()\spriteID
Else
ProcedureReturn 0
EndIf
EndIf
ProcedureReturn 0
EndProcedure
la variable index est utilisé car le tableau Level à une seule dimension , on fait donc une operation 2D -> 1D pour savoir quel élément
dans le tableau on recherche , avec bien entendu un garde fou if/endif afin de ne pas dépasser la taille du tableau.
Quand à l'affichage du niveau , il se fait facilement grâce à une petite fonction :
Code : Tout sélectionner
Procedure DisplayLevel()
For y = 0 To 15-1
For x = 0 To 20-1
If GetLevelSprite(x,y) <> 0
DisplaySprite( GetLevelSprite(x,y), x * 16 , y * 16)
EndIf
Next
Next
EndProcedure
Cette fonction est simple, mais gardez à l'esprit que dans notre exemple elle fonctionne correctement et répond à notre besoin.
si vous voulez avoir des niveaux plus grand avec gestion du scrooling , elle sera à revoir de façon à n'afficher que ce que l'on voit
à l'écran , sinon vous tournerez à 2fps...
Bon, on à notre squelette d'application, un affichage de niveau , reste plus qu'a codé notre "héro"
pour cela une petite structure minimaliste supplémentaire :
Code : Tout sélectionner
Structure Hero
x.f
y.f
w.f
h.f
vx.f
vy.f
EndStructure
j'utilise des floats, mais dans l'exemple , nous en avons pas vraiment besoin , comme je l'ai dit plus haut , la vitesse sera fixe, pas d'acceleration par exemple.
Maintenant, pour déplacer le personnage , rien de plus simple :
Code : Tout sélectionner
If KeyboardPushed(#PB_Key_Right)
Bob\vx = 1
ElseIf KeyboardPushed(#PB_Key_Left)
Bob\vx = -1
Else
Bob\vx = 0
EndIf
et dans notre boucle while :
Code : Tout sélectionner
; gestion de la physique
;
While( accumulator > 1/60 )
; ON GERERA LA PHYSIQUE ICI
Bob\x + Bob\vx
Bob\y + Bob\vy
accumulator - (1/60)
Wend
l'idée maintenant est de connaître sur quel tuile notre héro est , la "formule" est simple , il suffit de diviser les coordonées du héro
par la taille de la tuile :
la fonction floor n'existe pas en PB tel quel, il faut utiliser Round() avec le flag d'arrondissement vers le bas ( floor )tuile_x = floor( hero_position_x / taille_tuile_x )
tuile_y = floor( hero_position_y / taille_tuile_y )
cela dit , tuile_x & tuile_y sont maintenant des coordonées dans notre tableau Level() qui represente notre niveau.
mais c'est un tableau à une dimension , et nous on a 2 coordonées...
pour cela une autre "formule" à connaitre est le passage de coordonées 2D à un index 1D :
et l'inverse :INDEX = X + TAILLE_TABLEAU_2D_LARGEUR * Y
Il nous faut maintenant 2 fonctions pour savoir quel tuile en x,y à comme sprite ou si la tuile est solide ou non :X = INDEX % TAILLE_TABLEAU_2D_LARGEUR
Y = INDEX / TAILLE_TABLEAU_2D_LARGEUR
Code : Tout sélectionner
Procedure.l GetLevelSprite(x,y)
index = x + 20 * y
If index >= 0 And index < (20*15)
tile_id = Level(index)
If tile_id > 0
SelectElement(Tiles(),tile_id-1)
ProcedureReturn Tiles()\spriteID
Else
ProcedureReturn 0
EndIf
EndIf
ProcedureReturn 0
EndProcedure
Code : Tout sélectionner
Procedure.l GetLevelCollide(x,y)
index = x + 20 * y
If index >= 0 And index < (20*15)
tile_id = Level(index)
If tile_id > 0
SelectElement(Tiles(),tile_id-1)
ProcedureReturn Tiles()\collide
Else
ProcedureReturn 0
EndIf
EndIf
ProcedureReturn 0
EndProcedure
et une petite fonction qui nous permet en "coordonées monde" de savoir si une tuille est solide ou non :
Code : Tout sélectionner
Procedure.b IsSolid(x.f, y.f)
ProcedureReturn GetLevelCollide( Round(x / 16,#PB_Round_Down) , Round(y / 16,#PB_Round_Down) )
EndProcedure
et on à les coordonées dans le tableau correspondant au niveau. simple.
maintenant , nous allons pouvoir simplement tester les coins de notre héro , savoir si les coins sont en collision ou pas
avec une tuile solide, ici nous allons testé les collisions horizontale :

IsSolid(Bob\x + 15 + Bob\vx , Bob\y + Bob\vy) Or ; coin haut droit -> coté droit ( B & C dans notre schéma)
IsSolid(Bob\x + 15 + Bob\vx , Bob\y + 15 + Bob\vy) Or ; coin bas droit |
IsSolid(Bob\x + Bob\vx , Bob\y + Bob\vy) Or ; coin haut gauche |
IsSolid(Bob\x + Bob\vx , Bob\y + 15 + Bob\vy) ; coine bas gauche -> coté gauche ( A & D dans notre schéma)
ce qui nous donne en pb :
Code : Tout sélectionner
If IsSolid(Bob\x + 15 + Bob\vx , Bob\y + Bob\vy) Or IsSolid(Bob\x + 15 + Bob\vx , Bob\y + 15 + Bob\vy) Or IsSolid(Bob\x + Bob\vx , Bob\y + Bob\vy) Or IsSolid(Bob\x + Bob\vx , Bob\y + 15 + Bob\vy)
Bob\vx = 0
EndIf
Ajoutons de la gravité avec une collision vers le bas ( D & C dans le schéma )
Code : Tout sélectionner
If (IsSolid( Bob\x + Bob\vx , Bob\y + 16 + Bob\vy) Or IsSolid( Bob\x + 15 + Bob\vx , Bob\y + 16 + Bob\vy)) And Bob\vy => 0
Bob\vy = 0
Else
Bob\vy + 0.2
EndIf
Puis un test de collision avec le dessus ( A & B dans le schéma )
Code : Tout sélectionner
If Bob\vy < 0
If IsSolid( Bob\x + Bob\vx , Bob\y + Bob\vy) Or IsSolid( Bob\x + 15 + Bob\vx , Bob\y + Bob\vy)
Bob\vy = 0
EndIf
EndIf
And that'all !
vous avez le squelette d'un petit mario-like , a vous d'adaptez suivant vos besoins. Gardez à l'esprit que c'est une technique ( simpliste ) parmis tant d'autres plus ou moins complexe ( collision au pixel avec masque , algo SAT , swept , etc... )
voici le code complet :
Code : Tout sélectionner
Structure Tile
spriteID.l ; identifiant sprite purebasic
collide.b ; flag si la tuile est bloquante ou non
EndStructure
Global NewList Tiles.Tile()
; contenu du niveau
;
Global Dim Level.l(20*15)
CopyMemory(?Level,@Level(),(20*15)*4)
; Renvois le sprite du tableau déclaré au dessus
;
Procedure.l GetLevelSprite(x,y)
index = x + 20 * y
If index >= 0 And index < (20*15)
tile_id = Level(index)
If tile_id > 0
SelectElement(Tiles(),tile_id-1)
ProcedureReturn Tiles()\spriteID
Else
ProcedureReturn 0
EndIf
EndIf
ProcedureReturn 0
EndProcedure
; Renvois le flag de collision du tableau déclaré au dessus ( Level() )
;
Procedure.l GetLevelCollide(x,y)
index = x + 20 * y
If index >= 0 And index < (20*15)
tile_id = Level(index)
If tile_id > 0
SelectElement(Tiles(),tile_id-1)
ProcedureReturn Tiles()\collide
Else
ProcedureReturn 0
EndIf
EndIf
ProcedureReturn 0
EndProcedure
; Affiche le niveau
;
Procedure DisplayLevel()
For y = 0 To 15-1
For x = 0 To 20-1
If GetLevelSprite(x,y) <> 0
DisplaySprite( GetLevelSprite(x,y), x * 16 , y * 16)
EndIf
Next
Next
EndProcedure
; renvois si la tuile est solide ou pas en "coordonées monde"
;
Procedure.b IsSolid(x.f, y.f)
ProcedureReturn GetLevelCollide( Round(x / 16,#PB_Round_Down) , Round(y / 16,#PB_Round_Down) )
EndProcedure
; notre hero
;
Structure Hero
x.f
y.f
w.f
h.f
vx.f
vy.f
EndStructure
Bob.Hero
Bob\x = 10 * 16
Bob\y = 12 * 16
Bob\w = 16
Bob\h = 16
; Initialisation de l'application
;
;
deltatime.f = 1/60
oldtime.f = 0
accumulator.f = 0
running.a = #True
jump_flag = #False
InitSprite() : InitKeyboard()
OpenWindow(0,0,0,1024,768,"",#PB_Window_SystemMenu | #PB_Window_SizeGadget | #PB_Window_MaximizeGadget)
OpenWindowedScreen(WindowID(0),0,0,1024,768,1,0,0)
;
; Creation de la tuile 1
;
AddElement(Tiles())
Tiles()\collide = #True
Tiles()\spriteID = CreateSprite(#PB_Any,16,16)
StartDrawing(SpriteOutput(Tiles()\spriteID))
Box(0,0,16,16,RGB(0,192,64))
StopDrawing()
;
; Creation de la tuile 2
;
AddElement(Tiles())
Tiles()\collide = #False
Tiles()\spriteID = CreateSprite(#PB_Any,16,16)
StartDrawing(SpriteOutput(Tiles()\spriteID))
Box(0,0,16,16,RGB(0,32,8))
StopDrawing()
While( running )
; gestion du deltatime
;
deltatime = (ElapsedMilliseconds() - oldtime) / 1000
oldtime = ElapsedMilliseconds()
accumulator + deltatime
; gestion des évenement PureBasic
;
Repeat
event = WindowEvent()
If event = #PB_Event_CloseWindow
running = #False
EndIf
Until event = 0
; ON GERERA LES INPUTS ICI
ExamineKeyboard()
If KeyboardPushed(#PB_Key_Right)
Bob\vx = 1
ElseIf KeyboardPushed(#PB_Key_Left)
Bob\vx = -1
Else
Bob\vx = 0
EndIf
If KeyboardPushed(#PB_Key_Up) And jump_flag = #False And Bob\vy = 0
jump_flag = #True
Bob\vy = -4.5
ElseIf Not KeyboardPushed(#PB_Key_Up) And jump_flag = #True
jump_flag = #False
EndIf
; gestion de la physique
;
While( accumulator > 1/60 )
; collision latérale
;
If IsSolid(Bob\x + 15 + Bob\vx , Bob\y + Bob\vy) Or IsSolid(Bob\x + 15 + Bob\vx , Bob\y + 15 + Bob\vy) Or IsSolid(Bob\x + Bob\vx , Bob\y + Bob\vy) Or IsSolid(Bob\x + Bob\vx , Bob\y + 15 + Bob\vy)
Bob\vx = 0
EndIf
; collision basse
;
If (IsSolid( Bob\x + Bob\vx , Bob\y + 16 + Bob\vy) Or IsSolid( Bob\x + 15 + Bob\vx , Bob\y + 16 + Bob\vy)) And Bob\vy => 0
Bob\vy = 0
Else
Bob\vy + 0.2
EndIf
; collision haute
;
If Bob\vy < 0
If IsSolid( Bob\x + Bob\vx , Bob\y + Bob\vy) Or IsSolid( Bob\x + 15 + Bob\vx , Bob\y + Bob\vy)
Bob\vy = 0
EndIf
EndIf
Bob\x + Bob\vx
Bob\y + Bob\vy
accumulator - (1/60)
Wend
; on efface l'écran
;
ClearScreen($101010)
; ON FERA LE RENDU ICI
DisplayLevel()
StartDrawing(ScreenOutput())
Box(Bob\x,Bob\y,Bob\w,Bob\h,$FFFFFF)
StopDrawing()
; on fait le rendu dans un sprite
;
rendering_output.i = GrabSprite(#PB_Any,0,0,320,240)
; on re efface l'écran
;
ClearScreen(0)
ZoomSprite(rendering_output,800,600) ; on se fiche du ratio, ce n'est pas le sujet
DisplaySprite(rendering_output,512-400,384-300)
; rendu final
;
FlipBuffers()
Wend
CloseScreen()
CloseWindow(0)
End
; le niveau
DataSection
level: ; 20x15
Data.l 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1
Data.l 1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,0,0,2,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1
Data.l 1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,0,0,0,2,2,2,2,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,1,1,0,2,2,2,2,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,2,2,0,2,2,2,2,0,0,0,0,0,0,0,0,0,0,2,1
Data.l 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
EndDataSection