Concevoir un jeu d'exploration dans l'espace - Partie 04

< < Concevoir un jeu d'exploration dans l'espace - Partie 03

Tutoriels

Concevoir un jeu d'exploration dans l'espace - Partie 05 > >


Ce tutoriel a été réalisé avec la version 3.1.7 du moteur Dina.

Rappel des tutoriels précédents

Notre objectif est de concevoir un jeu qui se composera des éléments suivants :

 

Pour l'instant, voici ce que nous avons réalisé jusqu'à présent :

 

Pour finaliser le gameplay du jeu, il ne nous reste plus qu'à concevoir notre jeu d'atterrissage.

 

Préparations

Si vous ne l'avez pas encore fait, il faut créer le fichier landing.lua dans le répertoire game.
On reprend le même principe que pour le Logo : on charge le moteur Dina et on crée les fonctions load, update et draw.
Si vous avez fait un copier/coller depuis le fichier logo.lua, n'oubliez pas de renommer la variable Logo en Landing.

 

Ensuite, il faut rajouter le nouvel état dans le fichier main.lua :

Dina:addState("landing", "game/landing")

 

Avant de commencer à afficher des éléments, on va se créer plusieurs variables pour :

local Ship = {}
local Engine = {}
local Move = {}

local LifeGroup = {}
local nbLifes = 0

local GROUND_HEIGHT = 80
local Terrain = {}

local MAX_LANDING_SPEED = 0.5
local MIN_GROUND_LENGTH = 3
local GRAVITY = 0.6
local MAX_SHIP_SPEED = 1.5

 

Affichage des éléments du jeu

J'ai trouvé une image pour le fond qui reflète bien l'idée du Lunar-landing (lien dans les crédits). Nous allons donc l'ajouter.

Dans la fonction Landing.load, rajoutez les lignes suivantes :

local background = Dina("Image", "datas/images/game/landscape.png")
background:centerOrigin()
background:setPosition(Dina.width/2, Dina.height/2)
background:setZOrder(-100)

Le setZOrer permet de définir l'ordre d'affichage des images : plus il est petit et plus l'image sera affichée en premier.

 

Ensuite, nous allons afficher notre vaisseau. On va également centrer l'origine de l'image. Vous comprendrez pourquoi ultérieurement.

-- Affichage du vaisseau
Ship = Dina("Image", "datas/images/game/landing_ship.png")
Ship:centerOrigin()

Attention, il ne s'agit pas de la même image que pour le fichier game.lua ! Je vous les fournis dans les sources (lien tout en bas de la page).


Auteur de l'image originelle (celle de droite) : mongames (ship-space-craft)

 

Maintenant, on va le centrer en haut de l'écran. Pour cela, il nous faut ses dimensions.

local _, sh = Ship:getDimensions()

Et il ne nous reste plus qu'à indiquer la nouvelle position :

Ship:setPosition(Dina.width/2, sh/2)

 

On va rajouter quelques attributs à notre vaisseau :

Ship.vx = 0
Ship.vy = 0
Ship.speed = 3

 

Maintenant, nous allons ajouter le "moteur". Bon, en vrai, ce ne sont que les flammes mais cela devrait suffire non ?

Pour que le rendu soit "crédible", j'ai fait en sorte que le centre de l'image soit le même que celui du vaisseau. Vous comprendrez pourquoi plus loin.

--Affichage de l'engin
local sx, sy = Ship:getPosition()
Engine = Dina("Image", "datas/images/game/landing_engine.png", sx, sy)
Engine:centerOrigin()

 

Mais, on ne va pas afficher notre moteur tout de suite. On va le masquer :

Engine:setVisible(false)

 

Mouvements du vaisseau (partie 1)

Pour l'instant, tout ce que l'on a, c'est un fond d'écran avec un vaisseau en haut et au centre de l'écran.

Mettons un peu de mouvement dans tout cela !

On va se créer 3 fonctions :

Ces 3 fonctions recevrons toutes les 2 paramètres suivants :

function Move:left(Force, dt)
end

function Move:right(Force, dt)
end

function Move:up(Force, dt)
end

 

On va rajouter une 4ème fonction Move:move (avec les mêmes paramètres) qui sera appellée depuis Move:left et Move:right.
Comme dans game.lua, ces 2 fonctions font exactement la même chose. Donc, pour éviter de saisir 2 fois le même code (et devoir corriger à 2 emplacements si nécessaire), on se crée une fonction.

