Chủ nhật, ngày 4 tháng 12 năm 2016

HTML5-Canvas: Viết game Mario – Part 1

Ngày đăng: 29/6/2012, 8:47:55PM | Lượt xem: 3,780
Hot!

 Trong các bài viết trước, tôi có viết một ví dụ đơn giản trên Canvas để làm nền tảng cho các game slide-scrolling 2D rất nổi tiếng như Contra, Final Fight, Metal Slug, Castlevania,… Hãy coi lại part 0 để thử xem bạn có thể phát triển nó thành những game nào. Riêng với tôi, phát triển nó thành game Mario là một khởi đầu tốt và rất thích hợp cho người mới bắt đầu.

 Đây là tựa game gắn liền với tuổi thơ của rất nhiều những người cùng thế hệ với tôi. Và bây giờ chắc hẳn cũng còn nhiều người đam mê với nó. Cách đây vài năm tôi có dự định thực hiện game này nhưng vẫn chưa thể bắt tay vào làm do thiếu kiến thức, kinh nghiệm cũng như một nền tảng thích hợp.

Khi bắt đầu tìm hiểu về HTML5, và từng ngày tích lũy kiến thức về lập trình game, tôi dần quay trở lại với mục tiêu trước đây của mình. Và việc giới thiệu bài Tạo một game 2D side-scrolling cơ bản chính là lúc tôi biết mục tiêu của mình đã đạt được 50%. Nếu bạn đã đọc và hiểu bài đó, bạn cũng có thể làm được những game tương tự.
 
Lớp Character
 
Đây là lớp nền tảng cho tất cả cả đối tượng chuyển động (có thể thay đổi vị trí) trong game. Các đối tượng đó có thể là người chơi, quái vật hay các NPC (non-player character). Tôi định nghĩa khá nhiều thuộc tính trong lớp này để xác định vị trí, tốc độ, kích thước của nhân vật. Bên cạnh đó, tôi thêm một số thuộc tính boolean hành vi (hay khả năng) của nhân vật:
 
- canDestroyObstacles: khả năng phá hủy các chướng ngại vật (bằng cách dùng đầu) của nhân vật. Khi nhân vật nhảy lên, nếu đầu chạm trúng chướng ngại vật có thể phá hủy (gạch), ta sẽ kiểm tra thuộc tính này để xác định xem chướng ngại vật có bị phá hủy hay không.
- isAutoMoving: Dùng cho các nhân vật không bị người chơi (player) điều khiển (quái vật).
- canJump: Nhân vật có thể nhảy hay không.
function Character(map,options){

	if(!options)
		options = {};

	this.map = map;
	this.left = options.left || 0;
	this.top = options.top || 300;
	this.height = options.height || 15;
	this.width = options.width || 15;

	this.jumpPower = options.jumpPower || 5;
	this.speed = options.speed || 2;
	this.speedX = (Math.random()<0.5? -this.speed: this.speed);
	this.speedY = 0;

	this.isJumping = true;
	this.isDead = false;

	this.bottom = this.top+this.height;
	this.right = this.left+this.width;
	// behaviors
	this.canDestroyObstacles = options.canDestroyObstacles;
	this.isAutoMoving = options.isAutoMoving;
	this.canJump = options.canJump;

}
Nhìn chung phương thức chính update() cũng tương tự như trong bài part 0. Ngoại trừ việc khi nhân vật nhảy lên và phá gạch, các đối thủ (trong game này là các Monster) bên trên sẽ bị tiêu diệt. Vì vậy tôi bổ sung đoạn mã này vào phần kiểm tra va chạm phía trên:
// top collision
if(this.canDestroyObstacles)
{
	// destroy all monsters that are standing on the brick
	for(m in this.map.monsters)
	{
		var mon = this.map.monsters[m];
		if(!mon)
			continue;
		var cx = mon.left + mon.width/2;
		if(mon.bottom==b.top && cx>=b.left && cx<=b.right )
			mon.die();
	}
	this.top = b.bottom;
	this.speedY = 0;
} 
Thay đổi hướng di chuyển của nhân vật khi thuộc tính isAutoMoving có giá trị true và nhân vật bị va chạm ở phía trái hoặc phải:
// update() method
if(this.left<0 || (b = this.map.colllide(vleft,vtop+6,false) ||
	this.map.colllide(vleft,vbottom-4,false,this.canEat))) // left
{
	if(b && b!=true)
		this.left = b.right;

	if(this.speedX<0)
		this.speedX = this.isAutoMoving? this.speed: 0;

}else if(this.right>=this.map.width || (b = this.map.colllide(vright,vtop+6,false) ||
		this.map.colllide(vright,vbottom-4,false))) // right
{
	if(b && b!=true)
		this.left = b.left-this.width;

	if(this.speedX>0)
		this.speedX = this.isAutoMoving? -this.speed: 0;
} 
Lớp Monster
 
Monster (hay Enemy) là các đối tượng có khả năng hoạt động và có thể được tích hợp AI nhằm tiêu diệt hoặc cản trở người chơi. Trong bất kì game nào thì các loại đối tượng này rất đa dạng và là thường là nguyên nhân chính tạo ra sự lôi cuốn của game. Vì đây chỉ là phần bắt đầu nên tôi chỉ tạo một loại Monster duy nhất có khả năng tiêu diệt người chơi thông qua va chạm.
Bởi vì được thừa kế từ lớp Character bên trên, lớp này chỉ cần một công việc chính là hiện thực phương thức draw() để vẽ chính nó:
function Monster(map,left,top){
	// call the super-constructor
	Character.call(this,map,{
		left: left,
		top: top,
		width: 20,
		height: 20,
		speed: 1,
		isAutoMoving: true
	});
}

Monster.prototype = new Character();

