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.

David Geary, Tác giả và diễn giả, Clarity Training, Inc.

Ảnh của David GearyDavid Geary, tác giả của quyển sách Core HTML5 Canvas, cũng là đồng sáng lập của Nhóm người dùng HTML5 Denver và là tác giả của 8 cuốn sách Java, bao gồm cả những cuốn sách bán chạy nhất về Swing và JavaServer Faces. David là một diễn giả thường xuyên tại các hội nghị, bao gồm JavaOne, Devoxx, Loop Strange, NDC và OSCON và ông đã ba lần đạt danh hiệu JavaOne Rock Star (diễn giả hàng đầu tại hội nghị JavaOne). Ông đã viết loạt bài Lập trình game 2D trên HTML5 2D, JSF 2 fu, và GWT fu cho developerWorks.



15 10 2013

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
Hình ảnh các sprite 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.

Xem phần Tải xuống để lấy ví dụ mẫu cho bài viết này.

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: <script src='js/sprites.js'></script>.

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ínhChú giải
artistĐối tượng để vẽ một sprite.
behaviorsMộ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 kiểu hình dạng.
leftTọa độ X ở góc trên bên trái của sprite.
topTọa độ Y ở góc trên bên trái của sprite.
widthChiều ngang của sprite được tính theo pixel.
heightChiều cao của sprite được tính theo pixel.
opacityTrạng thái mờ, trong suốt hoặc kết hợp cả hai trạng thái của sprite.
typeMộ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).
velocityXTốc độ ngang của sprite, được tính theo số pixel trên giây.
velocityYTốc độ đứng của sprite, được tính theo số pixel trên giây.
visibleTì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 artistbehaviors.

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ứcChú 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ê 1Liệ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()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
Hình ảnh bảng sprite 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()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 (xem phần Tài nguyên):

Hình 3. Một bộ định vị Sprite sheet đơn giản
Hình ảnh công cụ dò tìm sprite

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        = new Sprite('coin', coinArtist);
      coin.width  = this.COIN_CELLS_WIDTH;
      coin.height = this.COIN_CELLS_HEIGHT;

      this.coins.push(coin);
    }
  },

  createPlatformSprites: function () {
    var sprite, pd;  // Sprite, Platform data

    for (var i=0; i < this.platformData.length; ++i) {
      pd = this.platformData[i];
      sprite           = new Sprite('platform-' + i, this.platformArtist);
      sprite.left      = pd.left;
      sprite.width     = pd.width;
      sprite.height    = pd.height;
      sprite.fillStyle = pd.fillStyle;
      sprite.opacity   = pd.opacity;
      sprite.track     = pd.track;
      sprite.button    = pd.button;
      sprite.pulsate   = pd.pulsate;
      sprite.power     = pd.power;
      sprite.top       = this.calculatePlatformTop(pd.track);

      this.platforms.push(sprite);
    }
  },

  createSapphireSprites: function () {
    // Listing omitted for brevity. Discussed in the next article in this series.
  },

  createRubySprites: function () {
    // Listing omitted for brevity. Discussed in the next article in this series.
  },

  createSnailSprites: function () {
    // Listing omitted for brevity. Discussed in the next article in this series.
  },
};

Các phương thức trong Liệt kê 10 đáng chú ý vì ba nguyên nhân. Đầu tiên, các phương thức cực kỳ đơn giản: mỗi phương thức tạo sprite, thiết lập bề rộng và bề cao cho chúng, và thêm chúng vào mảng sprite riêng lẻ. Thứ hai, createBatSprites()createButtonSprites() sử dụng hơn một đối tượng đồ họa để tạo sprite cùng kiểu. Phương thức createBatSprites() thay thế đồ họa để các con dơi (bat) có một nửa mắt là màu đỏ, còn nửa kia là màu trắng, như bạn thấy trong Hình 4. Phương thức createButtonSprites() dùng các đối tượng đồ họa để vẽ các nút màu xanh hay vàng.

Hình 4. Các con dơi có mắt đỏ và trắng
Hình ảnh Snail Bait chạy trên Chrome