function Move:move(Force, dt)
end

 

Dans Move:left, on rajoute :

self:move(math.abs(Force) * -1, dt)

 

Et dans Move:right :

self:move(math.abs(Force), dt)

 

Les déplacements d'une fusée ne sont pas directement de droite à gauche mais elle tourne sur elle-même.

Nous allons donc remplir Move:move.

 

En premier, nous devons récupérer l'angle de rotation du vaisseau. Facile avec :

local angle = Ship:getRotation()

 

Ensuite, nous allons calculer l'angle de déplacement que nous ajoutons à l'angle du vaisseau comme ceci :

angle = angle + 90 * dt * Force

Notez que Force contient également la direction de la rotation.
Si vous trouvez que le vaisseau tourne trop lentement, vous devrez changer la valeur 90 par une valeur plus grande (ou une valeur plus petite s'il tourne trop vite).

On vérifie si l'angle est supérieur à 360 degrés: Si c'est le cas, on met l'angle à 0.
Si l'angle est inférieur à 0, on le met à 360 degrés.

if angle > 360 then angle = 0 end
if angle < 0 then angle = 360 end

 

Il ne reste plus qu'à appliquer la rotation au vaisseau et au moteur.

Ship:setRotation(angle)
Engine:setRotation(angle)

Comme le centre de l'image du vaisseau et celui de l'image du moteur sont identique, on peut les faire tourner de la même façon sans devoir faire des calculs savants.

 

On va ensuite définir les touches d'actions pour faire tourner notre vaisseau.
Dans landing.load, on doit rajouter les lignes suivantes :

--Définition des touches
Dina:setActionKeys(Move, "left", "continuous", {"Keyboard", "left"}, {"Gamepad", "leftx", -1}, {"Gamepad", "dpleft"})
Dina:setActionKeys(Move, "right", "continuous", {"Keyboard", "right"}, {"Gamepad", "leftx", 1}, {"Gamepad", "dpright"})

 

On va faire une petite modification qu'il ne faudra pas oublier de changer une fois tout fini (je vous le rappellerai tout de même).

Au lieu de devoir afficher le logo, le menu et d'arriver jusqu'à la planète, on va tricher un peu : on va aller directement au mini-jeu.

Dans le fichier main.lua, on va changer la ligne suivante :

Dina:setState("logo")

Par :

Dina:setState("landing")

 

Maintenant, si vous lancez le jeu, vous pourrez faire tourner votre vaisseau avec les touches définies.

 

Interface graphique utilisateur (partie 1)

Si notre joueur n'est pas assez habile, on va lui laisser quelques chances (j'ai opté pour 3). Affichons donc son nombre d'essais à l'écran.

Dans Landing.load, nous allons rajouter 3 images du vaisseau en haut et à gauche de l'écran.

Une première solution serait de créer 3 composants Image. Mais cela vous force à gérer individuellement chaque élément.
J'ai une autre solution qui va demander moins d'effort : utiliser un composant Group (voir la Documentation pour plus de détails).

Commençons par initialiser le nombre de vies et créer le composant Group :

nbLifes = 3
LifeGroup = Dina("Group")

 

Il y a plusieurs manières pour gérer notre affichage.

On peut :

  1. positionner chaque image à inclure à la bonne place à l'écran
  2. mettre les images dans le groupe avec un espacement entre elles puis de positionner le groupe à la bonne place à l'écran

Je vais vous présenter les 2 façons. Ce sera à vous d'utiliser celle que vous comprenez le mieux.

Cas 1 - Théorie

On boucle sur le nombre de vies.
Pour chaque vie, on crée une image.
On positionne ensuite l'image à l'écran.
Puis on ajoute l'image au groupe.

Cas 1 - Pratique

On boucle sur le nombre de vie :

for i = 1, nbLifes do
end

Pour chaque vie (donc, dans la boucle), on crée une image :

local lifeImg = Dina("Image", "datas/images/game/landing_ship.png", 0, 0, 0.2, 0.2)

On positionne ensuite l'image à l'écran :

