Huỳnh Minh Tân Thao tác với Sprite, Sprite animation là một trong những kỹ thuật cần thiết trong lập trình game. Bài viết sẽ giới thiệu tổng quan về Sprite, Sprite animation, tổng quan quá trình hiển thị Sprite lên màn hình và làm rõ các khái niệm như texture, surface, backbuffer, frontbuffer, tile. Cuối cùng sẽ hiện thực load Sprite từ cơ bản đến các thao tác nâng cao như flip, scale, rotation, translate với DirectX9.
Nội dung bài viết

Giới thiệu

Thông thường những lập trình viên mới tiếp cận lĩnh vực lập trình game sẽ lúng túng trong việc tìm hiểu các kỹ thuật cơ bản. Load Sprite là một trong những kỹ thuật đó, nó có vai trò quan trọng khi bạn muốn thể hiện một hay nhiều đối tượng lên màn hình.

Trong bài viết này tôi và các bạn sẽ cùng tìm hiểu về Sprite, quá trình hiển thị một Sprite lên màn hình trên nền DirectX9. Bài viết cũng giúp bạn làm rõ một số khái niệm như surface, texture, backbuffer, frontbuffer, tile những khái niệm làm nhiều bạn cảm thấy khó khăn khi thấu hiểu chúng.

Phần cuối bài viết, tôi và các bạn sẽ hiện thực Sprite từ cơ bản đến các thao tác nâng cao như flip, scale, rotation và translate trong DirectX9.

Tiền đề bài viết

Bài viết với mong muốn giúp đỡ các bạn mới tìm hiểu về lập trình game tiếp cận nhanh chóng các kỹ thuật cơ bản và nguồn tài liệu tham khảo cho những bạn đang học Thiết kế engineDirectX (HLSL) tại STDIO Training.

Đối tượng hướng đến

Phải có kiến thức về lập trình game cơ bản (loop, fps), cài đặt được môi trường hỗ trợ DirectX9.

Tham khảo trước 3 bài viết để có kiến thức cơ bản về Sprite và Sprite sheet.

Sprite, Sprite sheet và Sprite animation?

Qua các bài viết tôi đã giới thiệu ở trên, tác giả đã trình bày chi tiết về Sprite và Sprite sheet nên tôi xin nhắc lại những điểm chính, để các bạn có thể nắm được nội dung bài viết rõ ràng hơn.

Sprite là gì?

i_am_sprite

Là một đối tượng đồ họa 2D được vẽ lên màn hình. Các Sprite thường được kết hợp với nhau tạo thành một phần khung cảnh trong game. Dĩ nhiên bạn có thể dịch chuyển nó thông qua cách đặt tọa độ trong hàm vẽ lên màn hình.

Sprite là cách phổ biến để tạo ra các cảnh lớn và phức tạp vì bạn có thể thao tác trên mỗi Sprite riêng biệt. Nhưng thực tế trong một game, thường có từ mười đến hàng trăm Sprite việc tải riêng từng ảnh sẽ tiêu tốn rất nhiều bộ nhớ và mất nhiều thời gian để xử lý. Để quản lý các Sprite và tránh sử dụng quá nhiều hình ảnh chúng ta sẽ sử dụng Sprite sheet.

Sprite sheet là gì?

Là tập hợp nhiều Sprite đơn lẻ thành một tập tin duy nhất. Giúp tăng tốc độ xử lý cho việc hiển thị hình ảnh lên màn hình. Do ta chỉ hiển thị một phần của bức ảnh (load 1 lần lên bộ nhớ) sẽ tốt hơn nhiều với việc lấy nhiều bức ảnh và hiển thị chúng.

Metroid_Aladdin
Sprite sheet game NES Metroid và Aladdin Genesis

Sprite animation là gì?

aladdin_walk

Dựa trên tập tin Sprite sheet ta cho hiển thị từng Sprite chuyển tiếp nhau một cách liên tục khi đó sẽ tạo ra cảm giác đối tượng như đang chuyển động.

Khi bạn thao tác với Sprite animation thì chắc có lẽ nghe đế khái niệm Tile. Một Sprite sheet lưu một bức ảnh là một chuỗi các Tile, mỗi Tile sẽ hiển thị một khung hình của chuỗi chuyển động. Như vậy Tile cũng giống với Sprite/ Frame trong hoàn cảnh này.

