是什么导致我的 AI 在没有选择真实目标的情况下在地图上找到随机点?

What causes my AI to find random points on the map without picking a real target?

背景

我正在编写一个具有基本定位和移动功能的 AI。 "creatures" 有两种类型:活动的 fauna 和非活动的目标 flora.

问题

我的 AI(附加到 fauna)首先瞄准 flora,但它只 "sees" 一些 flora。当 AI 看不到 flora 时,AI 会原地转圈,看似 运行domly 弹跳;即使还有 flora 剩余。

什么导致只能看到 flora 中的一部分?为什么 fauna 在找不到 flora 之后似乎漫无目的地四处乱跳?为什么 fauna 在代码 运行 一段时间后会聚集起来?是什么原因导致 flora 看不到?

如果您需要任何额外信息,请询问。

我尝试修复它

我第一次尝试解决这个问题取得了一些成功,但没有完全解决问题。这是我使用对象而不是数组重写代码的时候。一旦我这样做了,目标就起作用了,但是一些 fauna 会无休止地旋转。

然后我意识到生物的旋转很可能无法与 getAngle 函数的 return 相提并论。生物的旋转可能等同于 getAngle 的 return 但不会相等(例如 360deg ~= 720deg 但 360deg != 720deg)。修复此问题后,它似乎可以工作一段时间,但当我 运行 进行更多检查和更长时间的测试时,我发现了这些问题。

我真的不确定是什么会导致这样的问题,但我很想知道。感谢您的帮助 :)

代码说明

代码可在线获取:http://codepen.io/CKH4/pen/wgZqgL/

在我的代码开头,我有一些 Object 原型扩展,允许我使用数组等对象。这些大致相当于他们的 Array 同行。我不认为这些是问题的根源,但它们是程序 运行.

所必需的
Object.prototype.filter = function(fn) {
  let ob = this, keep = {},
      k = Object.keys(ob);

  for (let i = 0; i < k.length; i++) {
    if (fn(k[i], ob[k[i]]))
      keep[k[i]] = ob[k[i]];
  }

  return keep;
}

Object.prototype.forEach = function(fn) {
  let ob = this, k = Object.keys(ob);

  for (let i = 0; i < k.length; i++)
    fn(k[i], ob[k[i]]);
}

Object.prototype.reduce = function(test, initialValue = null) {
  let ob = this, k = Object.keys(ob),
      accumulator = initialValue || ob[k[0]],
      i = (initialValue === null) ? 1 : 0;

  for (; i < k.length; i++)
    accumulator = test(accumulator, k[i], ob[k[i]], ob);

  return accumulator;
}


接下来我有一些辅助函数来操作 "creatures"。

// calculates the distance between two creatures by taking their [pos] as inputs
function getDist(p1, p2) {
  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}
// calculates the angle from point1 to point2
function getAngle(p1, p2) {
  return (Math.atan2(p2.y - p1.y, p2.x - p1.x) / Math.PI * 180 + 360) % 360;
}

// moves the creature in the direction they are facing
function move() {
  this.pos.x += this.speed * Math.cos(this.direction * Math.PI / 180);
  this.pos.y += this.speed * Math.sin(this.direction * Math.PI / 180);
}
// rotates the creature towards the angle input by the turn speed of the creature
function rotateTowards(angle) {
  this.direction += Math.sign(angle - this.direction) * this.turnSpeed;
  this.direction = this.direction % 360;
}
// rotates the creature by the angle provided
function rotateBy(angle) {
  this.direction += angle;
  this.direction = this.direction % 360;
}


现在我有定位功能了。它首先取 运行 正在 ai 的生物,然后它取一个生物的对象来浏览,接下来它取目前只能找到最近的模式,最后它取一个过滤函数这让目标发现者只看植物群。

代码首先过滤掉不在 AI 视线范围内的那些。这就是我预计的问题所在。接下来它应用输入过滤器(所以在我的例子中只剩下 flora)。最后,只有当生物对象中还有剩余时,代码才会将对象缩减为最近的生物。如果 creatures 的对象中没有任何剩余,则它 return 是一个未定义的数组。

function getTarget(c, of, mode = `nearest`, filter) {
  let first;

  // filter so its only the ones in view
  of = of.filter((k, t) => {
    return Math.abs(getAngle(c.pos, t.pos) - c.direction) < c.viewAngle / 2;
  });

  // filter for target type; eg. only return flora
  if (filter)
    of = of.filter(filter);

  if (Object.keys(of).length) {
    first = of[Object.keys(of)[0]];

    if (mode == `nearest`) {
      return of.reduce((acc, k, cur) => {
        let dist = getDist(c.pos, cur.pos);
        if (dist < acc[0])
          return [dist, k];
        else
          return acc;
      }, [getDist(c.pos, first.pos), first]);
    }
  }
  else
    return [undefined, undefined];
}