-- On récupère les dimensions de l'image
local lw, lh = lifeImg:getDimensions()
-- on calcule sa position :
--   3            : espace à gauche pour ne pas coller sur le bord de l'écran
--   lw * (i - 1) : on ajoute la largeur du vaisseau (sauf pour le premier) pour qu'ils ne s'affichent pas les uns sur les autres
--   2 * (i - 1)  : espace entre les vaisseaux sauf pour le premier vaisseau
local x = 3 + lw * (i - 1) + 2 * (i - 1)
local y = 3
lifeImg:setPosition(x, y)

Puis on ajoute l'image au groupe.

LifeGroup:add(lifeImg)

 

Cas 2 - Théorie

On boucle sur le nombre de vies.
Pour chaque vie, on récupère les dimensions du groupe.
On crée l'image.
On positionne l'image à côté du groupe en ajoutant un décalage.
On ajoute l'image au groupe.
On positionne le groupe à l'écran.

Cas 2 - Pratique

On boucle sur le nombre de vie :

for i = 1, nbLifes do
end

Pour chaque vie (donc dans la boucle), on récupère les dimensions et le nombre d'éléments du groupe :

local lgw, lgh = LifeGroup:getDimensions()
local nbi = LifeGroup:nbComponents()

On crée l'image.

local lifeImg = Dina("Image", "datas/images/game/landing_ship.png", 0, 0, 0.2, 0.2)

On positionne l'image à côté du groupe en ajoutant un décalage :

local x = lgw + 2 * (nbi - 1)
local y = 0
lifeImg:setPosition(x, y)

On ajoute l'image au groupe.

LifeGroup:add(lifeImg)

On positionne le groupe à l'écran. Donc, après la boucle, on rajoute :

LifeGroup:setPosition(3,3)

J'ai laissé les 2 versions dans le code pour que vous puissiez voir une autre façon de procéder.

 

Construction du terrain

Pour construire le terrain d'atterrissage, je suis parti d'un tutoriel que j'avais réalisé : Générateur de sol pour Lunar-Landing.

Il a été adapté pour utiliser le moteur Dina.

Au début de cette partie du tutoriel, je vous ai fait rajouter 2 variables :

local GROUND_HEIGHT = 80
local Terrain = {}

 

On va créer une nouvelle fonction ConstructTerrain qui prendre 2 paramètres :

-- Fonction pour la construction du terrain
local function ConstructTerrain(minSections, maxSections)
end

 

En premier, nous allons calculer la coordonnée minimum en Y du terrain :

local minTerrain = Dina.height - GROUND_HEIGHT

Ensuite, nous allons calculer la taille minimale et maximale de chaque section :

local maxLargeurSection = math.floor(Dina.width / minSections)
local minLargeurSection = math.ceil(Dina.width / maxSections)

