Как создать препятствия в canvas

Я пытаюсь сделать простой платформер игра.Код, который я использую, показан ниже

window.onload = function(){
	var canvas = document.getElementById('game');
	var ctx = canvas.getContext("2d");

	var rightKeyPress = false;
	var leftKeyPress = false;
	var upKeyPress = false;
	var downKeyPress = false;
	var playerX = canvas.width / 2;
	var playerY = -50;
	var dx = 3;
	var dy = 3;
	var dxp = 3;
	var dyp = 3;
	var dxn = 3;
	var dyn = 3;
	var prevDxp = dxp;
	var prevDyp = dyp;
	var prevDxn = dxn;
	var prevDyn = dyn;
	var playerWidth = 50;
	var playerHeight = 50;
	var obstacleWidth = 150;
	var obstacleHeight = 50;
	var obstaclePadding = 10;
	var G = .98;
	var currentVelocity = 0;
	var obstacles = [];
	var imageLoaded = false;

	document.addEventListener("keyup",keyUp,false);
	document.addEventListener("keydown",keyDown,false);

	function keyDown(e){
		if(e.keyCode == 37){
			leftKeyPress = true;
			if(currentVelocity > 2){
				currentVelocity -= .1;
			}
		}
		if(e.keyCode == 38){
			upKeyPress = true;
		}
		if(e.keyCode == 39){
			rightKeyPress = true;
			if(currentVelocity < 2){
				currentVelocity += .1;
			}
		}
		if(e.keyCode == 40){
			downKeyPress = true;
		}
	}
	function keyUp(e){
		if(e.keyCode == 37){
			leftKeyPress = false;
		}
		if(e.keyCode == 38){
			upKeyPress = false;
		}
		if(e.keyCode == 39){
			rightKeyPress = false;
		}
		if(e.keyCode == 40){
			downKeyPress = false;
		}
	}
	function createObstacles(){
		for(x=0;x < 4;x++){
			var obX = (200 * x) + Math.round(Math.random() * 150);
			var obY = 50 + Math.round(Math.random() * 400);
			obstacles.push({"x":obX,"y":obY});
		}
	}
	createObstacles();
	function drawObstacles(){
		ctx.beginPath();
		for(x=0;x < 4;x++){
			var obX = obstacles[x].x;
			var obY = obstacles[x].y;
			ctx.rect(obX,obY,obstacleWidth,obstacleHeight)
		}	
		ctx.fillStyle = "grey";
		ctx.fill();
		ctx.closePath();
	}
	function initPlayer(){
		ctx.beginPath();
		ctx.rect(playerX,playerY,50,50);
		ctx.fillStyle="orange";
		ctx.fill();
		ctx.closePath();
	}
	function KeyPressAndGravity(){
		checkObstacleCollision();
		playerX += currentVelocity;
		if(rightKeyPress && playerX + 50 < canvas.width){
			playerX += dxp;
		}
		if(leftKeyPress && playerX > 0){
			playerX -= dxn;
		}
		if(upKeyPress && playerY > 0){
			playerY -= dyn;
		}
		if(downKeyPress && playerY + 50 < canvas.height){
			playerY += dyp;
		}
		if(playerY+50 < canvas.height){
			playerY += G;
		}
		if(playerX <= 0){
			currentVelocity = 0;
		}else if(playerX + 50 >= canvas.width){
			currentVelocity = 0;
		}
		dxp = prevDxp;
		dyp = prevDyp;
		dxn = prevDxn;
		dyn = prevDyn;
		G = .98;
		if(currentVelocity != 0){
			if(currentVelocity > 0){
				currentVelocity -= .01;
			}else{
				currentVelocity += .01;
			}
		}
	}
  /*-----------------------------------------------------------
  -------------------------------------------------------------
  -------------------------------------------------------------
  ---------------------------Check this part-------------------
  -------------------------------------------------------------
  -------------------------------------------------------------
  -------------------------------------------------------------
  ------------------------------------------------------------*/
	function checkObstacleCollision(){
		var obLen = obstacles.length;
		for(var x=0;x<obLen;x++){
			var obX = obstacles[x].x;
			var obY = obstacles[x].y;
			if((playerX + playerWidth > obX && playerX + playerWidth < obX + obstacleWidth || playerX > obX && playerX < obX + obstacleWidth) && playerY + playerHeight > obY - obstaclePadding && playerY + playerHeight < obY){
				dyp = 0;
				G = 0;
			}else if((playerX + playerWidth > obX && playerX + playerWidth < obX + obstacleWidth || playerX > obX && playerX < obX + obstacleWidth) && playerY > obY + obstacleHeight && playerY < obY + obstacleHeight + obstaclePadding){
				dyn = 0;
			}else if(playerX + playerWidth > obX - obstaclePadding && playerX + playerWidth < obX && ((playerY + playerHeight > obY && playerY + playerHeight < obY + obstacleHeight) || (playerY > obY && playerY < obY + obstacleHeight))){
				dxp = 0;
			}else if(playerX  > obX + obstacleWidth && playerX < obX + obstacleWidth + obstaclePadding && ((playerY + playerHeight > obY && playerY + playerHeight < obY + obstacleHeight) || (playerY > obY && playerY < obY +  obstacleHeight))){
				dxn = 0;
			}

		}
	}
	function draw(){
		ctx.clearRect(0,0,canvas.width,canvas.height);
		initPlayer();
		KeyPressAndGravity();
		drawObstacles();
	}

	setInterval(draw,15);
}
<canvas id="game" width="1000" height="600" style="border:1px solid #000;"></canvas>

