Java/Processing: A* 基于图节点的游戏

Java/Processing: A* graph node based game

我正在尝试构建一个 'simulation' 小游戏。 这个游戏没有真正的目的,我只是在做一些小项目,同时尝试学习一些初学者编程的来龙去脉。

这是我的目标:

  1. 在处理 canvas 中,有多个 'Nodes' 表示玩家可以移动到的位置。
  2. 用户将输入玩家所在的位置以及他们想移动到的位置。 (引用节点)
  3. 程序将使用 A* 算法确定最有效的路线。
  4. 路线确定后,玩家(由 Circle() 表示)将沿直线从一个节点移动到另一个节点。
  5. 一旦玩家停止,程序就会知道玩家当前所在的节点,并等待进一步的指令。

我以某种方式设法将我的前三个目标拼凑在一起,但下半场让我感到非常困惑和头痛。

我尝试过的(目标 4)。 我正在为 A* 算法使用自定义库,可以在此处找到:http://www.lagers.org.uk/pfind/ref/classes.html。 当算法绘制最佳路线的线时,我会将每个节点的 X、Y 位置存储到 ArrayList 中。 然后我将该 ArrayList 数据输入到我的 Player Class 中,它将通过 ArrayList 的 X/Y 位置在屏幕上移动圆圈。 我遇到的问题是,一旦玩家移动到第一个节点,我就无法报告玩家已停止移动并准备移动到下一个 ArrayList X/Y 位置。 我通过使用 Millis() 每 5 秒递增 ArrayList 来管理一个解决方法,但我知道这是实现我的目标的糟糕方法。

这可能没有多大意义,但这是我当前输出的图片。

我已经告诉程序我希望蓝色圆圈以最有效的路线从节点 0 行进到节点 8。我当前的代码将移动复制节点 0、2、8 的 X/Y 位置并将它们保存到 ArrayList 中。 该 ArrayList 信息将每 5 秒输入一次 player.setTarget() 方法,以便有时间让圆移动。

理想情况下,我想取消时间延迟并在玩家成功移动到节点以及玩家当前所在的节点时获得 class 报告。

import pathfinder.*;
import java.lang.Math;

// PathFinding_01
Graph graph;
// These next 2 are only needed to display 
// the nodes and edges.
GraphEdge[] edges;
GraphNode[] nodes;
GraphNode[] route;
// Pathfinder algorithm
IGraphSearch pathFinder;

// Used to indicate the start and end nodes as selected by the user.
GraphNode startNode, endNode;

PImage bg;
Player midBlue;
Player midRed;
int lastHittingBlue = 99;
int lastHittingRed = 99;
int blueSide = 0;
int redSide = 1;
boolean nodeCount = true;
boolean firstRun = true; //Allows data to be collected on the first node.
boolean movement;
int count;
int x;
int y;
float start;
float runtime;
int test = 1;

// create ArrayList for route nodes 
ArrayList<Float> xPos; 
ArrayList<Float> yPos;


void setup() {
  size(1200,1000);    //Set size of window to match size of the background image. 
  bg = loadImage("background.png");
  bg.resize(1200,1000);
  
  start = millis();
  
  textSize(20);
  // Create graph
  createGraph();
  // Get nodes and edges
  nodes = graph.getNodeArray();
  edges = graph.getAllEdgeArray();
  // Now get a path finder object
  pathFinder = new GraphSearch_Astar(graph);
  // Now get a route between 2 nodes
  // You can change the parameter values but they must be valid IDs
  pathFinder.search(0,8);
  route = pathFinder.getRoute();
  
  //Initialise the X/Y position arraylist.
  xPos = new ArrayList<Float>();
  yPos = new ArrayList<Float>();

  drawGraph();
  drawPath();

  midBlue = new Player(lastHittingBlue, blueSide);
  midRed = new Player(lastHittingRed, redSide);
  
}

void draw() {
  background(0);
  
  text((float)millis()/1000, 10,height/6);
  text(start/1000, 10,height/3);
  runtime = millis() - start;
  text(runtime/1000, 10,height/2);
  
  if (runtime >= 5000.0) {
    start = millis();
    float printX = midBlue.getXPos();
    float printY = midBlue.getYPos();
    int pX = round(printX);
    int pY = round(printY);
    print(pX, " ", pY, "\n");
    test += 1;
  }
  
  drawGraph();
  drawPath();
  
  movement = midBlue.movementCheck();
  midBlue.setTargetPosition(xPos.get(test), yPos.get(test));

  midBlue.drawPlayer();

  text( "x: " + mouseX + " y: " + mouseY, mouseX + 2, mouseY );

  //noLoop();
}

