Tài trợ bài viết này và giới thiệu dịch vụ, sản phẩm, thương hiệu, nhu cầu tuyển dụng của doanh nghiệp đến với cộng đồng.
Huỳnh Minh Tân Thao tác 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ẽ hướng dẫn tạo tập tin lưu tọa độ các tile từ một sprite sheet có sẵn và hiện thực Sprite animation một cách chi tiết trên nền DirectX9.
Nội dung bài viết

Giới thiệu

Trong bài viết này tôi và các bạn cùng tìm hiểu về Sprite animation, cách tạo mới một tập tin lưu tọa độ các tile từ một sprite sheet có sẵn, cuối cùng là công việc cài đặt Sprite animation chi tiết trên nền 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 OOP, lập trình game cơ bản (loop, fps) và cài đặt được môi trường hỗ trợ DirectX9.

Tham khảo trước bài viết Load Sprite Trong DirectX 9 để có kiến thức cơ bản trước khi bắt đầu.

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_reszie

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ì có lẽ nghe đến 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 hay Frame trong hoàn cảnh này.

tile_spirte_sheet_resize

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ạo tập tin lưu tọa độ các tile từ sprite sheet có sẵn

Để thực hiện việc load Sprite animation điều đầu tiên chúng ta phải có một sprite sheet cần load và một tập tin lưu tọa độ, chiều cao và chiều rộng của tất cả các tile có trong sprite sheet ấy, tập tin này rất cần thiết cho việc load Sprite animation bởi nó chỉ cho chúng ta biết phần nào sẽ hiển thị lên màn hình. Nhưng có một vấn đề chúng ta thường hay gặp phải, khi các bạn tìm resource cho game đa phần các bạn dễ dàng tìm và tải được sprite sheet còn tập tin lưu tọa độ các tile thông thường sẽ không thấy đâu ở hầu hết các website tải resource miễn phí.

Để giải quyết, chúng ta sẽ tự tay tạo ra tập tin lưu tọa độ này. Có nhiều cách để thực hiện và nhiều phần mềm hỗ trợ các bạn có thể tìm hiểu, một lưu ý nhỏ là một vài phần phần mềm sẽ không thể nhận dạng được một vài sprite sheet:

  • Sprite Extractor :: www.thejollyprogrammer.com/portfolio/sprite-extractor/
  • ShoeBox :: www.renderhjs.net/shoebox/
  • Texture Packer :: www.codeandweb.com/texturepacker
  • Alferd Spritesheet Unpacker :: www.github.com/ForkandBeard/Alferd-Spritesheet-Unpacker
  • Sprite Splitter :: www.github.com/bmarwane/spriteSplitter

Trong bài viết này, tôi sẽ giới thiệu cho các bạn 2 cách mà nhiều người thường hay áp dụng.

Genesis_aladdin

Tập tin sprite sheet chứa hành động của Aladdin Genesis.

Mục đích cuối cùng chúng ta sẽ lưu tọa độ (x, y), chiều dài và chiều rộng của tất các tile vào một tập tin có định dạng tùy ý phù hợp với mục đích sử dụng. Bài viết này tôi sẽ xuất ra tập tin có định dạng XML.

Cách 1: Từ sprite sheet có sẵn cắt thành nhiều tile

Từ sprite sheet có sẵn tiến hành cắt thành các tile riêng lẻ sau đó ghép lại thành một sprite sheet mới cùng lúc đó một tập tin XML lưu tọa độ tile cũng được tạo ra. Dùng phần mềm Alferd Spritesheet Unpacker hoặc Sprite Splitter để cắt, tôi sẽ sử dụng Alferd Spritesheet Unpacker để thực hiện.

Sau khi tải về chúng ta kéo thả sprite sheet vào chương trình. Ban đầu phần mềm sẽ tự động xác định các tile bằng việc đống khung chúng lại, nhưng đối với trường hợp tile nào có nhiều chi tiết nằm rời nhau thì kết quả nhận dạng sẽ không theo ý muốn, chúng ta phải dùng tay định vị lại.

open_first_resize

