pygame中的线段碰撞

6

我目前正在开发一款2D平台游戏,但我发现我的碰撞编程存在一个大问题。你看,为了找出与玩家的碰撞,我只是移动玩家矩形,然后在碰撞时通过查看他身上的x和y加速度将玩家推开。

问题在于,使用这种方法时,玩家的速度可能会使玩家跳过他应该与之碰撞的目标,因为他应该与之碰撞的矩形太小了。例如,如果平台大小为9px,而玩家的速度为11px,那么他有可能会跳过目标。这通常发生在玩家射击子弹时,因为子弹很小并且速度很快(由于游戏的性质,我不希望它们变成瞬间到达)。

所以我想了想,并想出了一个解决方案:从子弹之前所处的位置到现在所处的位置绘制一条线,然后检查目标矩形是否与其碰撞。我搜索了如何做到这一点的方法,但我没有找到任何好的解释来将其实现到Pygame中。

我应该使用像素蒙版吗?如果是,怎么做?Pygame中已经有使用这种方法的函数吗?我真的需要一些帮助。


@Jean-FrançoisFabre 嗯,我没有考虑到这一点...所以你基本上是告诉我要进行几次更新循环,然后才更新屏幕?我认为这会让事情过于复杂...或者你是在告诉我要提高帧率?但那会弄乱事情...不过现在我会等待别人对我的方法提供帮助,因为它听起来最简单和最清晰,但还是谢谢! - Neri Nigberg
@Jean-FrançoisFabre,你所说的“计算比更新更多”是什么意思?我对此感到困惑,能否请您具体解释一下? - Neri Nigberg
@Jean-FrançoisFabre 哦,我想我明白你的意思了...所以基本上要进行更多的碰撞计算,并在适当的时间只渲染,这就是概念对吧?我得调整一下才能完全弄清楚。没太听懂C代码...我学了一点#C,但现在还不太行。无论如何,感谢您的帮助,非常感激。 - Neri Nigberg
2个回答

3

简单的线性AABB碰撞检测

下面是一个拦截许多静止盒子的移动盒子的解决方案。这些盒子必须有与x和y轴平行的边。

它通过找到两个帧之间的第一个拦截来解决高速运动的问题,无论障碍物有多薄或物体移动得有多快,都会找到正确的拦截点。(请注意,盒子必须具有正宽度和高度)

线框拦截

它的工作原理是将移动盒子的路径表示为一条直线。不是将宽度和高度添加到该线上,而是将移动盒子的宽度和高度添加到障碍物盒子上,这大大减少了解决问题所需的工作量。(演示图形显示了一些中间抽象,包括扩展障碍物盒子)

在游戏中使用时,演示中的线就是游戏当前对象位置到下一帧位置沿着delta x和y的线。

拦截设置了从当前位置到拦截点(如果有)的x、y距离。法线(指向被击中侧的向量)也提供了帮助碰撞响应。您还可以得到拦截点的距离平方。您可以将该距离除以线长度的平方,以给出发生拦截的单位时间。例如,值为0.5表示它在两个帧之间的中间发生。0表示它发生在开始时,1表示它发生在结束时。如果没有拦截,则法线长度将为零。

演示

演示是JavaScript编写的,但数学和逻辑才是最重要的。感兴趣的函数位于代码段顶部,并进行了良好的注释(我希望如此)。下面是一些样板和支持。

要使用演示,请左键拖动以创建框。然后左键拖动以标记一条线。起始位置是浅绿色的框,另一个绿色的框是拦截点(如果有的话)。还有一些黄色标记,表示计算出的拦截点太远了。请查看完整页面以查看更多框。

限制和适应性

您可能会注意到,如果起始位置与框接触,则拦截点在起始位置之前(时间倒退)。这是正确的行为,您不应该在起点处重叠(在墙内)。

如果您有移动障碍物并且它们沿x或y轴移动,则可以通过向运动方向扩展盒子来适应解决方案(不完美,但适用于缓慢移动的障碍物(请注意在下一帧中不要重叠)。)

