Squeak.ru - шаблоны программирования

Столкновение строк в pygame

Итак, в настоящее время я работаю над 2D-платформером и осознал большую проблему с моим программированием столкновений. Видите ли, чтобы обнаружить столкновения с игроком, я просто перемещал игрока прямо вокруг, а затем при столкновении отталкивал игрока, глядя на его ускорение по осям x и y.

Проблема в том, что при использовании этого метода скорость игрока может заставить игрока пропускать цели, с которыми он должен столкнуться, потому что прямоугольник, с которым он должен столкнуться, слишком мал. Например, если размер платформы 9 пикселей, а скорость игрока составляет 11 пикселей, есть вероятность, что он пропустит цель. Обычно это происходит с пулями, которые стреляет игрок, которые маленькие и летят быстро (и я не хочу, чтобы они были мгновенными из-за характера игры).

Итак, я подумал об этом и придумал решение: провести линию от положения пули до того, в котором она сейчас находится, а затем проверить, не сталкивается ли целевой прямоугольник с ним. Я искал способы сделать что-то подобное, но не нашел хорошего объяснения того, как реализовать это в Pygame.

Я использую маскировку пикселей? Если да, то как? Есть ли в Pygame какая-то функция для использования этого метода? Мне действительно нужна помощь.


  • @ Jean-FrançoisFabre Хм, не думал об этом ... То есть вы в основном говорите мне сделать несколько циклов обновлений и только потом обновлять экран? Я думаю, это сильно усложнит мне задачу ... Или вы говорите мне о более высокой частоте кадров? Но это все испортит ... А пока я буду ждать помощи по моему методу, так как он кажется самым простым и чистым, но все равно спасибо! 05.11.2016
  • @ Jean-FrançoisFabre ну, а что именно вы имеете в виду, говоря больше вычислять, чем обновлять? Я запутался в этом вопросе, не могли бы вы объяснить более конкретно? 05.11.2016
  • @ Jean-FrançoisFabre о, я думаю, я понял то, к чему вы клоните ... Так что в основном выполняйте больше вычислений столкновений и визуализируйте только в подходящее время, это концепция правильная? Мне придется повозиться с этим, чтобы понять это полностью. Не совсем понял код C ... Я выучил совсем немного #C, но я не совсем там в данный момент. В любом случае, спасибо за помощь, очень признателен. 05.11.2016

Ответы:


1

Простое линейное обнаружение столкновений AABB

Ниже представлено решение для перехвата движущегося ящика со множеством стационарных ящиков. Ящики должны иметь стороны, параллельные осям x и y.

Он решает проблему высокоскоростного движения путем нахождения первого пересечения между двумя кадрами, независимо от того, насколько тонким является препятствие или насколько быстро движется объект, найден правильный пересечение. (Обратите внимание, что поля должны иметь положительную ширину и высоту)

Перехват строки

Он работает, представляя путь движущегося прямоугольника в виде одной линии. Вместо того, чтобы добавлять ширину и высоту к линии, ширина и высота подвижного блока добавляются к блокам препятствий, это значительно сокращает объем работы, необходимой для решения проблемы. (демонстрация графически показывает некоторые промежуточные абстракции, включая расширение блоков препятствий)

Для использования в игре линия в демонстрации - это просто текущая позиция игрового объекта по отношению к позиции в следующем кадре по дельте 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);

05.11.2016
  • Боже ... Я ... Очень смущен, если не сказать больше. Я думаю, вы действительно слишком усложнили для меня ответ. Я очень ценю ваш ответ вместе с ответом @ Jean-François Fabre, но я сам искал решение более глубоко и нашел это ссылка, которая в основном именно то, что мне нужно. Не знаю, как именно я сам об этом не подумал, если честно. Я собираюсь сказать, что вы ответили на мой вопрос и даже дали мне тестовый код (хотя я не понимаю javascript), поэтому я приму его. 05.11.2016
  • @NeriNigberg Извините, иногда то, что просто в голове, не так на бумаге. Для этого решения требуется несколько диаграмм (с которыми я надеялся, что демонстрация поможет), чтобы объяснить основную концепцию. Я приложу немного больше усилий, чтобы прояснить это. 05.11.2016

  • 2

    многие игровые системы имеют 2 метода обратного вызова:

    update(int elapsed_time)
    

    используется для обновления данных игры, и

    render(int elapsed_time)
    

    используется для отображения данных на экране

    Печально известный эффект «туннеля» возникает, когда скорость объекта слишком велика, поэтому расстояние вычисляется как

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

    Таким образом, вариации x и y могут быть слишком высокими и искусственно "пересекать" тонкие препятствия / цели.

    Вы можете экспериментально вывести пороговое значение прошедшего времени, за которым этот эффект произойдет (это происходит в Pac-Man на более поздних стадиях, так случается даже с лучшими кодировщиками :)

    Пример оболочки обновления на 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;
       }
    }
    
    04.11.2016
    Новые материалы

    Угловая структура архитектуры
    Обратите внимание, что эта статья устарела, я решил создать новую с лучшей структурой и с учетом автономных компонентов: https://medium.com/@marekpanti/angular-standalone-architecture-b645edd0d54a..

    «Данные, которые большинство людей используют для обучения своих моделей искусственного интеллекта, поставляются со встроенным…
    Первоначально опубликовано HalkTalks: https://hacktown.com.br/blog/blog/os-dados-que-a-maioria-das-pessoas-usa-para-treinar-seus-modelos-de-inteligencia-artificial- ja-vem-com-um-vies-embutido/..

    Сильный ИИ против слабого ИИ: различия парадигм искусственного интеллекта
    В последние годы изучению и развитию искусственного интеллекта (ИИ) уделяется большое внимание и прогресс. Сильный ИИ и Слабый ИИ — две основные парадигмы в области искусственного интеллекта...

    Правильный способ добавить Firebase в ваш проект React с помощью React Hooks
    React + Firebase - это мощная комбинация для быстрого и безопасного создания приложений, от проверки концепции до массового производства. Раньше (знаете, несколько месяцев назад) добавление..

    Создайте API с помощью Python FastAPI
    Создание API с помощью Python становится очень простым при использовании пакета FastAPI. После установки и импорта вы можете создать приложение FastAPI и указать несколько конечных точек. Каждой..

    Веселье с прокси-сервером JavaScript
    Прокси-серверы JavaScript — это чистый сахар, если вы хотите создать некоторую общую логику в своих приложениях, чтобы облегчить себе жизнь. Вот один пример: Связь клиент-сервер Мы..

    Получить бесплатный хостинг для разработчиков | Разместите свой сайт за несколько шагов 🔥
    Статические веб-сайты — это веб-страницы с фиксированным содержанием и его постоянным содержанием. Но теперь статические сайты также обрабатывают динамические данные с помощью API и запросов...