tile_spirte_sheet__380x248

Bạn không chỉ cần Sprite sheet để hiển thị ảnh lên màn hình mà còn phải cần có một tập tin để lưu tọa độ (x, y, width, height) của các tile trong Sprite sheet. Ví dụ tập tin .XML lưu tọa độ các tile Aladdin đang chạy.

<?xml version="1.0" encoding="UTF-8"?>
	<TextureAtlas imagePath="sprite_aladdin_walk.bmp" width="636" height="62">
	<sprite n="0.bmp" x="594" y="223" w="40" h="48" pX="0.5" pY="0.5"/>
	<sprite n="1.bmp" x="547" y="223" w="43" h="51" pX="0.5" pY="0.5"/>
	<sprite n="2.bmp" x="457" y="223" w="41" h="53" pX="0.5" pY="0.5"/>
	<sprite n="3.bmp" x="264" y="223" w="41" h="57" pX="0.5" pY="0.5"/>
	<sprite n="4.bmp" x="107" y="223" w="52" h="57" pX="0.5" pY="0.5"/>
	<sprite n="5.bmp" x="368" y="223" w="46" h="54" pX="0.5" pY="0.5"/>
	<sprite n="6.bmp" x="61" y="223" w="42" h="58" pX="0.5" pY="0.5"/>
	<sprite n="7.bmp" x="502" y="223" w="41" h="52" pX="0.5" pY="0.5"/>
	<sprite n="8.bmp" x="418" y="223" w="35" h="54" pX="0.5" pY="0.5"/>
	<sprite n="9.bmp" x="163" y="223" w="49" h="57" pX="0.5" pY="0.5"/>
	<sprite n="10.bmp" x="2" y="223" w="55" h="58" pX="0.5" pY="0.5"/>
	<sprite n="11.bmp" x="309" y="223" w="55" h="55" pX="0.5" pY="0.5"/>
	<sprite n="12.bmp" x="216" y="223" w="44" h="57" pX="0.5" pY="0.5"/>
</TextureAtlas>

Tổng quan quá trình vẽ một đối tượng đồ họa lên màn hình (monitor)

Trong phần này các bạn sẽ được tiếp cận những khái niệm mới, sẽ thật sự bổ ích khi các bạn hiểu rõ về nó và thường được nhắc đến khi chúng ta lập trình với DirectX.

Front buffer / Frame buffer (bề mặt chính)

Card đồ họa sẽ chứa thông tin mà chúng ta muốn hiển thị lên màn hình. Khi bạn muốn hiển thị thứ gì đó, card đồ họa sẽ lấy dữ liệu cần hiển thị tại vùng nhớ đệm được gọi là Front buffer (Frame buffer) sau đó sẽ gửi những thông tin đã lấy được đến monitor để hiển thị. Monitor sẽ cập nhật những phần mới và vẽ lại từ trên xuống dưới.

Front buffer nằm ở bộ nhớ đồ họa và biểu thị hình ảnh lên màn hình. Do đó, để thay đổi hiển thị trên màn hình theo ý muốn, cách đơn giản ta chỉ cần thay đổi trực tiếp dữ liệu trên Front buffer. Nhưng có vấn đề nhỏ xảy ra, màn hình sẽ hiển thị thành hai phần, nửa trên là bức ảnh cũ còn nửa dưới là ảnh mới (điều này diễn ra nhanh nên sẽ cảm thấy màn hình lag và giật). Hiện tượng xảy ra là do một chương trình nào đó cập nhật trên Front buffer trong khi monitor đang trong quá trình refresh. Hay có thể hiểu là GPU render với số lượng frame lớn hơn số lượng frame hiển thị lên màn hình trong một giây (refresh rate) của monitor, thông thường refresh rate chỉ từ 60 Hz (fps) đến 100 Hz ở đa số màn hình. Hiện tượng này được gọi là tearing screen.

Back buffer / off-screen surface (bề mặt phụ ngoài)

Để giải quyết vấn đề trên, ngoài việc bật V-Sync còn có một kỹ thuật khác là Back buffering (double buffering).

