Get the Mytodolist Free Android app from SlideME.

vendredi 27 juin 2014

8/ Comment sélectionner un objet 3D avec le moteur JMonkeyEngine


Les interactions typiques de jeux incluent la prise de vue, le ramassage des objets, et l'ouverture des portes. D'un point de vue de mise en œuvre, ces interactions apparemment différentes sont étonnamment semblables: L'utilisateur vise d'abord et sélectionne une cible dans la scène 3D, puis déclenche une action sur elle. Nous appelons ce processus le prélèvement ou la sélection (picking) .

Vous pouvez choisir un objet en appuyant sur une touche du clavier, ou en cliquant avec la souris. Dans les deux cas, vous identifiez la cible en visant avec un rayon (comme rayon lazer) droit dans la scène. Cette méthode de mise en œuvre de sélection est appelé projection de rayon (ce qui n'est pas le même que le traçage du rayon).



Ce tutoriel s'appuie sur ce que vous avez appris dans le didacticiel Les entrées claviers et souries


Codes sources

package jme3test.helloworld;
 
import com.jme3.app.SimpleApplication;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
 
/** Sample 8 - how to let the user pick (select) objects in the scene 
 * using the mouse or key presses. Can be used for shooting, opening doors, etc. */
public class HelloPicking extends SimpleApplication {
 
  public static void main(String[] args) {
    HelloPicking app = new HelloPicking();
    app.start();
  }
  private Node shootables;
  private Geometry mark;
 
  @Override
  public void simpleInitApp() {
    initCrossHairs(); // a "+" in the middle of the screen to help aiming
    initKeys();       // load custom key mappings
    initMark();       // a red sphere to mark the hit
 
    /** create four colored boxes and a floor to shoot at: */
    shootables = new Node("Shootables");
    rootNode.attachChild(shootables);
    shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f));
    shootables.attachChild(makeCube("a tin can", 1f, -2f, 0f));
    shootables.attachChild(makeCube("the Sheriff", 0f, 1f, -2f));
    shootables.attachChild(makeCube("the Deputy", 1f, 0f, -4f));
    shootables.attachChild(makeFloor());
    shootables.attachChild(makeCharacter());
  }
 
  /** Declaring the "Shoot" action and mapping to its triggers. */
  private void initKeys() {
    inputManager.addMapping("Shoot",
      new KeyTrigger(KeyInput.KEY_SPACE), // trigger 1: spacebar
      new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); // trigger 2: left-button click
    inputManager.addListener(actionListener, "Shoot");
  }
  /** Defining the "Shoot" action: Determine what was hit and how to respond. */
  private ActionListener actionListener = new ActionListener() {
 
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Shoot") && !keyPressed) {
        // 1. Reset results list.
        CollisionResults results = new CollisionResults();
        // 2. Aim the ray from cam loc to cam direction.
        Ray ray = new Ray(cam.getLocation(), cam.getDirection());
        // 3. Collect intersections between Ray and Shootables in results list.
        shootables.collideWith(ray, results);
        // 4. Print the results
        System.out.println("----- Collisions? " + results.size() + "-----");
        for (int i = 0; i < results.size(); i++) {
          // For each hit, we know distance, impact point, name of geometry.
          float dist = results.getCollision(i).getDistance();
          Vector3f pt = results.getCollision(i).getContactPoint();
          String hit = results.getCollision(i).getGeometry().getName();
          System.out.println("* Collision #" + i);
          System.out.println("  You shot " + hit + " at " + pt + ", " + dist + " wu away.");
        }
        // 5. Use the results (we mark the hit object)
        if (results.size() > 0) {
          // The closest collision point is what was truly hit:
          CollisionResult closest = results.getClosestCollision();
          // Let's interact - we mark the hit with a red dot.
          mark.setLocalTranslation(closest.getContactPoint());
          rootNode.attachChild(mark);
        } else {
          // No hits? Then remove the red mark.
          rootNode.detachChild(mark);
        }
      }
    }
  };
 
  /** A cube object for target practice */
  protected Geometry makeCube(String name, float x, float y, float z) {
    Box box = new Box(1, 1, 1);
    Geometry cube = new Geometry(name, box);
    cube.setLocalTranslation(x, y, z);
    Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.randomColor());
    cube.setMaterial(mat1);
    return cube;
  }
 
  /** A floor to show that the "shot" can go through several objects. */
  protected Geometry makeFloor() {
    Box box = new Box(15, .2f, 15);
    Geometry floor = new Geometry("the Floor", box);
    floor.setLocalTranslation(0, -4, -5);
    Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.Gray);
    floor.setMaterial(mat1);
    return floor;
  }
 
  /** A red ball that marks the last spot that was "hit" by the "shot". */
  protected void initMark() {
    Sphere sphere = new Sphere(30, 30, 0.2f);
    mark = new Geometry("BOOM!", sphere);
    Material mark_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mark_mat.setColor("Color", ColorRGBA.Red);
    mark.setMaterial(mark_mat);
  }
 
  /** A centred plus sign to help the player aim. */
  protected void initCrossHairs() {
    setDisplayStatView(false);
    guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
    BitmapText ch = new BitmapText(guiFont, false);
    ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
    ch.setText("+"); // crosshairs
    ch.setLocalTranslation( // center
      settings.getWidth() / 2 - ch.getLineWidth()/2, settings.getHeight() / 2 + ch.getLineHeight()/2, 0);
    guiNode.attachChild(ch);
  }
 
  protected Spatial makeCharacter() {
    // load a character from jme3test-test-data
    Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
    golem.scale(0.5f);
    golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);
 
    // We must add a light to make the model visible
    DirectionalLight sun = new DirectionalLight();
    sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f));
    golem.addLight(sun);
    return golem;
  }
}

