Html5 – Canvas: Viết game bắn đại bác – part1

Html5-Canvas-Cannon-Game-Demo-part1Dựa vào ví dụ Game xe tăng đơn giản, tôi sẽ phát triển nó thành một game bắn súng tương tự như Gunbound (hoặc Gunny). Trong phần này, ta sẽ cùng tìm hiểu cách tạo và phá hủy địa hình của bản đồ, thêm vào đó là cách xác định lực bắn cho đại bác (cannon) từ tọa độ chuột.


Bản đồ và địa hình

Đối với dạng game này, địa hình của bản đồ có thể ảnh hưởng rất lớn đến người chơi. Ví dụ người chơi có thể rơi xuống một hố sâu và không thể bắn hay thậm chí “thiệt mạng”. Tuy nhiên ở phần 1 này ta chưa cần quan tâm đến những vấn đề này. Phần chính mà tôi hướng dẫn là làm sao để người chơi có thể tương tác và chịu tác động của địa hình như di chuyển, bắn phá.

Về vấn đề kiểm tra va chạm với địa hình, không có phương pháp nào khác ngoài việc kiểm tra dựa trên pixel (do địa hình có hình dạng phức tạp và bất kì). Vì vậy tôi tạo một ImageData từ ảnh bản đồ:

var imageData = null;
img.onload = function(){
	context.drawImage(img,0,0,width,height);
	imageData = context.getImageData(0,0,width,height);
	ready = true;
};
img.src = "map.png";

Sau đó thêm một phương thức kiểm tra một điểm có nằm trong vùng ImageData có độ alpha bằng 0 hay không:

this.contain = function(x,y){
	if(!imageData)
		return false;
	var index = Math.floor((x+y*width)*4+3);
	return imageData.data[index]!=0;
}

Tạo địa hình ngẫu nhiên

Thay vì lệ thuộc vào các hình ảnh, ta có thể làm cho việc thiết kế các màn chơi của game đơn giản hơn bằng cách tạo ra các địa hình ngẫu nhiên. Phương pháp thực hiện đơn giản của tôi là sử dụng các phương thức vẽ line:

// create map
context.beginPath();
context.moveTo(0,Math.floor(Math.random()*buffer.height));
var x=0;
var step = Math.floor(buffer.width/10);
for(var i=1;i<10;i++)
{
	x += Math.floor(Math.random()*step)+step;
	context.lineTo(x,Math.floor(Math.random()*buffer.height/2)+100);
}
// line to bottom-right corner
context.lineTo(buffer.width,buffer.height);
// line to bottom-left corner
context.lineTo(0,buffer.height);

context.fillStyle = "green";
context.fill();
imageData = context.getImageData(0,0,buffer.width,buffer.height);

Phá hủy một phần địa hình

Sử dụng phương thức contain bên trên, ta sẽ kiểm tra được va chạm khi đạn bắn hoặc người chơi rơi xuống đất. Với trường hợp đạn bắn, ta phải phá hủy vùng địa hình nơi đạn bay vào. Rất may là API của Canvas cung cấp một phương pháp dùng để vẽ ra các vùng có độ alpha bằng 0, tương tự với việc xóa bỏ hoàn toàn vùng đó.

Ta thực hiện việc này bằng cách gán hai thuộc tính của context canvas là globalCompositeOperation thành “destination-out” và fillStyle thành “rgba(0,0,0,1)”. Và tại vị trí bị đạn bắn, ta sẽ vẽ một hình tròn để “khoan” vùng địa hình này. Bạn nên lưu và phục hồi lại context khi sử dụng thiết lập này:

this.collide = function(x,y){
	if(this.contain(x,y))
	{
		context.save();
		context.globalCompositeOperation = "destination-out";
		context.fillStyle = "rgba(0,0,0,1)";
		context.beginPath();
		context.arc(x,y,20,0,Math.PI*2,true);
		context.fill();
		context.restore();
		imageData = context.getImageData(0,0,width,height);
		return true;
	}
	return false;
};

Cannon: Xác định vị trí đứng

Thay vì cố định vị trí của cannon trên bản đồ, ta có thể làm giống như Gunbound, cho cannon có tọa độ x ngẫu nhiên, tọa độ y nhỏ để xe rớt từ trên cao xuống và ngừng lại khi chạm “mặt đất”. Việc đặt code kiểm tra trong phương thức update() giúp cho xe luôn cập nhật vị trí mới khi địa hình bị thay đổi (như bị đạn phá thành hố sâu):

