Comment obtenir des coordonnées pixel à hexadécimal sur une carte hexadécimale basée sur un tableau?

10

J'essaye de faire un pixel à la fonction coord pour une carte hexadécimale mais je ne reçois pas les calculs correctement, tout ce que j'essaie semble être un peu décalé, et les exemples que j'ai trouvés étaient basés sur des cartes centrées encerclées.

Par «basé sur un tableau», je veux dire la façon dont les hexs sont ordonnés, voir photo.

Le résultat le plus précis que j'ai obtenu était avec le code suivant, mais il est toujours désactivé et s'aggrave plus les valeurs augmentent:

public HexCell<T> coordsToHexCell(float x, float y){
    final float size = this.size; // cell size
    float q = (float) ((1f/3f* Math.sqrt(3) * x - 1f/3f * y) / size);
    float r = 2f/3f * y / size;
    return getHexCell((int) r, (int) q);
}

Hexmap

L'écran commence par 0,0 en haut à gauche, chaque cellule connaît son centre.

Tout ce dont j'ai besoin, c'est d'un moyen de traduire les coordonnées de l'écran en coordonnées hexadécimales. Comment pourrais-je faire ça?

petervaz
la source

Réponses:

12

Il existe de nombreux systèmes de coordonnées hexadécimales. Les approches «décalées» sont agréables pour stocker une carte rectangulaire mais les algorithmes hexadécimaux ont tendance à être plus délicats.

Dans mon guide de grille hexadécimale (que je crois que vous avez déjà trouvé), votre système de coordonnées est appelé "even-r", sauf que vous les étiquetez à la r,qplace de q,r. Vous pouvez convertir des emplacements de pixels en coordonnées hexadécimales avec ces étapes:

  1. Convertissez les emplacements des pixels en coordonnées hexagonales axiales à l'aide de l'algorithme décrit dans cette section . C'est ce que fait votre fonction. Cependant, vous devez faire un pas de plus.
  2. Ces coordonnées axiales sont fractionnaires. Ils doivent être arrondis à l'hex le plus proche. Dans votre code que vous utilisez, (int)r, (int)qmais cela ne fonctionne que pour les carrés; pour les hexagones, nous avons besoin d'une approche d'arrondi plus compliquée. Convertir le r, qau cube les coordonnées en utilisant l' axial cube formules ici . Utilisez ensuite la hex_roundfonction ici .
  3. Vous disposez maintenant d'un ensemble entier de coordonnées de cube . Votre carte utilise «even-r», pas un cube, vous devez donc reconvertir. Utilisez le cube pour égaliser les formules de décalage à partir d' ici .

J'ai besoin de réécrire le pixel en section de coordonnées hexadécimales pour le rendre beaucoup plus clair. Désolé!

Je sais, cela semble compliqué. J'utilise cette approche car c'est la moins sujette aux erreurs (pas de cas particulier!) Et permet la réutilisation. Ces routines de conversion peuvent être réutilisées. L'arrondi hexagonal peut être réutilisé. Si jamais vous voulez dessiner des lignes ou tourner autour d'une coordonnée hexadécimale ou faire un champ de vision ou d'autres algorithmes, certaines de ces routines seront également utiles.

amitp
la source
Je vais essayer ça. Merci. J'ai déjà trouvé une solution de travail, mais je veux vraiment creuser davantage dans les mathématiques hexadécimales, j'ai juste un peu de mal à m'enrouler la tête et à faire des pas de bébé.
petervaz
2
@amitp: J'adore votre guide, je suis tombé dessus quand j'ai écrit un générateur de grille hexagonale il y a quelques années. Voici ma solution si vous êtes intéressé: Stack Overflow - Algorithme pour générer une grille hexagonale avec système de coordonnées .
M. Polywhirl du
1
Où est l'origine des coordonnées pixel? Au centre de l'hexagone 0,0 en coordonnées décalées?
Andrew
1
@Andrew Oui. Vous pouvez décaler l'origine en coordonnées pixel avant d'exécuter la transformation en coordonnées hexadécimales.
amitp
9

Il y a deux façons de gérer ce problème, à mon avis.

  1. Utilisez un meilleur système de coordonnées. Vous pouvez vous faciliter la tâche si vous savez comment numéroter les hexs. Amit Patel a la référence définitive sur les grilles hexagonales. Vous voudrez rechercher des coordonnées axiales sur cette page.

  2. Emprunter du code à quelqu'un qui l'a déjà résolu. J'ai un code qui fonctionne, que j'ai retiré de la source de Battle for Wesnoth . Gardez à l'esprit que ma version a la partie plate des hexagones sur le dessus, vous devrez donc échanger x et y.