Monster.prototype.draw = function(context){

	context.save();
	context.beginPath();

	var left = this.left-this.map.offsetX;
	var top = this.top-this.map.offsetY;

	var right = left+this.width;
	var bottom = top+this.height;

	var hw = this.width/2;
	var cx = left+hw;

	context.fillStyle = "violet";
	context.arc(cx,top+hw,hw-2,0,Math.PI*2,true);
	//context.rect(left,top,this.width,this.height);
	context.fill();

	context.stroke();
	context.restore();
}
 
Lớp Player
 
Cũng tương tự như Monster, tuy nhiên công việc sẽ phức tạp hơn vì ta cần kiểm tra nhiều thứ và phải cung cấp các phím bấm điều khiển. Phương thức update này mặc định trả về 0. Trường hợp player hoàn thành vòng chơi, trả về 1; và nếu player chết, trả về 2:
Player.prototype.update = function(){
	if(this.right>=this.map.width){
		alert("Game Over!");
		return 1;
	}
	if(this.isFalling) // die
	{
		this.speedY += GRAVITY;
		this.top += this.speedY;
		return (this.top>this.map.height)? 2 : 0 ;
	}

	Character.prototype.update.call(this);

	this.collide(this.map.monsters);
}
 
Trong mario, khi bị “tử nạn”, player sẽ được nâng lên cao “lần cuối” rồi rớt xuống. Tôi cũng làm tương tự:
Player.prototype.die = function(){
	this.isFalling = true;
	this.top -= this.height;
	this.speedY = 0;
}
Kiểm tra va chạm với các monster, ta xem player và monster là các hình chữ nhật và chỉ cần kiểm tra xem hai hình này có cắt nhau không:
Player.prototype.collide = function(monsters){
	for(m in monsters)
	{
		var mon = monsters[m];
		if(!mon)
			continue;
		if(!(this.left > mon.right ||
			this.right < mon.left ||
			this.top > mon.bottom ||
			this.bottom < mon.top))
			{
				if(this.bottom<mon.bottom&& this.speedY>0)
				{
					mon.die();
				}
				else
				{
					this.die();
					break;
				}
			}
	}

}
 
Lớp Map
 
Nếu coi lập trình viên lập trình viên là thượng đế, thì bản đồ chính là một “thế giới” thu nhỏ nơi mà thượng đế cho vận hành những quy luật để tạo ra “cuộc sống”. Mặc dù bản đồ có thể rất đơn giản trong nhiều game, nhưng đối với dạng game phiêu lưu này, nó đóng vai trò rất quan trọng (có thể hơn so với Monster). Một “thế giới” quá nhỏ bé sẽ khiến người chơi nhanh kết thúc vòng chơi và không thể khơi lên sự tò mò muốn khám phá của họ.
 
Khi một vật thể (chướng ngại vật), ta sẽ xóa vùng tương ứng trên buffer (ảnh bản đồ) và gán giá trị lại cho ô đó trong dữ liệu là mặc định (0):
function clearCell(left,top,col,row)
{
	data[col+row*COLS] = 0;
	context.save();
	context.globalCompositeOperation = "destination-out";
	context.fillStyle = "rgba(0,0,0,1)";
	context.fillRect(left,top,CELL_SIZE,CELL_SIZE);
	context.restore();
}
 
Để kiểm tra va chạm với các chướng ngại vật, tôi tạo phương thức collide() với giá trị trả về là một đối tượng lưu giữ các giá trị về vị trí, kích thước của chướng ngại vật đó:
this.colllide = function(x,y,canDestroy){
	var b = this.contain(x,y);

	if(b)
	{
		if(canDestroy && b.type==BRICK)
		{
			clearCell(b.left,b.top,b.col,b.row);
		}
		return b;
	}
	return false;
};
this.contain = function(x,y){
	var col = Math.floor(x/CELL_SIZE);
	var row = Math.floor(y/CELL_SIZE);
	var val = data[col+row*COLS];
	if(val>0)
	{
		var b = {
			left: col*CELL_SIZE,
			top: row*CELL_SIZE,
			col: col,
			row: row,
			type: val,
		};
		b.right = b.left+CELL_SIZE;
		b.bottom = b.top+CELL_SIZE;

		return b;
	}
	return false;
};
 
Trong game này, lớp Map còn là nơi chứa các Monster. Để làm Monster ra liên tục nếu số lượng ít hơn một giá trị nào đó. Tôi viết phương thức update() như sau:
this.update = function(){

	// generate random monsters
	if(this.monsters.length<MONSTER_IN_VIEW)
		this.monsters.push(new Monster(this,this.offsetX+Math.random()*this.viewWidth+50,1));

	i = 0;
	var length = this.monsters.length;
	while(i<length){
		if(this.monsters[i].isDead || this.monsters[i].left<this.offsetX)
		{
			this.monsters.splice(i,1); // remove this monster from array
			length--;
		}else
		{
			this.monsters[i].update();
			i++;
		}
	}
};
 

 

YinYangIt’s Blog
 Chia sẻ qua: 
Hot!
Ý kiến bạn đọc

These items will be permanently deleted and cannot be recovered. Are you sure?

Gallery

image

Maecenas viverra rutrum pulvinar

Maecenas viverra rutrum pulvinar! Aenean vehicula nulla sit amet metus aliquam et malesuada risus aliquet. Vestibulum rhoncus, dolor sit amet venenatis porta, metus purus sagittis nisl, sodales volutpat elit lorem…

Read more

Text Links

Thiết kế logo chuyên nghiệp Insky
DAFABET
W88 w88b.com/dang-ky-tai-khoan-w88
W88
Copyright © 2011 - 2012 vietshare.vn by phamkhuong102@gmail.com doanhkisi2315@gmail.com. All rights reserved.