您还可以测试移动圆。这可以通过检查拦截点是否在圆角半径距离内来完成。如果是,则以盒子真实角落为圆心进行线圆拦截,半径与移动圆相同。

我知道这很难懂,所以如果您有任何问题,请务必提出。

// Moving box 2 box intercepts

var objBox = createBox(0, 0, 0, 0);   // the moving box
var objLine = createLine(0, 0, 0, 0); // the line representing the box movement
var boxes = [];                       // array of boxes to check against


//Find closest intercept to start of line
function findIntercepts(B, L) {
    lineAddSlopes(L);   // get slopes and extras for line (one off calculation)
                        // for each obstacles check for intercept;
    for (var i = 0; i < boxes.length; i++) {
        intercept(B, L, boxes[i]);
    }
    // Line will hold the intercept pos as minX, minY, the normals of the side hit in nx,ny
    // and the dist from the line start squared
}


function lineAddSlopes(l) {           // adds the slopes of the lie for x,y and length as dist
    var dx = l.x2 - l.x1;             // vector from start to end of line
    var dy = l.y2 - l.y1;
    var dist = dx * dx + dy * dy;
    l.dx = dx / dy;                   // slope of line in terms of y to find x
    l.dy = dy / dx;                   // slope of line in terms of x to find y
    l.dist = dist;
    l.minX = dx;                      // the 2D intercept point.
    l.minY = dy;
    l.nx = 0;                         // the face normal of the intercept point
    l.ny = 0;
}


function intercept(moveBox, moveLine, obstructionBox) { // find the closest intercept, if any
    var check, iPosX, iPosY, distSqrX, distSqrY;
    const b1 = moveBox, b2 = obstructionBox, l = moveLine;

    distSqrX = distSqrY = l.dist;
    const lr = l.x1 < l.x2; // lr for (l)eft to (r)ight is true is line moves from left to right.
    const tb = l.y1 < l.y2; // tb for (t)op to (b)ottom is true is line moves from top to bottom

    const w2 = b1.w / 2, h2 = b1.h / 2;
    const right  = b2.x + b2.w + w2;
    const left   = b2.x - w2;
    const top    = b2.y - h2;
    const bottom = b2.y + b2.h + h2;

    check = lr ?                      // quick check if collision is possible
        l.x1 < right && l.x2 > left:
        l.x2 < right && l.x1 > left;     
    check && (check = tb ?
            l.y1 < bottom && l.y2 > top:
            l.y2 < bottom && l.y1 > top);

    if (check) {      
        const lrSide = lr ? left : right;   // get closest left or right side
        const tbSide = tb ? top : bottom;   // get closest top or bottom side

        const distX = lrSide - l.x1;        // x Axis distance to closest side
        const distY = tbSide - l.y1;        // y Axis distance to closest side

        iPosX = l.x1 + distY * l.dx;        // X intercept of top or bottom
        iPosY = l.y1 + distX * l.dy;        // Y intercept of left or right

        if (iPosX >= left && iPosX <= right) { // is there a x Axis intercept?
            iPosX -= l.x1;
            distSqrX = Math.min(distSqrX, distY * distY + iPosX * iPosX); // distance squared   
        }
        if (iPosY >= top && iPosY <= bottom) { // is there a y Axis intercept?       
            iPosY -= l.y1;    
            distSqrY = Math.min(distSqrY, distX * distX + iPosY * iPosY);    
        }         
        
        if (distSqrX < l.dist || distSqrY < l.dist) {
            if (distSqrX < distSqrY) {
                l.dist = distSqrX;
                l.minX = iPosX;      
                l.minY = distY;
                l.nx = 0;                                                
                l.ny = tb ? -1 : 1;                
            } else {
                l.dist = distSqrY;
                l.minX = distX;                               
                l.minY = iPosY;
                l.nx = lr ? -1 : 1;                                   
                l.ny = 0;                
            }
            l.x2 = l.x1 + l.minX;  // Set new line end. This keeps the line
            l.y2 = l.y1 + l.minY;  // length as short as possible and avoid
                                   // unnneeded intercept tests
        }   
    }
}