Ví dụ, Aladdin và quả táo là một tile nhưng phần mềm lại xác định là hai tile riêng biệt, chúng ta sẽ chọn cả hai tile đó và nhấn vào Combine Selected để gộp lại thành một tile. Các bạn thao tác tương tự với các tile khác.

combine_tile_edit_resize(1)

Sau khi gộp các tile không phù hợp với yêu cầu, công việc tiếp theo là lựa chọn tile nào muốn xuất ra bằng việc nhấp chuột vào tile đó. Cần chú ý, phải chọn lần lượt các tile theo đúng thứ tự cho animation đó (tile nào diễn ra trước phải chọn trước), vì khi các tile được cắt ra sẽ được đặt tên theo số thứ tự tăng dần và bắt đầu từ 0. Sao chép và dán đường dẫn nơi lưu tập tin vào Export Location, tùy chỉnh lại định dạng .bmp và nền màu hồng (trong Options). Cuối cùng nhấn Export Selected, ta được kết quả.

cuted_resize

Tiếp theo dùng phần mềm TexturePacker để ghép các tile lại và xuất ra tập tin XML.

textutepacker_tile_edit_resize

  1. Tùy chỉnh định dạng tập tin lưu tọa độ tile, ở ví dụ này là XML.
  2. Chọn đường dẫn lưu tập tin.
  3. Tùy chỉnh định dạng tập tin sprite sheet.
  4. Tùy chỉnh nén tile. 
  5. Thực hiện công việc xuất tập tin sprite sheet và XML.

Kết quả

spriteSheet_aladdin_stand.png

aladdin_stand

spriteSheet_aladdin_stand.xml

<?xml version="1.0" encoding="UTF-8"?>
<TextureAtlas imagePath="aladdin_stand.png" width="255" height="426">
    <sprite n="0.bmp" x="166" y="1" w="37" h="50" pX="0.5" pY="0.5"/>
    <sprite n="1.bmp" x="1" y="1" w="41" h="47" pX="0.5" pY="0.5"/>
    <sprite n="2.bmp" x="45" y="54" w="40" h="52" pX="0.5" pY="0.5"/>
    <sprite n="3.bmp" x="129" y="220" w="44" h="55" pX="0.5" pY="0.5"/>
   ...
    <sprite n="35.bmp" x="129" y="277" w="42" h="62" pX="0.5" pY="0.5"/>
    <sprite n="36.bmp" x="88" y="164" w="41" h="54" pX="0.5" pY="0.5"/>
    <sprite n="37.bmp" x="131" y="164" w="40" h="54" pX="0.5" pY="0.5"/>
    <sprite n="38.bmp" x="1" y="164" w="39" h="53" pX="0.5" pY="0.5"/>
</TextureAtlas>

Cách 2: Từ sprite sheet có sẵn tự tay tạo tập tin lưu tọa độ các tile

Đầu tiên tạo một tập tin XML rỗng. Truy cập trang www.spritecow.com, trang này có chức năng tự nhận dạng, xác định vị trí, chiều cao, chiều rộng của tile trong tập tin sprite sheet. Nhấn Open Image chọn sprite sheet cần thực hiện, các bạn đợi vài giây để website tải ảnh lên, sau đó nhấn Pick Background chọn hoặc kéo để đống khung các tile và đọc thông tin x, y, w, h tương ứng rồi ghi vào tập tin XML. (lưu ý bỏ dấu "-" trước x, y). Cuối cùng, chúng ta được tập tin XML và sprite sheet.

spritecow_edit_resize

Cài đặt Sprite animation sử dụng đối tượng D3DXSprite trong DirectX9

Việc hiện thực Sprite animation có nhiều cách, bạn có thể cắt riêng lẻ từng tile rồi hiển thị chúng một cách liên tiếp, hoặc thao tác trên một sprite sheet lý tưởng (các tile có cùng chiều cao, chiều rộng và cách nhau với một tỉ lệ nhất định) muốn có source rect để hiển thị chúng ta sử dụng công thức “%” và “/” khi đã biết số dòng và số cột của tile. Trong bài viết này tôi và các bạn sẽ cùng nhau cài đặt Sprite animation theo cách tổng quát kết hợp giữa tập tin sprite sheet và tập tin XML.

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 animation.

