Thứ Sáu, 29 tháng 11, 2013

Kĩ thuật lập trình Game – Cơ bản - part 2

VI.  Giao điểm của hai đường thẳng
Từ hai đoạn thẳng (hoặc vector) trong mặt phẳng 2D, ta có  thể tìm được giao điểm của chúng để 
tính toán các góc phản xạ và hướng di chuyển.
1.  Tạo phương trình đường thẳ ng từ đoạn thẳng
Ta có hai điểm A(x1,y1) và B(x2,y2) tạo thành một đoạn thẳng, để tạo được một phương trình 
đường thẳng từ  hai điểm  này, ta cần  tính được độ nghiêng của đường thẳng (slope) theo công 
thức:  
a = (y2 –  y1)/(x2 –  x1)
Thay thế x2, y2 bằng hai biến x,y:
a = (y –  y1)/(x –  x1)
=> y –  y1 = a(x –  x1)
Hay:
y = ax + (y1 – ax1)
Đặt b = y1 – ax1, ta có:
y = ax + b
2.  Tính giao điể m của hai đường thẳng
Ta có hai phương trình đường thẳng
(1): y = a1x + b1
(2): y = a2x + b2
Ta có thể tính được giao điểm của chúng bằng cách tìm   giao điểm x trước:
a1x + b1 = a2x + b2;
=> x(a1 – a2) = b2 – b1
=> x = (b2 – b1)/(a1 – a2)
Khi đã có x, ta sẽ thế vào (1) hoặc (2 ) để tính được y.  Để kiểm tra hai đoạn thẳng có cắt nhau 
không, ta chỉ cần kiểm tra điểm {x,y} thuộc cả hai đoạn thẳng hay không.   Ngoài ra ta cần loại 
trừ trường  hợp  hai đường thẳng song song  hoặc trùng nhau, khi đó  giá trị slope của chúng sẽ 
bằng nhau. 