проблема в том, что иногда, когда скорость "игрока" высока, он может проходить через препятствия, такие как изображение ниже. Как я могу это предотвратить ?

enter image description here

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

3 ответов


существует осложнение при столкновении тестовых объектов, которые быстро движутся

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

enter image description here ... enter image description here

затем вы можете проверить, если игрок когда-либо пересекал препятствие во время движения, проверив, если трек игрока пересекает препятствие.

enter image description here

относительно эффективный метод тестирования столкновений с участием быстро движущихся объектов

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

enter image description here

  1. для любой из 3 линий, пересекающих препятствие, вычислите расстояние отрезка линии до препятствия. Выберите линию, которая имеет кратчайшее расстояние между начальной вершиной и препятствием.

enter image description here enter image description here

  1. вычислить расстояния" x "и" y " выбранной строки сегмент.

    var dx = obstacleIntersection.x - start.x;
    var dy = obstacleIntersection.y - start.y;
    
  2. переместите игрока из начальной позиции на расстояние, рассчитанное в #3. Это приводит к тому, что игрок перемещается на место, где он впервые столкнулся с препятствием.

    player.x += dx;
    player.y += dy;
    

enter image description here

код и демо:

полезные функции в коде:

  • setPlayerVertices определяет 3 отрезка линии, которые соединяют 3 вершины начального прямоугольника игрока, ближайшие к конечному прямоугольнику игрока.

  • hasCollided находит кратчайший отрезок, соединяющий вершину из начальной позиции игрока с точкой столкновения на препятствии.

  • line2lineIntersection находит точку пересечения (если есть) между 2 линиями. Это используется для проверки пересечения между начальным сегментом (от #1) и любым из 4 линейных сегментов, составляющих прямоугольник препятствия. атрибуция: эта функция адаптирована из полезного поля Бурка treatice на перекрестках.

enter image description here

вот пример кода и демо, показывающий, как остановить игрока в точке столкновения на препятствии:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
    var BB=canvas.getBoundingClientRect();
    offsetX=BB.left;
    offsetY=BB.top;        
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }

var isDown=false;
var startX,startY,dragging;

ctx.translate(0.50,0.50);
ctx.textAlign='center';
ctx.textBaseline='middle';

var pts;
var p1={x:50,y:50,w:25,h:25,fill:''};
var p2={x:250,y:250,w:25,h:25,fill:''};
var ob={x:100,y:150,w:125,h:25,fill:''};
var obVertices=[
    {x:ob.x,y:ob.y},
    {x:ob.x+ob.w,y:ob.y},
    {x:ob.x+ob.w,y:ob.y+ob.h},
    {x:ob.x,y:ob.y+ob.h}
];
var s1,s2,s3,e1,e2,e3,o1,o2,o3,o4;

draw();

$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});


function draw(){
    ctx.clearRect(0,0,cw,ch);
    //
    ctx.lineWidth=4;
    ctx.globalAlpha=0.250;
    ctx.strokeStyle='blue';
    ctx.strokeRect(ob.x,ob.y,ob.w,ob.h);
    ctx.globalAlpha=1.00;
    ctx.fillStyle='black';
    ctx.fillText('obstacle',ob.x+ob.w/2,ob.y+ob.h/2);
    //
    ctx.globalAlpha=0.250;
    ctx.strokeStyle='gold';
    ctx.strokeRect(p1.x,p1.y,p1.w,p1.h);
    ctx.strokeStyle='purple';
    ctx.strokeRect(p2.x,p2.y,p2.w,p2.h);
    ctx.fillStyle='black';
    ctx.globalAlpha=1.00;
    ctx.fillText('start',p1.x+p1.w/2,p1.y+p1.h/2);
    ctx.fillText('end',p2.x+p2.w/2,p2.y+p2.h/2);
}