Michael Kristofik
la source
6

Je pense que la réponse de Michael Kristofik est correcte, en particulier pour mentionner le site Web d'Amit Patel, mais je voulais partager mon approche novice des grilles hexadécimales.

Ce code a été tiré d'un projet auquel j'ai perdu tout intérêt et abandonné écrit en JavaScript, mais la position de la souris sur la tuile hexadécimale a très bien fonctionné. J'ai utilisé * cet article GameDev * pour mes références. De ce site Web, l'auteur avait cette image qui montrait comment représenter mathématiquement tous les côtés et positions hexagonaux.

Dans ma classe de rendu, j'ai défini ceci dans une méthode qui m'a permis de définir la longueur de côté hexadécimale que je voulais. Affiché ici parce que certaines de ces valeurs ont été référencées dans le code de coordonnées pixel à hex.

                this.s = Side; //Side length
                this.h = Math.floor(Math.sin(30 * Math.PI / 180) * this.s);
                this.r = Math.floor(Math.cos(30 * Math.PI / 180) * this.s);
                this.HEXWIDTH = 2 * this.r;
                this.HEXHEIGHT = this.h + this.s;
                this.HEXHEIGHT_CENTER = this.h + Math.floor(this.s / 2);

Dans la classe d'entrée de souris, j'ai créé une méthode qui a accepté une coordonnée d'écran x et y et a renvoyé un objet avec la coordonnée hexadécimale dans laquelle se trouve le pixel. * Notez que j'avais une fausse "caméra", donc les décalages pour la position de rendu sont également inclus.

    ConvertToHexCoords:function (xpixel, ypixel) {
        var xSection = Math.floor(xpixel / ( this.Renderer.HEXWIDTH )),
            ySection = Math.floor(ypixel / ( this.Renderer.HEXHEIGHT )),
            xSectionPixel = Math.floor(xpixel % ( this.Renderer.HEXWIDTH )),
            ySectionPixel = Math.floor(ypixel % ( this.Renderer.HEXHEIGHT )),
            m = this.Renderer.h / this.Renderer.r, //slope of Hex points
            ArrayX = xSection,
            ArrayY = ySection,
            SectionType = 'A';
        if (ySection % 2 == 0) {
            /******************
             * http://www.gamedev.net/page/resources/_/technical/game-programming/coordinates-in-hexagon-based-tile-maps-r1800
             * Type A Section
             *************
             *     *     *
             *   *   *   *
             * *       * *
             * *       * *
             *************
             * If the pixel position in question lies within the big bottom area the array coordinate of the
             *      tile is the same as the coordinate of our section.
             * If the position lies within the top left edge we have to subtract one from the horizontal (x)
             *      and the vertical (y) component of our section coordinate.
             * If the position lies within the top right edge we reduce only the vertical component.
             ******************/
            if (ySectionPixel < (this.Renderer.h - xSectionPixel * m)) {// left Edge
                ArrayY = ySection - 1;
                ArrayX = xSection - 1;
            } else if (ySectionPixel < (-this.Renderer.h + xSectionPixel * m)) {// right Edge
                ArrayY = ySection - 1;
                ArrayX = xSection;
            }
        } else {
            /******************
             * Type B section
             *********
             * *   * *
             *   *   *
             *   *   *
             *********
             * If the pixel position in question lies within the right area the array coordinate of the
             *      tile is the same as the coordinate of our section.
             * If the position lies within the left area we have to subtract one from the horizontal (x) component
             *      of our section coordinate.
             * If the position lies within the top area we have to subtract one from the vertical (y) component.
             ******************/
            SectionType = 'B';
            if (xSectionPixel >= this.Renderer.r) {//Right side
                if (ySectionPixel < (2 * this.Renderer.h - xSectionPixel * m)) {
                    ArrayY = ySection - 1;
                    ArrayX = xSection;
                } else {
                    ArrayY = ySection;
                    ArrayX = xSection;
                }
            } else {//Left side
                if (ySectionPixel < ( xSectionPixel * m)) {
                    ArrayY = ySection - 1;
                    ArrayX = xSection;
                } else {
                    ArrayY = ySection;
                    ArrayX = xSection - 1;
                }
            }
        }
        return {
            x:ArrayX + this.Main.DrawPosition.x, //Draw position is the "camera" offset
            y:ArrayY + this.Main.DrawPosition.y
        };
    },

