Расчет расстояния по шестиугольной сетке

Я пытаюсь найти, сколько шестиугольников находится между двумя точками на гекс-сетке. Я попытался найти формулу в Интернете, но мне не удалось найти форму, которая соответствует типу шестнадцатеричной сетки, которую я использую.

Шестнадцатеричная сетка выложена так же, как эта с той же системой координат: http://www.gamedev.net/index.php?app=core&module=attach§ion=attach&attach_rel_module=ccs&attach_id=1962

Я знаю, что это может быть невозможно с этой системой координат, но это последнее усилие, прежде чем я вернусь и изменю его. Заранее большое спасибо.

11 ответов

Решение

Спасибо @user2709663 и @jonathankoren за ответы. Я провожу много времени с вашими ответами, но обнаружил, что у обоих есть некоторые проблемы. Или, по крайней мере, тип сетки, рассматриваемый для этих ответов, не указан четко. Тем не менее, я нашел очень хороший учебник и код реализации этой проблемы, а также библиотеку для управления шестигранной сеткой по адресу: http://www.redblobgames.com/grids/hexagons/ (код библиотеки: http://www.redblobgames.com/grids/hexagons/implementation.html). Я также реализую версию кода расстояния Matlab для вертикальной разметки "odd-q" следующим образом:

function f = offset_distance(x1,y1,x2,y2)
    ac = offset_to_cube(x1,y1);
    bc = offset_to_cube(x2,y2);
    f = cube_distance(ac, bc);
end

function f = offset_to_cube(row,col)
    %x = col - (row - (row&1)) / 2;
    x = col - (row - mod(row,2)) / 2;
    z = row;
    y = -x-z;
    f = [x,z,y];
end

function f= cube_distance(p1,p2)
    a = abs( p1(1,1) - p2(1,1));
    b = abs( p1(1,2) - p2(1,2));
    c = abs( p1(1,3) - p2(1,3));
    f =  max([a,b,c]);
end

Вот код тестирования Matlab

sx = 6;
sy = 1;
for i = 0:7
    for j = 0:5
        k = offset_distance(sx,sy,i,j);
        disp(['(',num2str(sx),',',num2str(sy),')->(',num2str(i),',',num2str(j),')=',num2str(k)])
    end
end

Если бы вы использовали систему координат, которая проходит вдоль гексагон в двух направлениях, вы могли бы использовать:

distance = max(
     abs(dest.y - start.y),     
     abs(dest.x - start.x),
     abs((dest.x - dest.y)*-1 - (start.x - start.y)*-1)
)

Однако вы этого не сделали, вы используете волнистую систему координат, которая идет с зерном только в одном направлении (горизонтальном). К счастью, мы можем преобразовать между двумя, используя

straight.y = squiggle.y
straight.x = ciel(squiggle.y / -2) + squiggle.x

Итак, решение для расстояния с использованием этой системы уравнений дает вам:

distance = max(
     abs(dest.y - start.y),     
     abs(ceil(dest.y / -2) + dest.x - ceil(start.y / -2) - start.x),
     abs(-dest.y - ceil(dest.y / -2) - dest.x + start.y  + ceil(start.y / -2) + start.x)
)

Это даст вам манхэттенское расстояние между двумя гексами, используя только их координаты (при условии, что я не сделал никаких опечаток, транспонирующих x и y, так как ваша сетка повернута на 90 градусов от моей). Однако вы должны купить печенье для моего учителя алгебры средней школы, чтобы оно работало, иначе я испорчу дистрибутивную собственность.

Примечание: может потребоваться возиться с отрицательными координатами, я не проверял.

Принятый ответ неверен. Сначала я с подозрением относился к этому, когда упомянул об использовании ортогональных координат на неортогональных осях, но выполнение кода по моему собственному решению показывает, что переоценка расстояния усложняется.

Фактическое правильное решение:

def hexDistance(start, dest):
  if (start.x == dest.x):
    return abs(dest.y - start.y)
  elif (start.y == dest.y):
    return abs(dest.x - start.x)
  else:
    dx = abs(dest.x - start.x)
    dy = abs(dest.y - start.y)
    if start.y < dest.y:
      return dx + dy - int(math.ceil(dx / 2.0))
    else:
      return dx + dy - int(math.floor(dx / 2.0))

Это использует hex-> квадратное представление:

        ------        
 ------ 0, +1 ------ 
 -1, +1 ------ +1, +1  
 ------ 0, 0 ------ 
 -1, 0 ------ +1, 0  
 ------ 0, -1 ------ 
        ------        

 --------------------------
| -1, +1 | 0, +1 | +1, +1 |
| --------------------------
| -1, 0 | 0, 0 | +1, 0 |
| -------------------------- |
| | 0, -1 | |
 -------------------------- 

