首页 > 解决方案 > 如果我的玩家可以在回合制游戏中以 x 个动作结束,我应该如何计算每个可能的终点?

问题描述

我目前正在处理中重新创建文明游戏。我计划实现一个功能,一个给定的单位可以看到它可以使用给定数量的允许移动的十六进制进行的每一个可能的移动。所有可能的端点都用红色圆圈标记。但是,单位不能穿过山脉或水域。我试图通过找出我可以在不进入山或水体的情况下进行的所有可能的移动组合来解决这个问题,但我无法弄清楚如何确定每个组合。

任何单位都可以进入6个方向,东北,北,西北,东南,南,西南。我分配给任何单位的最大移动次数可能会上升到 6。任何更高,我担心每次移动一个单位时处理可能会变慢。

我正在尝试重新创建这个:

在此处输入图像描述

我希望结果看起来像两个可能的动作(没有黑色箭头):

在此处输入图像描述

该图像的原始版本:

在此处输入图像描述

这是我用来绘制十六进制网格的代码。在绘制每个单独的十六进制后,其中心的 x 坐标和 y 坐标分别存储在 xHexes 和 yHexes 中。此外,在生成瓦片类型(例如草地、海滩)之后,瓦片类型也立即存储在名为 hexTypes 的数组中。因此,我可以通过引用它的索引来获得我想要在地图上的任何十六进制的 x 和 y 坐标和十六进制类型。

用于绘制单个六边形的代码:

beginShape();
for (float a = PI/6; a < TWO_PI; a += TWO_PI/6) {
  float vx = x + cos(a) * gs*2;
  float vy = y + sin(a) * gs*2;
  vertex(vx, vy);
}

x 是六边形中心的 x 坐标 y 是六边形中心的 y 坐标 gs = 六边形的半径

用于在创建十六进制网格的窗口上镶嵌十六进制的代码:

void redrawMap() {
  float xChange = 1.7;
  float yChange = 6;
  for (int y = 0; y < ySize/hexSize; y++) {
    for (int x = 0; x < xSize/hexSize; x++) {
      if (x % 2 == 1) {
        // if any part of this hexagon being formed will be visible on the window and not off the window.
        if (x*hexSize*xChange <= width+2*hexSize && int(y*hexSize*yChange) <= height+3*hexSize) {
          drawHex(x*hexSize*xChange, y*hexSize*yChange, hexSize);
        }
// only record and allow player to react with it if the entire tile is visible on the window
        if (x*hexSize*xChange < width && int(y*hexSize*yChange) < height) {
          xHexes.add(int(x*hexSize*xChange));
          yHexes.add(int(y*hexSize*yChange));
        }
      } else {
        if (x*hexSize*xChange <= width+2*hexSize && int(y*hexSize*yChange) <= height+3*hexSize) {
          drawHex(x*hexSize*xChange, y*hexSize*yChange+(hexSize*3), hexSize);
        }
        if (x*hexSize*xChange < width && int(y*hexSize*yChange+(hexSize*3)) < height) {
          xHexes.add(int(x*hexSize*xChange));
          yHexes.add(int(y*hexSize*yChange+(hexSize*3)));
        }
      }
    }
  }
}

hexSize 是用户为每个六边形指定的大小,确定屏幕上的六边形数量。

标签: javaprocessing

解决方案


这个答案将帮助您解决这个问题(绿色是平原,红色是山丘,蓝色是水,也请不要点燃我可怕的网格):

旅行的可能性

请注意,此解决方案中没有寻路,只有一些非常简单的“我能到达那里”数学。我将在最后包含草图的完整代码,以便您可以重现我所做的并自己进行测试。最后一件事:这个答案不使用任何高级设计模式,但它假设您对基础知识和面向对象编程感到满意。如果我做了一些你不确定你理解的事情,你可以(并且应该)询问它。

另外:这是一个概念证明,而不是“复制并粘贴我”的解决方案。我没有你的代码,所以无论如何它不可能是第二件事,但由于你的问题可以用无数种方式解决,这只是我故意让你尽可能简单和直观的一个,这样你就可以得到这个想法和带着它跑。