Back buffering là quá trình vẽ hình ảnh lên bề mặt phụ ngoài màn hình (off-screen surface), kiểu bề mặt này thực chất là một mảng pixel giống như bitmap. Back buffer nó cũng thuộc loại bề mặt này nhưng đặt biệt hơn. Một bức ảnh thay vì được vẽ trực tiếp từ Front buffer lên monitor thì Direct3D sẽ vẽ ảnh từ Back buffer. Như vậy, chúng ta muốn hiển thị những gì lên màn hình thì chỉ cần vẽ lên Back buffer, dữ liệu sẽ sao chép sang Front buffer qua mỗi vòng lặp.

Chúng ta cần lưu ý, bất kỳ bề mặt (surface) nào khác Front buffer thì được gọi là off-screen surface bởi vì chỉ có Front buffer mới hiển thị hình ảnh trực tiếp lên monitor.

Thật sự khi sử dụng kỹ thuật Back buffering thì hiện tượng tearing vẫn còn xảy ra. Để tránh điều đó, DirectX sử dụng một con trỏ cho mỗi buffer (cả Front và Back) để chuyển đổi giá trị giữa chúng một cách đơn giản. Quá trình chuyển từ Back buffer sang Front buffer được gọi là surface flipping.

Surface và Texture

Khi bạn lập trình game với DirectX thì chắc đã từng nghe qua khái niệm surface, thực chất surface nó giống như off-screen surface. Direct3D sử dụng surface cho rất nhiều thứ, phổ biến nhất là thao tác vẽ hoặc load một ảnh bitmap (png, jpg..) lên nó.

Khi bạn thao tác với Sprite, DirectX sử dụng texture thay cho surface để lưu trữ Sprite image. Sự thay thế này thật sự cũng không có lợi ích nào cho việc nâng cao hiệu suất game, do phần cứng hiện nay khá mạnh để xử lý chúng ta cũng không cảm nhận được rõ sự khác biệt.

Surface và texture có thể ra tạo bao nhiêu cũng được miễn là còn đủ RAM và VRAM để chứa tất cả.

model_backbuffering__600x295
Mô hình Back buffering

Tóm lại

Front buffer là một vùng trên bộ nhớ được bộ xử lý hiển thị trực tiếp lên monitor. Trong Direct3D, các chương trình sẽ không được thao tác trực tiếp lên Front buffer.

Back Buffer là một vùng trên bộ nhớ, các chương trình có thể thao tác trực tiếp lên đây hoặc sao chép từ surface khác. Back buffer sẽ không hiển thị trực tiếp lên monitor.

Surface (off-screen surface) là một bề mặt thứ cấp được lưu trữ trong bộ nhớ của card đồ họa hoặc có thể trong bộ nhớ của hệ thống. Chúng ta có thể thực hiện nhiều thao tác như vẽ bitmap, load ảnh.

Texture một phần tương tự như surface và nó được sử dụng nhiều khi thao tác với Sprite.

Như đã nói, Back buffer và surface thực chất là giống nhau và đều được quản lý bởi interface IDirect3DSurface9, nên khi cài đặt chương trình chúng ta cần chỉ đâu là Back buffer thông qua hàm IDirect3DDevice9::GetBackBuffer(). Nhưng khi sử dụng đối tượng ID3DXSprite thì chỉ cần chúng ta gọi hàm D3DXCreateSprite() để kết nối đối tượng Sprite với Device.

Load Sprite sử dụng đối tượng D3DXSprite trong DirectX9

Việc hiển thị một Sprite lên màn hình có nhiều cách, bạn có thể nạp một Sprite vào surface và thực hiện các thao tác vẽ nó lên màn hình, y như thao tác ảnh với tập tin bitmap. Còn một cách nữa là sử dụng đối tượng D3DXSprite, rất thuận lợi cho việc vẽ Sprite trong suốt và các thao tác flip, rotation, scale, translate.

Chuẩn bị project với những thiết lặp ban đầu

Khởi tạo cửa sổ Windows, khởi tạo DirectX, tạo vòng loop game,.. thực hiện công việc chuẩn bị để load một Sprite.