Чтобы найти кратчайший путь между двумя гексами:

  1. Начиная с одного гекса,
  2. Находясь в разных рядах, следуйте по диагонали к другому ряду.
  3. Находясь в том же ряду, идите прямо в другой гекс.

Давайте назовем разницу в направлении х dx и разница в направлении у dy, Если dy / 2 > dx, вам не нужно делать второй шаг, поэтому расстояние просто dy, В противном случае расстояние dy + (dx - dy / 2), Если я не ошибся.

M H Rasel связал этот пост в своем предыдущем ответе: Hexagonal Grids. После этого отличного поста я понял, что мне нужны координаты куба; это дает самый простой способ рассчитать расстояния. Вот фрагмент кода в Котлине:

data class Point(val x: Int, val y: Int, val z: Int) {

    fun distance(b: Point): Int {
        return (abs(x - b.x) + abs(y - b.y) + abs(z - b.z)) / 2
    }

}

var x = 0
var y = 0
var z = 0

val p1 = Point(x, y, z)    // starting position

val steps = "ne,ne,ne".split(",")    // go to North-East 3 times

for (direction in steps) {
    when(direction) {
        "n"  -> { ++y; --z }
        "ne" -> { ++x; --z }
        "se" -> { ++x; --y }
        "s"  -> { --y; ++z }
        "sw" -> { --x; ++z }
        "nw" -> { ++y; --x }
    }
}

val p2 = Point(x, y, z)    // we arrived here
val dist = p1.distance(p2)    // the result is here (in this example: 3)

Редактировать: я предполагаю плоскую шестигранную сетку.

Если у вашего гексагонального тайлинга есть направления: N, NE, SE, S, SW, NW, как в задаче Advent of Code 2017, и вы смещаете цель на (0,0) (заранее вычитая свою позицию из цели), следующее логика у меня сработала

def distance(self):
    # Take diagonal steps towards self.x == 0
    steps = abs(self.x)
    # y moves closer to 0 by steps because moving diagonal, but never moving too far
    if self.y > 0:
        # Might still be positive, but never negative
        y = max(self.y - steps, 0)
    else:
        # Might still be negative, but not positive
        y = min(self.y + steps, 0)
    # You move 2 positions north/south by a single move so divide y's by 2
    return abs(y) // 2 + abs(steps)

Я думаю, что это может работать для гексагональной сетки с направлениями EAST и WEST вместо NORTH и SOUTH, как у вас, просто переключая роли x и y. В этом случае вы должны двигаться к self.y == 0 по диагонали (при необходимости) и т. Д.

Вот ответ для координат С# и Offset "even-r". Сначала проверьте эту арктайл: https://www.redblobgames.com/grids/hexagons/

      static int distanceBetweenCells(int x1, int y1, int x2, int y2)
{
    Vector3 evenr_to_cube(int row, int col)
    {
        int q = col - ((row + (row % 2))/2);
        int r = row;
        return new Vector3(q, r, -q - r);
    }
    Vector3 point_1 = evenr_to_cube(x1, y1);
    Vector3 point_2 = evenr_to_cube(x2, y2);
    int a = (int)Math.Abs(point_1.X - point_2.X);
    int b = (int)Math.Abs(point_1.Y - point_2.Y);
    int c = (int)Math.Abs(point_1.Z - point_2.Z);
    return Math.Max(a, Math.Max(b, c));
}

Вы можете легко изменить это для своего типа координат, используя статью выше.

Вот метод прямого расчета, основанный на четырех преобразованиях, которые отображают все входные данные в область между положительным вектором y и вектором под углом 30 градусов. Сначала рассмотрим плоские шестиугольники по оси Y. Ось x будет волнистой.

  1. Переведите первую координату в 0,0
  2. Сложите левую половину на правую половину, сохраняя ось Y
  3. Согните область между нисходящим вектором 30 градусов и осью y на область между осью y и направленным вверх 30-градусным вектором. Направленный вниз 30-градусный вектор будет отображен на направленный вверх 30-градусный вектор.
  4. Согните область под углом 30 градусов вверх к области между осью Y и вектором угла 30 градусов вверх.

После всего этого кратчайший путь лежит вверх на 30 градусов до цели x и прямо вверх до цели y.

      def range x1, y1, x2, y2
  # translate x1,y1 to origin and 
  # and fold left half over right side
  rx = (x2 - x1).abs
  ry = y2 - y1 - (rx % 2) * (x1 % 2)

  # fold along 30deg downward
  if ry <= -rx / 2
    ry = ry.abs - rx % 2
    # rx remains unchanged
  else   
    # fold along 30deg upward
    if ry < rx / 2
      c = rx / 2 - ry # steps down from 30deg line
      ry = rx / 2 + (c + (rx % 2)) / 2
      rx -= c  # rx update must be after ry
    end
  end
  rx + ry - rx / 2