首先,我强烈建议您将瓷砖变成对象。首先是因为它们需要携带大量信息(每个图块上有什么,它们穿越的难度,可能是资源或产量之类的东西……我不知道,但会有很多东西)。


基础知识

我像这样组织我的全局变量:

// Debug
int unitTravelPoints = 30; // this is the number if "travel points" currently being tested, you can change it

// Golbals
float _tileSize = 60;
int _gridWidth = 10;
int _gridHeight = 20;

ArrayList<Tile> _tiles = new ArrayList<Tile>(); // all the tiles
ArrayList<Tile> _canTravel = new ArrayList<Tile>(); // tiles you can currently travel to

基础是我喜欢能够即时更改网格大小,但这只是一个细节。接下来是为网格选择坐标系。我选择最简单的一个,因为我不想在复杂的事情上绞尽脑汁,但您可能希望将其调整到另一个坐标系。我选择网格的偏移坐标类型:我的“每隔一行”是半个瓷砖偏移。所以,而不是这样:

无偏移

我有这个:

带偏移

剩下的只是调整瓷砖的空间坐标,这样看起来不会太糟糕,但它们的坐标保持不变:

有偏移但瓷砖更近

请注意我如何认为空间坐标和网格坐标是两个不同的东西。我将主要使用空间坐标进行邻近检查,但那是因为我很懒,因为你可以制作一个很好的算法,在没有空间坐标的情况下做同样的事情,而且成本可能会更低。

旅行积分呢?这就是我决定的工作方式:你的单位有有限数量的“旅行点”。这里没有单位,而是一个全局变量unitTravelPoints,它会做同样的事情。我决定使用这个比例:一个普通的瓷砖值 10 个旅行点。所以:

  1. 平原:10分
  2. 山:15分
  3. 水:1000分(这是无法通行的地形,但不详述)

我不打算详细介绍绘制网格的细节,但这主要是因为你的算法在这方面看起来比我的要好得多。另一方面,我会花一些时间来解释我是如何设计 Tiles 的。

瓷砖

我们正在进入 OOP:它们是Drawable. Drawable是一个基类,它包含一些基本信息,每个可见的东西都应该有:一个位置和一个isVisible可以关闭的设置。还有一个绘制它的方法,我称之为,Render()因为draw()处理已经采用了:

class Drawable {
  PVector position;
  boolean isVisible;

  public Drawable() {
    position = new PVector(0, 0);
    isVisible = true;
  }

  public void Render() {
    // If you forget to overshadow the Render() method you'll see this error message in your console
    println("Error: A Drawable just defaulted to the catch-all Render(): '" + this.getClass() + "'.");
  }
}

瓷砖将更加复杂。它将有更多的基本信息:行、列、当前是否选择(为什么不选择)、平原、丘陵或水之类的类型、一堆相邻的瓦片、一种绘制自身的方法以及一种判断单位是否可以的方法穿越它:

class Tile extends Drawable {
  int row, column;
  boolean selected = false;
  TileType type;

  ArrayList<Tile> neighbors = new ArrayList<Tile>();

  Tile(int row, int column, TileType type) {
    super(); // this calls the parent class' constructor

    this.row = row;
    this.column = column;
    this.type = type;

    // the hardcoded numbers are all cosmetics I included to make my grid looks less awful, nothing to see here
    position.x = (_tileSize * 1.5) * (column + 1);
    position.y = (_tileSize * 0.5) * (row + 1);
    // this part checks if this is an offset row to adjust the spatial coordinates
    if (row % 2 != 0) {
      position.x += _tileSize * 0.75;
    }
  }

