使用html5 canvas进行墙壁碰撞检测

wall collision detection with html5 canvas

本文关键字:碰撞检测 html5 canvas 使用      更新时间:2023-09-26

我正在制作一个游戏,有一堵墙我不想让玩家通过。我使用的是html5画布,有一个玩家对象来保存x和y值。墙位于x:650和y:0处。由于当玩家的x坐标为630时,玩家是20x20像素,因此玩家会碰到墙壁。

if(player.x > 630 && player.y <= 500) {
    player.x = 630;
}

这个代码出了什么问题?我感谢任何帮助!

回答

你给出的代码还可以,没有什么问题。所以我怀疑问题出在代码的其他地方,很可能是在运动代码中。如果你在墙测试后移动播放器,然后显示它,播放器可能会开始爬到墙上,但如果没有其余的代码,很难知道你的代码出了什么问题。

我已经包含了更多关于碰撞测试正确方法的细节,因为有两个答案只显示了部分解决方案。它是碰撞测试的通用指南,可能不直接适用于该问题。

帧间移动

从曲面反射对象的正确方式。

您必须考虑到球在帧之间移动,并且碰撞可能在前一帧的任何时候发生。碰撞后球与墙的距离取决于上一帧中球何时碰到墙。如果球移动缓慢或快速,这一点非常重要。

var dx = 10; // delta x velocity of object in pixels
var wx = 10; // width of object in pixels
var px = 90;  // position of object in pixels
var wallX = 105; // position of wall

px += dx;  // move the ball. Its position is now  100.
           // its right side is at px + wx = 110.
// test if it has it the wall
if(px+wx > wallX){
    dx = -dx; // reflect delta x
    // The object is 5 pixel into the wall.
    // The object has hit the wall some time during the last frame
    // We need to adjust the position as the ball may have been
    // traveling away from the wall for some time during the last frame.
    var dist = (px+wx)-wallX; // get the distance into the wall
    px -= dist*2; // the object hit the wall at position 95 and has been 
                  // traveling away since then so it is easy to just 
                  // subtract 2 times the distance the ball entered the wall
    // the above two lines can be done in one
    // px -= ((px+wx)-wallX)*2;
}

为什么重要

下面是一个球在画布内反弹的模拟。

为了说明球在帧之间移动,对其进行了运动模糊处理,以显示其在帧之间的运动。请注意,这不是一个完美的解决方案,因为假设反弹发生在球直线运动时,而实际上它是在自由落体和恒定加速度下。但它仍然可以节省能源。

在正确的测试中,球反弹到的高度会随着时间的推移保持不变。没有能量损失或获得。

右键单击以关闭帧间调整,您会注意到球开始在每帧降低其高度。这是因为在每次碰撞时,球都会损失一点能量,因为在碰撞测试后定位球时,没有考虑它在前一帧中的运动。当碰撞恰好在帧时间发生时,它将稳定为恒定速率。什么时候会发生这种情况很难提前确定。

左键单击以降低模拟帧速率,再次左键单击可恢复正常。

下面的代码并不是答案的一部分,它是为了证明碰撞测试过程中未正确调整位置对模拟整体准确性的影响。