function handleMouseDown(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  
  startX=parseInt(e.clientX-offsetX);
  startY=parseInt(e.clientY-offsetY);

  // Put your mousedown stuff here
  var mx=startX;
  var my=startY;
  if(mx>p1.x && mx<p1.x+p1.w && my>p1.y && my<p1.y+p1.h){
      isDown=true;
      dragging=p1;
  }else if(mx>p2.x && mx<p2.x+p2.w && my>p2.y && my<p2.y+p2.h){
      isDown=true;
      dragging=p2;
  }
}

function handleMouseUpOut(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  // Put your mouseup stuff here
  isDown=false;
  dragging=null;
}

function handleMouseMove(e){
  if(!isDown){return;}
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();

  mouseX=parseInt(e.clientX-offsetX);
  mouseY=parseInt(e.clientY-offsetY);

  // Put your mousemove stuff here
  var dx=mouseX-startX;
  var dy=mouseY-startY;
  startX=mouseX;
  startY=mouseY;
  //
  dragging.x+=dx;
  dragging.y+=dy;
  //
  draw();
  //
  setPlayerVertices(p1,p2);
  var c=hasCollided(obVertices);
  if(c.dx){
      ctx.strokeStyle='gold';
      ctx.strokeRect(p1.x+c.dx,p1.y+c.dy,p1.w,p1.h);
      ctx.fillStyle='black';
      ctx.fillText('hit',p1.x+c.dx+p1.w/2,p1.y+c.dy+p1.h/2);
      line(c.s,c.i,'red');
  }
}

function setPlayerVertices(p1,p2){
    var tl1={x:p1.x,      y:p1.y};
    var tl2={x:p2.x,      y:p2.y};
    var tr1={x:p1.x+p1.w, y:p1.y};
    var tr2={x:p2.x+p2.w, y:p2.y};
    var br1={x:p1.x+p1.w, y:p1.y+p1.h};
    var br2={x:p2.x+p2.w, y:p2.y+p2.h};
    var bl1={x:p1.x,      y:p1.y+p1.h};
    var bl2={x:p2.x,      y:p2.y+p2.h};
    //
    if(p1.x<=p2.x && p1.y<=p2.y){
        s1=tr1; s2=br1; s3=bl1;
        e1=tr2; e2=br2; e3=bl2;
        o1=0; o2=1; o3=3; o4=0;
    }else if(p1.x<=p2.x && p1.y>=p2.y){
        s1=tl1; s2=tr1; s3=br1;
        e1=tl2; e2=tr2; e3=br2;
        o1=2; o2=3; o3=3; o4=0;
    }else if(p1.x>=p2.x && p1.y<=p2.y){
        s1=tl1; s2=br1; s3=bl1;
        e1=tl2; e2=br2; e3=bl2;
        o1=0; o2=1; o3=1; o4=2;
    }else if(p1.x>=p2.x && p1.y>=p2.y){
        s1=tl1; s2=tr1; s3=bl1;
        e1=tl2; e2=tr2; e3=bl2;
        o1=1; o2=2; o3=2; o4=3;
    }
}

function hasCollided(o){
    //
    var i1=line2lineIntersection(s1,e1,o[o1],o[o2]);
    var i2=line2lineIntersection(s2,e2,o[o1],o[o2]);
    var i3=line2lineIntersection(s3,e3,o[o1],o[o2]);
    var i4=line2lineIntersection(s1,e1,o[o3],o[o4]);
    var i5=line2lineIntersection(s2,e2,o[o3],o[o4]);
    var i6=line2lineIntersection(s3,e3,o[o3],o[o4]);
    //
    var tracks=[];
    if(i1){tracks.push(track(s1,e1,i1));}
    if(i2){tracks.push(track(s2,e2,i2));}
    if(i3){tracks.push(track(s3,e3,i3));}
    if(i4){tracks.push(track(s1,e1,i4));}
    if(i5){tracks.push(track(s2,e2,i5));}
    if(i6){tracks.push(track(s3,e3,i6));}
    //
    var nohitDist=10000000;
    var minDistSq=nohitDist;
    var halt={dx:null,dy:null,};
    for(var i=0;i<tracks.length;i++){
        var t=tracks[i];
        var testdist=t.dx*t.dx+t.dy*t.dy;
        if(testdist<minDistSq){
            minDistSq=testdist;
            halt.dx=t.dx;
            halt.dy=t.dy;
            halt.s=t.s;
            halt.i=t.i;
        }
    }
    return(halt);
}
//
function track(s,e,i){
    dot(s);dot(i);line(s,i);line(i,e);
    return({ dx:i.x-s.x, dy:i.y-s.y, s:s, i:i });
}