void drawGraph() {
  // Edges first
  strokeWeight(2);
  stroke(180, 180, 200);
  for (int i = 0; i < edges.length; i++) {
    GraphNode from = edges[i].from();
    GraphNode to = edges[i].to();
    line(from.xf(), from.yf(), to.xf(), to.yf());
  }
  // Nodes next
  noStroke();
  fill(255, 180, 180);
  for (int i = 0; i < nodes.length; i++) {
    GraphNode node = nodes[i];
    ellipse(node.xf(), node.yf(), 20, 20);
    text(node.id(), node.xf() - 24, node.yf() - 10);
  }
}

void drawPath() {
  strokeWeight(10);
  stroke(200, 255, 200, 160);
  for (int i = 1; i < route.length; i++) {
    GraphNode from = route[i-1];
    GraphNode to = route[i];
    
    while (firstRun) {      
      xPos.add(from.xf());
      yPos.add(from.yf());
      firstRun = false;
    }
    
    xPos.add(to.xf());
    yPos.add(to.yf());
    
    line(from.xf(), from.yf(), to.xf(), to.yf());
    
    if (nodeCount == true) {
       count = route.length;
       nodeCount = false;
    }
    
  }
}


public void createGraph() {
  graph = new Graph();
  // Create and add node
  GraphNode node;
  //                   ID   X    Y
  node = new GraphNode(0, 175, 900);
  graph.addNode(node);
  node = new GraphNode(1, 190, 830);
  graph.addNode(node);
  node = new GraphNode(2, 240, 890);
  graph.addNode(node);
  node = new GraphNode(3, 253, 825);
  graph.addNode(node);
  node = new GraphNode(4, 204, 750);
  graph.addNode(node);
  node = new GraphNode(5, 315, 770);
  graph.addNode(node);
  node = new GraphNode(6, 325, 880);
  graph.addNode(node);
  node = new GraphNode(7, 440, 880);
  graph.addNode(node);
  node = new GraphNode(8, 442, 770);
  graph.addNode(node);
  node = new GraphNode(9, 400, 690);
  graph.addNode(node);
  node = new GraphNode(10, 308, 656);
  graph.addNode(node);
  node = new GraphNode(11, 210, 636);
  graph.addNode(node);

  // Edges for node 0
  graph.addEdge(0, 1, 0, 0);
  graph.addEdge(0, 2, 0, 0);
  graph.addEdge(0, 3, 0, 0);
  // Edges for node 1
  graph.addEdge(1, 4, 0, 0);
  graph.addEdge(1, 5, 0, 0);
  graph.addEdge(1, 10, 0, 0);
  // Edges for node 2
  graph.addEdge(2, 5, 0, 0);
  graph.addEdge(2, 6, 0, 0);
  graph.addEdge(2, 8, 0, 0);
  // Edges for node 3
  graph.addEdge(3, 5, 0, 0);
  graph.addEdge(3, 8, 0, 0);
  graph.addEdge(3, 10, 0, 0);
  // Edges for node 4
  graph.addEdge(4, 10, 0, 0);
  graph.addEdge(4, 11, 0, 0);
  // Edges for node 5
  graph.addEdge(5, 8, 0, 0);
  graph.addEdge(5, 9, 0, 0);
  graph.addEdge(5, 10, 0, 0);
  // Edges for node 6
  graph.addEdge(6, 7, 0, 0);
  graph.addEdge(6, 8, 0, 0);
  // Edges for node 7
  graph.addEdge(7, 0, 0, 0);

    // Edges for node 7
  graph.addEdge(9, 0, 0, 0);
    // Edges for node 7
  //graph.addEdge(10, 0, 0, 0);
    // Edges for node 7
  graph.addEdge(11, 0, 0, 0);
}


class Player {
  int lastHitting;
  int side; //0 = Blue, 1 = Red.
  float xPos;
  float yPos;
  float xTar;
  float yTar;
  color circleColour = color(255,0,0);
  boolean isPlayerStopped;
  int xDir;
  int yDir;
  
  Player(int lastHitting, int side) {
    this.lastHitting = lastHitting;
    this.side = side;
    
    /* Set the Colour of the circle depending on their side selection */
    if (this.side == 0) { 
      circleColour = color(0,0,255);
      xPos = 180;
      yPos = 900;
    } else if (this.side == 1) {
      circleColour = color(255,0,0);
      xPos = 990;
      yPos = 125;
    }
  }
  
  
  void drawPlayer() {
    fill(circleColour);
    circle(xPos,yPos,35);
    
    float speed = 100.0;
    PVector dir = new PVector(xTar - xPos, yTar - yPos);
    
    while (dir.mag() > 1.0) {
      dir.normalize();
      dir.mult(min(speed, dir.mag()));
      
      xPos += dir.x;
      yPos += dir.y;
      isPlayerStopped = false;
    }
    
    if (dir.mag() < 1.0) {
      isPlayerStopped = true;
    }
  }
  
  
  void setTargetPosition(float targetX, float targetY) {
    xTar = targetX;
    yTar = targetY;
  }
  
  
  boolean movementCheck() {
     return isPlayerStopped;
  }
  