Khía cạnh thứ ba và thú vị nhất trong Liệt kê 10 là chúng tạo sprite từ mảng của siêu dữ liệu sprite (sprite metadata).


Tạo sprite với siêu dữ liệu

Liệt kê 11 thể hiện một vài siêu dữ liệu của sprite sử dụng trong Snail Bait:

Liệt kê 11. Sprite metadata
var SnailBait = function (canvasId) {
  // Bats..............................................................
   
   this.batData = [
      { left: 1150, top: this.TRACK_2_BASELINE - this.BAT_CELLS_HEIGHT },
      { left: 1720, top: this.TRACK_2_BASELINE - 2*this.BAT_CELLS_HEIGHT },
      { left: 2000, top: this.TRACK_3_BASELINE }, 
      { left: 2200, top: this.TRACK_3_BASELINE - this.BAT_CELLS_HEIGHT },
      { left: 2400, top: this.TRACK_3_BASELINE - 2*this.BAT_CELLS_HEIGHT },
   ],
   
   // Bees..............................................................

   this.beeData = [
      { left: 500,  top: 64 },
      { left: 944,  top: this.TRACK_2_BASELINE - this.BEE_CELLS_HEIGHT - 30 },
      { left: 1600, top: 125 },
      { left: 2225, top: 125 },
      { left: 2295, top: 275 },
      { left: 2450, top: 275 },
   ],

   // Buttons...........................................................

   this.buttonData = [
      { platformIndex: 7 },
      { platformIndex: 12 },
   ],

   // Metadata for Snail Bait's other sprites is omitted for brevity
};

Tạo sprite từ siêu dữ liệu là một ý tưởng tốt vì:

  • Siêu dữ liệu cho sprite được đặt ở một vị trí thay vì rải rác trong toàn bộ mã nguồn.
  • Các phương thức để tạo ra sprite sẽ đơn giản hơn khi được tách ra khỏi siêu dữ liệu.
  • Siêu dữ liệu có thể được tạo từ bất kỳ nguồn nào.

Vì siêu dữ liệu của sprite được tập trung tại một vị trí trong mã nguồn, việc tìm kiếm và sửa đổi sẽ đơn giản hơn. Hơn nữa vì siêu dữ liệu được định nghĩa bên ngoài các phương thức tạo sprite, các phương thức này sẽ đơn giản hơn, nhờ vậy mà dễ đọc hiểu và sửa đổi. Cuối cùng, mặc dù siêu dữ liệu được nhúng trực tiếp trong mã nguồn, nhưng có thể được tạo ra từ nhiều nguồn — bao gồm, ví dụ như từ mục biên tập các màn của trò chơi có thể tạo ra siêu dữ liệu tại thời gian thực thi. Nói tóm lại thì siêu dữ liệu dễ dàng hơn cho việc sửa chữa và linh động hơn so với đặc tả dữ liệu trực tiếp trong phương thức tạo sprite.

Xem lại Liệt kê 9 rằng phương thức createSprites() của Snail Bait khởi chạy hai phương thức — initializeSprites()addSpritesToSpriteArray() — sau khi tạo các sprite cho trò chơi. Liệt kê 12 thể hiện phương thức initializeSprites():

Liệt kê 12. Khởi tạo sprite cho Snail Bait
SnailBait.prototype = { // methods
   ...
  
   initializeSprites: function() {  
      this.positionSprites(this.bats,       this.batData);
      this.positionSprites(this.bees,       this.beeData);
      this.positionSprites(this.buttons,    this.buttonData);
      this.positionSprites(this.coins,      this.coinData);
      this.positionSprites(this.rubies,     this.rubyData);
      this.positionSprites(this.sapphires,  this.sapphireData);
      this.positionSprites(this.snails,     this.snailData);
   },

   positionSprites: function (sprites, spriteData) {
      var sprite;

      for (var i = 0; i < sprites.length; ++i) {
         sprite = sprites[i];

         if (spriteData[i].platformIndex) { // put sprite on a platform
            this.putSpriteOnPlatform(sprite, this.platforms[spriteData[i].platformIndex]);
         }
         else {
            sprite.top  = spriteData[i].top;
            sprite.left = spriteData[i].left;
         }
      }
   },
};