  // this method looks recursive, but isn't. It doesn't call itself, but it calls it's twin from neighbors tiles
  void FillCanTravelArrayList(int travelPoints, boolean originalTile) {
    if (travelPoints >= type.travelCost) {
      // if the unit has enough travel points, we add the tile to the "the unit can get there" list
      if (!_canTravel.contains(this)) {
        // well, only if it's not already in the list
        _canTravel.add(this);
      }
      
      // then we check if the unit can go further
      for (Tile t : neighbors) {
        if (originalTile) {
          t.FillCanTravelArrayList(travelPoints, false);
        } else {
          t.FillCanTravelArrayList(travelPoints - type.travelCost, false);
        }
      }
    }
  }

  void Render() {
    if (isVisible) {
      // the type knows which colors to use, so we're letting the type draw the tile
      type.Render(this);
    }
  }
}

瓷砖类型

TileType 是一种奇怪的动物:它是一个真正的类,但它从未在任何地方使用过。那是因为它是所有瓷砖类型的共同根,它将继承它的基础。“城市”图块可能需要与“沙漠”图块非常不同的变量。但是两者都需要能够绘制自己,并且都需要由瓷砖拥有。

class TileType {
  // cosmetics
  color fill = color(255, 255, 255);
  color stroke = color(0);
  float strokeWeight = 2;
  // every tile has a "travelCost" variable, how much it cost to travel through it
  int travelCost = 10;

  // while I put this method here, it could have been contained in many other places
  // I just though that it made sense here
  void Render(Tile tile) {
    fill(fill);
    if (tile.selected) {
      stroke(255);
    } else {
      stroke(stroke);
    }
    strokeWeight(strokeWeight);
    DrawPolygon(tile.position.x, tile.position.y, _tileSize/2, 6);
    textAlign(CENTER, CENTER);
    fill(255);
    text(tile.column + ", " + tile.row, tile.position.x, tile.position.y);
  }
}

现在,每种磁贴类型都可以自定义,但每个磁贴......只是一个磁贴,无论它的内容是什么。这是TileType我在此演示中使用的:

// each different tile type will adjust details like it's travel cost or fill color
class Plains extends TileType {
  Plains() {
    this.fill = color(0, 125, 0);
    this.travelCost = 10;
  }
}

class Water extends TileType {
  // here I'm adding a random variable just to show that you can custom those types with whatever you need
  int numberOfFishes = 10;
  
  Water() {
    this.fill = color(0, 0, 125);
    this.travelCost = 1000;
  }
}

class Hill extends TileType {
  Hill() {
    this.fill = color(125, 50, 50);
    this.travelCost = 15;
  }
}

非类方法

我添加了一种mouseClicked()方法,因此我们可以选择一个十六进制来检查该单元可以行进多远。在您的游戏中,您必须做到这一点,因此当您选择一个单位时,所有这些事情都会到位,但这只是一个概念证明,单位是虚构的,它的位置就是您单击的任何位置。

void mouseClicked() {
  // clearing the array which contains tiles where the unit can travel as we're changing those
  _canTravel.clear();

  for (Tile t : _tiles) {
    // select the tile we're clicking on (and nothing else)
    t.selected = IsPointInRadius(t.position, new PVector(mouseX, mouseY), _tileSize/2);
    if (t.selected) {
      // if a tile is selected, check how far the imaginary unit can travel
      t.FillCanTravelArrayList(unitTravelPoints, true);
    }
  }
}

最后,我添加了 2 个“辅助方法”以使事情变得更容易:

// checks if a point is inside a circle's radius
boolean IsPointInRadius(PVector center, PVector point, float radius) {
  // simple math, but with a twist: I'm not using the square root because it's costly
  // we don't need to know the distance between the center and the point, so there's nothing lost here
  return pow(center.x - point.x, 2) + pow(center.y - point.y, 2) <= pow(radius, 2);
}