function line2lineIntersection(p0,p1,p2,p3) {
    var unknownA = (p3.x-p2.x) * (p0.y-p2.y) - (p3.y-p2.y) * (p0.x-p2.x);
    var unknownB = (p1.x-p0.x) * (p0.y-p2.y) - (p1.y-p0.y) * (p0.x-p2.x);
    var denominator  = (p3.y-p2.y) * (p1.x-p0.x) - (p3.x-p2.x) * (p1.y-p0.y);        
    // Test if Coincident
    // If the denominator and numerator for the ua and ub are 0
    //    then the two lines are coincident.    
    if(unknownA==0 && unknownB==0 && denominator==0){return(null);}
    // Test if Parallel 
    // If the denominator for the equations for ua and ub is 0
    //     then the two lines are parallel. 
    if (denominator == 0) return null;
    // If the intersection of line segments is required 
    // then it is only necessary to test if ua and ub lie between 0 and 1.
    // Whichever one lies within that range then the corresponding
    // line segment contains the intersection point. 
    // If both lie within the range of 0 to 1 then 
    // the intersection point is within both line segments. 
    unknownA /= denominator;
    unknownB /= denominator;
    var isIntersecting=(unknownA>=0 && unknownA<=1 && unknownB>=0 && unknownB<=1)
    if(!isIntersecting){return(null);}
    return({
        x: p0.x + unknownA * (p1.x-p0.x),
        y: p0.y + unknownA * (p1.y-p0.y)
    });
}

function dot(pt){
    ctx.beginPath();
    ctx.arc(pt.x,pt.y,3,0,Math.PI*2);
    ctx.closePath();
    ctx.fill();
}

function line(p0,p1,stroke,lw){
    ctx.beginPath();
    ctx.moveTo(p0.x,p0.y);
    ctx.lineTo(p1.x,p1.y);
    ctx.lineWidth=lw || 1;
    ctx.strokeStyle=stroke || 'gray';
    ctx.stroke();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Drag start & end player position rects<br>The shortest segment intersecting the obstacle is red.<br>The repositioned player is shown on the obstacle.</h4>
<canvas id="canvas" width=400 height=400></canvas>

то, что вы испытываете, обычно называется туннелированием.

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

A

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

 ------------
|| LP |      |
||____|      |
|       ____ |
|      | NP ||
|______|____||

теперь, если вы используете этот новый флажок для проверки столкновений, он будет учитывать пройденный путь, чтобы избежать туннелирования. Это может создать неожиданные столкновения в верхнем правом и нижнем левом углу, но это простая реализация и компромисс может быть стоит.

B

Проверьте столкновения для каждого шага по пути, пройденному от последней позиции до новой позиции. Если ваш элемент прошел 5 пикселей начиная с последнего кадра, вы проверяете столкновение один раз для каждого пикселя (или минимальное допустимое расстояние столкновения).

 ____
| LP |      
|____||      
  ---- |___ 
   |___|NP |
      |____|

Это, конечно, увеличит количество обнаружений столкновений и повлияет на производительность. Здесь вы можете посмотреть на quadtrees, чтобы компенсировать потерю производительности.

двигаясь вперед, есть гораздо более элегантные и передовые решения, но тема является широкой для полного ответа здесь.

надеюсь, что это помогает!


Ну, я сделал "калькулятор столкновений" около нескольких месяцев назад, поэтому вы можете изменить и использовать код ниже, как вам нравится :) для лучшего объяснения:

  • p_x-последняя позиция игрока x плюс его ширина
  • p_y-последняя позиция игрока y плюс его высота
  • p_x_m-последняя позиция игрока x
  • p_y_m-последняя позиция игрока y
  • y_m-новая позиция игрока (его y - somevalue)
  • x_m-новая позиция игрока (его x - некоторое_значение)
  • y_p-новая позиция игрока (его y + somevalue + его высота)
  • y_p_m-новая позиция игрока (его y + somevalue)
  • x_p-новая позиция игрока (его x + somevalue + его ширина)
  • x_p_m-новая позиция игрока (его x + somevalue)
  • w_x-положение стены x
  • w_y-положение стены y
  • w_w-это ширина стены
  • w_h-высота стены

  • pressedKeys является строка, указывающая, какие клавиши игрок нажал (например, " был "или" wd "или" ad " и т. д.)

  • this.walls является переменной со стенами (например, если у меня есть 4 стены, массив будет выглядеть как [false,'s',false,false] потому что я коснулся второй стены с помощью клавиши" s").

