Apache Adobe Flex TutorialTutoriaux Adobe Flex & AIR en Français

24jan/126

Starling API – 30 – Fin du fil rouge et pistes d'amélioration

Après 30 billets, c'est bien la fin du fil rouge Starling! Si vous avez loupé un épisode, vous pouvez retrouver la liste des tutoriaux sur la page qui y est consacré:

Fil rouge Starling API

Si vous avez suivi ces billets de bout en bout, vous avez appris:

  • A connaitre les classes de base de l'API Starling
  • De l'animation avec Tween et MovieClip
  • Comment structurer votre code grâce aux Factory présentes dans plusieurs billets
  • Créer un niveau complet à partir d'une description
  • Utiliser Box2D pour gérer les collisions
  • Ecrire quelques règles de gestion pour votre jeu

Alors bien sûr, il manque beaucoup d'éléments pour créer un jeu digne de ce nom. Voici quelques pistes:

Rendu final

This movie requires Flash Player 11

Télécharger les sources

Télécharger les sources du tutorial Starling

Le livre officiel Starling

Pendant l'écriture de ces billets sur Starling est paru l'ouvrage final sur Starling par Thibault Imbert. Celui-ci est en anglais et aborde certains points que je n'ai pas évoqué:

http://shop.oreilly.com/product/0636920024217.do

N'hésitez pas à y jeter un oeil. Si vous avez besoin d'aide, pensez bien à utiliser le forum de Starling qui compte de nombreux utilisateurs actifs:

http://forum.starling-framework.org/

Bonus