Enfin, voici une capture d'écran de mon projet avec le débogage du rendu activé. Il montre les lignes rouges où le code vérifie les cellules TypeA vs TypeB ainsi que les coordonnées hexadécimales et les cellules décrivent entrez la description de l'image ici
J'espère que cela aide certains.

user32959
la source
4

J'ai en fait trouvé une solution sans calcul hexadécimal.
Comme je l'ai mentionné dans la question, chaque cellule enregistre ses propres coordonnées centrales, en calculant le centre hexadécimal le plus proche des coordonnées pixel, je peux déterminer la cellule hexadécimale correspondante avec une précision en pixels (ou très proche).
Je ne pense pas que ce soit la meilleure façon de le faire, car je dois itérer sur chaque cellule et je peux voir comment cela pourrait être taxable, mais je laisserai le code comme solution alternative:

public HexCell<T> coordsToHexCell(float x, float y){
    HexCell<T> cell;
    HexCell<T> result = null;
    float distance = Float.MAX_VALUE;
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            cell = getHexCell(r, c);

            final float dx = x - cell.getX();
            final float dy = y - cell.getY();
            final float newdistance = (float) Math.sqrt(dx*dx + dy*dy);

            if (newdistance < distance) {
                distance = newdistance;
                result = cell;
            }           
        }
    }
    return result;
}
petervaz
la source
3
Il s'agit d'une approche raisonnable. Vous pouvez l'accélérer en analysant une plus petite plage de lignes / colonnes au lieu de les analyser toutes. Pour ce faire, vous avez besoin d'une idée approximative de l'emplacement de l'hex. Puisque vous utilisez des grilles décalées, vous pouvez obtenir une estimation approximative en divisant x par l'espacement entre les colonnes et en divisant y par l'espacement entre les lignes. Ensuite, au lieu d'analyser toutes les colonnes 0…cols-1et toutes les lignes 0…rows-1, vous pouvez analyser col_guess - 1 … col_guess+1et row_guess - 1 … row_guess + 1. Cela ne fait que 9 hexagones donc c'est rapide et ne dépend pas de la taille de la carte.
amitp
3

Voici les tripes d'une implémentation C # d'une des techniques publiées sur le site Web d'Amit Patel (je suis sûr que la traduction en Java ne sera pas un défi):

public class Hexgrid : IHexgrid {
  /// <summary>Return a new instance of <c>Hexgrid</c>.</summary>
  public Hexgrid(IHexgridHost host) { Host = host; }