//======================================================================================================================
// SUPPORT CODE FROM HERE DOWN
//======================================================================================================================
// The following code is support code that provides me with a standard interface to various forums.
// It provides a mouse interface, a full screen canvas, and some global often used variable
// like canvas, ctx, mouse, w, h (width and height), globalTime
// This code is not intended to be part of the answer unless specified and has been formated to reduce
// display size. It should not be used as an example of how to write a canvas interface.
// By Blindman67
const RESIZE_DEBOUNCE_TIME = 100;
var w, h, cw, ch, canvas, ctx, onResize, mouse, createCanvas, resizeCanvas, setGlobals, globalTime = 0, 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 = window.innerWidth;
    canvas.height = window.innerHeight;
    ctx = canvas.getContext("2d");
    if (typeof setGlobals === "function") {
        setGlobals();
    }
    if (typeof onResize === "function") {
        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.updateBounds();
}
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.x = e.clientX - m.bounds.left;
        m.y = e.clientY - m.bounds.top;
        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  && (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;
        }
        e.preventDefault();
    }
    m.updateBounds = function () {
        if (m.active) {
            m.bounds = m.element.getBoundingClientRect();
        }
    }
    m.addCallback = function (callback) {
        if (typeof callback === "function") {
            if (m.callbacks === undefined) {
                m.callbacks = [callback];
            } else {
                m.callbacks.push(callback);
            }
        } else {
            throw new TypeError("mouse.addCallback argument must be a function");
        }
    }
    m.start = function (element, blockContextMenu) {
        if (m.element !== undefined) {
            m.removeMouse();
        }
        m.element = element === undefined ? document : element;
        m.blockContextMenu = blockContextMenu === undefined ? false : blockContextMenu;
        m.mouseEvents.forEach(n => {
            document.addEventListener(n, mouseMove);
        });
        if (m.blockContextMenu === true) {
            m.element.addEventListener("contextmenu", preventDefault, false);
        }
        m.active = true;
        m.updateBounds();
    }
    m.remove = function () {
        if (m.element !== undefined) {
            m.mouseEvents.forEach(n => {
                m.element.removeEventListener(n, mouseMove);
            });
            if (m.contextMenuBlocked === true) {
                m.element.removeEventListener("contextmenu", preventDefault);
            }
            m.element = m.callbacks = m.contextMenuBlocked = undefined;
            m.active = false;
        }
    }
    return mouse;
})();


resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);

w = canvas.width;
h = canvas.height;
cw = w / 2;  // center 
ch = h / 2;
globalTime = new Date().valueOf();  // global to this 


var numRandomBoxes = 10; // number of obstacles
var movePoint = 0;         // which end of the line to move
var boxes = [];            // array of boxes.
onresize = function(){
    boxes = [];
    numRandomBoxes = Math.floor(((w * h) / (30*130)) * 0.25);   // approx box density of 1/8th canvas pixels
    boxes.push(createBox(0,h-100,w,10));  // create a ground box
    var i = 0;   // create some random boxes
    while(i++ < numRandomBoxes){
        boxes.push(createBox(rand(-10,w + 10),rand(-10,h + 10),rand(10,30),rand(10,130)));
    }
}
onresize(); // set up 

var objBoxE = createBox(0,0,0,0);  // a mirror of moving used for display
var boxSizing = false;