struct_code

Cấu trúc code load Sprite animation

Hãy mở Visual Studio -> tạo mới một project và cài đặt môi trường hỗ trợ DirectX9 -> 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 animation.

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

Cài đặt Sprite animation với tập tin XML

Tạo mới tập tin CAnimationSprite.h, khai báo lớp CAnimaionSprite và tạo mới lớp CTile để lưu thông tin x, y, w, h của các tile trong sprite sheet.

#pragma once
#include <Windows.h>
#include <d3dx9.h>
#include <list>

// include lib extend
#include "tinyxml.h"
using namespace std;

class CTile;

class CAnimationSprite
{
public:
	CAnimationSprite(LPDIRECT3DDEVICE9);
	~CAnimationSprite();

	bool Init();

	bool Load(LPWSTR ImagePath, const char* XMLPath, D3DCOLOR Transcolor);

	// DeltaTime; X; Y; Scale; AnimationRate; FlipX
	void Render(float DeltaTime, float X, float Y, float Scale, float AnimationRate, float FlipX = 1);

	void Release();

	// read file XML store list tile
	bool ReadXML(const char* XMLPath);

private:

	list<CTile>		_ListTile;			  // lưu tất cả các tile trong sprite sheet
	int				_IndexTile;			  // chỉ số hiển thị tile theo vòng lặp
	float			_AnimationRate_Index; // chỉ số thời gian deplay cho việc chuyển tile

	// texture store sprite sheet
	LPDIRECT3DTEXTURE9		_Texture;
	// handler to sprite
	LPD3DXSPRITE			_SpriteHandler;
	// store device
	LPDIRECT3DDEVICE9		_d3ddv;
	// sotre infomation sprite
	D3DXIMAGE_INFO			_Info;

};

class CTile
{
public:
	CTile();
	~CTile();

	void SetTile(const char* Name, int X, int Y, int Width, int Height);
	CTile GetTile();

	RECT GETLocaTionTile();

private:
	RECT		_RectTile;
	const char*	_NameTile;
};

Tạo mới tập tin CAnimationSprite.cpp và định nghĩa lớp CTile.

CTile::CTile()
{
	_RectTile.top = 0;
	_RectTile.bottom = 0;
	_RectTile.left = 0;
	_RectTile.right = 0;

	_NameTile = nullptr;
}

CTile::~CTile()
{
}

void CTile::SetTile(const char* Name, int X, int Y, int Width, int Height)
{
	_NameTile = Name;

	_RectTile.left = X;
	_RectTile.top = Y;
	_RectTile.right = Width;
	_RectTile.bottom = Height;
}

CTile CTile::GetTile()
{
	CTile temp;

	temp._NameTile = _NameTile;
	temp._RectTile = _RectTile;

	return temp;
}

RECT CTile::GETLocaTionTile()
{
	RECT rectTemp;
	rectTemp.top = _RectTile.top;
	rectTemp.left = _RectTile.left;
	rectTemp.bottom = _RectTile.bottom;
	rectTemp.right = _RectTile.right;

	return rectTemp;
}

Định nghĩa các phương thức constructor, destructor, Init và release.

CAnimationSprite::CAnimationSprite(LPDIRECT3DDEVICE9 d3ddv)
{
	_Texture = nullptr;
	_SpriteHandler = nullptr;
	_d3ddv = d3ddv;
	_IndexTile = 0;
	_AnimationRate_Index = 0;
}

CAnimationSprite::~CAnimationSprite()
{
}

bool CAnimationSprite::Init()
{
	// sprite handler associate main Direct3D and Device that how to draw sprite at backbuffer
	HRESULT res = D3DXCreateSprite(_d3ddv, &_SpriteHandler);

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

	return true;
}

void CAnimationSprite::Release()
{
	if (_Texture != nullptr)
		_Texture->Release();

	if (_d3ddv != nullptr)
		_d3ddv->Release();

	_ListTile.clear();
}