Vous devriez voir quatre cubes colorés flottant sur un sol gris, et une croix. Viser la ligne de mire et cliquez ou appuyez sur la barre d'espace pour tirer. La zone touchée par votre tir est marquée d'un point rouge. 

Gardez un œil sur le flux de sortie de l'application, il vous donnera plus de détails: Le nom de la maille qui a été touché, les coordonnées de la zone touchée, et la distance qui vous sépare.


Comprendre les méthodes d'assistance (la méthode Helpers)

Les méthodes makeCube (), makeFloor (), initMark (), et initCrossHairs, sont des méthodes d'assistance personnalisés. Nous les appelons de simpleInitApp () pour initialiser le scénographe avec le contenu de l'échantillon.



  • makeCube () crée de simples cases colorées pour "pratique de la cible". 
  • makeFloor () crée un nœud de sol gris pour "viser la cible". 
  • initMark () crée une sphère rouge ("mark"). Nous allons l'utiliser plus tard pour marquer l'endroit qui a été touché. 
    • Notez que la marque n'est pas fixé et celui-ci n'est pas visible au début! 
  • initCrossHairs () crée la ligne de mire simple par l'impression d'un signe "+" dans le milieu de l'écran. 
    • On notera que les lignes de mires sont fixées à la guiNode, pas à la rootNode.

Dans cet exemple, nous avons attaché tous les objets "sur lequel on peut tirer" sur un noeud personnalisé que nous avons nommé "Shootables". Il s'agit d'une optimisation pour permettre au moteur de ne calculer que les intersections avec les objets qui nous interessent. Le noeud Shootables est attaché au noeud rootNode comme d'habitude.


Comprendre la projection des rayons (Ray casting) pour les tests de tir 

Notre objectif est de déterminer sur quel(le) objet ou boîte l'utilisateur a tiré "shot" (sélectionné). En général, nous voulons déterminer la maille que l'utilisateur a sélectionné en visant sa ligne de mire. Mathématiquement, nous traçons une ligne de la caméra pour voir si elle coupe les objets dans la scène 3D. Cette ligne est appelée un rayon (comme rayon lazer)