Puis, on calcule les coordonnées du premier point (on le positionne àdroite de l'écran) :

local x = 0
local y = love.math.random(minTerrain, Dina.height)

Ensuite, on va boucler tant que la coordonnée en X ne dépasse pas la largeur de l'écran.
On ajoute ce point dans la liste des points (la variable Terrain) puis on recalcule un nouveau point.

while (x < Dina.width) do
  local vector = Dina("Vector", x, y)
  table.insert(Terrain, vector)
  -- Calcul d'une nouvelle position
  x = x + love.math.random(minLargeurSection, maxLargeurSection)
  y = love.math.random(minTerrain, Dina.height)
end

Puis, pour finir, on crée le dernier point le plus à droite de l'écran qu'on ajoute dans la liste des points :

table.insert(Terrain, Dina("Vector", Dina.width, Terrain[1].y))

Vous comprendrez plus loin pourquoi j'utilise la même hauteur que le premier point.

 

Il ne nous reste plus qu'à appeler la fonction dans Landing.load :

love.math.setRandomSeed(love.timer.getTime()) -- pour avoir un terrain différent à chaque fois
  --Construction du terrain
  ConstructTerrain(10,20)

J'ai mis un minimum de 10 sections et un maximum de 20 sections mais vous pouvez augmenter ou réduire ces nombres, dépendant de ce que vous voulez obtenir.

Les valeurs 10 et 20 me semblent être un bon compromis entre des zones trop petites (donc trop escarpées) ou trop grande (donc peu de dénivelé).

 

Maintenant que nous avons créé nos points, on va les afficher pour former le terrain.

Pour cela, on va simplement boucler sur la liste de points tout à la fin de Landing:draw.

for ind=1,#Terrain - 1 do
  local pointA = Terrain[ind]
  local pointB = Terrain[ind + 1]
  love.graphics.line(pointA.x, pointA.y, pointB.x, pointB.y)
end

Important : on s'arrête à l'avant-dernier point.

 

Gravité et collision

A ce stade, on affiche le sol de la planète, notre vaisseau et le nombre de vies disponibles.

On va rajouter un petit élément : la gravité.

Dans Landing:update, après l'appel de Dina:update, nous allons rajouter la ligne suivante :

-- Application de la gravité sur le vaisseau
Ship.vy = Ship.vy + (GRAVITY * dt)

Cette ligne permet de simuler la gravité.

 

On va ensuite mettre à jour la position du vaisseau et du moteur. Pour cela, nous allons (encore) créer une nouvelle fonction :

local function MoveShip()
end

En premier lieu, on va limiter la vitesse de déplacement :

-- Limitation des vitesses de déplacement
if math.abs(Ship.vx) > MAX_SHIP_SPEED then
  if Ship.vx > 0 then
    Ship.vx = MAX_SHIP_SPEED
  else
    Ship.vx = MAX_SHIP_SPEED * -1
  end
end
if math.abs(Ship.vy) > MAX_SHIP_SPEED then
  if Ship.vy > 0 then
    Ship.vy = MAX_SHIP_SPEED
  else
    Ship.vy = MAX_SHIP_SPEED * -1
  end
end

Ensuite, on va repositionner le vaisseau :

--Positionnement du vaisseau
local sx, sy = Ship:getPosition()
Ship:setPosition(sx + Ship.vx, sy + Ship.vy)

Et le moteur à la même position que le vaisseau :

--Positionnement du moteur
sx, sy = Ship:getPosition()
Engine:setPosition(sx, sy)

 

Enfin, on rajoute l'appel de la fonction MoveShip juste après le calcul de la gravité :

-- Application de la gravité sur le vaisseau
Ship.vy = Ship.vy + (GRAVITY * dt)
--Déplacement du vaisseau
MoveShip()

 

 

Maintenant que notre vaisseau "tombe", on va regarder s'il entre en collision avec le sol.

Comme notre sol est composé de sections, on va rechercher sous quelle section se trouve notre vaisseau.

Pour cela, on va parcourir l'ensemble des points et regarder si la position du vaisseau en X est entre 2 points (ou sur un point).

Voici ce que cela donne :

local function SearchSectionX(posX)
  local ind=1
  for ind=1,#Terrain - 1 do
    if Terrain[ind].x <= posX and posX < Terrain[ind+1].x then
      return ind
    end
  end
  return 0
end

Cette fonction ne retourne 0 que dans les cas suivants :

 

On va ensuite récupérer la hauteur de la section exactement sous la position du vaisseau.

Je vous invite à lire mon tutoriel Générateur de sol pour Lunar-Landing pour plus d'explications (troisième étape).

local function getTerrainHauteur(posX)
  local debSection = SearchSectionX(posX)
  local dx = Terrain[debSection + 1].x - Terrain[debSection].x
  local as = posX - Terrain[debSection].x
  local dy = Terrain[debSection + 1].y - Terrain[debSection].y
  local ap = dy * (as / dx)
  return Terrain[debSection].y + ap
end

Dans Landing:update, après avoir déplacé le vaisseau (appel de MoveShip), on va lancer le calcul de la hauteur du sol. On en profite pour récupérer la position et la dimension du vaisseau :

--Vérification de la collision avec le terrain
local GroundY = getTerrainHauteur(Ship.x)
local sx, sy = Ship:getPosition()
local _, sh = Ship:getDimensions()

Après avoir calculé la hauteur du sol, on va vérifier si le vaisseau ne rentre pas en collision avec le sol.
Comme nous avons centré l'origine de l'image, on va devoir ajouter une demi-hauteur du vaisseau à sa position.

Comme notre vaisseau est en mouvement (gravité oblige), on va vérifier si la différence entre la position du sol et celle du vaisseau est inférieure à une valeur minimum (ici, MIN_GROUND_LENGTH vaut 3 pixels). Si c'est inférieur à 0, c'est que le vaisseau est en dessous du sol, donc il s'est écrasé...

Ensuite, on va contrôler la vitesse d'atterrissage. Si elle est supérieure à MAX_LANDING_SPEED (qui vaut 0.5), la vitesse est trop grande et il s'écrasera au sol.

Bien entendu, si le vaisseau s'écrase, le joueur perd une vie et on repositionne le vaisseau à sa position de départ. Si le joueur n'a plus de vie, on affiche le GameOver.

Autrement, on va stopper la vitesse et la rotation du vaisseau, repositionner le vaisseau et le moteur et indiquer que le vaisseau vient d'atterrir (Ship.landed = true).

if (GroundY - (sy + sh/2)) < MIN_GROUND_LENGTH then
  if Ship.vy < MAX_LANDING_SPEED then
    --Atterrissage réussi
    Ship.landed = true
    Ship.vx = 0
    Ship.vy = 0
    local y = GroundY - sh/2
    Ship:setPosition(sx, y)
    Ship:setRotation(0)
    Engine:setVisible(false)
    Engine:setPosition(sx, y)
    Engine:setRotation(0)
  else
    --Atterrissage manqué
    Ship:setPosition(Dina.width/2, sh/2)
    nbLifes = nbLifes - 1
    LifeGroup:removeComponent(LifeGroup.components[LifeGroup:nbComponents()])
    if nbLifes < 1 then
      Dina:setState("gameover")
    end
  end
end

 

Il y a un point dans le code ci-dessus qui doit être expliqué.

Quand le joueur s'est écrasé au sol, on repositionne le vaisseau à sa position de départ (ça vous l'avez compris).
Puis on décrémente nbLifes. Jusqu'ici, vous devriez avoir tout compris.