// draw a polygon (I'm using it to draw hexagons, but any regular shape could be drawn)
void DrawPolygon(float x, float y, float radius, int npoints) {
  float angle = TWO_PI / npoints;
  beginShape();
  for (float a = 0; a < TWO_PI; a += angle) {
    float sx = x + cos(a) * radius;
    float sy = y + sin(a) * radius;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}

旅行的计算方式

在幕后,程序就是这样知道单元可以移动到哪里的:在这个例子中,单元有 30 个移动点。平原花费 10,丘陵花费 15。如果单位剩余的积分足够,则该图块会标记为“可以前往那里”。每次一个瓦片在移动距离内时,我们也会检查该单位是否也可以从这个瓦片走得更远。

虚数单位可以走这么远

现在,如果你还在关注我,你可能会问:这些瓦片如何知道哪个瓦片是它们的邻居?这是一个很好的问题。我认为检查其坐标的算法将是处理此问题的最佳方法,但由于此操作只会在我们创建地图时发生一次,我决定采用简单的路线并检查哪些瓷砖在空间上足够接近:

void setup() {
  // create the grid
  for (int i=0; i<_gridWidth; i++) {
    for (int j=0; j<_gridHeight; j++) {
      int rand = (int)random(100);
      if (rand < 20) {
        _tiles.add(new Tile(j, i, new Water()));
      } else if (rand < 50) {
        _tiles.add(new Tile(j, i, new Hill()));
      } else {
        _tiles.add(new Tile(j, i, new Plains()));
      }
    }
  }

  // detect and save neighbor tiles for every Tile
  for (Tile currentTile : _tiles) {
    for (Tile t : _tiles) {
      if (t != currentTile) {
        if (IsPointInRadius(currentTile.position, t.position, _tileSize)) {
          currentTile.neighbors.add(t);
        }
      }
    }
  }
}

复制粘贴的完整代码

这是一个地方的全部内容,因此您可以轻松地将其复制并粘贴到处理 IDE 中并使用它(另外,它包括我如何绘制我糟糕的网格):

// Debug
int unitTravelPoints = 30; // this is the number if "travel points" currently being tested, you can change it

// Golbals
float _tileSize = 60;
int _gridWidth = 10;
int _gridHeight = 20;

ArrayList<Tile> _tiles = new ArrayList<Tile>();
ArrayList<Tile> _canTravel = new ArrayList<Tile>();

void settings() {
  // this is how to make a window size's dynamic
  size((int)(((_gridWidth+1) * 1.5) * _tileSize), (int)(((_gridHeight+1) * 0.5) * _tileSize));
}

void setup() {
  // create the grid
  for (int i=0; i<_gridWidth; i++) {
    for (int j=0; j<_gridHeight; j++) {
      int rand = (int)random(100);
      if (rand < 20) {
        _tiles.add(new Tile(j, i, new Water()));
      } else if (rand < 50) {
        _tiles.add(new Tile(j, i, new Hill()));
      } else {
        _tiles.add(new Tile(j, i, new Plains()));
      }
    }
  }

  // detect and save neighbor tiles for every Tile
  for (Tile currentTile : _tiles) {
    for (Tile t : _tiles) {
      if (t != currentTile) {
        if (IsPointInRadius(currentTile.position, t.position, _tileSize)) {
          currentTile.neighbors.add(t);
        }
      }
    }
  }
}

void draw() {
  background(0);

  // show the tiles
  for (Tile t : _tiles) {
    t.Render();
  }

  // show how far you can go
  for (Tile t : _canTravel) {
    fill(0, 0, 0, 0);
    if (t.selected) {
      stroke(255);
    } else {
      stroke(0, 255, 0);
    }
    strokeWeight(5);
    DrawPolygon(t.position.x, t.position.y, _tileSize/2, 6);
  }
}

class Drawable {
  PVector position;
  boolean isVisible;

  public Drawable() {
    position = new PVector(0, 0);
    isVisible = true;
  }

  public void Render() {
    // If you forget to overshadow the Render() method you'll see this error message in your console
    println("Error: A Drawable just defaulted to the catch-all Render(): '" + this.getClass() + "'.");
  }
}

class Tile extends Drawable {
  int row, column;
  boolean selected = false;
  TileType type;

  ArrayList<Tile> neighbors = new ArrayList<Tile>();

  Tile(int row, int column, TileType type) {
    super(); // this calls the parent class' constructor

    this.row = row;
    this.column = column;
    this.type = type;

    // the hardcoded numbers are all cosmetics I included to make my grid looks less awful, nothing to see here
    position.x = (_tileSize * 1.5) * (column + 1);
    position.y = (_tileSize * 0.5) * (row + 1);
    // this part checks if this is an offset row to adjust the spatial coordinates
    if (row % 2 != 0) {
      position.x += _tileSize * 0.75;
    }
  }

      // this method looks recursive, but isn't. It doesn't call itself, but it calls it's twin from neighbors tiles
      void FillCanTravelArrayList(int travelPoints, boolean originalTile) {
        if (travelPoints >= type.travelCost) {
          // if the unit has enough travel points, we add the tile to the "the unit can get there" list
          if (!_canTravel.contains(this)) {
            // well, only if it's not already in the list
            _canTravel.add(this);
          }
          
          // then we check if the unit can go further
          for (Tile t : neighbors) {
            if (originalTile) {
              t.FillCanTravelArrayList(travelPoints, false);
            } else {
              t.FillCanTravelArrayList(travelPoints - type.travelCost, false);
            }
          }
        }
      }

  void Render() {
    if (isVisible) {
      // the type knows which colors to use, so we're letting the type draw the tile
      type.Render(this);
    }
  }
}

class TileType {
  // cosmetics
  color fill = color(255, 255, 255);
  color stroke = color(0);
  float strokeWeight = 2;
  // every tile has a "travelCost" variable, how much it cost to travel through it
  int travelCost = 10;

  // while I put this method here, it could have been contained in many other places
  // I just though that it made sense here
  void Render(Tile tile) {
    fill(fill);
    if (tile.selected) {
      stroke(255);
    } else {
      stroke(stroke);
    }
    strokeWeight(strokeWeight);
    DrawPolygon(tile.position.x, tile.position.y, _tileSize/2, 6);
    textAlign(CENTER, CENTER);
    fill(255);
    text(tile.column + ", " + tile.row, tile.position.x, tile.position.y);
  }
}

// each different tile type will adjust details like it's travel cost or fill color
class Plains extends TileType {
  Plains() {
    this.fill = color(0, 125, 0);
    this.travelCost = 10;
  }
}

class Water extends TileType {
  // here I'm adding a random variable just to show that you can custom those types with whatever you need
  int numberOfFishes = 10;

  Water() {
    this.fill = color(0, 0, 125);
    this.travelCost = 1000;
  }
}

class Hill extends TileType {
  Hill() {
    this.fill = color(125, 50, 50);
    this.travelCost = 15;
  }
}


void mouseClicked() {
  // clearing the array which contains tiles where the unit can travel as we're changing those
  _canTravel.clear();

  for (Tile t : _tiles) {
    // select the tile we're clicking on (and nothing else)
    t.selected = IsPointInRadius(t.position, new PVector(mouseX, mouseY), _tileSize/2);
    if (t.selected) {
      // if a tile is selected, check how far the imaginary unit can travel
      t.FillCanTravelArrayList(unitTravelPoints, true);
    }
  }
}

// checks if a point is inside a circle's radius
boolean IsPointInRadius(PVector center, PVector point, float radius) {
  // simple math, but with a twist: I'm not using the square root because it's costly
  // we don't need to know the distance between the center and the point, so there's nothing lost here
  return pow(center.x - point.x, 2) + pow(center.y - point.y, 2) <= pow(radius, 2);
}

// draw a polygon (I'm using it to draw hexagons, but any regular shape could be drawn)
void DrawPolygon(float x, float y, float radius, int npoints) {
  float angle = TWO_PI / npoints;
  beginShape();
  for (float a = 0; a < TWO_PI; a += angle) {
    float sx = x + cos(a) * radius;
    float sy = y + sin(a) * radius;
    vertex(sx, sy);
  }
  endShape(CLOSE);
}

希望它会有所帮助。玩得开心!


推荐阅读