  /// <inheritdoc/>
  public virtual Point ScrollPosition { get { return Host.ScrollPosition; } }

/// <inheritdoc/>
public virtual Size  Size           { get { return Size.Ceiling(Host.MapSizePixels.Scale(Host.MapScale)); } }

/// <inheritdoc/>
public virtual HexCoords GetHexCoords(Point point, Size autoScroll) {
  if( Host == null ) return HexCoords.EmptyCanon;

  // Adjust for origin not as assumed by GetCoordinate().
  var grid    = new Size((int)(Host.GridSizeF.Width*2F/3F), (int)Host.GridSizeF.Height);
  var margin  = new Size((int)(Host.MapMargin.Width  * Host.MapScale), 
                         (int)(Host.MapMargin.Height * Host.MapScale));
  point      -= autoScroll + margin + grid;

  return HexCoords.NewCanonCoords( GetCoordinate(matrixX, point), 
                                   GetCoordinate(matrixY, point) );
}

/// <inheritdoc/>
public virtual Point   ScrollPositionToCenterOnHex(HexCoords coordsNewCenterHex) {
  return HexCenterPoint(HexCoords.NewUserCoords(
          coordsNewCenterHex.User - ( new IntVector2D(Host.VisibleRectangle.Size.User) / 2 )
  ));
}

/// <summary>Scrolling control hosting this HexGrid.</summary>
protected IHexgridHost Host { get; private set; }

/// <summary>Matrix2D for 'picking' the <B>X</B> hex coordinate</summary>
Matrix matrixX { 
  get { return new Matrix(
      (3.0F/2.0F)/Host.GridSizeF.Width,  (3.0F/2.0F)/Host.GridSizeF.Width,
             1.0F/Host.GridSizeF.Height,       -1.0F/Host.GridSizeF.Height,  -0.5F,-0.5F); } 
}
/// <summary>Matrix2D for 'picking' the <B>Y</B> hex coordinate</summary>
Matrix matrixY { 
  get { return new Matrix(
            0.0F,                        (3.0F/2.0F)/Host.GridSizeF.Width,
            2.0F/Host.GridSizeF.Height,         1.0F/Host.GridSizeF.Height,  -0.5F,-0.5F); } 
}

/// <summary>Calculates a (canonical X or Y) grid-coordinate for a point, from the supplied 'picking' matrix.</summary>
/// <param name="matrix">The 'picking' matrix</param>
/// <param name="point">The screen point identifying the hex to be 'picked'.</param>
/// <returns>A (canonical X or Y) grid coordinate of the 'picked' hex.</returns>
  static int GetCoordinate (Matrix matrix, Point point){
  var pts = new Point[] {point};
  matrix.TransformPoints(pts);
      return (int) Math.Floor( (pts[0].X + pts[0].Y + 2F) / 3F );
  }

Le reste du projet est disponible ici en Open Source, y compris les classes MatrixInt2D et VectorInt2D référencées ci-dessus:
http://hexgridutilities.codeplex.com/

Bien que l'implémentation ci-dessus concerne les hexs à sommet plat, la bibliothèque HexgridUtilities inclut l'option de transposition de la grille.

Pieter Geerkens
la source
0

J'ai trouvé une approche simple et alternative qui utilise la même logique qu'un damier ordinaire. Il crée un effet d'accrochage à la grille avec des points au centre de chaque tuile et à chaque sommet (en créant une grille plus serrée et en ignorant les points alternés).

Cette approche fonctionne bien pour des jeux comme Catan, où les joueurs interagissent avec les tuiles et les sommets, mais n'est pas adaptée aux jeux où les joueurs n'interagissent qu'avec les tuiles, car elle renvoie le point central ou le sommet dont les coordonnées sont les plus proches, plutôt que la tuile hexagonale les coordonnées sont à l'intérieur.

La géométrie

Si vous placez des points dans une grille avec des colonnes qui font le quart de la largeur d'une tuile et des lignes qui font la moitié de la hauteur d'une tuile, vous obtenez ce modèle:

comme décrit ci-dessus

Si vous modifiez ensuite le code pour ignorer chaque deuxième point dans un motif en damier (sauter if column % 2 + row % 2 == 1), vous vous retrouvez avec ce motif:

comme décrit ci-dessus

la mise en oeuvre

Avec cette géométrie à l'esprit, vous pouvez créer un tableau 2D (comme vous le feriez avec une grille carrée), en stockant les x, ycoordonnées de chaque point de la grille (à partir du premier diagramme) - quelque chose comme ceci:

points = []
for x in numberOfColumns
    points.push([])
    for y in numberOfRows
        points[x].push({x: x * widthOfColumn, y: y * heightOfRow})

Remarque: Comme d'habitude, lorsque vous créez une grille autour des points (plutôt que de placer des points sur les points eux-mêmes), vous devez décaler l'origine (en soustrayant la moitié de la largeur d'une colonne xet la moitié de la hauteur d'une ligne y).

Maintenant que votre tableau 2D ( points) est initialisé, vous pouvez trouver le point le plus proche de la souris comme vous le feriez sur une grille carrée, n'ayant qu'à ignorer tous les autres points pour produire le motif dans le deuxième diagramme:

column, row = floor(mouse.x / columnWidth), floor(mouse.y / rowHeight)
point = null if column % 2 + row % 2 != 1 else points[column][row]

Cela fonctionnera, mais les coordonnées sont arrondies au point le plus proche (ou aucun point) en fonction du rectangle invisible dans lequel se trouve le pointeur. Vous voulez vraiment une zone circulaire autour du point (donc la plage d'accrochage est égale dans toutes les directions). Maintenant que vous savez quel point vérifier, vous pouvez facilement trouver la distance (en utilisant le théorème de Pythagore). Le cercle implicite devrait toujours tenir à l'intérieur du rectangle de délimitation d'origine, limitant son diamètre maximal à la largeur d'une colonne (un quart de la largeur d'une tuile), mais qui est toujours assez grand pour bien fonctionner en pratique.

Carl Smith
la source