Lập trình game 2D trên HTML5, Phần 4: Sprites

Trong loạt bài này, David Geary sẽ hướng dẫn bạn từng bước thực hiện trò chơi video HTML5 2D. Sprites — đối tượng đồ họa mà bạn có thể gán các hành vi đến — là một trong những khía cạnh cơ bản nhất và quan trọng của trò chơi video. Trong phần này, bạn sẽ học làm thế nào để thực hiện sprite để tạo nên sự chuyển động của các nhân vật trong Snail Bait.

pdf35 trang | Chia sẻ: lylyngoc | Lượt xem: 1480 | Lượt tải: 2download
Bạn đang xem trước 20 trang tài liệu Lập trình game 2D trên HTML5, Phần 4: Sprites, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
Lập trình game 2D trên HTML5, Phần 4: Sprites Tiến hành phân vai các nhân vật trong Snail Bait Trong loạt bài này, David Geary sẽ hướng dẫn bạn từng bước thực hiện trò chơi video HTML5 2D. Sprites — đối tượng đồ họa mà bạn có thể gán các hành vi đến — là một trong những khía cạnh cơ bản nhất và quan trọng của trò chơi video. Trong phần này, bạn sẽ học làm thế nào để thực hiện sprite để tạo nên sự chuyển động của các nhân vật trong Snail Bait. Giống như các lĩnh vực nghệ thuật khác — chẳng hạn như phim, kịch và tiểu thuyết — trò chơi có một dàn các nhân vật, mỗi nhân vật có một vai trò cụ thể. Ví dụ, Snail Bait có nhân vật "Runner" (giữ vai chính của trò chơi), các đồng tiền, ngọc ruby, ngọc sapphire, các con ong, con dơi, các nút bấm, và một con ốc sên, phần lớn chúng đều có trong Hình 1. Trong bài viết đầu tiên của loạt bài này (xem phần Các hình ảnh 2D: Phân vai các nhân vật), tôi đã thảo luận về các nhân vật này và vai trò của chúng. Hình 1. Các nhân vật của Snail Bait Mỗi nhân vật trong Snail Bait là một sprite. Sprites là đối tượng đồ họa mà bạn có thể cấp cho nó các hành vi; ví dụ, nhân vật có thể chạy, nhảy, rơi, và va chạm với các sprite khác, trong khi đó, các viên ngọc lấp lánh, nhúc nhích lên xuống, và biến mất khi chúng va chạm với nhân vật. Sử dụng thuật ngữ Sprite Những người thực hiện bộ xử lý hiển thị video của nhạc cụ Texas 9918A lần đầu tiên sử dụng thuật ngữ sprite cho các nhân vật hoạt hình. (Trong tiếng Anh chuẩn, từ sprite — đến từ tiếng Latin spiritus — nghĩa là yêu tinh hay thần tiên.) Sprites cũng được thực hiện trong cả phần cứng và phần mềm; ví dụ máy tính Commodore Amiga sản xuất năm 1985 đã hỗ trợ đến 8 sprite về phần cứng. Vì sprite là một trong những khía cạnh cơ bản nhất của bất kỳ trò chơi, và vì trò chơi thường có nhiều sprite, nó có ý nghĩa để đóng gói khả năng cơ bản của chúng trong các đối tượng có thể tái sử dụng. Trong bài viết này, bạn sẽ học làm thế nào để:  Thực hiện một đối tượng Sprite mà bạn có thể tái sử dụng trong bất kỳ trò chơi nào.  Tách rời sprite từ các đối tượng vẽ chúng (sprite artists) để linh động vào lúc khởi chạy.  Dùng sprite sheets để giảm thời gian khởi động và các yêu cầu về bộ nhớ.  Tạo sprite với siêu dữ liệu.  Kết hợp sprite vào một vòng lặp. Các đối tượng Sprite Tôi thiết lập sprite của Snail Bait như là các đối tượng Javascript để có thể sử dụng trong bất kỳ trò chơi nào, vì vậy sprite sẽ nằm ở tệp riêng. Và tôi sẽ gọi tệp đó trong đoạn mã HTML của Snail Bait như sau: . Bảng 1 liệt kê các thuộc tính của Sprite: Bảng 1. Các thuộc tính của Sprite Thuộc tính Chú giải artist Đối tượng để vẽ một sprite. behaviors Một mảng các trạng thái, mỗi trạng thái là các thao tác chuyển động của sprite trong một vài Thuộc tính Chú giải kiểu hình dạng. left Tọa độ X ở góc trên bên trái của sprite. top Tọa độ Y ở góc trên bên trái của sprite. width Chiều ngang của sprite được tính theo pixel. height Chiều cao của sprite được tính theo pixel. opacity Trạng thái mờ, trong suốt hoặc kết hợp cả hai trạng thái của sprite. type Một dòng chú thích loại của sprite, như bat (con dơi), bee (con ong), hay runner (nhân vật). velocityX Tốc độ ngang của sprite, được tính theo số pixel trên giây. velocityY Tốc độ đứng của sprite, được tính theo số pixel trên giây. visible Tình trạng hiển thị của sprite. Nếu giá trị là false thì sprite sẽ không được vẽ. Sprite là những đối tượng đơn giản để duy trì thông tin tọa độ và kích cỡ của nó (thường được biết như là một bounding box (hộp biên)), tốc độ và tình trạng hiển thị. Nó còn có kiểu riêng để phân biệt sprite này với sprite khác và độ mờ của nó, nghĩa là từng phần riêng của sprite có thể trong suốt. Sprite còn được ủy quyền cho các đối tượng khác để vẽ nó hoặc thao tác tới nó, tương ứng với artist và behaviors. Liệt kê 1 là đoạn mã khởi tạo một Sprite, và nó thiết lập một số giá trị mặc định cho các thuộc tính của sprite: Liệt kê 1. Khởi tạo Sprite var Sprite = function (type, artist, behaviors) { // constructor this.type = type || ''; this.artist = artist || undefined; this.behaviors = behaviors || []; this.left = 0; this.top = 0; this.width = 10; // Something other than zero, which makes no sense this.height = 10; // Something other than zero, which makes no sense this.velocityX = 0; this.velocityY = 0; this.opacity = 1.0; this.visible = true; return this; }; Trình diễn và hành vi Dấu hiệu của các phương thức của Sprite bắt buộc phải tách riêng mối liên hệ giữa sự trình diễn và hành vi: phương thức draw() dùng Canvas để vẽ sprite, trong khi đó phương thức update() được thiết kế để cập nhật trạng thái của sprite dựa trên thời gian hiện tại và tần số khung hình. Các hành vi không thể vẽ và các artist (đối tượng đồ họa) không thể cập nhật trạng thái. Tất cả các tham số của hàm khởi tạo trong Liệt kê 1 là tùy chọn. Nếu bạn không khởi tạo các hành vi, hàm khởi tạo mặc định tạo một mảng rỗng, và nếu bạn tạo sprite mà không chỉ định kiểu dữ liệu thì kiểu dữ liệu của nó là một chuỗi rỗng. Nếu bạn không chỉ định một artist (đối tượng đồ họa), thì nó sẽ không khởi tạo. Bên cạnh các thuộc tính, sprite còn có hai phương thức được liệt kê trong Bảng 2: Bảng 2. Các phương thức của Sprite Phương thức Chú giải draw(context) Gọi đến phương thức draw() của artist trong sprite nếu sprite được hiện lên và có một artist. update(time, fps) Gọi đến phương thức update() cho mỗi hành vi của sprite. Liệt kê 2 hiển thị đoạn mã thực thi của các phương thức trong Bảng 2: Liệt kê 2. Thực thi các phương thức của Sprite Sprite.prototype = { // methods draw: function (context) { context.save(); // Calls to save() and restore() make the globalAlpha setting temporary context.globalAlpha = this.opacity; if (this.artist && this.visible) { this.artist.draw(this, context); } context.restore(); }, update: function (time, fps) { for (var i=0; i < this.behaviors.length; ++i) { if (this.behaviors[i] === undefined) { // Modified while looping? return; } this.behaviors[i].execute(this, time, fps); } } }; Tốc độ của Sprite: Được xác định bằng số pixel trên giây (pixel/second) Nếu bạn đã đọc qua bài viết thứ hai của loạt bài này (xem tại mục Chuyển động theo thời gian), sự di chuyển của sprite phải độc lập với tần số khung hình của hoạt cảnh trong trò chơi. Và vì vậy, tốc độ của sprite được tính bằng công thức: pixels/second. Bạn có thể thấy trong Liệt kê 1 và Liệt kê 2, sprite thì không phức tạp lắm. Hầu hết sự phức tạp xoay quanh việc sprite được đóng gói vào trong đối tượng đồ họa và các hành vi của nó. Thật cần thiết nếu bạn hiểu rằng bạn có thể thay đổi đối tượng đồ họa và các hành vi của sprite trong lúc chạy chương trình (run time) bởi vì sprite được tách ra từ những đối tượng đó. Thực tế, bạn sẽ thấy trong bài tiếp theo của loạt bài này, rằng hoàn toàn có thể — và mong muốn — thực hiện những hành vi chung được dùng cho nhiều sprite khác nhau. Bây giờ chúng ta sẽ xem cách mà sprite được thực hiện, bạn hãy sẵn sàng xem cách thực hiện đối tượng đồ họa của sprite. Đối tượng đồ họa và các trang của Sprite (Artist và Sprite sheet) Đối tượng đồ họa của Sprite có thể được thực hiện theo một trong ba cách:  Đối tượng Stroke and fill: Vẽ các kiểu đồ họa nguyên thủy như dòng, cung, và đường cong  Đối tượng Image: Vẽ hình ảnh 2D thông qua ngữ cảnh phương thức drawImage()  Đối tượng Sprite sheet: Vẽ hình ảnh từ một trang Sprite (giống như phương thức drawImage()) Bất kể kiểu đối tượng đồ họa nào mà bạn thấy trong Liệt kê 2, đều phải thực hiện một yêu cầu: Chúng phải là đối tượng thực hiện phương thức vẽ draw() mà nó cần một sprite và một ngữ cảnh Canvas 2D (Canvas 2D context) làm tham số. Kế tiếp tôi sẽ thảo luận về mỗi loại đối tượng đồ họa và giải thích về Sprite sheet. Các đối tượng đường nét và phối màu (Stroke và fill) Các đối tượng đường nét và phối màu không có một quy tắc thực hiện tiêu chuẩn; thay vào đó, bạn thực hiện chúng theo phong cách bất kỳ bằng cách sử dụng khả năng đồ họa của ngữ cảnh Canvas 2D. Liệt kê 3 hiển thị cách thực hiện các đối tượng đường nét và phối màu để vẽ các sprite nền tảng của Smail Bait: Liệt kê 3. Các đối tượng đường nét và phối màu // Stroke and fill artists draw with Canvas 2D drawing primitives var SnailBait = function (canvasId) { // constructor ... this.platformArtist = { draw: function (sprite, context) { var top; context.save(); top = snailBait.calculatePlatformTop(sprite.track); // Calls to save() and restore() make the following settings temporary context.lineWidth = snailBait.PLATFORM_STROKE_WIDTH; context.strokeStyle = snailBait.PLATFORM_STROKE_STYLE; context.fillStyle = sprite.fillStyle; context.strokeRect(sprite.left, top, sprite.width, sprite.height); context.fillRect (sprite.left, top, sprite.width, sprite.height); context.restore(); } }, }; Khung nền, như khi bạn thấy từ Hình 1, đơn giản chỉ là các hình chữ nhật. Đối tượng đồ họa của khung nền trong Liệt kê 3 vẽ các hình chữ nhật đó với phương thức strokeRect() và fillRect() của ngữ cảnh Canvas 2D. Bài viết thứ hai trong loạt bài này (xem phần Tổng quan về Canvas của HTML5) để có nhiều thông tin về các phương thức này. Tọa độ và kích cỡ của hình chữ nhật được xác định bởi biên bao bên ngoài. Đối tượng Image Không giống như các đối tượng đường nét và phối màu, đối tượng image có thể thực hiện nó một cách đồng nhất, được hiển thị trong Liệt kê 4: Liệt kê 4. Đối tượng Image // ImageArtists draw an image var ImageArtist = function (imageUrl) { // constructor this.image = new Image(); this.image.src = imageUrl; }; ImageArtist.prototype = { // methods draw: function (sprite, context) { context.drawImage(this.image, sprite.left, sprite.top); } }; Bạn tạo ra hình ảnh bằng URL của nó, và phương thức draw() của đối tượng đồ họa vẽ toàn bộ hình ảnh tại vị trí của sprite. Snail Bait không sử dụng đối tượng Image, bởi vì sẽ hiệu quả hơn khi vẽ từ Sprite sheet. Các Sprite sheet Một trong những cách nhanh chóng để đảm bảo rằng trang web của bạn tải nhanh hơn thì giảm số yêu cầu HTTP đến mức tối thiểu vừa đủ. Phần lớn các trò chơi sử dụng nhiều hình ảnh, và thời gian tải trang sẽ bị ảnh hưởng nếu cứ phải thực hiện các yêu cầu HTTP cho từng tấm hình. Vì nguyên nhân đó, các nhà lập trình trò chơi HTML5 tạo ra một tấm hình lớn duy nhất chứa tất cả các hình ảnh trong trò chơi của họ. Tấm hình duy nhất đó có tên là Sprite sheet. Hình 2 hiển thị Sprite sheet của Snail Bait: Hình 2. Sprite sheet của Snail Bait Cho một Sprite sheet, bạn cần một cách để vẽ một hình chữ nhật cụ thể của sprite lên khung hình ảnh. May thay, ngữ cảnh Canvas 2D cho bạn dễ dàng làm điều đó với phương thức drawImage(). Kỹ thuật đó được sử dụng bởi đối tượng Sprite sheet. Các đối tượng Sprite sheet Liệt kê 5 hiển thị cách thực hiện đối tượng Sprite sheet: Liệt kê 5. Đối tượng Sprite sheet // Sprite sheet artists draw an image from a sprite sheet SpriteSheetArtist = function (spritesheet, cells) { // constructor this.cells = cells; this.spritesheet = spritesheet; this.cellIndex = 0; }; SpriteSheetArtist.prototype = { // methods advance: function () { if (this.cellIndex == this.cells.length-1) { this.cellIndex = 0; } else { this.cellIndex++; } }, draw: function (sprite, context) { var cell = this.cells[this.cellIndex]; context.drawImage(this.spritesheet, cell.left, cell.top, // source x, source y cell.width, cell.height, // source width, source height sprite.left, sprite.top, // destination x, destination y cell.width, cell.height); // destination width, destination height } }; Bạn khởi tạo đối tượng vẽ Sprite sheet với một tham chiếu đến một Sprite sheet và một mảng chứa những đường biên bao quanh hình, được gọi là các ô (cells). Các ô này thể hiện một hình chữ nhật trong Sprite sheet, chứa hình ảnh đơn lẻ của Sprite sheet. Đối tượng vẽ Sprite sheet cũng đồng thời lưu một chỉ mục (index) cho từng ô. Phương thức draw() của Sprite sheet sử dụng chỉ mục đó để truy cập ô hiện thời và sau đó sử dụng phiên bản chín-tham-số của ngữ cảnh Canvas 2D là drawImage() để vẽ nội dung của ô đó trong khung hình ảnh ở vị trí của sprite. Phương thức advance() của đối tượng vẽ Sprite sheet còn sử dụng để trỏ sang ô kế tiếp trong Sprite sheet, và trở lại ô ban đầu khi con trỏ chỉ mục tới ô cuối cùng. Tiếp theo gọi tới phương thức draw() của đối tượng vẽ Sprite sheet để vẽ hình tương ứng. Bằng cách liên tục thúc đẩy việc lập chỉ mục và vẽ hình, đối tượng Sprite sheet có thể vẽ một tập hợp các ảnh liên tục từ Sprite sheet. Đối tượng vẽ Sprite sheet, như bạn thấy từ Liệt kê 5, dễ dàng thực hiện. Chúng cũng dễ sử dụng; bạn chỉ việc khởi tạo đối tượng vẽ với Sprite sheet và các ô, và sau đó gọi phương thức advance() và draw(). Phần khó khăn là xác định các ô. Xác định các ô chứa trong Sprite sheet Đoạn mã trong Liệt kê 6 xác định các ô từ Sprite sheet của Snail Bait cho các con dơi, con ong và con ốc trong trò chơi: Liệt kê 6. Xác định các ô Sprite sheet của Snail Bait var BAT_CELLS_HEIGHT = 34, BEE_CELLS_WIDTH = 50, BEE_CELLS_HEIGHT = 50, ... SNAIL_CELLS_WIDTH = 64, SNAIL_CELLS_HEIGHT = 34, ... // Spritesheet cells................................................ batCells = [ { left: 1, top: 0, width: 32, height: BAT_CELLS_HEIGHT }, { left: 38, top: 0, width: 46, height: BAT_CELLS_HEIGHT }, { left: 90, top: 0, width: 32, height: BAT_CELLS_HEIGHT }, { left: 129, top: 0, width: 46, height: BAT_CELLS_HEIGHT }, ], beeCells = [ { left: 5, top: 234, width: BEE_CELLS_WIDTH, height: BEE_CELLS_HEIGHT }, { left: 75, top: 234, width: BEE_CELLS_WIDTH, height: BEE_CELLS_HEIGHT }, { left: 145, top: 234, width: BEE_CELLS_WIDTH, height: BEE_CELLS_HEIGHT } ], ... snailCells = [ { left: 142, top: 466, width: SNAIL_CELLS_WIDTH, height: SNAIL_CELLS_HEIGHT }, { left: 75, top: 466, width: SNAIL_CELLS_WIDTH, height: SNAIL_CELLS_HEIGHT }, { left: 2, top: 466, width: SNAIL_CELLS_WIDTH, height: SNAIL_CELLS_HEIGHT }, ]; Việc xác định các khung bao quanh từng ô là một công việc tẻ nhạt, do đó, cần đầu tư thời gian để thực hiện một công cụ hỗ trợ là việc làm cần thiết. Hình 3 cho thấy một công cụ hỗ trợ như vậy, nó có sẵn tại trang Core HTML Canvas Hình 3. Một bộ định vị Sprite sheet đơn giản Những công cụ cần thiết cho nhà phát triển trò chơi Công việc của một nhà phát triển trò chơi không phải lúc nào cũng vui và thú vị. Các nhà phát triển trò chơi dành rất nhiều thời gian vào công việc tẻ nhạt như là quyết định các ô Sprite sheet và thiết kế các mức độ của trò chơi. Hầu hết các nhà phát triển trò chơi dành thời gian để phát triển công cụ, như một trong những thể hiện trong Liệt kê 3, để giúp họ hoàn thành những nhiệm vụ tẻ nhạt. Ứng dụng được hiển thị trong Liệt kê 3 hiển thị hình ảnh và theo dõi sự di chuyển của chuột trong hình ảnh. Khi bạn di chuyển chuột, ứng dụng vẽ đường dẫn và cập nhật ở góc trên bên trái của ứng dụng hiển thị vị trí hiện tại của con trỏ chuột. Công cụ này giúp bạn dễ dàng để xác định khung giới hạn cho mỗi hình ảnh và Sprite sheet. Bây giờ bạn đã có một ý tưởng tốt để thực hiện các mảng sprite và đối tượng vẽ của chúng, giờ là lúc để xem làm thế nào mà Snail Bait có thể tạo và khởi chạy sprite của nó. Tạo và khởi chạy sprite của Snail Bait Snail Bait định nghĩa các mảng chứa các sprite, được hiển thị trong Liệt kê 7: Liệt kê 7. Định nghĩa các mảng sprite trong hàm khởi tạo của trò chơi var SnailBait = function (canvasId) { // constructor ... this.bats = [], this.bees = [], this.buttons = [], this.coins = [], this.platforms = [], this.rubies = [], this.sapphires = [], this.snails = [], this.runner = new Sprite('runner', this.runnerArtist); this.sprites = [ this.runner ]; // Add other sprites later ... }; Mỗi mảng trong Liệt kê 7 chứa các sprite cùng kiểu; ví dụ, mảng bats chứa sprite con dơi, mảng bees chứa các sprite con ong, và cứ thế. Trò chơi cũng duy trì mảng chứa tất cả sprite của nó. Không cần thiết để có các mảng riêng lẻ cho con ong, con dơi, v.v... — chúng dư thừa — nhưng chúng lại tạo thuận lợi về hiệu suất; ví dụ, khi trò chơi kiểm tra xem liệu nhân vật có đáp xuống nền hay không, sẽ hiệu quả hơn để lặp qua mảng platforms hơn là lặp qua mảng sprites để tìm kiếm bậc thềm. Liệt kê 7 cũng chỉ ra làm thế nào để tạo sprite nhân vật và thêm nó vào mảng sprites. Chúng ta không có mảng nhân vật, vì trò chơi chỉ có một nhân vật duy nhất. Chú ý, trò chơi khởi chạy nhân vật với một kiểu — runner — và một đối tượng đồ họa, nhưng nó không chỉ định hành vi khi nhân vật được khởi chạy. Về những hành vi, tôi sẽ thảo luận chúng trong bài kế tiếp, được thêm sau bằng đoạn mã. Khi trò chơi bắt đầu, Snail Bait (cùng với những thứ khác) khởi chạy phương thức createSprites() như bạn thấy trong Liệt kê 8: Liệt kê 8. Khởi động trò chơi SnailBait.prototype = { // methods ... start: function () { this.createSprites(); this.initializeImages(); this.equipRunner(); this.splashToast('Good Luck!'); }, }; Phương thức createSprites() tạo tất cả sprite của trò chơi ngoại trừ sprite của nhân vật, được hiển thị trong Liệt kê 9: Liệt kê 9. Tạo và khởi chạy các sprite của Snail Bait SnailBait.prototype = { // methods ... createSprites: function() { this.createPlatformSprites(); this.createBatSprites(); this.createBeeSprites(); this.createButtonSprites(); this.createCoinSprites(); this.createRubySprites(); this.createSapphireSprites(); this.createSnailSprites(); this.initializeSprites(); this.addSpritesToSpriteArray(); }, createSprites() khởi chạy hàm trợ giúp để tạo những kiểu khác nhau của sprite, theo sau là các phương thức khởi tạo các sprite và thêm chúng vào mảng sprites. Việc thực hiện các hàm trợ giúp này được hiển thị trong Liệt kê 10: Liệt kê 10. Tạo các sprite riêng lẻ SnailBait.prototype = { // methods ... createBatSprites: function () { var bat, batArtist = new SpriteSheetArtist(this.spritesheet, this.batCells), redEyeBatArtist = new SpriteSheetArtist(this.spritesheet, this.batRedEyeCells); for (var i = 0; i < this.batData.length; ++i) { if (i % 2 === 0) bat = new Sprite('bat', batArtist); else bat = new Sprite('bat', redEyeBatArtist); bat.width = this.BAT_CELLS_WIDTH; bat.height = this.BAT_CELLS_HEIGHT; this.bats.push(bat); } }, createBeeSprites: function () { var bee, beeArtist = new SpriteSheetArtist(this.spritesheet, this.beeCells); for (var i = 0; i < this.beeData.length; ++i) { bee = new Sprite('bee', beeArtist); bee.width = this.BEE_CELLS_WIDTH; bee.height = this.BEE_CELLS_HEIGHT; this.bees.push(bee); } }, createButtonSprites: function () { var button, buttonArtist = new SpriteSheetArtist(this.spritesheet, this.buttonCells), goldButtonArtist = new SpriteSheetArtist(this.spritesheet, this.goldButtonCells); for (var i = 0; i < this.buttonData.length; ++i) { if (i === this.buttonData.length - 1) { button = new Sprite('button', goldButtonArtist); } else { button = new Sprite('button', buttonArtist); } button.width = this.BUTTON_CELLS_WIDTH; button.height = this.BUTTON_CELLS_HEIGHT; button.velocityX = this.BUTTON_PACE_VELOCITY; button.direction = this.RIGHT; this.buttons.push(button); } }, createCoinSprites: function () { var coin, coinArtist = new SpriteSheetArtist(this.spritesheet, this.coinCells); for (var i = 0; i < this.coinData.length; ++i) { coin