GameDev – Va chạm giữa đường tròn và đoạn thẳng

Ball bounce lineTìm điểm va chạm của một đường tròn với đoạn thẳng và xác định hướng phản xạ của di chuyển thông qua các phép toán trên vector.
Xem Demo.

Va chạm

Để xác định va chạm, ta tính toán khoảng cách từ tâm đường tròn đến đoạn thẳng theo phương pháp Tính khoảng cách từ điểm đến đoạn thẳng và so sánh với bán kính của đường tròn.

Ví dụ trong phần kiềm tra khoảng cách này chưa xét trường hợp va chạm với hai đầu của đoạn thẳng. Bởi vì việc kiểm tra phản xạ khi va chạm với đầu đoạn thẳng sẽ cần tính toán thêm một vài bước nên tôi sẽ bỏ qua.

Phương thức sau sẽ trả về điểm va chạm của trái bóng với đoạn thẳng hoặc null nếu như không có.

Ball.prototype.findCollisionPoint = function(line) {

	// create a vector from the point line.p1 to the center of this ball.
	var v1= {
		x: this.cx - line.p1.x,
		y: this.cy - line.p1.y
	};

	var v2 = line.getVector();

	var v3 = findProjection(v1,v2);

	v3.length = Math.sqrt(v3.x*v3.x+v3.y*v3.y);

	var collisionPoint = null;

	if(v3.dp>0 && v3.length <= v2.length)
	{
		var dx = v1.x-v3.x;
		var dy = v1.y-v3.y;
		var d = Math.sqrt(dx*dx+dy*dy);	// distance

		if(d>this.radius)
			return null;

		collisionPoint = {
			x: line.p1.x + v3.x,
			y: line.p1.y + v3.y
		};
		// don't overlap
		if(d < this.radius)
		{
			this.cx -=	(this.movement.x/this.speed)*(this.radius-d);
			this.cy -=	(this.movement.y/this.speed)*(this.radius-d);
		}
	}
	return collisionPoint;
}

Phản xạ

Dựa vào vector chuyển động của trái bóng và vector được tạo ra từ đoạn thẳng, ta có thể tính được vector chuyển động mới của bóng (xem Vector2D: Va chạm và phản xạ).

Ball.prototype.bounceAgainst = function(line) {

	if(this.findCollisionPoint(line))
	{
		var v1 = this.movement;

		var v2 = line.getVector();

		var v3 = findProjection(v1,v2);

		// perpendicular vector of v2
		var v4 = {
			x: v2.y,
			y: -v2.x
		};

		v4 = findProjection(v1,v4);
		v4.x = -v4.x;
		v4.y = -v4.y;

		// bounce vector
		var v5 = {
			x: v3.x + v4.x,
			y: v3.y + v4.y
		};

		v5.length = Math.sqrt(v5.x*v5.x+v5.y*v5.y);

		// normalize vector
		v5 = {
			x: v5.x/v5.length,
			y: v5.y/v5.length
		};
		this.setMovement(v5);
	}
}

Đối với việc kiểm tra phản xạ khi trái bóng va chạm với 1 điểm (như hai đầu của đoạn thẳng), bạn cần xác định một vector vuông góc với đường thẳng nối tâm trái bóng đến điểm va chạm và coi đó là mặt phẳng va chạm.

Minh họa

Ball bounce line

Sourcecode:

<html>
<head>
  <script>
  	/***************** ball ***************/
	function Ball(mapWidth, mapHeight) {
		this.mapWidth = mapWidth;
		this.mapHeight = mapHeight;

		this.radius = 10;
		this.speed = 5;
		this.movement = {
				x: this.speed,
				y: -this.speed
			};
		this.cx = Math.floor(mapWidth/2);
		this.cy = Math.floor(mapHeight/2);

		this.color = "green";
	}
	Ball.prototype.update = function() {

		this.cx += this.movement.x;
		this.cy += this.movement.y;

		if(this.cx-this.radius < 0 || this.cx+this.radius>this.mapWidth)
			this.movement.x = -this.movement.x;

		if(this.cy-this.radius < 0 || this.cy+this.radius>this.mapHeight)
			this.movement.y = -this.movement.y;

	}
	Ball.prototype.draw = function(context) {
		context.beginPath();
		context.fillStyle = this.color;
		context.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2, true);
		context.closePath();
		context.fill();
	}
	Ball.prototype.setMovement = function(vector){
		this.movement = {
			x: vector.x * this.speed,
			y: vector.y * this.speed
		};
	}
	Ball.prototype.findCollisionPoint = function(line) {

		// create a vector from the point line.p1 to the center of this ball.
		var v1= {
			x: this.cx - line.p1.x,
			y: this.cy - line.p1.y
		};

		var v2 = line.getVector();

		var v3 = findProjection(v1,v2);

		v3.length = Math.sqrt(v3.x*v3.x+v3.y*v3.y);

		var collisionPoint = null;

		if(v3.dp>0 && v3.length <= v2.length)
		{
			var dx = v1.x-v3.x;
			var dy = v1.y-v3.y;
			var d = Math.sqrt(dx*dx+dy*dy);	// distance

			if(d>this.radius)
				return null;

			collisionPoint = {
				x: line.p1.x + v3.x,
				y: line.p1.y + v3.y
			};
			// don't overlap
			if(d < this.radius)
			{
				this.cx -=	(this.movement.x/this.speed)*(this.radius-d);
				this.cy -=	(this.movement.y/this.speed)*(this.radius-d);
			}
		}
		return collisionPoint;
	}