end

Вот субоптимальный, но не слишком субоптимальный (должен быть O(n)) алгоритм:

Сначала создайте функцию, которая определяет, пересекает ли шестиугольник в определенном месте в шестигранной сетке отрезок линии с определенной начальной и конечной точкой (например, рассчитайте шесть линий, из которых он состоит, и сделайте что-то вроде: http://alienryderflex.com/intersect/.)

Во-вторых, создайте функцию, которая определяет, в каком шестиугольнике на шестигранной сетке находится точка.

Теперь напишите свой алгоритм так:

  • Держите список всех шестиугольников, которые пока перекрывают отрезок
  • Начните с шестиугольника, в котором находится начало отрезка
  • Для каждого шестиугольника, окружающего последний перекрывающийся, если его нет в списке, посмотрите, пересекает ли lin segmente этот шестиугольник. Если так, сделайте это новым заголовком списка и повторите
  • Если у нас закончились шестиугольники, вернем список, который мы сделали

Я бы посоветовал вам протестировать случай, когда отрезок прямой будет точно параллелен и проходит вдоль шва между шестиугольниками, чтобы увидеть, получаете ли вы шестиугольники с одной стороны, с обеих сторон или ни с одной (и, таким образом, останавливаете).

Изображение, объясняющее систему координат

поэтому, к сожалению, я не знаю, какую систему координат вы использовали тогда, потому что ссылка больше не работает, но большинство решений, размещенных здесь, не сработали для меня. Это система координат, которую я использовал:

              ------        
 ------  0, +1 ------ 
 -1, +1 ------ +1, 0  
 ------  0,  0 ------ 
 -1,  0 ------ +1, -1  
 ------  0, -1 ------ 
        ------        

 --------------------------
| -1, +1 |  0, +1 |        |
|--------------------------|
| -1,  0 |  0,  0 | +1, 0  |
|--------------------------|
|        |  0, -1 | +1, -1 |
 -------------------------- 

И это код/формула, которая работала у меня для точек (x1,y1) и (x2,y2):

          public int distance(int x1, int y1, int x2, int y2){ //distance of hexfields, 1 is source 2 is target
        int dx = Mathf.Abs(x1 - x2);
        int dy = Mathf.Abs(y1 - y2);

        if(dx == 0){ return dy; }
        else if(dy == 0){ return dx; }
        else{
            if(x2 < x1 && y2 < y1){ //empty Corner
                return dx+dy;
            }else if(x2 < x1 && y2 > y1){ //Filled Corner
                return Mathf.Max(dx, dy);
            }else if(x2 > x1 && y2 < y1){ //Filled Corner
                return Mathf.Max(dx, dy);
            }else if(x2 > x1 && y2 > y1){ //empty Corner
                return dx+dy;
            }else return 0;
        }

    }

Это, конечно, можно было бы оптимизировать с точки зрения качества кода, но его было бы легче понять, как сейчас.

Если плитки на сетке потенциально могут быть заблокированы, то вас интересует алгоритм решения лабиринтов A* (или A-Star): http://labs.makemachine.net/2010/03/a-star-maze-solver/

Механизм, использованный в этом видео, применяется к сетке квадратов, но при отсутствии какого-либо дополнительного кодирования его можно применить к шестиугольной сетке. Для каждой плитки решатель знает, какие плитки использовать дальше, потому что плитки хранят указатели на окружающие плитки. В сетке квадратов каждая ячейка будет хранить максимум 4 указателя (максимум, потому что они хранят только указатели на незаблокированные ячейки), и единственное отличие в вашем случае будет хранить максимум 6 указателей.

Если плитки всегда проходимы, то A * наверняка выполнит свою работу, однако, вероятно, есть более быстрый путь. Если я правильно истолковываю ваш вопрос, вас интересует не расстояние, а подсчет целых чисел числа гексов между двумя заданными гексами? Попробуйте следующее:

class Coord {
    int x;
    int y;
}

int dist(Coord hex1, Coord hex2) {
    int xSteps = Math.abs(hex1.x - hex2.x);
    int ySteps = Math.abs(hex1.y - hex2.y);

    return Math.max(xSteps, ySteps) + Math.abs(xSteps - ySteps);
}

Почему вы можете спросить? Этот вопрос касается определения того, сколько раз мы должны двигаться вертикально или горизонтально, а не по диагонали. Мы хотим двигаться по диагонали настолько, насколько это возможно, иначе мы не будем умно относиться к нашим расстояниям. Math.abs(xSteps - ySteps) это число недиагональных ходов, которые мы должны сделать. Добавьте к этому большее из расстояний x и y, и все готово.

Другие вопросы по тегам