Hãy mở Visual Studio -> tạo mới một project và cài đặt môi trường hỗ trợ DirectX9 -> tạo mới tập tin MainPrg.cpp -> sau đó tự tay gõ lại hoặc copy (không khuyến khích) source code tôi đã chuẩn bị bên dưới để chúng ta cùng nhau thực hiện các thao tác với Sprite.

Xem code mẫu trên Bugs.vn, Entry_LoadSprite :: https://bugs.vn/8574

Cài đặt Sprite cơ bản

Các thao tác chính:

  • Khai báo: Sprite Handler, Texture, Information of sprite
  • Khởi tạo:
    • D3DXCreateSprite: gắn sprite handler vào Direct3D và device
    • D3DXGetImageInfoFromFile: lấy thông tin Sprite
    • D3DXCreateTextureFromFileEx: khởi tạo texture chứa Sprite
  • Vẽ:
    • LPD3DXSPRITE::Begin
    • LPD3DXSPRITE::Draw
    • LPD3DXSPRITE::End

Thêm thư viện “d3dx9.lib” vào Linker/Input trong project của bạn trước khi bắt đầu.

Việc đầu tiên chúng ta sẽ khai báo các biến toàn cục, đặt trước WinMain

// sprite handler quản lý, gọi hàm thao tác sprite
LPD3DXSPRITE sprite_handler;
// texture lưu sprite
LPDIRECT3DTEXTURE9 texture = NULL;
// lưu trữ thông tin sprite
D3DXIMAGE_INFO info; 
// path sprite
#define SPRITE_PATH L"pikachu.png"

Thực hiện thao tác khởi tạo trong hàm initSprite()

void initSprite()
{
	// biến kiểm tra thành công
	HRESULT result;

	// kết nối sprite handler với device và Direct3D
	result = D3DXCreateSprite(d3ddev, &sprite_handler);

	if (result != D3D_OK)
	{
		MessageBox(NULL, L"Can't associate main Direct3D and Device", L"Error", MB_OK);
		return;
	}

	// lấy thông tin sprite lưu vào biến info
	result = D3DXGetImageInfoFromFile(SPRITE_PATH, &info);

	if (result != D3D_OK)
	{
		MessageBox(NULL, L"Failed to get information from image file", L"Error", MB_OK);
		return;
	}

	// khởi tạo texture từ sprite
	/*
	Chỉ cần quan tấm đến:
	device, 
	đường dẫn sprite, 
	biến info lưu thông tin, 
	màu cần trong suốt,
	texture để lưu sprite.
	*/
	result = D3DXCreateTextureFromFileEx(
		d3ddev,                   // device liên kết với texture 
		SPRITE_PATH,		      // đường dẫn của sprite
		info.Width,			      // chiều dài của sprite
		info.Height,		      // chiều rộng của sprite
		1,						
		D3DPOOL_DEFAULT,	      // kiểu surface
		D3DFMT_UNKNOWN,	          // định dạng surface
		D3DPOOL_DEFAULT,	      // lớp bộ nhớ cho texture
		D3DX_DEFAULT,		      // bộ lọc ảnh
		D3DX_DEFAULT,		      // bộ lọc mip
		D3DCOLOR_XRGB(0, 0, 0),   // chỉ ra màu trong suốt
		&info,			          // thông tin của sprite
		NULL,			          // đổ màu
		&texture			      // texture sẽ chứa sprite
	);

	if (result != D3D_OK)
	{
		MessageBox(NULL, L"Failed to create texture from file", L"Error", MB_OK | MB_ERR_INVALID_CHARS);
		return;
	}
}

Tiến hành vẽ Sprite (với kích thước full width, full height) lên màn hình, thao tác trong hàm drawSprite()

void drawSprite()
{
	// khóa surface để sprite có thể vẽ 
	// D3DXSPRITE_ALPHABLEND hỗ trợ vẽ trong suốt nếu không thì để giá trị NULL
	sprite_handler->Begin(NULL);

	// cài đặt vị trí cần hiển thị lên màn hình
	D3DXVECTOR3 position(0, 0, 0);

	sprite_handler->Draw(
		texture,				        // texture lưu sprite
		NULL,				            // diện tích cần hiển thị 
		NULL,				            // tâm dùng để vẽ, xoay
		&position,			            // vị trí sprite
		D3DCOLOR_XRGB(255, 255, 255)	// màu thay thế
	);

	// mở khóa surface để tiến trình khác sử dụng
	sprite_handler->End();
}