VII.  Va chạm và phản xạ
Để tính được hướng phản xạ của khi một vật thể va chạm vào mặt phẳng, ta có thể dựa vào góc 
nghiêng của mặt phẳng và vector theo từng vùng giá  trị. Tuy nhiên cách tổng quát hơn là sử dụng 
các phép toán trên vector để thực hiện.
1.  Kiểm tra hai đoạn thẳng cắt nhau
Từ bài viết trước về tìm  giao điể m củ a hai đường th ẳ ng , ta có thể kiểm tra xem giao điểm tìm 
được có phải  là  giao điểm của  hai đoạn thẳng  hay không. Cách kiểm  tra đơn  giản  là bạn  xem 
điểm này có thuộc cả hai phần bao hình chữ nhật của hai đoạn thẳng.  Ta   có phương thức kiểm 
tra như sau:
// detect if a point is inside the rectangle bound of this line
Line.prototype.insideRectBound = function(x, y){
if(  Math.min(this.p1.x,this.p2.x) - x > EPSILON ||
x - Math.max(this.p1.x,this.p2.x) > EPSILON ||
Math.min(this.p1.y,this.p2.y) - y > EPSILON ||
y - Math.max(this.p1.y,this.p2.y) > EPSILON)
return false;
return true;
}
// ...
var intersected = _line1.insideRectBound(x,y) && _line2.insideRectBound(x,y);
2.  Phương pháp
Trong hình minh họa sau, vector v là hướng di chuyển của vật thể va chạm vào u. Vector phản 
xạ của v là b. Từ vector b ta phân tích ra hai vector thành phần là a và c (như vậy ta có a + c = b). 
Trong đó: a: vector chiếu của v trên u. c: vector chiếu của v trên đường thẳng vuông góc với u. 
Đây là vector pháp tuyến của u và có độ dài bằng khoảng cách từ gốc của v đến u. Như vậy để 
tìm được vector phản xạ b, ta chỉ cần theo 4 bước:
1. Tìm giao điểm của hai đoạn thẳng (tương ứng với hai vector v và u).
2. Tìm vector chiếu a của v trên u.
3. Tìm vector pháp tuyến của u có độ dài bằng khoảng cách từ gốc của v đến u.
4. Tính tổng vector a và c.
VIII.  Va chạm giữa đường tròn và đoạn thẳng
Tì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.
1.  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 ta  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;
}
2.  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:
IX.   Va chạm giữa nhiều đường tròn
Để xác đ ịnh hướng di chuyển của hai quả cầu có khối lượng khác nhau sau khi va chạm, ta sử 
dụng công thức của định luật bảo toàn động lượng trong một hệ cô lập.
Xét trường hợp va chạm trong không  gian  một chiều (1D  –  các vật di chuyển trên  một đường 
thẳng), ta gọi   m là khối lượng và vận tốc của quả cầu C, u là vận tốc trước khi va chạm và v là 
vận tốc sau khi va chạm. Tại thời điểm hai quả cầu C1 và C2 va chạm nhau, ta có công thức:
m1u1 + m2u2 = m1v1 + m2v2
Suy ra:
v1 = (u1*(m1- m2) + 2*m2*u2)/(m1+m2)
v2 = (u2*(m2- m1) + 2*m1*u1)/(m1+m2)
Ta có thể áp dụng công  thức  này trong không  gian  hai chiều  (2D) để tính  vận  tốc  theo  một 
phương xác định.
Bằng cách chia  vector  vận  tốc  thành  hai  vector thành phần,  một  vector  ux1  nằm trên đường 
thẳng chứa hai tâm C1 và C2 (phương ng ang), vector uy1 còn lại vuông góc với vx1 (phương 
dọc).ta  chuyển  không  gian  2  chiều  thành  1  chiều  theo  phương  chứa  đoạn  thẳng  nối  hai  tâm 
đường tròn.
var angle1 = Math.atan2(u1.y, u1.x);
var ux1 = u1.length * Math.cos(angle1- angle);
var uy1 = u1.length * Math.sin(angle1- angle);
Áp dụng công thức bên trên, ta tính được vector vận tốc mới theo phương ngang là vx1. Do đây 
là không gian 1D nên vận tốc uy1 sẽ không ảnh hưởng.
var vx1 = ((ball1.mass- ball2.mass)*ux1+(ball2.mass+ball2.mass)*ux2)/(ball1.mass+ball2.mass);
var vy1 = uy1;
Đã có hai vector thành phần là vx1 và vy1, ta cần chuyển lại các vector mới này sang không gian 
2D và hợp thành vector vector chuyển động mới là v1. Do vector vx1 vuông góc với vy1 nên ta 
cần cộng với angle một góc 90 độ hay PI/2. Ở   đây, angle là góc tạo bởi đoạn thẳng nối hai tâm 
đường tròn.
var v1 = {};
v1.x = Math.cos(angle)*vx1+Math.cos(angle+Math.PI/2)*vy1;
v1.y = Math.sin(angle)*vx1+Math.sin(angle+Math.PI/2)*vy1;
1.  Xử lý va chạm của nhiều đường tròn
Cách đơn giản nhất và  có  độ phức tạp tương đối nhỏ  (số  l ần l ặp là  n(n-1)/2) trong việc kiểm tra 
và xử  l ý va chạm cho nhiều đối tượng l à sử dụng hai vòng l ặp l ồng nhau theo dạng sau:
for(var i=0;i<_balls.length;i++)
{
for(var j=i+1;j<_balls.length;j++)
checkCollision(_balls[i],_balls[j]);
}
Hàm checkCollision:
function checkCollision(ball1,ball2){
var dx = ball1.cx - ball2.cx;
var dy = ball1.cy - ball2.cy;
// check collision between two balls
var distance = Math.sqrt(dx*dx+dy*dy);
if(distance > ball1.radius + ball2.radius)
return;
var angle = Math.atan2(dy,dx);
var u1 = ball1.getVelocity();
var u2 = ball2.getVelocity();
var angle1 = Math.atan2(u1.y, u1.x);
var angle2 = Math.atan2(u2.y, u2.x);
var ux1 = u1.length * Math.cos(angle1-angle);
var uy1 = u1.length * Math.sin(angle1-angle);
var ux2 = u2.length * Math.cos(angle2-angle);
var uy2 = u2.length * Math.sin(angle2-angle);
var  vx1  =  ((ball1.massball2.mass)*ux1+(ball2.mass+ball2.mass)*ux2)/(ball1.mass+ball2.mass);
var  vx2  =  ((ball1.mass+ball1.mass)*ux1+(ball2.ma ssball1.mass)*ux2)/(ball1.mass+ball2.mass);
var vy1 = uy1;
var vy2 = uy2;
// now we transform the 1D coordinate back to 2D
var v1 = {}, v2 = {};
v1.x = Math.cos(angle)*vx1+Math.cos(angle+Math.PI/2)*vy1;
v1.y = Math.sin(angle)*vx1+Math.sin(angle+Math.PI/2)*vy1;
v2.x = Math.cos(angle)*vx2+Math.cos(angle+Math.PI/2)*vy2;
v2.y = Math.sin(angle)*vx2+Math.sin(angle+Math.PI/2)*vy2;
ball1.velocity = v1;
ball2.velocity = v2;
}
X.  Kiểm tra va chạm dựa trên  pixel
Các đối tượng đồ họa (hoặc hình ảnh) trong ga me thường được một giới hạn trong một khung 
bao hình chữ nhật có nền trong suốt (pixel có alpha = 0). Như vậy đối với các đối tượng phức tạp 
và muốn kiểm tra va chạm chính xác, ta cần kiểm tra các pixel có độ alpha > 0 của hai đối tượng 
đồ họa có cùng nằm trên một vị trí hay không.  
1.  Một wrapper của Image
Để tiện xử lý các hình ảnh trong canvas dưới dạng một đối tượng trong game với các chức năng 
cần  thiết,  ta  tạo  một  lớp ImageObj chứa bên  trong  một đối tượng Image để  lưu  trữ  hình ảnh. 
Phương thức quan trọng  mà ImageObj phải có là draw(), tuy nhiên việc nạp ảnh có thể diễn ra 
khá  lâu  vì  vậy  ta  cần  một  cách  thức  xử  lý  trường  hợp  này.
Chẳng  hạn  ta  sẽ  viết ra  một dòng  thông báo thay  thế cho ảnh  với kích thước  mặc định trong 
trường hợp ảnh chưa được tải xong:
function ImageObj(){
// ...
this.draw = function(context){
if(ready){
context.drawImage(this.img,this.left,this.top);
}
else{
// image has not finished loading
// draw something useful instead
context.save();
context.fillText("Image  is  not 
ready",this.left+10,this.top+10);
context.restore();
}
context.strokeRect(this.left,this.top,this.width,this.height);
context.stroke();
};
// ...
}
Chưa đủ, để đối tượng ImageObj có thể tự vẽ lại sau ảnh được nạp xong, đồng thời lấy được đối 
tượng ImageData (chứa mảng các pixel), ta sẽ viết một số lệnh xử lý trong sự kiện onload của 
Image.
function ImageObj(){
// ...
var self = this;
this.img.onload = function(){
self.width = this.width;
self.height = this.height;
ready = true; // this image is ready to use
// draw image after loading
context.clearRect(self.left,self.top,self.width,self.height);
self.draw(context);
// get ImageData from this image
self.data  = 
context.getImageData(self.left,self.top,self.width,self.height).data;
};
this.img.src = url;
}
Do vấn đề bảo mật, ta sẽ không lấy được ImageData của các ảnh không nằm trong host hiện tại 
(khác host mà script được thực thi).
2.  Xác định vùng giao hai hình chữ  nhật
Thay vì lặp qua toàn bộ hai hình ảnh và kiểm tra từng pixel  của chúng. Ta sẽ giới hạn lại phần 
ảnh cần kiểm  tra bằng cách  xác định  vùng giao  giữa hai  hình ảnh. Phần này cũng  giúp ta  xác 
định nhanh hai đối tượng có thể xảy ra va chạm hay không.
function findIntersectionRect(img1,img2){
var rect = {
left: Math.max(img1.left,img2.left),
top: Math.max(img1.top,img2.top),
right: Math.min(img1.left+img1.width,img2.left+img1.width),
bottom: Math.min(img1.top+img1.height,img2.top+img2.height)
};
if(rect.left>rect.right || rect.top>rect.bottom)
return null;
return rect;
}
3.  Kiểm tra va chạm
Nếu đã tìm hiểu về đối tượng ImageData, bạn biết rằng các pixel sẽ được lưu trữ trong một mảng 
một chiều. Mỗi pixel bao  gồm bốn phần tử đứng  liền  nhau trong  mảng theo thứ tự  là  [Red, 
Green, Blue, Alpha]. Như vậy một ảnh có kích thước 20×30 sẽ có 600 pixel và có 600×4=2400 
phần tử trong ImageData.
function checkPixelCollision(img1,img2){
if(!img1.data || !img2.data)
return null;
var rect =   findIntersectionRect(img1,img2);
if(rect)
{
// this array will hold all collision points of two images
var points = [];
for(var i=rect.left;i<=rect.right;i++){
for(var j=rect.top;j<=rect.bottom;j++){
var index1 = ((i-img1.left)+(jimg1.top)*img1.width)*4+3;
var index2 = ((i-img2.left)+(jimg2.top)*img2.width)*4+3;
if(img1.data[index1+3]!=0 && 
img2.data[index2+3]!=0)
{
points.push({x:i,y:j});
//  you  can  exit  here  instead  of 
continue looping
}
}
}
return {
rect: rect,
points: points
};
}
return null;
}
Để tăng tốc độ kiểm tra va chạm, thay vì lặp qua từng pixel, bạn có thể lặp ngắt quãng n pixel 
(theo cả 2 chiều ngang và dọc) tùy theo mức độ yêu cầu chính xác của ứng dụng. Như vậy số lần 
lặp để  kiểm tra sẽ giảm đi 2n lần tương ứng. Dưới đây là ảnh minh họa việc thay đổi bước tăng 
của vòng lặp từ 1 lên 3. Vùng màu đỏ là các pixel có độ alpha > 0 thuộc cả hai ảnh.

Không có nhận xét nào:

Đăng nhận xét