Để đọc tập tin XML tôi sử dụng thư viện TinyXML các bạn có thể tham khảo bài viết Thao Tác Với XML Sử Dụng TinyXML Trong C++. Định nghĩa hàm CAnimationSprite::ReadXML(), tiến hành lưu thông tin tất cả các tile vào _ListTile.

bool CAnimationSprite::ReadXML(const char* XMLPath)
{
	TiXmlDocument doc(XMLPath);

	if (!doc.LoadFile())
	{
		MessageBox(NULL, L"Failed to read file XML\nPlease check path file XML", L"Error", MB_OK);
		return false;
	}

	// get info root
	TiXmlElement* root = doc.RootElement();
	TiXmlElement* tile = nullptr;

	// loop to get element name, x, y, width, height
	for (tile = root->FirstChildElement(); tile != NULL; tile = tile->NextSiblingElement())
	{
		int x, y, w, h;
		const char* nameTileTemp;
		CTile TileTemp;

		// get value from file xml
		nameTileTemp = tile->Attribute("n");
		tile->QueryIntAttribute("x", &x);
		tile->QueryIntAttribute("y", &y);
		tile->QueryIntAttribute("w", &w);
		tile->QueryIntAttribute("h", &h);

		TileTemp.SetTile(nameTileTemp, x, y, w, h);

		// add into ListTile
		_ListTile.push_back(TileTemp);
	};

	return true;
}

Định nghĩa hàm CAnimationSprite::Load(), công việc tương tự như load một sprite và điều quan trọng là khởi tạo danh sách các tile bằng việc gọi hàm ReadXML().

bool CAnimationSprite::Load(LPWSTR ImagePath, const char* XMLPath, D3DCOLOR Transcolor)
{
	// get infomation (widht, height) sprite sheet
	HRESULT result = D3DXGetImageInfoFromFile(ImagePath, &_Info);

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

	// create texture from sprite sheet
	result = D3DXCreateTextureFromFileEx(
		_d3ddv,
		ImagePath,
		_Info.Width,
		_Info.Height,
		1,
		D3DUSAGE_DYNAMIC,
		D3DFMT_UNKNOWN,
		D3DPOOL_DEFAULT,
		D3DX_DEFAULT,
		D3DX_DEFAULT,
		Transcolor,
		&_Info,
		NULL,
		&_Texture
	);

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

	// read file XML
	if (!ReadXML(XMLPath))
	{
		PostQuitMessage(WM_QUIT);
	}

	return true;
}

Cuối cùng, định nghĩa phương thức CAnimationSprite::Render(), hàm này có chức năng hiển thị lần lượt tất cả các tile có trong _ListTile và được lặp lại liên tục để tạo ra hiệu ứng animation. Trong thực tế khi bạn lập trình tùy theo mục đích sử dụng bạn có thể tùy biến lại cấu trúc hay chức năng của nó. Ví dụ, hiển thị lần lượt các tile có chỉ số từ IndexTileStart đến tile có chỉ số IndexTileEnd trong _ListTile và được lặp lại liên tục, nó sẽ rất hữu dụng nếu bạn muốn hiển thị một animation khi có nhiều animation trong một sprite sheet.

  • DeltaTime: thời gian chuyển đổi giữa 2 frame.
  • X, Y: vị trí X, Y muốn hiển thị lên màn hình.
  • ScalesSize: tỉ lệ phóng đại.
  • AnimationRate: thời gian deplay chuyển đổi giữa hai tile.
  • FlipX: lật theo trục Ox, -1 là lật nếu không để trống.