Vous l'avez remarqué, les descriptions de niveau sont celles d'Angry Birds et les graphismes ont été légèrement remodelés pour ne pas être identiques. Mais si vous récupérez les SpriteSheet du jeu officiel (avec Firebug ou l'inspecteur de Google Chrome) depuis http://chrome.angrybirds.com/, vous aurez exactement le jeu Angry Birds, avec les vrais blocs ;)

Merci pour avoir suivi ce travail de longue haleine !

Fabien

Remplis sous: Starling 6 Commentaires
23jan/120

Starling API – 29 – Réagir à la collision entre 2 body Box2D

Pour l'instant, on peut avec notre poisson, détruire le décor composé de body Box2D associés à des MovieClip que l'on a créé dans un tutorial précédent:

Starling API - 17 - Création des éléments du décor avec BlockFactory, TerrainFactory et EnemyFactory

Nos blocs sont donc des MovieClip contenant plusieurs Texture. Les Textures sont en fait les mêmes mais avec une opacité différente. Si vous avez un graphiste, vous auriez des dessins différents pour chaque état de destruction du bloc. Pour notre test en tout cas, tout bloc qui sera touché va devenir de plus en plus transparent suivant la force qui lui sera appliquée.

Détecter une collision avec b2World::SetContactListener()

Il y a une méthode simple pour détecter les collisions qui est de placer un "ContactListener" sur le monde (objet de type b2World). Ce ContactListener est une classe qui étend b2ContactListener. Créons donc la classe BlockContactListener:

package com.fnicollet {
  import Box2D.Dynamics.b2ContactListener;

  public class BlockContactListener extends b2ContactListener {
    public function BlockContactListener() {
      super();
    }
  }
}

Dans la méthode init() de AquariumScreen, on l'associé à notre monde:

world.SetContactListener(new BlockContactListener);

Ensuite, dans BlockContactListener, il faut surcharger la méthode suivante:

function PostSolve(contact:b2Contact, impulse:b2ContactImpulse):void

Cette méthode nous donne accès à un objet b2Contact qui a des références vers les 2 objets en contact et à un objet b2ContactImpulse qui contient des informations sur la collision comme sa force. Pour récupérer la force de la collision, on va récupérer la valeur de la normale:

override public function PostSolve(contact:b2Contact, impulse:b2ContactImpulse):void {
  var firstNormalImpulse:Number = impulse.normalImpulses[0];
}

Si cette valeur est supérieure à un certain seul, on va récupérer les 2 objets en contact:

override public function PostSolve(contact:b2Contact, impulse:b2ContactImpulse):void {
  const MAX_IMPULSE:Number = 10;
  var firstNormalImpulse:Number = impulse.normalImpulses[0];
  if (firstNormalImpulse > MAX_IMPULSE) {
    var userDataA:DisplayObject = contact.GetFixtureA().GetBody().GetUserData();
    var bodyDefinitionA:b2BodyDef = contact.GetFixtureA().GetBody().GetDefinition();
    var userDataB:DisplayObject = contact.GetFixtureB().GetBody().GetUserData();
    var bodyDefinitionB:b2BodyDef = contact.GetFixtureB().GetBody().GetDefinition();
    var nameA:String = userDataA.name;
    var nameB:String = userDataB.name;
  }
}

Ensuite, on va détecter la collision entre un fish (dont les noms commencent par "bird_") et un corps Box2D dynamique (nos blocks). En même temps, on va faire pareil pour les collisions avec le sol. Le code n'est pas super mais c'est pour vous expliquer en gros:

override public function PostSolve(contact:b2Contact, impulse:b2ContactImpulse):void {
  const MAX_IMPULSE:Number = 10;
  var firstNormalImpulse:Number = impulse.normalImpulses[0];
  if (firstNormalImpulse > MAX_IMPULSE) {
    var userDataA:DisplayObject = contact.GetFixtureA().GetBody().GetUserData();
    var bodyDefinitionA:b2BodyDef = contact.GetFixtureA().GetBody().GetDefinition();
    var userDataB:DisplayObject = contact.GetFixtureB().GetBody().GetUserData();
    var bodyDefinitionB:b2BodyDef = contact.GetFixtureB().GetBody().GetDefinition();
    var nameA:String = userDataA.name;
    var nameB:String = userDataB.name;
    var densityA:Number = contact.GetFixtureA().GetDensity();
    var densityB:Number = contact.GetFixtureB().GetDensity();
    var isAFloor:Boolean = densityA == 0;
    var isBFloor:Boolean = densityB == 0;
    var isAFish:Boolean = nameA && nameA.indexOf("GUPPY_") != -1;
    var isBFish:Boolean = nameB && nameB.indexOf("GUPPY_") != -1;
    if ((isAFloor || isAFish) && bodyDefinitionB.type == b2Body.b2_dynamicBody) {
      destroyDisplayObjectBy(userDataB, firstNormalImpulse);
    }
    if ((isBFloor || isBFish) && bodyDefinitionA.type == b2Body.b2_dynamicBody) {
      destroyDisplayObjectBy(userDataA, firstNormalImpulse);
    }
  }
}

private function destroyDisplayObjectBy(dObj:DisplayObject, damage:Number):void {
  if (damage < 1) {
    return;
  }
  var mc:MovieClip = dObj as MovieClip;
  if (!mc) {
    return;
  }
  if (mc.currentFrame == mc.numFrames - 1) {
    mc.removeFromParent();
    return;
  }
  mc.currentFrame++;
}

Faire le ménage avant de partir

Si vous testez l'application comme cela, vous verrez que lorsque nos blocs ont pris beaucoup de dégâts, ils disparaissent. Mais il sont encore pris en compte dans le calcul des collisions ! En effet,  on a supprimé le MovieClip, donc l'affichage graphique. Le "body" Box2D est lui toujours présent et pris en compte dans la simulation.

On va donc rajouter le "body" à notre méthode destroyDisplayObjectBy et supprimer l'objet du monde pour de bon:

private function destroyDisplayObjectBy(dObj:DisplayObject, damage:Number, body:b2Body):void {
  if (damage < 1) {
    return;
  }
  var mc:MovieClip = dObj as MovieClip;
  if (!mc) {
    return;
  }
  if (mc.currentFrame == mc.numFrames - 1) {
    mc.removeFromParent();
    body.GetWorld().DestroyBody(body);
    return;
  }
  mc.currentFrame++;
}

Si vous essayez, vous verrez que cela ne fonctionne pas. En cherchant un peu sur le net, on s'aperçoit que la suppression d'un body pendant la simulation n'est pas la bonne solution. Il faut garder une liste des éléments à supprimer et le faire avant le prochain rendu (=Step).

On rajoute donc une variable static à BlockContactListener pour faire simple:

public static var toRemove:Array = [];

puis:

  if (mc.currentFrame == mc.numFrames - 1) {
    mc.removeFromParent();
    if (toRemove.indexOf(body) == -1){
      toRemove.push(body);
    }
    return;
  }

Et dans notre méthode updateWorld, on supprime les éléments en attente:

private function updateWorld(event:Event):void {
  var toRemove:Array = BlockContactListener.toRemove;
  for each (var bodyToRemove:b2Body in toRemove) {
    world.DestroyBody(bodyToRemove);
  }
  BlockContactListener.toRemove = [];

Voilà le rendu final:

This movie requires Flash Player 11

Conclusion

Et voilà, le fil rouge des tutoriaux Starling touche à sa fin. Dans le prochain article (et le dernier), on verra les pistes d'amélioration qu'il vous reste si vous souhaitez continuer :) .

Télécharger les sources

Remplis sous: Starling Aucun commentaire
22jan/123

Starling API – 28 – Détecter l'arrêt d'un body Box2D

Dans le tutorial Starling précédent, on a vu comment lancer un personnage en le transformant en body dynamique Box2D:

Starling API – 27 – Lancer des objets dynamiques Box2D

Pour l'instant, on ne peut lancer qu'un poisson, ce qui est bien dommage. Pour déterminer qu'un poisson a fini son tour, on aura 3 conditions:

  • Sa position est trop éloignée de la scène
  • Sa position est à gauche du point de lancer
  • Sa vitesse est nulle ou presque pendant un certain laps de temps

Déterminer la vitesse d'un body Box2D

Pour déterminer la vitesse d'un élément, il faut mesurer sa vitesse à un instant T et à un instant T+1. C'est en fait ce que l'on fait dans notre méthode updateWorld(), qui fait avancer la simulation d'un pas. Heureusement pour nous, Box2D va nous mâcher le travail en nous proposant de récupérer le vecteur de déplacement d'un body par la méthode GetLinearVelocity(). Ensuite, on peut récupérer la vitesse par ce vecteur avec la méthode LengthSquared() qui va nous donner la longueur du vecteur au carré.

Dans notre boucle sur tous les body de la scène, on détermine d'abord quand on tombe sur l'oiseau courant et on calcule sa vitesse:

  for (var currentBody:b2Body = world.GetBodyList(); currentBody; currentBody = currentBody.GetNext()) {
    var dObj:DisplayObject = currentBody.GetUserData() as DisplayObject;
    if (!dObj) {
      continue;
    }
    var currentXPosition:Number = currentBody.GetPosition().x * _screenToWorldFactor;
    var currentYPosition:Number = currentBody.GetPosition().y * _screenToWorldFactor;
    var isCurrentFish:Boolean = _currentFish && dObj.name == _currentFish.name;
    if (isCurrentFish) {
      var linearVelocity:b2Vec2 = currentBody.GetLinearVelocity();
      var speed:Number = linearVelocity.LengthSquared();
      var isStopped:Boolean = speed < 3;

Au passage, on va aussi vérifier nos autres conditions:

  var isBehindSlingshot:Boolean = currentXPosition < 0;
  var isTooFar:Boolean = currentYPosition > _worldWidth;

Si au moins une condition est remplie, on démarre un Timer, sinon on met notre Timer en pause:

if (isBehingSlingshot || isStopped || isTooFar) {
  if (!_nonMovingBirdTimer.running) {
    _nonMovingBirdTimer.reset();
    _nonMovingBirdTimer.start();
  }
} else {
  _nonMovingBirdTimer.stop();
}

Dans la méthode init(), on aura pris soin d'initialiser notre Timer:

      _nonMovingBirdTimer = new Timer(3000, 1);
      _nonMovingBirdTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onNonMovingTimer);

Lorsque 3 secondes se seront passées pendant lesquelles au moins une condition n'est pas vérifiée, on va passer dans la méthode onNonMovingTimer.

Passer au personnage suivant

Voici le corps de la méthode onNonMovingTimer:

protected function onNonMovingTimer(event:TimerEvent):void {
  // supprimer le fish courant, plus utilisé
  removeCurrentFish();
  // passer à l'étape suivante (next fish, score, ...)
  onStepFinished();
}

La méthode removeCurrentBird est assez simple puisqu'il nous suffit d'appeler la méthode "removeFromParent()" sur notre "_currentFish" et de le mettre à null. Si vous voulez ajouter un effet d'explosion ou de particule, cela se fera dans cette méthode:

    private function removeCurrentBird():void {
      _currentBird.removeFromParent(true);
      _currentBird = null;
    }

Ensuite, la méthode onStepFinished() va contenir notre logique métier. C'est elle qui devrait vérifier si le joueur a complété le niveau ou pas. S'il n'a pas terminé, c'est elle qui va donner l'ordre de placer un nouveau poisson sur le point de lancer.

Voici un début d'implémentation pour cette méthode:

/**
 *  passer à l'étape suivante (next bird, score, ...)
 */
private function onStepFinished():void {
  // TODO
  // checker la vie de tous les pigs
  var arePigsAlive:Boolean = true;
  if (arePigsAlive) {
    // on passe à l'oiseau suivant
    _fishIndex++;
    var objectId:String = "bird_" + _fishIndex;
    var nextFish:FishBase = FishBase(_fishDisplayObjects[objectId]);
    if (!nextFish) {
      // no more birds in stock!
      // game over !
      return;
    }
    _currentFish = nextFish;
    _currentFishWO = WorldObject(_fishWorldObjects[objectId]);
    moveCurrentFishToSlingshot();
    goToCamera(Camera.SLINGSHOT);
  } else {
    // TODO
    // SCORE
  }
}

Voici donc notre jeu, qui devient "jouable":

This movie requires Flash Player 11

Conclusion

Dans le dernier tutorial, je vous disais que les valeurs de friction que l'on a donné à Box2D n'étaient pas les bonnes. En effet, notre poisson semble glisser sur le sol indéfiniment. Même en modifiant les valeurs de friction / restitution / density de Box2D, je n'ai pas réussi à faire mieux. Si vous avez une idée, utilisez les commentaires :) .

Dans le prochain tutorial, on va voir comment réagir à la collision sur les blocs

Télécharger les sources

Remplis sous: Starling 3 Commentaires
22jan/120

Starling utilisé par le Citrus Engine pour vos jeux 2D

Dans le fil rouge Starling de flex-tutorial.fr, vous avez vu comment utiliser Starling de A à Z, en comprenant bien l'utilité de chaque classe. Cela nous a fait écrire pas mal de code mais en le réutilisant bien, on a essayé de le maintenir au minimum. Au fur et à mesure de la création du jeu, on a abordé les problématiques "classiques" de la création d'un jeu : Création d'éléments graphiques, contrôles au clavier, ajout de détection de collision, changement de niveau / écran , ...

Starling est une API plus haut-niveau que celle de Stage3D pour vous faciliter la tâche si vous souhaitez réaliser du contenu 2D accéléré GPU. Si vous voulez créer un jeu de plate-forme (qu'on appelle souvent sidescroller), vous allez devoir résoudre résoudre les problèmes classiques de ce genre de jeu et garder votre code propre.

Pour vous aider dans cette tâche, il existe des frameworks "encore plus haut-niveau", plus spécialisés dans le jeu 2D que Starling. L'un d'entre eux se nomme le "Citrus Engine":

http://citrusengine.com/

Ce dernier existe depuis assez longtemps mais vient juste d'utiliser Starling pour la partie rendu. Cela signifie que votre jeu 2D sera directement accéléré GPU.

Voici une petite démonstration de ce que cela peut donner:

http://goo.gl/dvZrd

Comme vous le voyez, vous pouvez obtenir un jeu 2D à 60 FPS grâce au Citrus Engine. De nombreux aspects que l'on a vu dans le tutorial Starling sont pris en compte directement par le Citrus Engine comme la gestion de Box2D.

Pour vous aider à dompter le framework, une suite de tutoriaux est disponible mais aussi des vidéos pour vous expliquer comment faire un mini-jeu de plate-forme en 20 minutes:

Citrus Engine 2 Hello World from Eric Smith on Vimeo.

Si vous avez envie d'en savoir plus sur les rouages de cette nouvelle version du Citrus Engine, Aymeric Lamboley (un français!) donne plus de détails sur son blog:

http://www.aymericlamboley.fr/blog/

Remplis sous: Starling Aucun commentaire
21jan/120

Starling API – 27 – Lancer des objets dynamiques Box2D

On a une caméra qui fonctionne, un décor qui se construit mais il manque encore un point important, les éléments que l'on va lancer sur le décor.

Ajout des Fish sur la scène

Rappelez-vous dans la LevelFactory, notre Factory qui construit les différentes parties de notre niveau, on avait omis une catégorie "BIRD":

    public static function getPart(id:String):DisplayObject {
      var ret:DisplayObject = null;
      if (contains(id, "WOOD") || contains(id, "STONE") || contains(id, "ICE")) {
        ret = BlockFactory.get(id);
      } else if (contains(id, "MISC")) {
        // TODO
      } else if (contains(id, "TERRAIN")) {
        ret = TerrainFactory.get(id);
      } else if (contains(id, "PIG")) {
        ret = EnemyFactory.get(id);
      } else if (contains(id, "BIRD")) {
        // TODO
      }
      return ret;
    }

Les "BIRD" qui sont présents dans notre fichier de description JSON du niveau sont ici nos poissons. Appelons donc notre FishFactory:

} else if (contains(id, "BIRD")) {
        ret = FishFactory.getFish("GUPPY_O_");
      }

Le résultat:

Nos poissons sont bien entre le premier et l'arrière-plan, mais leur taille fait qu'ils sont légèrement cachés. On verra cela plus tard. Ensuite, on va indiquer qu'au commencement du jeu, le premier fish doit aller se mettre en place pour être jeté par la suite.

On va faire cela lors de la construction du niveau, pendant laquelle on va garder en mémoire le fish courant (DisplayObject) et son WorldObject (Model) associé:

    // bird currently being thrown
    private var _currentFish:DisplayObject = null;
    private var _currentFishWO:WorldObject = null;
    private var _fishDisplayObjects:Array /* of DisplayObject */ = [];
    private var _fishWorldObjects:Array /* of WorldObject */ = [];
    private var _fishIndex:int = 1;

puis dans la méthode initWorld:

        if (objectId.indexOf("bird") != -1) {
          _fishDisplayObjects[objectId] = obj;
          _fishWorldObjects[objectId] = wo;
          if (objectId == "bird_1") {
            _currentFish = obj;
            _currentFishWO = wo;
            moveCurrentFishToSlingshot();
          }
        }

La méthode moveCurrentFishToSlingshot() indique qu'il faut amener le poisson à lancer:

    private function moveCurrentFishToSlingshot():void {
      _currentFish.x = 0;
      _currentFish.y = -160;
    }

Si vous lancez l'application telle quelle, vous verrez que les poissons vont "flotter" dans la scène:

Le problème est que nos poissons, même en attente, sont déjà des "body" pour Box2D car on les a passé dans notre Box2dFactory. On va donc les enlever de la construction:

if (objectId.indexOf("bird") != -1) {
  _fishDisplayObjects[objectId] = obj;
  _fishWorldObjects[objectId] = wo;
  obj.y -= 30;
  if (objectId == "bird_1") {
    _currentFish = obj;
    _currentFishWO = wo;
    moveCurrentFishToSlingshot();
  }
} else {
  var body:b2Body = _box2dFactory.getBox2dBody(wo, obj, objectWidth, objectHeight);
}
_stageContainer.addChild(obj);

Nos poissons sont donc maintenant immobiles tant qu'on ne les aura pas modifié.

Modification de la position du fish à la souris

Dans notre code, on a déjà une méthode qui réagit au clic de la souris:

    private function onScreenTouched(event:TouchEvent):void {
      var touch:Touch = event.getTouch(stage);
      var location:Point = touch.getLocation(stage);
      if (touch.phase == TouchPhase.ENDED) {
        switchCamera();
      }
    }

C'est dans cette méthode que l'on va faire le drag n drop de notre poisson. Contrairement aux Sprite classiques, les Sprite Starling ne disposent pas de startDrag() et stopDrag(). Il faut donc le faire à la main. On reprend ses cours de trigo de 3ème et c'est parti.

Tous nos calculs vont se faire par rapport à notre Sprite conteneur nommée _stageContainer. Vous avez par contre sur Sprite les méthodes globalToLocal(point) et localToGlobal(point) pour faire des transformations de coordonnées. Pour éviter que le joueur puisse faire un drag and drop trop éloigné, on va calculer la distance entre le point de lancer et la position de la souris pour fixer des bornes:

  const THROW_MAX_DISTANCE:int = 80;
  var touch:Touch = event.getTouch(stage);
  var location:Point = touch.getLocation(stage);
  var localLocation:Point = _stageContainer.globalToLocal(location);
  // position du poisson courant (centre)
  var globalThrowPosition:Point = new Point(0, -160).add(new Point(_currentFish.width / 2, _currentFish.height / 2));
  var distance:Number = Point.distance(globalThrowPosition, localLocation);
  var fishAngle:Number = Math.atan2(localLocation.y - globalThrowPosition.y, localLocation.x - globalThrowPosition.x);
  trace("distance: " + distance);
  trace("fishAngle: " + fishAngle);
  // retour à une position locale par rapport à la throwPosition
  var constraintDistance:Number = distance >= THROW_MAX_DISTANCE ? THROW_MAX_DISTANCE : distance;

On va commencer par s'intéresser aux phases "BEGAN" et "MOVED":

  if (touch.phase == TouchPhase.BEGAN || touch.phase == TouchPhase.MOVED) {
    // on regarde si on est proche du point de lancer
    if (distance < THROW_MAX_DISTANCE) {
      // on est dans la zone de lancer
      _currentFish.x = localLocation.x - (_currentFish.width / 2);
      _currentFish.y = localLocation.y - (_currentFish.height / 2);
      _isFishDragged = true;
    }
  } else if (touch.phase == TouchPhase.ENDED) {
    switchCamera();
  }

Au passage, on ajoute un Boolean "_isFishDragged", qui nous permettra de savoir si au lâcher du clic, l'utilisateur était en train de déplacer le poisson.

Voici le résultat pour l'instant:

This movie requires Flash Player 11

C'est un début, on a fait un drag & drop contraint. Un peu trop contraint même puisque dès que vous sortez de la zone, on le stoppe. C'est là que l'on va devoir faire un peu de trigonométrie pour calculer la position du poisson sur le cercle de rayon 80 maximal. Perso, mes cours de 3ème sont très loin et je tâtonne pas mal pour faire de pauvres cosinus. Ma technique est donc de découper en beaucoup de variable le code, pour essayer de bien comprendre. C'est plus verbeux et j'espère que je ne dis pas n'importe quoi:

if (touch.phase == TouchPhase.BEGAN || touch.phase == TouchPhase.MOVED) {
    // on regarde si on est proche du point de lancer
    if (distance < THROW_MAX_DISTANCE) {
      // on est dans la zone de lancer
      _currentFish.x = localLocation.x - (_currentFish.width / 2);
      _currentFish.y = localLocation.y - (_currentFish.height / 2);
      _isFishDragged = true;
    }
    if (_isFishDragged) {
      // on vérifie que l'objet est toujours dans la zone max
      // coordonnées globales du point de lancement
      var fishPositionX:Number = localLocation.x - (_currentFish.width / 2);
      var fishPositionY:Number = localLocation.y - (_currentFish.height / 2);
      var currentCos:Number = Math.cos(fishAngle);
      var currentSin:Number = Math.sin(fishAngle);
      if (distance >= THROW_MAX_DISTANCE) {
        fishPositionX = globalThrowPosition.x - (_currentFish.width / 2) + (THROW_MAX_DISTANCE * currentCos);
        fishPositionY = globalThrowPosition.y - (_currentFish.height / 2) + (THROW_MAX_DISTANCE * currentSin);
      }
      // positionnement de l'oiseau
      _currentFish.x = fishPositionX;
      _currentFish.y = fishPositionY;
    }
  } else if (touch.phase == TouchPhase.ENDED) {
    if (_isFishDragged) {
      //TODO
    } else {
      switchCamera();
    }
    _isFishDragged = false;
  }

C'est un peu mieux:

This movie requires Flash Player 11

Lancé à la souris

Vous voyez le TODO plus haut? C'est à ce moment que l'on sait que le fish a été déplacé puis lâché. C'est donc à ce moment qu'on va s'occuper de le lancer. Pour cela, il faut en fait transformer notre fish en body Box2D:

_currentFishBody = _box2dFactory.getBox2dBody(_currentFishWO, _currentFish, _currentFish.width, _currentFish.height);

Pour lancer un objet Box2D, il faut en fait lui donner une "impulsion". Celle-ci est définie par un vecteur (b2Vec2), un peu comme la gravité lors de la construction de notre monde. Une fois qu'on a déterminé la force de l'impulsion, on peut la passer à la méthode SetLinearVelocity:

          _fishBeingThrown = true;
          _currentFishBody = _box2dFactory.getBox2dBody(_currentFishWO, _currentFish, _currentFish.width, _currentFish.height);
          constraintDistance *= 2;
          var vectorX:Number = -constraintDistance * Math.cos(fishAngle) / 4;
          var vectorY:Number = -constraintDistance * Math.sin(fishAngle) / 4;
          _currentFishBody.SetLinearVelocity(new b2Vec2(vectorX, vectorY));

On va même faire mieux et déplacer la caméra vers le deuxième point de focus si on détecte que le poisson a été lancé dans le bon sens (vers la droite):

          if (vectorX > 0) {
            goToCamera(Camera.CASTLE);
          }

Il y a un problème assez drôle, je vous laisse tester :) :

This movie requires Flash Player 11

Définition du point de rotation / point de pivot

Vous avez vu dans l'exemple précédent, notre poisson tourne n'importe comment. En fait, il tourne autour de son point d'origine qui est 0, 0. Il faut que son point d'origine soit calculé au centre de l'objet, car c'est comme cela que procède Box2D dans ses calculs.

Là, c'est très simple, il suffit de modifier notre Factory et d'assigner les propriétés pivotX et pivotY:

    public static function getFish(id:String):Sprite {
      init();
      var ret:Sprite = new FishBase(mediumguppy_textureAtlas, id);
      ret.name = id;
      ret.pivotX = ret.width >> 1;
      ret.pivotY = ret.height >> 1;
      return ret;
    }

Par contre, cela fout en l'air tous nos calculs dans le positionnement à la souris. Il faut enlever tous les offsets ( +...width / 2). Je vous passe les détails (vous pouvez aller voir les sources en fin de tuto). Voici donc le résultat avec le point de pivot correctement positionné:

This movie requires Flash Player 11

Conclusion

Un grand pas en avant dans notre jeu puisque l'on peut désormais lancer le poisson dans le décor et commencer à le détruire! Pour l'instant, pas de "vraie gestion" du jeu, pour continuer vous devrez recharger la page. Cela fait partie de la "logique métier" à implémenter, l'étape un peu lourde du projet. Vous pouvez aussi voir que certains de mes coefficients sont faux puisque le poisson glisse sur le sol à l'infini. Il faut juste que je trouve lequel :) .

Télécharger les sources

Remplis sous: Starling Aucun commentaire