背景
我正在编写一个具有基本目标选择和移动功能的AI。有两种类型的“生物”:活跃的fauna
和不活跃的目标flora
。
问题
我的AI(附着在fauna
上)首先瞄准flora
,但它只能“看到”一些flora
。当AI看不到任何flora
时,AI会原地旋转并看似随机地弹跳;即使还有剩余的flora
。
为什么只有一些flora
会被看到?为什么fauna
在停止找到flora
后会看似无目的地弹跳?为什么fauna
在代码运行一段时间后会聚集在一起?是什么导致flora
看不到?
如果您需要更多信息,请告诉我。
我的修复尝试
我第一次尝试修复这个问题取得了一些成功,但没有完全解决问题。那时我重写了代码,使用对象而不是数组。完成这一步后,目标选择功能正常了,但一些fauna
会无休止地旋转。
然后我意识到可能是生物的旋转与getAngle函数的返回值不兼容。生物的旋转可能与getAngle的返回值等效,但不相等(例如,360度 ~= 720度,但360度 != 720度)。在修复这个问题后,它似乎在一段时间内工作正常,但当我进行更仔细的测试并运行更长时间时,我发现了这些问题。
我真的不确定是什么导致了这样的问题,但我非常好奇想知道。感谢任何帮助 🙂
代码解释
代码在线可用,地址是: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;}
接下来我有一些用于操作“生物”的辅助函数。
// 计算两个生物之间的距离,通过它们的[pos]作为输入function getDist(p1, p2) { return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));}// 计算从点1到点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;}
现在我有目标选择函数。它首先接受运行AI的生物,然后接受一个要查找的生物对象,接下来接受模式,目前只能找到最近的,最后接受一个过滤函数,让目标查找器只查看flora。
代码首先过滤掉不在AI视线内的生物。我认为问题可能出在这里。接下来应用输入过滤器(这样在我的情况下只剩下flora
)。最后,只有当生物对象中还有剩余时,代码才将对象减少到仅剩最近的生物。如果生物对象中没有任何剩余,它会返回一个包含undefined的数组。
function getTarget(c, of, mode = `nearest`, filter) { let first; // 过滤以便只查看视野内的生物 of = of.filter((k, t) => { return Math.abs(getAngle(c.pos, t.pos) - c.direction) < c.viewAngle / 2; }); // 过滤目标类型;例如,只返回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像素范围内,生物会摧毁目标。否则,生物会朝正方向转动,“寻找”另一个目标。
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);}
在这里我生成一个包含随机放置的flora
和fauna
的对象。我使用一个基于Object
而不是Array
的ID系统。所有的生物都被存储在一个动态的全局对象中。
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时调用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 = () => { // 清除画布 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; // 过滤以便只查看视野内的生物 of = of.filter((k, t) => { return Math.abs(getAngle(c.pos, t.pos) - c.direction) < c.viewAngle / 2; }); // 过滤目标类型;例如,只返回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 = () => { // 清除画布 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; // 过滤以便只查看视野内的生物 of = of.filter((k, t) => { return Math.abs(getAngle(c.pos, t.pos) - c.direction) < c.viewAngle / 2; }); // 过滤目标类型;例如,只返回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];}