// helper functions. NOT part of the answer
var canvas = document.getElementById("canV"); 
var ctx = canvas.getContext("2d");
var mouseButton = 0;
canvas.addEventListener('mousedown',function(event){mouseButton = event.which;});
canvas.addEventListener('mouseup'  ,function(){mouseButton = 0;});
canvas.addEventListener("contextmenu", function(e){ e.preventDefault();}, false);
var currentSurface = ctx;
var createImage = function (w, h) {// create an canvas image of size w,h and attach context 2d
    var image = document.createElement("canvas");  
    image.width = w;
    image.height = h !== undefined?h:w; 
    currentSurface = image.ctx = image.getContext("2d"); 
    return image;
}  
var setColour = function (fillC, strokeC, lineW) { 
    currentSurface.fillStyle = fillC !== undefined ? fillC : currentSurface.fillStyle;
    currentSurface.strokeStyle = strokeC !== undefined ? strokeC : currentSurface.strokeStyle;
    currentSurface.lineWidth = lineW !== undefined ? lineW : currentSurface.lineWidth;
}
var circle = function(x,y,r,how){
    currentSurface.beginPath();
    currentSurface.arc(x,y,r,0,Math.PI*2);
    how = how.toLowerCase().replace(/[os]/g,"l"); // how to draw
    switch(how){
        case "f":  // fill
            currentSurface.fill();
            break;
        case "l":
            currentSurface.stroke();
            break;
        case "lf":
            currentSurface.stroke();
            currentSurface.fill();
            break;
        case "fl":
            currentSurface.fill();
            currentSurface.stroke();
            break;
    }
}
function createGradImage(size,col1,col2){
    var image = createImage(size);
    var g = currentSurface.createLinearGradient(0,0,0,currentSurface.canvas.height);
    g.addColorStop(0,col1);
    g.addColorStop(1,col2);
    currentSurface.fillStyle = g;
    currentSurface.fillRect(0,0,currentSurface.canvas.width,currentSurface.canvas.height);    
    return image;
}
function createColouredBall (ballR,col) {
    var ball = createImage(ballR*2);
    var unit = ballR/100;
    setColour("black");
    circle(ballR,ballR,ballR,"f");
    setColour("hsl("+col+",100%,30%)");
    circle(ballR-unit*3,ballR-unit*3,ballR-unit*7,"f");
    setColour("hsl("+col+",100%,50%)");
    circle(ballR-unit*10,ballR-unit*10,ballR-unit*16,"f");
    setColour("White");
    circle(ballR-unit*50,ballR-unit*50,unit*16,"f");
    
    return ball;
}
//===================================    
//    _                          
//   /_'  _ _  ____ __ _____ _ _ 
//  / _ '| ' '(_-< V  V / -_) '_|
// /_/ '_'_||_/__/'_/'_/'___|_|  
//                              
// ==================================
// Answer code
// lazy coder variables
var w = canvas.width;
var h = canvas.height;
// ball is simulated 5cm 
var pixSize = 0.24; // in millimeters for simulation
// Gravity is 9.8 ms^2 so convert to pixels per frame squared
// Assuming constant 60 frames per second. ()
var gravity = 9800*pixSize/60; 
gravity *= 0.101; // because Earth's gravity is stupidly large let's move to Pluto
// ball 5cm 
var ballR = (25/pixSize)/2;          // radius is 2.5cm for 5cm diamiter ball
var ballX = w/2;                     // get center of canvas
var ballY = ballR+3;                 // start at the top
var ballDX = (Math.random()-0.5)*15; // start with random x speed
ballDX += ballDX < 0 ? -5 : 5;       // make sure it's not too slow
var ballDY = 0;                      // star with no downward speed;
var ballLastX = ballX;
var ballLastY = ballY;
//create an image of the Ball
var ball = createColouredBall(ballR,Math.floor(Math.random()*360)); // create an image of ball
// create a background. Image is small as it does not have much detail in it
var background = createGradImage(16,"#5af","#08C");
// time to run for
var runFor = 10*60; // ten secons yimes 60 frames per second
// draws the ball motion blured. This introduces extra complexity
var drawMotionBlur = function(image,px,py,dx,dy,steps){
    var i,sx,sy;
    sx = dx / steps;
    sy = dy / steps;
    px -= dx; // move back to start position
    py -= dy; 
    ctx.globalAlpha = 1/(steps*0.8); // set alpha to slightly higher for each step
    for(i = 0; i < steps; i+= 1){
        ctx.drawImage(image,px+i*sx,py+i*sy);
    }
    ctx.globalAlpha = 1; // reset alpha
    
}
// style for text
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.textAlign = "center";
ctx.lineJoin = "round"; // stop some letters getting ears.
ctx.lineWidth = 3;
ctx.textBaseline = "bottom";
var textCenterX = w/2;
var maxHeight = Infinity;
var lastMaxHeight = ballY;
var slowMotion = false;  // slow motion flag
var frameTravel = true;  // use frame travel in collision test 
var update = function(){
    var blurSteps = 10;  // motion blur ball render steps
    const bSteps = 10;
    if(mouseButton === 1){
        slowMotion = ! slowMotion;
        mouseButton = 0;
    }
    if(mouseButton === 3){
        frameTravel = ! frameTravel;
        ballX = w/2;                     // get center of canvas
        ballY = ballR+3;                 // start at the top
        ballDY = 0;                      // start at 0 y speed
        mouseButton = 0;
    }
    // clear the canvas with background canvas image
    ctx.drawImage(background,0,0,w,h);
    
    ballDY += gravity; // accelrate due to grav
    // add deltas to ball position
    ballX += ballDX; 
    ballY += ballDY;
    // test for collison on left and right walls. Need to 
    // ajust for motion blur
    if (ballX < ballR) {
        ballDX = -ballDX; // refect delta x
        if (frameTravel) { // if using frame travel time
            // blur the outward traveling ball only for the time it has been traveling away
            blurSteps = Math.ceil(10 * ((ballX - ballR) / -ballDX));
            // get position it should have traveled since
            ballX -= (ballX - ballR) * 2;
        }else{
            ballX = ballR; // move ball to touching wall
            blurSteps = 1; // there is no outward motion
        }
    } else
    if (ballX > w - ballR) {
        ballDX = -ballDX;
        if (frameTravel) { // if using frame travel time
            // blur the outward traveling ball only for the time it has been traveling away
            blurSteps = Math.ceil(10 * ((ballX - (w - ballR)) / -ballDX));
            ballX -= (ballX - (w - ballR)) * 2;
        }else{
            ballX = w - ballR; // move ball to touching wall
            blurSteps = 1; // there is no outward motion
        }
    }
    if (ballY > h - ballR) {
        ballDY = -ballDY;
        // to show max height
        lastMaxHeight = maxHeight;
        maxHeight = Infinity;
        if (frameTravel) { // if using frame travel time
            // blur the outward traveling ball only for the time it has been traveling away
            blurSteps = Math.ceil(10 * ((ballY - (h - ballR)) / -ballDY));
            ballY -= (ballY - (h - ballR)) * 2;
        }else{
            ballY = h - ballR; // move ball to touching wall
            blurSteps = 1; // there is no outward motion
        }
    }        
    // draw the ball motion blured
    drawMotionBlur(
        ball,                    // image to draw
        ballX - ballR,             // offset radius
        ballY - ballR,
        ballDX * (blurSteps / bSteps),  // speed and adjust for bounced
        ballDY * (blurSteps / bSteps),
        blurSteps                // number of blurs
    );
    // show max height. Yes it is min but everything is upside down.
    maxHeight = Math.min(maxHeight,ballY);
    lastMaxHeight = Math.min(ballY,lastMaxHeight);
    // show max height
    ctx.font = "12px arial black";
    ctx.beginPath();
    ctx.moveTo(0,lastMaxHeight - ballR);
    ctx.lineTo(w,lastMaxHeight - ballR);
    ctx.stroke();
    ctx.fillText("Max height.",40,lastMaxHeight - ballR + 6);
    var str = ""; // display status string
    if(slowMotion){   // show left click help
        str += "10fps."
        ctx.fillText("click for 60fps.",textCenterX,43);
    }else{
        str += "60fps."
        ctx.fillText("click for 10fps.",textCenterX,43);
    }
    if(frameTravel){ // show mode and right click help
        str += " Mid frame collision.";
        ctx.fillText("Right click for Simple collision",textCenterX,55);
    }else{
        str += " Simple collision.";
        ctx.fillText("Right click for mid frame collision",textCenterX,55);
    }
    // display help text
    ctx.font = "18px arial black";  
    ctx.strokeText(str,textCenterX,30);
    ctx.fillText(str,textCenterX,28);
    if(slowMotion){
        setTimeout(update,100); // show in slow motion
    }else{
        requestAnimationFrame(update); // request next frame (1/60) seconds from now
    }
    // all done
}
update(); // to start the ball rolling
.canC { width:500px;  height:500px;}
<canvas class="canC" id="canV" width=500 height=500></canvas>

对于650 X 650场底部(Y=0)沿X轴运行的墙,我们希望:

if (player.y <= 20) {
    player.y = 20;
}

对于在650 X 650场的左侧(X=0)沿Y轴运行的墙,我们希望:

if (player.x <= 20) {
    player.x = 20;
}

对于在650 X 650场的右侧(X=650)沿Y轴运行的墙,我们希望:

if (player.x >= 630) {
    player.x = 630;
}

对于在650 X 650场的顶部(Y=650)沿X轴运行的墙,我们希望:

if (player.y >= 630) {
    player.y = 630;
}

这个代码与我使用的代码类似,如果我们将水平(h)和垂直(v)速度属性附加到玩家对象上,我们可以将它们乘以负1,以便在玩家要超越边界时使它们从墙上反弹。或者,如果你想让它停止,在墙上将它们设置为零。

//player.x+player.h gives us the future position of the player
if (player.x+player.h>630||player.x+player.h<0)
{
    player.h*=-1;//bounce
    //stop player.h=0;
}
if (player.y+player.v>500||player.y+player.v<0)
{
   player.v*=-1;
   //stop player.v=0;
}
//new player coordinates
player.x+=player.h;
player.y+=player.v;

希望这能有所帮助。