Cannon.prototype.update = function(map){
	if(!map.contain(this.cx,this.cy+this.radius))
		this.cy += this.speed;
    // ...
}

Cannon: Xác định lực và hướng bắn

Ban đầu, ta sẽ sử dụng chức năng click chuột để cannon bắn đạn. Dựa vào khoảng cách từ vị trí click chuột đến của cannon, ta sẽ xác định được độ lớn và hướng của lực bắn. Để lực bắn không quá mạnh khiến đạn bay quá xa, tôi chia giá trị khoảng cách (từ vị trí chuột đến cannon) cho 20:

Cannon.prototype.fire = function(targetX, targetY){
	var dx = targetX - this.cx;
    var dy = targetY - this.cy;
    var power = Math.floor(Math.sqrt(dx*dx+dy*dy)/20);

	if(this.balls.length > 5)
        return;
    var dirX = Math.cos(this.angle);
    var dirY = Math.sin(this.angle);

    var startX = this.cx + this.cannonWidth * dirX;
    var startY = this.cy + this.cannonWidth * dirY;

    var ball = new Ball(this.mapWidth,this.mapHeight,startX,startY,dirX,dirY,power);
    this.balls.push(ball);
}

Demo và Sourcecode

Xem Demo.
Html5-Canvas-Cannon-Game-Demo-part1
map.js:

function Map(width,height){
	var buffer = document.createElement("canvas");
	buffer.width = width;
	buffer.height = height;
	var context = buffer.getContext("2d");
	var offsetX = 0;

	var self = this;
	var ready = false;
	var imageData = null;

	// create map
	context.beginPath();
	context.moveTo(0,Math.floor(Math.random()*buffer.height));
	var x=0;
	var step = Math.floor(buffer.width/10);
	for(var i=1;i<10;i++)
	{
		x += Math.floor(Math.random()*step)+step;
		context.lineTo(x,Math.floor(Math.random()*buffer.height/2)+100);
	}
	// line to bottom-right corner
	context.lineTo(buffer.width,buffer.height);
	// line to bottom-left corner
	context.lineTo(0,buffer.height);

	context.fillStyle = "green";
	context.fill();
	imageData = context.getImageData(0,0,buffer.width,buffer.height);
	this.draw = function(ctx){

		offsetX++;
		if(offsetX>width)
			offsetX = 0;

		ctx.drawImage(buffer,0,0,width,height);

	};
this.contain = function(x,y){
	if(!imageData)
		return false;
	var index = Math.floor((x+y*width)*4+3);
	return imageData.data[index]!=0;
}

this.collide = function(x,y){
	if(this.contain(x,y))
	{
		context.save();
		context.globalCompositeOperation = "destination-out";
		context.fillStyle = "rgba(0,0,0,1)";
		context.beginPath();
		context.arc(x,y,20,0,Math.PI*2,true);
		context.fill();
		context.restore();
		imageData = context.getImageData(0,0,width,height);
		return true;
	}
	return false;
};
}

cannon.js:

function Cannon(mapWidth, mapHeight,x,y){
    this.mapWidth = mapWidth;
    this.mapHeight = mapHeight;
    this.radius = 10;
    this.speed = 5;
    this.power = 3;
    this.cx = x;
    this.cy = y;
    this.angle = 0;
    this.balls = [];
    this.cannonHeight = this.radius/2;
    this.cannonWidth = this.cannonHeight*3;
}
Cannon.prototype.draw = function(context){
    context.beginPath();
    context.fillStyle = "blue";
    context.arc(this.cx,this.cy,this.radius,0,Math.PI*2,true);
    context.closePath();
    context.fill();

    context.save();
    context.translate(this.cx,this.cy);
    context.rotate(this.angle);
    context.beginPath();
    context.fillStyle = "red";
    context.rect(0,-this.cannonHeight/2,this.cannonWidth,this.cannonHeight);
    context.closePath();
    context.fill();
    context.restore();

    context.beginPath();
    context.fillStyle = "yellow";
    context.arc(this.cx,this.cy,this.radius/2,0,Math.PI*2,true);
    context.closePath();
    context.fill();

    for(var i=0;i<this.balls.length;i++)
    {
        this.balls[i].draw(context);
    }
}