Kết quả

show_color__376x253

Đó là 1 chương trình cơ bản nhất. Bây giờ chúng ta sẽ thực hiện một số thao tác tùy chỉnh cho phần vẽ.

Cài đặt Sprite nâng cao

Vẽ Sprite trong suốt (Sprite transparent)

Thực hiện 2 công việc:

  • Tại hàm D3DXCreateTextureFromFileEx() sửa lại mã màu cần xóa, ở ví dụ trên là màu hồng (255, 0, 255).
  • Bật cờ D3DXSPRITE_ALPHABLEND khi gọi hàm LPD3DXSPRITE::Begin()
D3DCOLOR_XRGB(255, 0, 255), // chỉ ra màu trong suốt

// khóa surface để sprite có thể vẽ 
// D3DXSPRITE_ALPHABLEND hỗ trợ vẽ trong suốt nếu không thì để giá trị NULL
sprite_handler->Begin(D3DXSPRITE_ALPHABLEND);

Kết quả

show__367x249

Thay đổi tâm vẽ

Tâm vẽ là một điểm trên Sprite lấy làm mốc để vẽ lên một vị trí nào đó trên màn hình, nó cũng là điểm làm mốc khi thực hiện các chuyển đổi scale, rotation, flip, translate. Giá trị NULL được hiểu ngầm là vị trí (0, 0, 0) trên sprite. (góc trái phía trên)

center_point_spirte__480x252
Biến center là tọa độ làm mốc

Tạo biến center lưu tọa độ cần làm mốc. Ví dụ bên dưới sẽ hiển thị Sprite ở vị trí position(0, 0, 0) góc trái phía trên màn hình và lấy điểm chính giữa của Sprite làm mốc để hiển thị.

// vị trí cần hiển thị lên màn hình
D3DXVECTOR3 position(0, 0, 0);

// lấy điểm chính giữa, info lưu thông tin sprite
D3DXVECTOR3 center(info.Width / 2, info.Height / 2, 0);

sprite_handler->Draw(
	texture,	
	NULL,		 
	&center,	// tâm dùng để vẽ, xoay
	&position,	// vị trí sprite
	D3DCOLOR_XRGB(255, 255, 255) 
);

Kết quả

center_point__367x244

Chúng ta cần chú ý đến tâm vẽ của Sprite để sau này có áp dụng một cách linh hoạt và nó rất cần thiết khi thao tác với Sprite animation.

Chỉ hiển thị một phần của Sprite lên màn hình

pikachu_srect%20Finally_2__300x265

Khai báo biến srect để xác định diện tích cần vẽ.

// chỉ hiển thị 100x100
RECT srect;
srect.left = 115;	                 // tọa độ x
srect.top = 110;		             // tọa độ y
srect.right = rect.left + 100;	     // tọa độ x'
srect.bottom = rect.top + 100;	     // tọa độ y'

Thêm srect vào hàm sprite_handler->Draw()

sprite_handler->Draw(
	texture,			
	&rect,		// diện tích cần hiển thị 
	NULL,			
	NULL,		
	D3DCOLOR_XRGB(255, 255, 255)	
);

Kết quả

srect_show__350x237

Scale (độ phóng đại), Flip (lật), Rotation (xoay), Translation (dịch chuyển)

Trong Direct3D, ma trận được sử dụng lưu thông tin trong suốt quá trình chuyển đổi, tùy theo chúng ta thực hiện thao tác nào sẽ gọi hàm khởi tạo ma trận đó tương ứng.

Translation

transition__480x231
Dịch chuyển đối tượng sang tọa độ (100, 90, 0)

// khai báo ma trận lưu thông tin dịch chuyển
D3DXMATRIX matTranslate;

// khởi tạo ma trận để duy chuyển 100 theo trục Ox và 90 theo trục Oy 
D3DXMatrixTranslation(&matTranslate, 100.f, 90.0f, 0.0f);

// Thực hiện việc chuyển đổi.
sprite_handler->SetTransform(&matTranslate);