我终于有了将瞄准系统与移动代码联系在一起的通用 AI。如果有目标,该生物将转向并朝目标移动。如果目标在生物的 5 像素范围内,则该生物会摧毁目标。否则该生物转向正方向,"looking" 另一个目标。

function findfood() {
  let target = getTarget(this, ob, `nearest`, (k, c) => c.type == `flora`);

  this.target = target[1];

  if (ob[this.target]) {
    rotateTowards.call(this, getAngle(this.pos, ob[this.target].pos));
    if (getDist(this.pos, ob[this.target].pos) > 5)
      move.call(this);
    else {
      delete ob[this.target];
    }
  }
  else
    rotateBy.call(this, this.turnSpeed);
}


这里我生成了一个对象,其中 运行domly 放置了 florafauna。我使用的 ID 系统使用 Object 而不是 Array。所有的生物都存储在一个动态的全局对象中。

ob = {};

for (let i = 20; i > 0; i--) {
  let id = Math.floor(Math.random() * 1000000000),
      type = (Math.random() > .2 ? `flora` : `fauna`);

  ob[id] = {
    type: type,
    pos: { x: Math.floor(Math.random() * canvas.width), y: Math.floor(Math.random() * canvas.height) },
    direction: Math.random() * 360
  }
  if (type == `fauna`) {
    ob[id].ai = findfood;
    ob[id].viewAngle = 90;
    ob[id].speed = .8;
    ob[id].turnSpeed = 1.6;
  }
}


然后我运行在一个setInterval中模拟,如果生物有AI函数就调用它。问题也不在这里。

let fixedUpdate = setInterval(function() {
  Object.keys(ob).forEach((ck) => {
    let c = ob[ck];

    if (c && c.ai)
      c.ai.apply(c);
  });
}, 1000 / 60);


这是我用来显示它的代码。它只是基本的 canvas 东西,所以问题肯定不在这里。

let draw = () => {
  // clear canvas
  ctx.putImageData(emptyCanvas, 0, 0);

  Object.keys(ob).forEach((ck) => {

    let c = ob[ck];

    if (c.type == 'flora')
      ctx.fillStyle = '#22cc33';
    else if (c.type == 'fauna') {
      ctx.fillStyle = '#0066ee';
      ctx.beginPath();
      ctx.moveTo(c.pos.x, c.pos.y);
      // ctx.lineTo(c.pos.x + 100, c.pos.y - 50);
      // ctx.lineTo(c.pos.x + 100, c.pos.y + 50);
      ctx.lineTo(c.pos.x, c.pos.y);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(c.pos.x, c.pos.y, 100, (c.direction - c.viewAngle / 2) * Math.PI / 180, (c.direction + c.viewAngle / 2) * Math.PI / 180);
      ctx.fill();
    }
    else
      ctx.fillStyle = '#424242';

    ctx.beginPath();
    ctx.arc(c.pos.x, c.pos.y, 10, 0, 2 * Math.PI);
    ctx.fill();
  });

  requestAnimationFrame(draw);
}

draw();


这是嵌入的代码:

console.clear();

Object.prototype.filter = function(fn) {
  let ob = this, keep = {},
      k = Object.keys(ob);

  for (let i = 0; i < k.length; i++) {
    if (fn(k[i], ob[k[i]]))
      keep[k[i]] = ob[k[i]];
  }

  return keep;
}

Object.prototype.forEach = function(fn) {
  let ob = this, k = Object.keys(ob);

  for (let i = 0; i < k.length; i++)
    fn(k[i], ob[k[i]]);
}

Object.prototype.reduce = function(test, initialValue = null) {
  let ob = this, k = Object.keys(ob),
      accumulator = initialValue || ob[k[0]],
      i = (initialValue === null) ? 1 : 0;

  for (; i < k.length; i++)
    accumulator = test(accumulator, k[i], ob[k[i]], ob);

  return accumulator;
}



function getDist(p1, p2) {
  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}
function getAngle(p1, p2) {
  return (Math.atan2(p2.y - p1.y, p2.x - p1.x) / Math.PI * 180 + 360) % 360;
}

function move() {
  this.pos.x += this.speed * Math.cos(this.direction * Math.PI / 180);
  this.pos.y += this.speed * Math.sin(this.direction * Math.PI / 180);
}
function rotateTowards(angle) {
  this.direction += Math.sign(angle - this.direction) * this.turnSpeed;
  this.direction = this.direction % 360;
}
function rotateBy(angle) {
  this.direction += angle;
  this.direction = this.direction % 360;
}