Cannon.prototype.update = function(map){
	if(!map.contain(this.cx,this.cy+this.radius))
		this.cy += this.speed;
    for(var i=0;i<this.balls.length;i++)
    {
        var item = this.balls[i];
        var x = Math.floor(item.cx);
		var y = Math.floor(item.cy);
		if(item.update() || map.collide(x,y))
        {
            this.balls.splice(i,1);
        }
    }
}

Cannon.prototype.fire = function(targetX, targetY){
	var dx = targetX - this.cx;
    var dy = targetY - this.cy;
    var power = Math.floor(Math.sqrt(dx*dx+dy*dy)/20);

	if(this.balls.length > 5)
        return;
    var dirX = Math.cos(this.angle);
    var dirY = Math.sin(this.angle);

    var startX = this.cx + this.cannonWidth * dirX;
    var startY = this.cy + this.cannonWidth * dirY;

    var ball = new Ball(this.mapWidth,this.mapHeight,startX,startY,dirX,dirY,power);
    this.balls.push(ball);
}
Cannon.prototype.rotate = function(targetX, targetY){
    var dx = targetX - this.cx;
    var dy = targetY - this.cy;
    this.angle = Math.atan2(dy,dx);
}

ball.js:


/********************* ball ****************/
function Ball(mapWidth, mapHeight,startX,startY,directionX,directionY,power){
    // the "life-zone"
    this.minX = this.radius;
    this.minY = this.radius;
    this.maxX = mapWidth - this.radius;
    this.maxY = mapHeight - this.radius;

    this.speedX = directionX * power;
    this.speedY = directionY * power;

    this.cx = startX;
    this.cy = startY;
}

Ball.prototype.draw = function(context){

    context.fillStyle = "black";
    context.beginPath();
    context.arc(this.cx,this.cy,4,0,Math.PI*2,true);
    context.closePath();
    context.fill();
}
Ball.prototype.update = function(){
    this.speedY += GRAVITY;
	this.cx += this.speedX;
    this.cy += this.speedY;
    if(this.cx < this.minX || this.cx > this.maxX ||
        this.cy < this.minY || this.cy > this.maxY)
        return true;
    return false;
}

test.html:

<html>
<head>
<script src="map.js"></script>
<script src="cannon.js"></script>
<script src="ball.js"></script>

<script>

/************** MAIN *******************/

var FPS = 20;
var GRAVITY = 0.5;
var _canvas;
var _context;
var _cannon;
var _map;

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

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

	_map = new Map(_canvas.width,_canvas.height);
	_cannon = new Cannon(_canvas.width,_canvas.height,20,20);
	draw();
}
function draw() {
    clear();
	_map.draw(_context);
    _cannon.draw(_context);
}
function update() {
    _cannon.update(_map);
    draw();
}
function canvas_mousemove(e){
    var x = e.pageX - _canvas.offsetLeft;
    var y = e.pageY - _canvas.offsetTop;
    _cannon.rotate(x,y);
}
function canvas_mousedown(e){
	var x = e.pageX - _canvas.offsetLeft;
    var y = e.pageY - _canvas.offsetTop;
    _cannon.fire(x,y);
}

// onload
window.onload = function(){
	init();
	_canvas.onmousemove = canvas_mousemove;
	_canvas.onmousedown = canvas_mousedown;

	window.setInterval(update,1000/FPS);
}
</script>
</head>
<body>
<canvas id="canvas" tabindex="0" width="400px" height="400px" style="border: 1px solid"></canvas>
</body>
</html>

Trong phần tới, chúng ta sẽ tìm hiểu về ảnh hưởng của trọng lực và gió đến đường bay của đạn và cách làm cho cannon di chuyển trên địa hình.
YinYangIt’s Blog

7 thoughts on “Html5 – Canvas: Viết game bắn đại bác – part1

  1. Hi anh!
    Anh cho em hỏi nếu địa hình là một tấm hình lớn, dạng giống như game gunny thì làm sao để kiểm tra được các va chạm giữa đạn và địa hình được anh? Vì nếu mình cho địa hình bao quanh một hình nào đó như hình chữ nhật thì nhìn nó không thật chính xác khi xảy ra va chạm.

    Trả lời

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