Đây cũng là một cách để duy chuyển Sprite thay vì phải tạo một biến position (lưu vị trí Sprite trên màn hình) như những ví dụ trước. Một điểm cần lưu ý, khi bạn sử dụng ma trận để dịch chuyển Sprite thì chúng ta không cần phải tạo biến position mà hãy để giá trị ở đối số thứ 4 của hàm sprite_handler->Draw() là NULL. Nếu bạn sử dụng cả 2 cách để dịch chuyển thì giá trị dịch chuyển cuối cùng sẽ được cộng lại (x cộng theo x, y cộng theo y). 

Scaling

// ma trận lưu thông tin về tỉ lệ
D3DXMATRIX matScale;

// khởi tạo ma trận phóng to theo trục Ox 2 lần, trục Oy 3 lần.
D3DXMatrixScaling(&matScale, 2.0f, 3.0f, .0f);

// thực hiện việc chuyển đổi.
sprite_handler->SetTransform(&matScale);
 

scale__480x243
Scale đối tượng 3 lần theo Oy, 2 lần theo Ox

Flipping

Cách đơn giản nhất là ta sẽ thực hiện dựa trên thao tác scale, thêm dấu ‘ - ’ trước trục Ox/Oy cần flip.

// khởi tạo ma trận phóng to theo trục Ox 1 lần, trục Oy 1 lần.
// flip theo chiều dọc (trục Oy)
D3DXMatrixScaling(&matScale, 1.0f, -1.0f, .0f);

// flip theo chiều ngang (trục Ox) và chiều dọc (trục Oy)
D3DXMatrixScaling(&matScale, -1.0f, -1.0f, .0f);

// phóng to theo trục Ox 2 lần, trục Oy 2 lần
// và flip theo chiều ngang
D3DXMatrixScaling(&matScale, -2.0f, 2.0f, .0f);

flip__480x284
Thao tác Flip trên trục Ox/Oy, với tâm Sprite là NULL

Để dễ kiểm soát cho việc chọn điểm mốc để flip và vị trí hiển thị lên màn hình ta sẽ kết hợp với tâm vẽ như đã giới thiệu ở nội dung phía trên.

// lấy điểm chính giữa sprite
D3DXVECTOR3 center(info.Width/2, info.Height/2, 0);

sprite_handler->Draw(
	texture,						
	NULL,							 
	&center, //tọa độ làm mốc để flip và vị trí vẽ trên màn hình 
	NULL, // giá trị null
	D3DCOLOR_XRGB(255, 255, 255)	
);

flip_center_point__480x184
Thao tác Flip, điểm làm mốc là tọa độ chính giữa của Sprite

Một điểm cần lưu ý, khi thực hiện flip Sprite nếu muốn dịch chuyển thì cách tốt nhất chúng ta sẽ sử dụng ma trận để dịch chuyển. Nếu sử dụng cách tạo biến position thì kết quả sẽ không như mong muốn, để khắc phục ta thêm dấu ‘ - ’ trước tọa độ x và y như sau position(-x, -y, 0).

Rotation

// ma trận lưu thông tin xoay
D3DXMATRIX matRotate;

// khởi tạo ma trận xoay 30 độ từ phải sang trái
D3DXMatrixRotationZ(&matRotate, D3DXToRadian(30.0f));

// Thực hiện việc chuyển đổi.
sprite_handler->SetTransform(&matRotate);

Đối số thứ 2 của hàm D3DXMatrixRotationZ() là góc mà chúng ta muốn xoay được tính bằng radian.
Direct3D cũng cung cấp một số hàm xoay trong 3D như D3DXMatrixRotationX() xoay theo trục Ox, D3DXMatrixRotationY()  xoay theo trục Oy, nhưng ở 2D chúng ta chỉ cần quan tâm đến hàm D3DXMatrixRotationZ() xoay theo trục Oz.

rotation__480x200
Thao tác xoay, với mốc xoay có tọa độ (0, 0, 0) trên Sprite

Cũng giống như thao tác flipping, chúng ta cũng có thể kết hợp với tâm vẽ chọn điểm làm mốc để xoay và vẽ Sprite lên màn hình.

Kết hợp Translation, Rotation, Flip và Scale