function getTarget(c, of, mode = `nearest`, filter) {
  let first;

  // filter so its only the ones in view
  of = of.filter((k, t) => {
    return Math.abs(getAngle(c.pos, t.pos) - c.direction) < c.viewAngle / 2;
  });

  // filter for target type; eg. only return flora
  if (filter)
    of = of.filter(filter);

  if (Object.keys(of).length) {
    first = of[Object.keys(of)[0]];

    if (mode == `nearest`) {
      return of.reduce((acc, k, cur) => {
        let dist = getDist(c.pos, cur.pos);
        if (dist < acc[0])
          return [dist, k];
        else
          return acc;
      }, [getDist(c.pos, first.pos), first]);
    }
  }
  else
    return [undefined, undefined];
}

function findfood() {

  let target = getTarget(this, ob, `nearest`, (k, c) => c.type == `flora`);

  this.target = target[1];

  if (ob[this.target]) {
    rotateTowards.call(this, getAngle(this.pos, ob[this.target].pos));
    if (getDist(this.pos, ob[this.target].pos) > 5)
      move.call(this);
    else {
      delete ob[this.target];
    }
  }
  else
    rotateBy.call(this, this.turnSpeed);

}




ob = {};

for (let i = 20; i > 0; i--) {
  let id = Math.floor(Math.random() * 1000000000),
      type = (Math.random() > .2 ? `flora` : `fauna`);

  ob[id] = {
    type: type,
    pos: { x: Math.floor(Math.random() * canvas.width), y: Math.floor(Math.random() * canvas.height) },
    direction: Math.random() * 360
  }
  if (type == `fauna`) {
    ob[id].ai = findfood;
    ob[id].viewAngle = 90;
    ob[id].speed = .8;
    ob[id].turnSpeed = 1.6;
  }
}

console.log(ob);



let ctx = canvas.getContext(`2d`);
let emptyCanvas = ctx.getImageData(0,0,canvas.width,canvas.height);

let draw = () => {
  // clear canvas
  ctx.putImageData(emptyCanvas, 0, 0);

  Object.keys(ob).forEach((ck) => {

    let c = ob[ck];

    if (c.type == 'flora')
      ctx.fillStyle = '#22cc33';
    else if (c.type == 'fauna') {
      ctx.fillStyle = '#0066ee';
      ctx.beginPath();
      ctx.moveTo(c.pos.x, c.pos.y);
      // ctx.lineTo(c.pos.x + 100, c.pos.y - 50);
      // ctx.lineTo(c.pos.x + 100, c.pos.y + 50);
      ctx.lineTo(c.pos.x, c.pos.y);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(c.pos.x, c.pos.y, 100, (c.direction - c.viewAngle / 2) * Math.PI / 180, (c.direction + c.viewAngle / 2) * Math.PI / 180);
      ctx.fill();
    }
    else
      ctx.fillStyle = '#424242';

    ctx.beginPath();
    ctx.arc(c.pos.x, c.pos.y, 10, 0, 2 * Math.PI);
    ctx.fill();
  });

  requestAnimationFrame(draw);
}

draw();



let fixedUpdate = setInterval(function() {
  Object.keys(ob).forEach((ck) => {
    let c = ob[ck];

    if (c && c.ai)
      c.ai.apply(c);
  })
}, 1000 / 60);
body {
  margin: 0;
}
<canvas height="1000" id="canvas" width="1000"></canvas>

我在我的代码中发现了错误。我在 getTarget() 中,当我获得第一个可能的目标 (of[0]) 时,我将 first 存储为对生物的引用而不是生物的 ID。

要修复它,我不得不通过删除对对象的引用来存储 ID。我改变了:

first = of[Object.keys(of)[0]];

收件人:

first = Object.keys(of)[0];

这在 getTarget() 的其余代码中引起了问题,因为我试图获取附加到生物对象的属性,而不是生物 ID。我通过更改解决了这个问题:

}, [getDist(c.pos, first.pos), first]);

收件人:

}, [getDist(c.pos, of[first].pos), first]);


这给了我完成的getTarget()

function getTarget(c, of, mode = `nearest`, filter) {
  let first;

  // filter so its only the ones in view
  of = of.filter((k, t) => {
    return Math.abs(getAngle(c.pos, t.pos) - c.direction) < c.viewAngle / 2;
  });

  // filter for target type; eg. only return flora
  if (filter)
    of = of.filter(filter);

  if (Object.keys(of).length) {
    first = Object.keys(of)[0];

    if (mode == `nearest`) {
      return of.reduce((acc, k, cur) => {
        let dist = getDist(c.pos, cur.pos);
        if (dist < acc[0])
          return [dist, k];
        else
          return acc;
      }, [getDist(c.pos, of[first].pos), first]);
    }
  }
  else
    return [undefined, undefined];
}