function createBox(x, y, w, h) {
    return { x : x, y : y, w : w, h : h};
}
function createLine(x1, y1, x2, y2) {
    return { x1 : x1, y1 : y1, x2 : x2, y2 : y2};
}
function copyBox(b1, b2) { // copy coords from b1 to b2
    b2.x = b1.x;
    b2.y = b1.y;
    b2.w = b1.w;
    b2.h = b1.h;
}
function rand(min, max) { // returns a random int between min and max inclusive 
    return Math.floor(Math.random() * (max - min) + min);
}
// draw a box
function drawBox(b, ox = 0, oy = 0, xx = 0, yy = 0, fill) { // ox,oy optional expand box.
    if (!fill) {
        ctx.strokeRect(b.x - ox + xx, b.y - oy + yy, b.w + ox * 2, b.h + oy * 2);
    } else {
        ctx.fillRect(b.x - ox + xx, b.y - oy + yy, b.w + ox * 2, b.h + oy * 2);
    }
}
// draw a line
function drawLine(l, ox, oy) { // ox and oy optional offsets
    ox = ox ? ox : 0;
    oy = oy ? oy : 0;
    ctx.moveTo(l.x1 + ox, l.y1 + oy)
    ctx.lineTo(l.x2 + ox, l.y2 + oy);
}
// draw a a cross (mark)
function drawMark(x, y, size) {
    ctx.fillRect(x - size / 2, y - 0.5, size, 1);
    ctx.fillRect(x - 0.5, y - size / 2, 1, size);
}




// main update function
function update(timer){
    requestAnimationFrame(update);

    var L,B;  // short cuts to line and box to make code readable
    L = objLine;
    B = objBox;
    globalTime = timer;
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.clearRect(0,0,w,h);
    if(mouse.buttonRaw & 4){  // right button to clear the box and line
        B.x = B.y = 0;
        B.w = B.h = 0;
        L.x1 = L.x2 = 0; 
        L.y1 = L.y2 = 0; 
        copyBox(B,objBoxE);
    }
    if(mouse.buttonRaw & 1){ // if left button drag new box or move line ends
        if(B.w === 0){  // if the box has no size 
            boxSizing = true;  // create a box and flag that we are sizing the box
            B.x = mouse.x;
            B.y = mouse.y;
            B.w = 1;
            B.h = 1;
        }else{
            if(boxSizing){   // drag out the box size
                B.x = Math.min(mouse.x,B.x);
                B.y = Math.min(mouse.y,B.y);
                B.w = Math.max(1,mouse.x-B.x);
                B.h = Math.max(1,mouse.y-B.y);
            }else{
                if(L.x1 === L.x2 && L.y1 === L.y2 ){  // else if line does not exist start a new one
                    movePoint = 1;
                    L.x1 = B.x + B.w / 2;
                    L.y1 = B.y + B.h / 2;
                    L.x2 = mouse.x + 1;
                    L.y2 = mouse.y + 1;
                }else{
                    // if line does exist find closest end
                    if(mouse.oldBRaw !== mouse.buttonRaw){  // mouse button just down
                        movePoint = 1;
                    }

                    L.x2 = mouse.x;
                    L.y2 = mouse.y;
                }
                B.x = L.x1 - B.w / 2;
                B.y = L.y1 - B.h / 2;
                objBoxE.x = L.x2 - B.w / 2;
                objBoxE.y = L.y2 - B.h / 2;
                objBoxE.w = B.w;
                objBoxE.h = B.h;
            }
        }
    }else{
        boxSizing = false;
    }
    // draw obstical boxes
    ctx.strokeStyle = "black";
    for(var i = 0; i < boxes.length; i ++){
        drawBox(boxes[i]);
    }
    // draw start and end boxes
    ctx.strokeStyle = "red"
    drawBox(B);
    drawBox(objBoxE);
    // draw the line
    ctx.beginPath();
    drawLine(L);
    ctx.stroke();
    // draw the box outer edges
    ctx.globalAlpha = 0.25;
    ctx.beginPath();
    drawLine(L,-B.w/2,-B.h/2);
    drawLine(L,B.w/2,-B.h/2);
    drawLine(L,B.w/2,B.h/2);
    drawLine(L,-B.w/2,B.h/2);
    ctx.stroke();

    // if the line has length then check for intercepts
    if(!(L.x1 === L.x2 && L.y1 === L.y2 )){
        ctx.strokeStyle = "Blue"
        findIntercepts(B,L);
        ctx.fillStyle = "#0F0";
        ctx.strokeStyle = "black"
        ctx.globalAlpha = 0.2;
        drawBox(B,0,0,0,0,true);
        drawBox(B);
        ctx.globalAlpha = 1;
        drawBox(B,0,0,L.minX,L.minY,true);
        drawBox(B,0,0,L.minX,L.minY);
        ctx.beginPath();
        ctx.moveTo(L.x1 + L.minX, L.y1 + L.minY);
        ctx.lineTo(L.x1 + L.minX+ L.nx * 30, L.y1 + L.minY+ L.ny * 30);
        ctx.stroke();
    }

    if(mouse.buttonRaw === 0){
        ctx.globalAlpha = 1;
        ctx.font = "16px arial";
        ctx.textAlign = "center";
        ctx.fillStyle = "rgba(240,230,220,0.8)";
        ctx.strokeStyle = "black"
        ctx.fillRect(20,h - 42, w- 40,40);
        ctx.strokeRect(20,h - 42, w- 40,40);
        ctx.fillStyle = "black"    
        if(B.w === 0){ 
             ctx.fillText("Left click drag to size a box",w / 2, h - 20);
             ctx.canvas.style.cursor = "crosshair";
        }else if(!(L.x1 === L.x2 && L.y1 === L.y2 )){
             ctx.fillText("Left click drag to move box destination",w / 2, h - 26);
             ctx.fillText("Right click to clear.",w / 2, h - 6);
              ctx.canvas.style.cursor = "move";
        }else{
             ctx.fillText("Left click drag to move box destination",w / 2, h - 26);
             ctx.fillText("Right click to clear.",w / 2, h - 6);
             ctx.canvas.style.cursor = "move";

       }
    } else {  ctx.canvas.style.cursor = "none"; }





    mouse.oldBRaw = mouse.buttonRaw;
}
requestAnimationFrame(update);