Trong thực tế khi thao tác với Sprite đôi khi chúng ta cần phải thực hiện nhiều thao tác chuyển đổi cùng lúc. Nếu sử dụng cách thông thường là gọi nhiều hàm sprite_handler->SetTransform() để thực hiện việc chuyển đổi tương ứng thì khi đó sẽ có vấn đề xảy ra, chúng ta chỉ thực hiện được chuyển đổi cho lời gọi hàm cuối cùng.

Giải quyết vấn đề này bằng cách chúng ta sẽ nhân các ma trận lại với nhau thành một ma trận mới và gọi hàm sprite_handler->SetTransform() để thực hiện việc chuyển đổi.

// nhân các ma trận lưu thông tin các chuyển đổi
sprite_handler->SetTransform(&(matScale * matRotate * matTranslate));

Hoặc có thể khai báo một ma trận mặc định (identity matrix) để lưu kết quả các phép nhân.

// khai báo ma trận mặc định
D3DXMATRIX matCombined;
// khởi tạo ma trận
D3DXMatrixIdentity(&matCombined);

// nhân với tỉ lệ phóng đại/ lật sprite
matCombined *= matScale;
// nhân với ma trận xoay
matCombined *= matRotate;
// nhân với ma trận dịch chuyển
matCombined *= matTranslate;

// áp dụng các chuyển đổi
sprite_handler->SetTransform(&matCombined);

Vẽ nhiều Sprite cùng lúc

Để vẽ nhiều Sprite thì chỉ cần thiết lập thêm hàm sprite_handler->Draw() miễn sao nó phải nằm trong hàm sprite_handler->Begin()sprite_handler->End(). Hoặc bạn có thể tạo thêm nhiều cặp Begin()End() để vẽ khi muốn thay đổi chế độ trong suốt cho phù hợp với mục đích sử dụng.

Sprite vẽ sau hiển thị đè lên Sprite vẽ trước.

void drawSprite()
{
	RECT rect;
	rect.left = 115;				 // tọa độ x
	rect.top = 110;					 // tọa độ y
	rect.right = rect.left + 200;	 // tọa độ x'
	rect.bottom = rect.top + 200;	 // tọa độ y'

	// cài đặt vị trí cần hiển thị lên màn hình
	D3DXVECTOR3 position(200, 100, 0);
	D3DXVECTOR3 position2(100, 50, 0);

	// biến info lưu thông tin sprite
	D3DXVECTOR3 center(info.Width / 2, info.Height / 2, 0);

	// khóa surface để sprite có thể vẽ 
	// D3DXSPRITE_ALPHABLEND hỗ trợ vẽ trong suốt nếu không thì để giá trị NULL
	sprite_handler->Begin(D3DXSPRITE_ALPHABLEND);

	sprite_handler->Draw(
		texture,		                 // texture lưu sprite
		NULL,				             // diện tích cần hiển thị 
		&center,			             // tâm dùng để vẽ, xoay
		NULL,				             // vị trí sprite
		D3DCOLOR_XRGB(255, 255, 255)     // màu thay thế
	);

	sprite_handler->Draw(
		texture,						
		&rect,							
		NULL,						
		&position2,						
		D3DCOLOR_XRGB(255, 255, 255)	
	);

	// mở khóa surface để tiến trình khác sử dụng
	sprite_handler->End();

	// vẽ sprite không trong suốt
	sprite_handler->Begin(NULL);
	sprite_handler->Draw(
		texture,				
		NULL,						 
		NULL,						
		&position,						
		D3DCOLOR_XRGB(255, 255, 255)	
	);
	sprite_handler->End();
}

Kết quả cho ví dụ trước

multi__350x237

Tổng kết

Source code đầy đủ LoadSprite tại https://bugs.vn/8575.

Bài viết đã làm rõ các khái niệm thường được đề cập đến khi chúng ta lập trình game với DirectX, cung cấp một cái nhìn tổng quan về Sprite và đi đến cài đặt Sprite một cách chi tiết. Như vậy, chúng ta đã giải quyết được các vấn đề đã đặt ra, tôi hy vọng qua bài viết này sẽ giúp ích nhiều cho tất cả các bạn đặc biệt đối với những lập trình viên mới tiếp cận lập trình game với DirectX.

THẢO LUẬN
ĐÓNG