void CAnimationSprite::Render(float DeltaTime, float X, float Y, float ScaleSize, float AnimationRate, float FlipX)
{
	D3DXMATRIX Combined;

	D3DXMATRIX Scale;
	D3DXMATRIX Translate;

	// Initialize the Combined matrix.
	D3DXMatrixIdentity(&Combined);

	// set location
	D3DXVECTOR3 position((float)X, (float)Y, 0);
	// Scale the sprite.
	D3DXMatrixScaling(&Scale, FlipX * ScaleSize, ScaleSize, ScaleSize);
	Combined *= Scale;
	// Translate the sprite
	D3DXMatrixTranslation(&Translate, X, Y, 0.0f);
	Combined *= Translate;
	// Apply the transform.
	_SpriteHandler->SetTransform(&Combined);

	auto l_front = _ListTile.begin();
	advance(l_front, _IndexTile);

	// set position tile
	RECT srect;
	srect.left = l_front->GETLocaTionTile().left;
	srect.top = l_front->GETLocaTionTile().top;
	srect.bottom = srect.top + l_front->GETLocaTionTile().bottom;
	srect.right = srect.left + l_front->GETLocaTionTile().right;

	// get anchor point of sprite
	D3DXVECTOR3 center = D3DXVECTOR3((srect.right - srect.left) / 2, srect.bottom, 0);

	_SpriteHandler->Begin(D3DXSPRITE_ALPHABLEND);

	_SpriteHandler->Draw(
		_Texture,
		&srect,
		&center,
		NULL,    
		D3DCOLOR_XRGB(255, 255, 255)
	);

	_SpriteHandler->End();

	if (_AnimationRate_Index > AnimationRate)
	{
		_AnimationRate_Index = 0;
		_IndexTile = ((_IndexTile + 1) % _ListTile.size());
	}

	_AnimationRate_Index += DeltaTime;
} 

Như vậy chúng ta đã hoàn thành việc cài đặt lớp CAnimationSprite, để vẽ ta sẽ thực hiện việc khai báo, khởi tạo một thể hiện của CAnimationSprite và gọi hàm render() trong lớp CGame.

// Include lớp CAnimationSprite trong CGame.h
#include "CAnimationSprite.h"
// Khai báo hằng resource trong CGame.h
#define SPRITE_TITLE_ALADDIN L"resource/spriteSheet_aladdin_walk.bmp"
#define SPRITE_TITLE_ALADDIN_XML "resource/spriteSheet_aladdin_walk.xml"

// Khai báo một Sprite animation trong CGame.h
CAnimationSprite*	_AspAladdin;	

// Khởi tạo giá trị mặc định trong CGame::CGame()
_AspAladdin = NULL;

// Khởi tạo bên trong CGame::Init()
_AspAladdin = new CAnimationSprite(_d3ddv);
_AspAladdin->Init();
_AspAladdin->Load(SPRITE_TITLE_ALADDIN, SPRITE_TITLE_ALADDIN_XML, D3DCOLOR_XRGB(255, 0, 255));

// Gọi hàm trong CGame::render()
_AspAladdin->Render(DeltaTime, 350, 420, 2.5, 50, -1);
//// Vẽ lên màn hình tọa độ (350, 420)
//// Tỉ lệ scale: 2.5
//// AnimationRate: 50
//// Flip tile theo trục Ox

// Giải phóng đối tượng trong CGame::Terminate()
_AspAladdin->Release();

// Thu hồi vùng nhớ trong CGame::~CGame()
delete _AspAladdin;

Lời kết

Qua bài viết này, chúng ta đã biết cách tạo một tập tin lưu tọa độ tile từ sprite sheet có sẵn và hoàn thành cài đặt một sprite animation theo cách tổng quát nhất. Trên đó là một ví dụ điển hình cho việc cài đặt load Sprite animation, nhưng trong thực tế khi viết game các bạn sẽ phải tùy biến hoặc tổ chức lại code sao cho phù hợp yêu cầu bài toán hoặc đúng với mục đích sử dụng. Chúc các bạn thành công.

Download demo

Source code đầy đủ tại Bug.vn, SpriteAnimationSheet :: https://bugs.vn/8683.

Tải solution Demo_SpriteAnimationSheet_STDIO_VS2017.zip

THẢO LUẬN
Tôi luôn ước rằng máy tính của mình dễ sử dụng như điện thoại của mình, điều ước đó đã thành hiện thực khi tôi không biết làm sao để sử dụng điện thoại của mình nữa. Bjarne Stroustrup
300x250

Liên hệ tài trợ cho STDIO

Tham gia tài trợ STDIO và đặt quảng cáo theo định kỳ của cá nhân, cửa hàng, doanh nghiệp của bạn tại đây.
Gọi: 028.36205514 www.stdio.vn/contact
ĐÓNG