天啊...我...非常困惑,至少可以这么说。我觉得你的回答对我来说过于复杂了。我非常感谢你和@Jean-François Fabre提供的答案,但是我自己深入搜索了解决方案,找到了这个链接,基本上就是我需要的。说实话,我不知道为什么我自己没有想到。不过我要说,你确实回答了我的问题,甚至给了我一个测试代码(尽管我不懂JavaScript),所以我会接受它。 - Neri Nigberg
1
@NeriNigberg 抱歉,有时候脑海中的简单并不一定能在纸上表达清楚。这个解决方案需要一些图表(我希望演示可以帮助解释核心概念),以便更好地阐明。我会再多花点功夫来澄清它。 - Blindman67

2
许多游戏系统都有两种“回调”方法:
update(int elapsed_time)

用于更新游戏数据,以及

render(int elapsed_time)

用于在屏幕上呈现数据

臭名昭著的“隧道”效应是当物体的速度过快时,计算距离的方式会出现问题,就像...

delta_x = x_speed * elapsed_time;
delta_y = y_speed * elapsed_time;

因此,x和y的变化可能过高,并且在薄障碍物/目标上人为地“交叉”。

您可以通过实验推断出一个阈值经过的时间值,超过这个值将发生这种效应(即使是最好的编码者在“吃豆人”的后期阶段也会发生)。

C语言中更新包装器的示例,该示例确保不会用太长的经过时间调用update

void update_wrapper(int elapsed_time)
{
   int i;
   while(elapsed_time>0)
   {
       int current_elapsed = elapsed_time<max_elapsed_time_without_tunnel_effect ? elapsed_time : max_elapsed_time_without_tunnel_effect;
       update(current_elapsed);
       elapsed_time -= max_elapsed_time_without_tunnel_effect;
   }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接