我正在使用JavaScript和HTML Canvas创建游戏。这是一款多人2D游戏,坦克试图互相攻击。坦克既可以移动也可以旋转。如何通过旋转矩形物体找出碰撞检测?我知道,我可以把它们做成方形并使用圆形检测,但是当一个坦克撞到一堵墙时它看起来很乱。感谢所有试图提供帮助的人:)
答案 0 :(得分:2)
有很多方法可以做到。最简单的方法。当您计算点和线之间的叉积时,如果该点位于该线的右侧,则该值将为负,如果为左侧则为正。如果你依次对四个边中的每个边做,并且它们都是相同的符号,则必须在内部。
获得直线和点的叉积
//x1,y1,x2,y2 is a line
// px,py is a point
// first move line and point relative to the origin
// so that the line and point is a vector
px -= x1;
py -= y1;
x2 -= x1;
y2 -= y1;
var cross = x2 * py - y2 * px;
if(cross < 0){
// point left of line
}else if(cross > 0) {
// point right of line
}else {
// point on the line
}
但对于每个物体和每个子弹,这都是很多数学。
最好的方法是将子弹转换为坦克局部坐标系,然后只需测试边界,左,右,顶部,底部就可以了。
为此,您需要反转储罐转换矩阵。不幸的是,这样做的简单方法目前仍然落后于浏览器标志/前缀,因此您需要在javascript中创建和操作转换。 (不应该太长,直到ctx.getTransform()全面实施并填充画布2d API中非常需要的性能漏洞)
所以你有一个坦克在x,y和旋转r,你用
绘制它ctx.translate(x,y);
ctx.rotate(r);
// render the tank
ctx.fillRect(-20,-10,40,20); // rotated about center
转换包含我们进行计算所需的一切,我们需要做的就是反转它然后将子弹与倒置矩阵相乘
var tankInvMatrix = ctx.getTransform().invertSelf(); // get the inverted matrix
子弹是bx,所以创建一个DOMPoint
var bullet = new DOMPoint(bx,by);
然后为每个坦克改造子弹DOMMatrix.transformPoint
var relBullet = tankInvMatrix.transformPoint(bullet); // transform the point
// returning the bullet
// relative to the tank
现在只需在坦克局部坐标空间进行测试
if(relBullet.x > -20 && relBullet.x < 20 && relBullet.x > -10 && relBullet.x < 10){
/// bullet has hit the tank
}
直到成为常态,你必须做很长的路。使用相同的x,y,r表示坦克,bx,表示子弹。
// create a vector aligned to the tanks direction
var xdx = Math.cos(r);
var xdy = Math.sin(r);
// set the 2D API to the tank location and rotation
ctx.setTransform(xdx,xdy,-xdy,xdx,x,y); // create the transform for the tank
// draw the tank
ctx.fillRect(-20,-10,40,20); // rotated about center
// create inverted matrix for the tank
// Only invert the tank matrix once per frame
var d = xdx * xdx - xdy * -xdy;
var xIx = xdx / d;
var xIy = -xdy / d;
// I am skipping c,d of the matrix as it is perpendicular to a,b
// thus c = -b and d = a
var ix = (-xdy * y - xdx * x) / d;
var iy = -(xdx * y - xdy * x) / d;
// For each bullet per tank
// multiply the bullet with the inverted tank matrix
// bullet local x & y
var blx = bx * xIx - by * xIy + ix;
var bly = bx * xIy + by * xIx + iy;
// and you are done.
if(blx > -20 && blx < 20 && bly > -10 && bly < 10){
// tank and bullet are one Kaaboommmm
}
太多的负片,xdx,xdy等让我能够看到我是否正确(事实证明我把错误的标志放在行列式中)所以这里是一个快速演示,以显示它在行动和工作。
使用鼠标移动坦克身体,它会显示它被击中红色。您可以轻松地将其延伸到水箱移动部件。你只需要使用炮塔的逆变换来获得当地空间的子弹来进行测试。
添加代码以阻止坦克在视觉上以交叉的画布边缘弹出。这是通过在显示时从每个坦克中减去OFFSET
来完成的。通过将OFFSET
添加到测试坐标来进行命中测试时,必须考虑此偏移量。
const TANK_LEN = 40;
const TANK_WIDTH = 20;
const GUN_SIZE = 0.8; // As fraction of tank length
// offset is to ensure tanks dont pop in and out as the cross screen edge
const OFFSET = Math.sqrt(TANK_LEN * TANK_LEN + TANK_WIDTH * TANK_WIDTH ) + TANK_LEN * 0.8;
// some tanks
var tanks = {
tanks : [], // array of tanks
drawTank(){ // draw tank function
this.r += this.dr;
this.tr += this.tdr;
if(Math.random() < 0.01){
this.dr = Math.random() * 0.02 - 0.01;
}
if(Math.random() < 0.01){
this.tdr = Math.random() * 0.02 - 0.01;
}
if(Math.random() < 0.01){
this.speed = Math.random() * 2 - 0.4;
}
var xdx = Math.cos(this.r);
var xdy = Math.sin(this.r);
// move the tank forward
this.x += xdx * this.speed;
this.y += xdy * this.speed;
this.x = ((this.x + canvas.width + OFFSET * 2) % (canvas.width + OFFSET * 2));
this.y = ((this.y + canvas.height + OFFSET * 2) % (canvas.height + OFFSET * 2)) ;
ctx.setTransform(xdx,xdy,-xdy,xdx,this.x - OFFSET, this.y - OFFSET);
ctx.lineWidth = 2;
ctx.beginPath();
if(this.hit){
ctx.fillStyle = "#F00";
ctx.strokeStyle = "#800";
this.hit = false;
}else{
ctx.fillStyle = "#0A0";
ctx.strokeStyle = "#080";
}
ctx.rect(-this.w / 2, -this.h / 2, this.w, this.h);
ctx.fill();
ctx.stroke();
ctx.translate(-this.w /4, 0)
ctx.rotate(this.tr);
ctx.fillStyle = "#6D0";
ctx.beginPath();
ctx.rect(-8, - 8, 16, 16);
ctx.rect(this.w / 4, - 2, this.w * GUN_SIZE, 4);
ctx.fill()
ctx.stroke()
// invert the tank matrix
var d = (xdx * xdx) - xdy * -xdy;
this.invMat[0] = xdx / d;
this.invMat[1] = -xdy / d;
// I am skipping c,d of the matrix as it is perpendicular to a,b
// thus c = -b and d = a
this.invMat[2] = (-xdy * this.y - xdx * this.x) / d;
this.invMat[3] = -(xdx * this.y - xdy * this.x) / d;
},
hitTest(x,y){ // test tank against x,y
x += OFFSET;
y += OFFSET;
var blx = x * this.invMat[0] - y * this.invMat[1] + this.invMat[2];
var bly = x * this.invMat[1] + y * this.invMat[0] + this.invMat[3];
if(blx > -this.w / 2 && blx < this.w / 2 && bly > -this.h / 2 && bly < this.h / 2){
this.hit = true;
}
},
eachT(callback){ // iterator
for(var i = 0; i < this.tanks.length; i ++){ callback(this.tanks[i],i); }
},
addTank(x,y,r){ // guess what this does????
this.tanks.push({
x,y,r,
dr : 0, // turn rate
tr : 0, // gun direction
tdr : 0, // gun turn rate
speed : 0, // speed
w : TANK_LEN,
h : TANK_WIDTH,
invMat : [0,0,0,0],
hit : false,
hitTest : this.hitTest,
draw : this.drawTank,
})
},
drawTanks(){ this.eachT(tank => tank.draw()); },
testHit(x,y){ // test if point x,y has hit a tank
this.eachT(tank => tank.hitTest(x,y));
}
}
// this function is called from a requestAnimationFrame call back
function display() {
if(tanks.tanks.length === 0){
// create some random tanks
for(var i = 0; i < 100; i ++){
tanks.addTank(
Math.random() * canvas.width,
Math.random() * canvas.height,
Math.random() * Math.PI * 2
);
}
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
// draw the mouse
ctx.fillStyle = "red";
ctx.strokeStyle = "#F80";
ctx.beginPath();
ctx.arc(mouse.x,mouse.y,3,0,Math.PI * 2);
ctx.fill();
ctx.stroke();
// draw the tanks
tanks.drawTanks();
// test for a hit (Note there should be a update, then test hit, then draw as is the tank is hit visually one frame late)
tanks.testHit(mouse.x,mouse.y);
}
//====================================================================================================
// Boilerplate code not part of answer ignore all code from here down
//====================================================================================================
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,y : 0,w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left + scrollX;
m.y = e.pageY - m.bounds.top + scrollY;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
if ((m.buttonRaw & 2) && m.crashRecover !== null) {
if (typeof m.crashRecover === "function") {
setTimeout(m.crashRecover, 0);
}
}
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
}
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
// Clean up. Used where the IDE is on the same page.
var done = function () {
window.removeEventListener("resize", resizeCanvas)
if(mouse !== undefined){
mouse.remove();
}
document.body.removeChild(canvas);
canvas = ctx = mouse = undefined;
}
function update(timer) { // Main update loop
if(ctx === undefined){
return;
}
globalTime = timer;
display(); // call demo code
//if (!(mouse.buttonRaw & 2)) {
requestAnimationFrame(update);
//} else {
// done();
//}
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
mouse.crashRecover = done;
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
&#13;