GameDev – Cuộn ảnh nền và bản đồ (Map Scrolling)

multi-level-background-scrolling-thumbCuộn bản đồ là chức năng không thể thiếu trong những game có bản đồ lớn vượt quá kích thước khung nhìn (viewport) của màn hình. Bài viết này giúp bạn tìm hiểu một số phương pháp để tạo hiệu ứng, cuộn ảnh nền và bản đồ trong game.

Ảnh nền nhiều tầng

Một phương pháp đơn giản và tạo hiệu ứng đẹp là sử dụng ảnh nền nhiều tầng. Bằng cách cho mỗi tầng có tốc độ cuộn khác nhau, khung cảnh trong game trở nên có chiều sâu và thật hơn. Số lượng tầng này thường chỉ giới hạn từ 2 đến 3 và các tầng nằm bên dưới sẽ có tốc độ cuộn chậm hơn.

Multi level background scrolling

Cuộn giả

Bạn có thể thấy nhiều game có bản đồ rất lớn nhưng ảnh nền chỉ lặp đi lặp lại theo một mẫu duy nhất. Điều này được thực hiện bằng cách sử dụng một ảnh nền có kích thước nhỏ và được vẽ lặp đi lặp lại trên viewport. Tùy theo kích thước của ảnh nền này mà ta sử dụng phương pháp vẽ khác nhau.
Cách thông thường là sử dụng một ảnh nền có kích thước bằng viewport. Sau đó ta chia ảnh nền này thành hai phần theo chiều dọc dựa vào một đường phân cách:
– Cắt phần ảnh nền từ vị trí đường phân cách đến hết và vẽ lên viewport tại vị trí {0,0}.
– Cắt phần ảnh nền từ vị trí đầu tiên đến đường phân cách (phần còn lại) và vẽ lên viewport tại vị trí tiếp nối với phần lúc nãy.

// position that divide the background into two parts
offsetX++;
if(offsetX>width)
	offsetX = 0;

// draw the first part
ctx.drawImage(bgimg,offsetX,0,width-offsetX,height,0,0,width-offsetX,height);
// the second part
ctx.drawImage(bgimg,0,0,offsetX,height,width-offsetX,0,offsetX,height);

… và cuộn thật

Với một bản đồ lớn, việc cuộn để giữ nhân vật chính của game luôn ở giữa màn hình rất đơn giản. Ta có thể xác định được tọa độ (left và top) trên bản đồ sẽ được dùng để làm viewport bằng cách:

// inside Map object
// obj = character
this.offsetX = obj.left - viewWidth/2;
this.offsetY = obj.top - viewHeight/2;

Tuy nhiên việc thay đổi khung nhìn liên tục có thể khiến cho người chơi nhức mắt, mất tập trung và ảnh hưởng đến hiệu suất. Vì vậy ta áp dụng một giải phải là tạo một vùng giới hạn trong viewport gọi là “dead zone“. Khi nhân vật di chuyển nhưng vẫn nằm trong vùng này, viewport sẽ không bị thay đổi.

// inside Map object

var dx = obj.left - _map.offsetX;
var dy = obj.top - _map.offsetY;

if(dx<this.deadzone.left)
	this.offsetX = obj.left - this.deadzone.left;
else if(dx+obj.size>this.deadzone.right)
	this.offsetX = obj.right - this.deadzone.right;

if(dy<this.deadzone.top)
	this.offsetY = obj.top -  this.deadzone.top;
else if(dy+obj.size>this.deadzone.bottom)
	this.offsetY = obj.bottom - this.deadzone.bottom;

Ví dụ hoàn chỉnh

Xem Demo

Canvas 2D Map scolling Demo

test.html:

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

/****************** Keys *******************/
var Keys = {
  LEFT_ARROW: 37,
  UP_ARROW: 38,
  RIGHT_ARROW: 39,
  DOWN_ARROW: 40,
};

/************** MAIN *******************/
var AVAILABLE_KEYS =
    [   Keys.DOWN_ARROW,
        Keys.RIGHT_ARROW,
        Keys.LEFT_ARROW,
        Keys.UP_ARROW
    ];
var FPS = 20;
var GRAVITY = 0.5;
var _canvas;
var _context;
var _ball;
var _keypressed = {};
var _map;

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

function init() {
    _canvas = document.getElementById("canvas");
    _context = _canvas.getContext("2d");
	_context.font = "16px Arial";
	_map = new Map(_canvas.width,_canvas.height);
	_ball = new Ball(_map,_canvas.width,_canvas.height);
    draw();
}
function draw() {
    clear();

	_map.draw(_context,_ball);

	_ball.draw(_context)
}
function canvas_keyDown(e){
	e.preventDefault();

    if(AVAILABLE_KEYS.indexOf(e.keyCode)!=-1)
    {
        _keypressed[e.keyCode] = true;

        doKeypress();
    }
}
function canvas_keyUp(e){
	e.preventDefault();
    if(_keypressed[e.keyCode])
    {
        _keypressed[e.keyCode] = false;
    }
}

function doKeypress(){
    _ball.move(_keypressed);
    draw();
}
// onload
window.onload = function(){
	init();
	_canvas.onkeydown = canvas_keyDown;
	_canvas.onkeyup = canvas_keyUp;
}
</script>
</head>
<body>
<canvas id="canvas" tabindex="0" width="400px" height="400px" style="border: 1px solid"></canvas>
</body>
</html>

map.js:

var MAP_WIDTH = 50;
var MAP_HEIGHT = 50;
var CELL_SIZE = 20;
function Map(viewWidth,viewHeight){
	var offset = CELL_SIZE*5;
	this.deadzone = {
		left: offset,
		top: offset,
		right: viewWidth - offset,
		bottom: viewHeight - offset,
	};
	this.deadzone.width = this.deadzone.right-this.deadzone.left;
	this.deadzone.height = this.deadzone.bottom-this.deadzone.top;

	this.width = MAP_WIDTH*CELL_SIZE;
	this.height = MAP_HEIGHT*CELL_SIZE;
	this.offsetX = 0;
	this.offsetY = 0;
	// buffer
	var buffer = document.createElement("canvas");
	buffer.width = this.width;
	buffer.height = this.height;
	var context = buffer.getContext("2d");
	context.fillStyle = "black";
	for(var i=1;i<MAP_WIDTH-1;i++)
	{
		for(var j=1;j<MAP_HEIGHT-1;j++)
		{
			if(Math.random()<0.3)
			{
				context.fillRect(i*CELL_SIZE,j*CELL_SIZE,CELL_SIZE,CELL_SIZE);
				context.fill();
			}
		}
	}

	var imgData = context.getImageData(0,0,this.width,this.height);

	this.draw = function(ctx,obj){
		var dx = obj.left - _map.offsetX;
		var dy = obj.top - _map.offsetY;

		if(dx<this.deadzone.left)
			this.offsetX = obj.left - this.deadzone.left;
		else if(dx+obj.size>this.deadzone.right)
			this.offsetX = obj.right - this.deadzone.right;

		if(dy<this.deadzone.top)
			this.offsetY = obj.top - this.deadzone.top;
		else if(dy+obj.size>this.deadzone.bottom)
			this.offsetY = obj.bottom - this.deadzone.bottom;

		if(this.offsetX < 0)
			this.offsetX = 0;
		else if(this.offsetX + viewWidth > buffer.width)
			this.offsetX = buffer.width - viewWidth;

		if(this.offsetY < 0)
			this.offsetY = 0;
		else if(this.offsetY + viewHeight> buffer.height)
			this.offsetY = buffer.height - viewHeight;

		ctx.drawImage(buffer,this.offsetX,this.offsetY,viewWidth,viewHeight,
							0,0,viewWidth,viewHeight);
		ctx.fillStyle = "rgba(255,0,0,0.5)";
		ctx.fillRect(this.deadzone.left,this.deadzone.top,this.deadzone.width,this.deadzone.height);
		ctx.fillStyle = "blue";
        ctx.fillText("Dead Zone", viewWidth/2-50,viewHeight/2-10);
	};
	this.contain = function(x,y){
		var index = Math.floor((x+y*this.width)*4+3);
		return imgData.data[index]!=0;
	}

}

ball.js:

var SPEED = 5;
function Ball(map,viewWidth,viewHeight){
	this.size = 20;
    this.speedX = 0;
    this.speedY = 0;
    this.left = 0;
    this.top = 0;
	this.right = this.left + this.size;
	this.bottom = this.top + this.size;

	var radius = this.size/2;
	this.draw = function(context){
		var x = this.left-map.offsetX;
		var y = this.top-map.offsetY;
		context.beginPath();
		context.fillStyle = "blue";
		context.arc(x+radius,y+radius,radius,0,Math.PI*2,true);
		context.closePath();
		context.fill();
	};

	this.move = function(keyStates){
		this.speedX = 0;
		this.speedY = 0;

		if(keyStates[Keys.UP_ARROW])
			this.speedY = -SPEED;
		if(keyStates[Keys.DOWN_ARROW])
			this.speedY = SPEED;
		if(keyStates[Keys.LEFT_ARROW])
			this.speedX = -SPEED;
		if(keyStates[Keys.RIGHT_ARROW])
			this.speedX = SPEED;

		var left = this.left + this.speedX;
		var top = this.top + this.speedY;
		var right = left + this.size;
		var bottom = top + this.size;

		var collided = map.contain(left,top) || map.contain(right-1,top) ||
						map.contain(left,bottom-1) || map.contain(right-1,bottom-1);
		if(!collided)
		{
			if(left>=0 && right<=map.width)
			{
				this.left = left;
				this.right = right;
			}
			if(top>=0 && bottom<=map.height)
			{
				this.top = top;
				this.bottom = bottom;
			}
		}
	};
}

YinYangIt’s Blog

5 thoughts on “GameDev – Cuộn ảnh nền và bản đồ (Map Scrolling)

  1. Thank tác giả. Bài viết thật chi tiết. Anh có thể viết tut hướng dẫn sử dụng con lăn của chuột để phóng to hoặc thu nhỏ bản đồ được không anh em đang làm cái này mà chưa được.

    Trả lời
    • Ý bạn hỏi là hàm này?

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

      Để hiểu rõ thì bạn có thể phải đọc trước bài sau: https://yinyangit.wordpress.com/2012/02/14/html5-canvas-ve-anh-va-thao-tac-voi-pixel/

      Cụ thể các image data của canvas sẽ được thể hiện dưới dạng mảng 1 chiều(mỗi pixel chiếm 4 phần tử của mảng).

      # (x + y * width): Từ 2 vị trí {x,y} ở dạng mảng 2 chiều, mình chuyển nó thành 1 vị trí duy nhất trong mảng 1 chiều.
      # (* 4): do mỗi pixel chiếm 4 phần tử của mảng image data. Muốn nhảy đến pixel thứ n ta phải n * 4.
      # (+ 3): Trong pixel hiện tại, kiểm tra giá trị Alpha (nằm ở vị trí cuối) của nó có khác 0 hay ko. Nếu = 0 tức là nó trong suốt.

      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