initializeSprites() gọi positionSprites() cho mỗi mảng sprite trong trò chơi. Phương thức này lúc này sẽ cố định vị trí của sprite theo thông tin được mô tả trong siêu dữ liệu. Chú ý rằng một số sprite như nút và vỏ ốc được đặt ở trên cùng của nền. Phương thức putSpriteOnPlatform() được thể hiện trong Liệt kê 13:

Liệt kê 13. Đặt sprite lên nền
SnailBait.prototype = { // methods
   ...

   putSpriteOnPlatform: function(sprite, platformSprite) {
      sprite.top  = platformSprite.top - sprite.height;
      sprite.left = platformSprite.left;
      sprite.platform = platformSprite;
   },
}

Với mỗi sprite và nền cho trước, phương thức putSpriteOnPlatform() sẽ đặt sprite lên trên cùng của nền và lưu một tham khảo đến nền trong sprite cho truy xuất sau này.

Như bạn có thể thắc mắc và như Liệt kê 14 xác nhận, thêm từng sprite lên chung quanh mảng sprites là một vấn đề đơn giản:

Liệt kê 14. Tạo và khởi chạy các sprite của Snail Bait
SnailBait.prototype = { // methods
   ...
  
   addSpritesToSpriteArray: function () {
      var i;
  
      for (i=0; i < this.bats.length; ++i) {
         this.sprites.push(this.bats[i]);
      }

      for (i=0; i < this.bees.length; ++i) {
         this.sprites.push(this.bees[i]);
      }

      for (i=0; i < this.buttons.length; ++i) {
         this.sprites.push(this.buttons[i]);
      }

      for (i=0; i < this.coins.length; ++i) {
         this.sprites.push(this.coins[i]);
      }

      for (i=0; i < this.rubies.length; ++i) {
         this.sprites.push(this.rubies[i]);
      }

      for (i=0; i < this.sapphires.length; ++i) {
         this.sprites.push(this.sapphires[i]);
      }

     for (i=0; i < this.snails.length; ++i) {
         this.sprites.push(this.snails[i]);
      }

      for (i=0; i < this.snailBombs.length; ++i) {
         this.sprites.push(this.snailBombs[i]);
      }
   },
};

Bây giờ bạn đã biết làm sao để thực hiện sprite và các đối tượng đồ họa sprite đã tạo và khởi chạy các sprite như thế nào, tôi sẽ chỉ cho bạn biết làm thế nào để kết hợp các sprite trong vòng lặp trò chơi Snail Bait.


Kết hợp các sprite trong vòng lặp trò chơi

Hãy xem lại bài viết thứ hai trong loạt bài này (xem phần Cuộn hình nền) rằng gần như tất cả các chuyển động ngang trong Snail Bait là kết quả của dịch chuyển ngữ cảnh Canvas 2D. Snail Bait luôn vẽ phần lớn các sprite ở cùng vị trí nằm ngang, và rõ ràng là sự chuyển động theo chiều ngang là kết quả của sự dịch chuyển đó. Hầu hết các sprite của Snail Bait di chuyển theo chiều ngang trong sự hòa hợp với nền tảng của trò chơi, được hiển thị trong Liệt kê 15:

Liệt kê 15. Cập nhật hiệu số của sprite (sprite offsets)
SnailBait.prototype = {
   draw: function (now) {
      this.setPlatformVelocity();
      this.setTranslationOffsets();

      this.drawBackground();

      this.updateSprites(now);
      this.drawSprites();
   },

   setPlatformVelocity: function () {
      // Setting platform velocity was discussed in the second article in this series

      this.platformVelocity = this.bgVelocity * this.PLATFORM_VELOCITY_MULTIPLIER; 
   },

   setTranslationOffsets: function () {
      // Setting the background translation offset was discussed
      // in the second article in this series

      this.setBackgroundTranslationOffset();
      this.setSpriteTranslationOffsets();
   },
   
   setSpriteTranslationOffsets: function () {
      var i, sprite;

      this.spriteOffset += this.platformVelocity / this.fps; // In step with platforms
   
      for (i=0; i < this.sprites.length; ++i) {
         sprite = this.sprites[i];
      
         if ('runner' !== sprite.type) {
            sprite.offset = this.platformOffset; // In step with platforms
         }
      }
   },
   ...
};