  float getXPos() {
    return xPos;
  }
  
  float getYPos() {
    return yPos;
  }
  
  
  
  
}

提前感谢您的帮助。我知道这是一个有点复杂的问题。我真的只是在寻找方向,我尝试了很多不同的东西,但我不确定我应该使用什么工具来帮助我进步。

请不要对我糟糕的代码吹毛求疵,我对这一切还很陌生。

我不会抨击你糟糕的代码,因为我知道你正在做的事情的学习曲线有多陡,我尊重这一点。这就是说,我认为如果您放弃图书馆并自己编写 A* 代码,您将对正在发生的事情有更多的了解。

如果涉及到那个,我稍后可以提供帮助,但现在,我们要做的是:我将指出您如何获得此结果:

作为奖励,我会给你一些技巧来改善你的编码习惯。


我可以看出您通过阅读自己对代码的理解(不错 post 总的来说顺便说一句)知道自己在做什么,而且在代码中我还可以看到您还有很多了解你真正在做什么。

你应该保持现在的样子。将它通过电子邮件发送给自己,一年后收到,这样明年当你对变得更好感到绝望时,你会惊喜地看到你到底进步了多少——或者,就我而言,我只是决定我已经那时候智障,现在还是,但我希望你不要对自己那么苛刻。

由于有很多 space 需要改进,我将忽略所有这些,除了几个与项目无关的关键点:

  1. 使用明确的变量名。 您的一些变量看起来命名得当,例如 nodeCount。除了这个变量不是整数;这是一个布尔值。然后,它应该被命名为 nodesHaveBeenCounted。给你的变量命名,就像一个愤怒的骑自行车的人不得不检查你的代码,每次他必须读入代码以理解什么是变量的目的时,他都会打断你的一根手指。当你这样做的时候,尽量不要缩短变量名,即使它非常明显。 xPos 应该是 xPosition。这也适用于方法签名,包括方法的名称(恭喜,你真的很擅长)和方法的参数。

  2. 注意全局变量。我不反对使用全局变量,但你应该注意不要仅仅使用它们来绕过作用域.另外,请注意不要将局部变量和全局变量命名为相同的名称,就像您对 xPos 所做的那样,它可能是 ArrayList 或 float,具体取决于您在代码中的位置。有条不紊:您可以向所有全局变量添加一些内容,明确地将它们标识为全局变量。有些人在他们的名字前加上 g_,例如 g_xPos。我喜欢只使用下划线,比如 _xPos.

  3. 当一个方法做不止一件事时,考虑将它分成更小的部分。调试更新值的 8 行更容易而不是筛选 60 行代码,想知道魔法在哪里发生。

下面是我为使动作更流畅并避免使用计时器所做的更改。

全局变量中:

  1. xPos 重命名为 xPosArray 或类似名称,只要它不被 Player 的 xPos 模态变量遮盖即可。对 yPos ArrayList 执行相同的操作。

setup()方法中:

  1. 把这行添加到方法的最后一行(只要在midBlue和运行实例化drawPath方法之后就可以了):

midBlue.setTargetPosition(xPosArray.get(test), yPosArray.get(test));

draw()方法中:

  1. 完全删除 if (runtime >= 5000.0) {...} 块,它不再有用了。

  2. 删除这些行:

movement = midBlue.movementCheck();
midBlue.setTargetPosition(xPosArray.get(test), yPosArray.get(test));

Player.drawPlayer()方法中:

擦除 while 之后的所有内容,包括 while。将其替换为这些行(它们几乎相同但逻辑略有不同):

if (dir.mag() > 1.0) {
  dir.normalize();
  dir.mult(min(speed, dir.mag()));

  xPos += dir.x;
  yPos += dir.y;
} else {
  // We switch target only once, and only when the target has been reached
  setTargetPosition(xPosArray.get(test), yPosArray.get(test));
}

运行 程序。你不再需要计时器了。什么想法?很简单:我们只在达到当前目标后才更改玩家的目标。别处没有。

额外的想法:每个玩家都应该有自己的坐标 ArrayList,而不是坐标数组的全局变量。然后每个玩家都可以独立旅行。

玩得开心!