La ligne suivante est plus difficile à appréhender car c'est elle qui permet de retirer une vie de l'affichage. Avec quelques détails, vous allez mieux comprendre.

Comme nous avons utilisé un groupe, c'est lui qui gère la position, la mise à jour et l'affichage de tous les composents qu'il contient.

Donc, il nous suffit de supprimer le dernier élément du groupe pour que celui-ci ne soit plus affiché.

 

Pour vous simplifier la compréhension, je vais décomposer la ligne : LifeGroup:removeComponent(LifeGroup.components[LifeGroup:nbComponents()])

Pour supprimer le dernier élément du groupe, on va récupérer le nombre d'éléments dans le groupe :

local nbElems = LifeGroup:nbComponents()

Ensuite, on va récupérer le dernier élément du groupe :

local lastElem = LifeGroup.components[nbElems]

Puis, on le supprime du groupe :

LifeGroup:removeComponent(lastElem)

 

Donc la ligne :

LifeGroup:removeComponent(LifeGroup.components[LifeGroup:nbComponents()])

équivaut aux lignes suivantes :

local nbElems = LifeGroup:nbComponents()
local lastElem = LifeGroup.components[nbElems]
LifeGroup:removeComponent(lastElem)

 

Comme par magie, lorsqu'on retire un élément du groupe, il n'est plus affiché ("évidemment", vous me direz).

Mais, et c'est là où est la "force" de Dina, c'est qu'il ne faut que la ligne ci-dessous dans Landing:draw pour que la magie opère :

Dina:draw(false)

 

Mouvements du vaisseau (partie 2)

Maintenant, je vous entends me dire : "OK, notre vaisseau subit la gravité mais comment on allume les moteurs ?".

Et bien, on s'en occupe de suite.

 

Je vous avais fait préparer une fonction Move:up dans la première partie des mouvements du vaisseau. Nous allons la remplir !

 

Dans cette fonction Move:up, on va, dans un premier temps, vérifier si le vaisseau est posé.