Phương thức draw() đặt tốc độ nền tảng và sau đó thiết lập hiệu số dịch chuyển cho tất cả sprite ngoại trừ nhân vật. (Vị trí theo chiều ngang của nhân vật là cố định, nó không di chuyển trong sự hòa hợp với nền tảng.)

Sau khi thiết lập hiệu số dịch chuyển và vẽ nền, phương thức draw() cập nhật và vẽ sprite của trò chơi với updateSprites()drawSprites(). Những phương thức này được hiển thị trong Liệt kê 16:

Liệt kê 16. Cập nhật và vẽ các sprite
SnailBait.prototype = {
   ...
   updateSprites: function (now) {
      var sprite;
   
      for (var i=0; i < this.sprites.length; ++i) {
         sprite = this.sprites[i];

         if (sprite.visible && this.spriteInView(sprite)) {
            sprite.update(now, this.fps);
         }
      }
   },
   
   drawSprites: function() {
      var sprite;
   
      for (var i=0; i < this.sprites.length; ++i) {
         sprite = this.sprites[i];
   
         if (sprite.visible && this.spriteInView(sprite)) {
            this.context.translate(-sprite.offset, 0);

            sprite.draw(this.context);

            this.context.translate(sprite.offset, 0);
         }
      }
   },
   
   spriteInView: function(sprite) {
      return sprite === this.runner || // runner is always visible
         (sprite.left + sprite.width > this.platformOffset &&
          sprite.left < this.platformOffset + this.canvas.width);   
   },

Khi các sprite không ở trong khung nhìn

Phiên bản cuối cùng của Snail Bait có một màn chơi rộng bốn lần khung hình vẽ. (Chiều rộng là tùy ý; nó có thể lớn hơn rất nhiều). Tại bất kỳ thời điểm nào, do đó, ba phần tư của cảnh quan Snail Bait không có trong khung nhìn. Không cần thiết phải cập nhật hoặc vẽ sprite nằm trong ba phần tư của cảnh quan, vì vậy Snail Bait không làm như vậy. Nói đúng ra, không cần thiết để loại trừ các sprite khi vẽ, vì ngữ cảnh Canvas sẽ loại trừ chúng.

Cả updateSprites()drawSprites() duyệt qua tất cả các sprite của trò chơi và, tương ứng, cập nhật và vẽ sprite — nhưng chỉ khi các sprite có thể nhìn thấy và trong phần của khung hình vẽ đó đang được hiển thị.

Trước khi vẽ sprite, phương thức drawSprites() chuyển dịch ngữ cảnh dựa trên phần bù của sprite được tính trong setTranslationOffsets() và sau đó chuyển ngữ cảnh trở lại vị trí ban đầu của nó, do đó cho phép các sprite chuyển động ​​theo chiều ngang.


Phần tiếp theo

Trong bài viết này, tôi đã chỉ cho bạn cách thực hiện sprite và các đối tượng vẽ sprite và làm thế nào để kết hợp sprite vào trong vòng lặp trò chơi. Trong bài viết kế tiếp của loạt bài Lập trình game 2D trên HTML5, bạn sẽ học cách làm thế nào để thực hiện các hành vi của sprite và gắn chúng tới các sprite được chỉ định. Hẹn gặp lại lần sau.


Tải về

Mô tảTênKích thước
Sample codej-html5-game4.zip3.9MB

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

  • Replica Island: Bạn có thể tải về mã nguồn của trò chơi đi cảnh nổi tiếng này cho Android.

Thảo luận

  • Tham gia vào Cộng đồng developerWorks. Kết nối với các thành viên developerWorks và khám phá các diễn đàn, blog, nhóm và thư viện wiki.

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Nguồn mở, Công nghệ Java
ArticleID=948392
ArticleTitle=Lập trình game 2D trên HTML5, Phần 4: Sprites
publish-date=10152013