код:

if(
    pressedKeys.indexOf("s")>-1 &&
    (
        (                                                                                   //      P
            p_y>w_y&&p_y<(w_y+w_h)&&x_p_m>w_x && x_p-5>w_x && x_m<w_x                       //      +----
        ) ||                                                                                //      |

        (                                                                                   //      P
            y_p>w_y&&p_y<(w_y+w_h) && x_p-5>w_x && x_p<=(w_x+w_w)                           //  +--------+
        ) ||                                                                                //  |        |

        (                                                                                   //      P
            y_p>w_y&&p_y<(w_y+w_h) && x_p>(w_x+w_w)&&p_x_m<(w_x+w_w) && x_m+5<(w_x+w_w)     //  ----+
        )                                                                                   //      |
    )
)
{
    if(this.walls[i] == false)
        this.walls[i] = "";
    this.walls[i] += "s";
}
if(
    pressedKeys.indexOf("d")>-1 &&
    (
        (                                                                                   //      P+----
            p_x>w_x&&p_x<(w_x+w_w)&&y_p_m>w_y && y_p-5>w_y && y_m<w_y                       //       |
        ) ||                                                                                //       |

        (                                                                                   //       |
            x_p>w_x&&p_x<(w_x+w_w) && y_p-5>w_y && y_p<=(w_y+w_h)                           //      P|
        ) ||                                                                                //       |

        (                                                                                   //       |
            x_p>w_x&&p_x<(w_x+w_w) && y_p>(w_y+w_h)&&p_y_m<(w_y+w_h) && y_m+5<(w_y+w_h)     //       |
        )                                                                                   //      P+----
    )
)
{
    if(this.walls[i] == false)
        this.walls[i] = "";
    this.walls[i] += "d";
}
if(
    pressedKeys.indexOf("w")>-1 &&
    (
        (                                                                                   //      |
            y_m<(w_y+w_h)&&y_p-5>w_y && x_p-5>w_x && x_m<w_x &&x_p_m>w_x                    //      +----
        ) ||                                                                                //      P

        (                                                                                   //  |        |
            y_m<(w_y+w_h)&&y_p-5>w_y && x_p-5>w_x && x_p<=(w_x+w_w)                         //  +--------+
        ) ||                                                                                //      P    

        (                                                                                   //      |
            y_m<(w_y+w_h)&&y_p-5>w_y && x_p>(w_x+w_w)&&p_x_m<(w_x+w_w) && x_m+5<(w_x+w_w)   //  ----+
        )                                                                                   //      P
    )
)
{
    if(this.walls[i] == false)
        this.walls[i] = "";
    this.walls[i] += "w";
}
if(
    pressedKeys.indexOf("a")>-1 &&
    (
        (                                                                                   //  ----+P
            x_m<(w_x+w_w)&&x_p-5>w_x && y_p-5>w_y && y_m<w_y &&y_p_m>w_y                    //      |
        ) ||                                                                                //      |

        (                                                                                   //      |
            x_m<(w_x+w_w)&&x_p-5>w_x && y_p-5>w_y && y_p<=(w_y+w_h)                         //      |P
        ) ||                                                                                //      |    

        (                                                                                   //      |
            x_m<(w_x+w_w)&&x_p-5>w_x && y_p>(w_y+w_h)&&p_y_m<(w_y+w_h) && y_m+5<(w_y+w_h)   //      |P
        )                                                                                   //  ----+P
    )
)
{
    if(this.walls[i] == false)
        this.walls[i] = "";
    this.walls[i] += "a";
}

комментарии в правой части кода показывают, как игрок сталкивается.

этот код 100% работает, я использую его каждый раз, когда я хочу проверить столкновение.

Надежда это немного помогло:)