Ball.prototype.bounceAgainst = function(line) {

	if(this.findCollisionPoint(line))
	{
		var v1 = this.movement;

		var v2 = line.getVector();

		var v3 = findProjection(v1,v2);

		// perpendicular vector of v2
		var v4 = {
			x: v2.y,
			y: -v2.x
		};

		v4 = findProjection(v1,v4);
		v4.x = -v4.x;
		v4.y = -v4.y;

		// bounce vector
		var v5 = {
			x: v3.x + v4.x,
			y: v3.y + v4.y
		};

		v5.length = Math.sqrt(v5.x*v5.x+v5.y*v5.y);

		// normalize vector
		v5 = {
			x: v5.x/v5.length,
			y: v5.y/v5.length
		};
		this.setMovement(v5);
	}
}

 /***************** Circle ***************/
function Circle(x,y) {
    this.x = x;
    this.y = y;
	this.radius = 5;
}

Circle.prototype.draw = function(context) {
    context.fillStyle = "red";
	context.beginPath();
    context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
    context.closePath();
    context.fill();
}
Circle.prototype.setPosition = function(x, y) {
    this.x = x + this.radius;
    this.y = y + this.radius;
}
Circle.prototype.contains = function(x, y) {
    var dx = this.x - x;
    var dy = this.y - y;

    return Math.sqrt(dx * dx + dy * dy) <= this.radius;
}

/***************** Line ***************/
function Line(x1,y1,x2,y2) {

	this.p1 = {x:x1,y:y1};
	this.p2 = {x:x2,y:y2};
	this.getVector = function() {
		var x = this.p2.x-this.p1.x;
		var y = this.p2.y-this.p1.y;
		return {
			x: x,
			y: y,
			root: this.p1,
			length: Math.sqrt(x*x+y*y)
		};
	}
}
/***************** Polygon ***************/
function Polygon(points) {
	this.vertices = new Array();
	for(i in points)
		this.vertices[i] = new Circle(points[i].x,points[i].y,5);
}

Polygon.prototype.captureMouse = function(x,y) {
	for(i in this.vertices)
		if(this.vertices[i].contains(x,y))
			return this.vertices[i];
}
Polygon.prototype.getLines = function() {
	var lines = new Array();
	var length = this.vertices.length;
	for(var i=0;i<length-1;i++)
		lines[i] = new Line(this.vertices[i].x,this.vertices[i].y,this.vertices[i+1].x,this.vertices[i+1].y);

	lines[length-1] = new Line(this.vertices[length-1].x,this.vertices[length-1].y,
								this.vertices[0].x,this.vertices[0].y);
	return lines;
}
Polygon.prototype.draw = function(context) {

	var i=0;
	for(;i<this.vertices.length-1;i++)
	{
		context.beginPath();
		context.moveTo(this.vertices[i].x,this.vertices[i].y);
		context.lineTo(this.vertices[i+1].x,this.vertices[i+1].y);
		context.closePath();
		context.stroke();
		if(i>0)
			this.vertices[i].draw(context);
	}

	context.beginPath();
	context.moveTo(this.vertices[i].x,this.vertices[i].y);
	context.lineTo(this.vertices[0].x,this.vertices[0].y);
	context.closePath();
	context.stroke();
	this.vertices[0].draw(context);
	this.vertices[i].draw(context);
}
 /************** Main *************/
var EPSILON = 0.1;
var _canvas;
var _context;
var _movingShape = false;
var _polygon;
var _ball;

function canvas_mousedown(e) {

    var x = e.pageX - _canvas.offsetLeft;
    var y = e.pageY - _canvas.offsetTop;

    _movingShape = _polygon.captureMouse(x, y);
}

function canvas_mousemove(e) {
	if (_movingShape) {
		var x = e.pageX - _canvas.offsetLeft;
		var y = e.pageY - _canvas.offsetTop;
        _movingShape.setPosition(x, y);
    }
}

function canvas_mouseup(e) {
    _movingShape = null;
}

function clear() {
    _context.clearRect(0, 0, _canvas.width, _canvas.height);
}
function findProjection(v1,v2){

	if(!v2.length)
		v2.length = Math.sqrt(v2.x*v2.x+v2.y*v2.y);
	// dot product
	var dp = v1.x*v2.x + v1.y*v2.y;

	// length of the projection of v1 on v2
	var length = dp/v2.length;

	// normalize vector v2
	v2.dx = v2.x/v2.length;
	v2.dy = v2.y/v2.length;

	return {
		x: length*v2.dx,
		y: length*v2.dy,
		dp: dp
	};
}
function update(){
	var lines = _polygon.getLines();

	for(var i=0;i<lines.length;i++)
	{
		_ball.bounceAgainst(lines[i]);
	}
	_ball.update();

	// draw
	clear();
	_ball.draw(_context);
    _polygon.draw(_context);

	_reqAnimation(update);
}

window.onload = function(){
	_canvas = document.getElementById("canvas");
	_context = _canvas.getContext("2d");

	var p1 = {x:50,y:100};
	var p2 = {x:350,y:50};
	var p3 = {x:380,y:300};
	var p4 = {x:30,y:350};
	_polygon = new Polygon([p1,p2,p3,p4]);
	_ball = new Ball(_canvas.width,_canvas.height);
	_canvas.onmousedown = canvas_mousedown;
	_canvas.onmousemove = canvas_mousemove;
	_canvas.onmouseup = canvas_mouseup;

	_reqAnimation = window.requestAnimationFrame 	||
			window.mozRequestAnimationFrame	||
			window.webkitRequestAnimationFrame	||
			window.msRequestAnimationFrame		||
			window.oRequestAnimationFrame		;

	if(_reqAnimation)
		update();
	else
		alert("Your browser doesn't support requestAnimationFrame.");

}
  </script>
</head>
<body>
<canvas id="canvas" width="400px" height="400px" style="border: 1px solid"></canvas>
</body>
</html>

YinYang’s Blog

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s