Voici l'algorithme simple de projection de rayons pour atteindre les objets:

  • Rafraîchir la liste des résultats. 
  • projetez le rayon à partir de la position de la caméra ayant la même direction que celle de la caméra (on parle de la direction de vue). 
  • Recueillir toutes les intersections entre le rayon et les nœuds "Shootable" dans la liste de résultats. 
  • Utilisez la liste des résultats pour déterminer ceux qui ont été touchés: 
    • Pour chaque coup, JME présente sa distance de la caméra, le point d'impact, et le nom de la maille. 
    • Trie les résultats par distance. 
    • Prend le résultat le plus proche, c'est le maillage qui a été touché.


La mise en œuvre du teste de tir 


Chargement de la scène 

D'abord initialisez les nœuds pouvant sur lesquel on peut tirer et les attacher à la scène. Vous utiliserez l'objet mark plutard.

 Node shootables;
  Geometry mark;
 
  @Override
  public void simpleInitApp() {
    initCrossHairs();
    initKeys();
    initMark();
 
    shootables = new Node("Shootables");
    rootNode.attachChild(shootables);
    shootables.attachChild(makeCube("a Dragon",    -2f, 0f, 1f));
    shootables.attachChild(makeCube("a tin can",    1f,-2f, 0f));
    shootables.attachChild(makeCube("the Sheriff",  0f, 1f,-2f));
    shootables.attachChild(makeCube("the Deputy",   1f, 0f, -4));
    shootables.attachChild(makeFloor());
  }

Configuration de l'écoute d'entrée 

Ensuite, vous déclarez l'action de tir. Il peut être déclenché soit en cliquant ou en appuyant sur la barre d'espace. La méthode initKeys () est appelée depuis simpleInitApp () pour mettre en place ces mappages d'entrée.

 /** Declaring the "Shoot" action and its triggers. */
  private void initKeys() {
    inputManager.addMapping("Shoot",      // Declare...
      new KeyTrigger(KeyInput.KEY_SPACE), // trigger 1: spacebar, or
      new MouseButtonTrigger(MouseInput.BUTTON_LEFT));         // trigger 2: left-button click
    inputManager.addListener(actionListener, "Shoot"); // ... and add.
  }
Sélection d'actions utilisant Crosshairs 

Ensuite, nous mettons en œuvre l'ActionListener qui répond à la gâchette de prise de vue avec une action. L'action suit l'algorithme de projection de rayons décrite ci-dessus:


  • Pour chaque clic ou appuie sur la barre d'espace l'action de prise de vue est déclenchée. 
  • L'action projecte un rayon avant et détermine des intersections avec des objets sur lesquels on peut tirer (= projection de rayon). 
  • Pour toute cible qui a été touchée ou atteinte, il affiche le nom, la distance et les coordonnées de la zone atteint. 
  • Enfin, il attache la marque rouge au résultat le plus proche,afin de mettre en évidence la place qui a été effectivement touchée. 
  • Lorsque rien n'a été touché, la liste des résultats est vide, et la marque rouge est retiré.


Notez comment il imprime un grand nombre de résultats pour vous montrer lesquels ont été touchés.



/** Defining the "Shoot" action: Determine what was hit and how to respond. */
  private ActionListener actionListener = new ActionListener() {
    @Override
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Shoot") && !keyPressed) {
        // 1. Reset results list.
        CollisionResults results = new CollisionResults();
        // 2. Aim the ray from cam loc to cam direction.
        Ray ray = new Ray(cam.getLocation(), cam.getDirection());
        // 3. Collect intersections between Ray and Shootables in results list.
        shootables.collideWith(ray, results);
        // 4. Print results.
        System.out.println("----- Collisions? " + results.size() + "-----");
        for (int i = 0; i < results.size(); i++) {
          // For each hit, we know distance, impact point, name of geometry.
          float dist = results.getCollision(i).getDistance();
          Vector3f pt = results.getCollision(i).getContactPoint();
          String hit = results.getCollision(i).getGeometry().getName();
          System.out.println("* Collision #" + i);
          System.out.println("  You shot " + hit + " at " + pt + ", " + dist + " wu away.");
        }
        // 5. Use the results (we mark the hit object)
        if (results.size() > 0){
          // The closest collision point is what was truly hit:
          CollisionResult closest = results.getClosestCollision();
          mark.setLocalTranslation(closest.getContactPoint());
          // Let's interact - we mark the hit with a red dot.
          rootNode.attachChild(mark);
        } else {
        // No hits? Then remove the red mark.
          rootNode.detachChild(mark);
        }
      }
    }
  };