On enclenche les moteurs (c'est juste les rendre visibles mais ça fait plus classe je trouve).

Ensuite, on va calculer la force de poussée des moteurs selon l'angle du vaisseau.
Pour cela, on a besoin de l'angle de rotation du vaisseau, de sa vitesse et de la force mise sur le stick du gamepad (entre -1 et 0 ou entre 0 et 1).
Info : si le joueur utilise le clavier ou un bouton, la force est toujours de 1.
De plus, si la force calculée est inférieure à 0.001, on considère qu'elle vaut 0.

Enfin, on va appliquer cette force à la vitesse du vaisseau.
Info : si vous ne le saviez pas, deux forces s'additionnent toujours.

Voici ce que cela donne :

function Move:up(Force, dt)
  if not Ship.landed then
    Engine:setVisible(true)
    local angle = math.rad(Ship:getRotation())
    local force_x = math.sin(angle) * (Ship.speed * dt) * math.abs(Force)
    local force_y = math.cos(angle) * (Ship.speed * dt) * math.abs(Force) * -1
    if math.abs(force_x) < 0.001 then force_x = 0 end
    if math.abs(force_y) < 0.001 then force_y = 0 end
    Ship.vx = Ship.vx + force_x
    Ship.vy = Ship.vy + force_y
  end
end

 

Pour que le joueur puisse enclencher les moteurs, il ne reste plus qu'à définir les touches d'action dans Landing.load.

Dina:setActionKeys(Move, "up", "continuous", {"Keyboard", "up"}, {"Gamepad", "lefty", -1}, {"Gamepad", "dpup"})

 

On va rajouter les ligne ci-dessous au tout début de la fonction Landing:update :

--On masque le moteur
Engine:setVisible(false)

Cela permet de "couper" les moteurs.

 

Résolution de problèmes

Actuellement, il y a les problèmes suivants :

Et un dernier problème, visible uniquement si on refait une tentative après un GameOver :

On va régler les problèmes du plus facile au plus compliqué.

 

Problème 1 - Sol affiché plusieurs fois

Avoir plusieurs sols affichés nous indique que la liste des points n'a pas été effacée. Pour régler cela, il suffit de rajouter la ligne ci-dessous avant le changement d'état :

if nbLifes < 1 then
  Terrain = {} --efface la liste des points
  Dina:setState("gameover")
end

 

Problème 2 - Faire tourner lorsqu'on a atterri

Pour ne plus avoir ce problème, on va empêcher le vaisseau de tourner quand le joueur appuie sur la touche d'action.

Pour cela, nous allons rajouter une condition dans la fonction Move:move :

function Move:move(Force, dt)
  if not Ship.landed then --le vaisseau n'a pas atterri donc on peut le faire tourner
    local angle = Ship:getRotation()
    angle = angle + 90 * dt * Force
    if angle > 360 then angle = 0 end
    if angle < 0 then angle = 360 end
    Ship:setRotation(angle)
    Engine:setRotation(angle)
  end --if not Ship.landed
end

 

Problème 3 - Crash du jeu lorsque le vaisseau sort de l'écran

Pour ce problème, on va simplement faire en sorte que le vaisseau change de position.

De plus, on va utiliser 2 autres images du vaisseau et de son moteur. Cela permettra de faire que le vaisseau ne se téléporte pas pour le joueur.

On va donc créer 4 nouvelles variables :

local ShipLeft = {}
local EngineLeft = {}
local ShipRight = {}
local EngineRight = {}

Ensuite, nous allons créer les 2 nouveaux vaisseaux et leurs engins dans Landing.load.
Les lignes ci-dessous doivent être insérées juste après la création du moteur

--Affichage de l'engin
local sx, sy = Ship:getPosition()
Engine = Dina("Image", "datas/images/game/landing_engine.png", sx, sy)
Engine:centerOrigin()
Engine:setVisible(false)

-- Création de la copie gauche du vaisseau
ShipLeft = Dina("Image", "datas/images/game/landing_ship.png")
ShipLeft:centerOrigin()
ShipLeft:setPosition(sx - Dina.width, sy) --on retire la largeur de l'écran
local slx, sly = ShipLeft:getPosition()
EngineLeft = Dina("Image", "datas/images/game/landing_engine.png", sx, sy)
EngineLeft:centerOrigin()
EngineLeft:setVisible(false)
EngineLeft:setPosition(slx, sly)
-- Création de la copie droite du vaisseau
ShipRight = Dina("Image", "datas/images/game/landing_ship.png")
ShipRight:centerOrigin()
ShipRight:setPosition(sx + Dina.width, sy) --on ajoute la largeur de l'écran
local srx, sry = ShipRight:getPosition()
EngineRight = Dina("Image", "datas/images/game/landing_engine.png", sx, sy)
EngineRight:centerOrigin()
EngineRight:setVisible(false)
EngineRight:setPosition(srx, sry)

 

Dans la fonction Move:up, on va allumer les moteurs des copies en même temps que l'original :

Engine:setVisible(true)
EngineLeft:setVisible(true)
EngineRight:setVisible(true)

 

Dans la fonction Move:move, on rajoute les lignes suivantes juste après le changement d'angle du vaisseau et du moteur pour avoir le même angle pour les 3 vaisseaux et leurs moteurs respectifs :

Ship:setRotation(angle)
Engine:setRotation(angle)
EngineLeft:setRotation(angle)
ShipLeft:setRotation(angle)
ShipRight:setRotation(angle)
EngineRight:setRotation(angle)

 

Dans la fonction MoveShip, juste après le positionnement du vaisseau, on va vérifier si le vaisseau est en dehors de l'écran. Si oui, on le téléporte de l'autre côté et on change les positions des 2 autres vaisseaux. Avec ça, on a l'impression que le vaisseau passe d'un côté à l'autre tout à fait normalement alors qu'il se téléporte. Sympa ce petit truc non ?

  --Positionnement du vaisseau
  local sx, sy = Ship:getPosition()
  Ship:setPosition(sx + Ship.vx, sy + Ship.vy)
  
  -- Téléportation du vaisseau de l'autre côté
  sx, sy = Ship:getPosition()
  if sx < 0 then
    Ship:setPosition(Dina.width - sx, sy)
  elseif sx > Dina.width then
    Ship:setPosition(sx - Dina.width, sy)
  end
  
  --Positionnement du moteur
  sx, sy = Ship:getPosition()
  Engine:setPosition(sx, sy)
  
  --Positionnement de la copie gauche du vaisseau
  ShipLeft:setPosition(sx - Dina.width, sy)
  local slx, sly = ShipLeft:getPosition()
  EngineLeft:setPosition(slx, sly)
  --Positionnement de la copie droite du vaisseau
  ShipRight:setPosition(sx + Dina.width, sy)
  local srx, sry = ShipRight:getPosition()
  EngineRight:setPosition(srx, sry)

 

Enfin, nous allons refaire la vérification de la collision du vaisseau :

if (GroundY - (sy + sh/2)) < MIN_GROUND_LENGTH then
  if Ship.vy < MAX_LANDING_SPEED then
    --Atterrissage réussi
    Ship.landed = true
    Ship.vx = 0
    Ship.vy = 0
    local y = GroundY - sh/2
    Ship:setPosition(sx, y)
    Ship:setRotation(0)
    Engine:setVisible(false)
    Engine:setPosition(sx, y)
    Engine:setRotation(0)
  else --if Ship.vy < MAX_LANDING_SPEED
    --Atterrissage manqué
    Ship:setPosition(Dina.width/2, sh/2)
    MoveShip() --ici, c'est pour forcer le calcul des positions de tous les vaisseaux
    nbLifes = nbLifes - 1
    LifeGroup:removeComponent(LifeGroup.components[LifeGroup:nbComponents()])
    if nbLifes < 1 then
      Terrain = {} -- efface la liste des points
      Dina:setState("gameover")
    end
  end --if Ship.vy < MAX_LANDING_SPEED
end --if (GroundY - (sy + sh/2)) < MIN_GROUND_LENGTH

 

J'ai obtenu un crash lorsque je faisais les tests de cette correction. Il s'est avéré que la fonction SearchSectionX retournait 0. Pour éviter cela, on va simplement modifier le premier et le dernier point pour respectivement retirer 1 pixel et ajouter 1 pixel.

Donc, dans la fonction ConstructTerrain, on va changer la ligne :

local x = 0

Par :

local x = -1

 

Et la ligne :

table.insert(Terrain, Dina("Vector", Dina.width, math.random(minTerrain, Dina.height)))

Par :

table.insert(Terrain, Dina("Vector", Dina.width + 1, math.random(minTerrain, Dina.height)))

Ici, on a ajouté le + 1.

 

Vous devriez avoir ceci :

 

Le code de ce tutoriel se trouve ici.



< < Concevoir un jeu d'exploration dans l'espace - Partie 03

Tutoriels

Concevoir un jeu d'exploration dans l'espace - Partie 05 > >