Astuce: Remarquez comment vous utilisez la méthode results.getClosestCollision().getContactPoint() pour déterminer l'emplacement de la frappe la plus proche.. Si votre jeu comprend une «arme» ou «sort» qui peut frapper plusieurs cibles à la fois, vous pourrez boucler la liste des résultats, afin d'interagir avec chacun d'eux.

Sélection d'actions utilisant le pointeur de souris 

L'exemple ci-dessus suppose que le joueur vise la ligne de mire (fixés au centre de l'écran) à la cible. Mais vous pouvez modifier le code de prélèvement pour vous permettre de cliquer librement les objets de la scène avec un pointeur de la souris visible. Pour ce faire, vous devez convertir les coordonnées d'écran 2d du clic en coordonnées 3D du monde pour obtenir le point du rayon prélèvement de départ.


  • Rafraîchir la liste des résultats. 
  • Obtenez les coordonnées 2D du clic. 
  • Convertir les coordonnées écran 2D à leur équivalent 3D. 
  • Projetez le rayon de l'emplacement en 3D cliqué sur la scène. 
  • Recueillir les intersections entre les rayons et tous les nœuds dans une liste de résultats.

...
CollisionResults results = new CollisionResults();
Vector2f click2d = inputManager.getCursorPosition();
Vector3f click3d = cam.getWorldCoordinates(
    new Vector2f(click2d.x, click2d.y), 0f).clone();
Vector3f dir = cam.getWorldCoordinates(
    new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal();
Ray ray = new Ray(click3d, dir);
shootables.collideWith(ray, results);
...
Utilisez cela avec inputManager.setCursorVisible (true) pour s'assurer que le curseur soit visible. 

Notez que depuis que vous utilisez maintenant la souris pour sélectionner, vous ne pouvez plus l'utiliser pour faire pivoter la caméra. Si vous voulez avoir un pointeur de souris visible pour la sélection dans votre jeu, vous devez redéfinir les mappages de rotation de la caméra.



conclusion 


Vous avez appris à utiliser la projection de rayons pour déterminer quel objet l'utilisateur a sélectionné sur l'écran. Vous avez appris que cela peut être utilisé pour une variété d'interactions, comme le tir, l'ouverture,la sélection et le dépôt des éléments, en appuyant sur un bouton ou un levier, etc 

Utilisez votre imagination à partir d'ici: 


  • Dans votre jeu, le clic peut déclencher une action sur la géométrie identifiée: Détachez-le et mettez-la dans l'inventaire, attacher quelque chose, déclencher une animation ou un effet, ouvrir une porte ou une caisse, - etc 
  • Dans votre jeu, vous pouvez remplacer la marque rouge avec un émetteur de particules, ajouter un effet d'explosion, jouer un son, calculer le nouveau score après chaque coup en fonction de ce qui a été frappé - etc 


Maintenant, ça ne serait cool si ces cibles et sols étaient des objets solides et on pouvait se promener entre eux. Continuons à en apprendre davantage sur la détection de collision (prochainement ).

<< Précédent                                Sommaire                                    Suivant>>





1 commentaire:

  1. je pensais pas que le code du curseur était vraiement si simple que ça.
    C'est exellent comme tuto merci.

    RépondreSupprimer