Lập trình game 2D trên HTML5, Phần 3: Thiết lập các giai đoạn

Hiện thực đối tượng trò chơi, tạm ngưng, đóng băng, tan băng, và nhập liệu bàn phím

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. Trong phần này, bạn sẽ học cách để đóng gói trò chơi trong một đối tượng, làm thế nào để thực hiện tạm dừng và bỏ tạm dừng, và làm thế nào để sử dụng quá trình chuyển đổi của CSS3 để thực hiện đồng hồ đếm ngược.

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

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 JSF 2 fuGWT fu cho developerWorks. Bạn có thể theo dõi David trên Twitter tại @davidgeary.



31 07 2013

Nhiều khía cạnh của phát triển trò chơi không liên quan đến cách chơi. Hiển thị hướng dẫn, tạm dừng trò chơi, chuyển đổi giữa các cấp, và bảng công trạng của trò chơi là một số các tính năng mà các nhà phát triển trò chơi phải thực hiện ngoài bản thân trò chơi.

Khi cảm hứng về một trò chơi tấn công, nó thường không bao gồm những cách để hiển thị điểm số cao (high scores) hoặc chuyển tiếp giữa các cấp, vì vậy đương nhiên là sẽ tập trung vào thực hiện cơ chế của trò chơi mà không cần suy nghĩ nhiều về cơ sở hạ tầng của trò chơi. Nhưng trong hầu hết các dự án, xem xét chức năng này sau đó dẫn đến nhiều việc hơn thay vì kết hợp nó ngay từ ban đầu.

Trong bài viết trước, tôi đã thảo luận về đồ họa và hình ảnh động, đó là nền tảng cho lối chơi của Snail Bait. Trong bài viết này, tôi tạm thời quay lại để thực hiện một vài cơ sở hạ tầng của trò chơi. Tôi bắt đầu bằng cách đóng gói mã của Snail Bait trong đối tượng Game. Khi tôi hiện thực trò chơi ngay từ đầu, tôi đã bắt đầu với bước này, nhưng vì các bài viết, tôi không muốn làm rối cuộc thảo luận về đồ họa và hình ảnh động bằng cách hiện thực chúng trong một đối tượng, vì vậy tôi đã trì hoãn đối tượng Game cho tới bây giờ.

Tôi cũng sẽ chỉ cho bạn cách để tạm dừng và đóng băng Snail Bait và sau đó làm thế nào để làm tan băng và khởi động lại trò chơi với một bộ đếm ngược. Vào cuối bài viết, tôi trở về cơ chế lối chơi để giới thiệu cho các bạn cách xử lý sự kiện bàn phím để điều khiển vị trí theo chiều thẳng đứng của nhân vật.

Trong bài viết này, bạn sẽ học được làm thế nào để:

  • Đóng gói chức năng của trò chơi trong một đối tượng.
  • Tạm dừng và bỏ tạm dừng trò chơi.
  • Tự động tạm dừng các trò chơi khi cửa sổ mất tiêu điểm (focus).
  • Tiếp tục trò chơi với một bộ đếm động khi cửa sổ lấy lại tiêu điểm.
  • Hiển thị thông báo tạm thời (được biết là toasts) tới người sử dụng.
  • Xử lí nhập liệu bàn phím..

Trên hành trình, bạn sẽ học cách làm thế nào định nghĩa và khởi chạy đối tượng JavaScript, làm thế nào để sử dụng chuyển tiếp CSS3 và làm thế nào để kết hợp setTimeout() với những hiệu ứng chuyển tiếp để thực hiện từng bước cho hình ảnh động.

Đối tượng game

Trong loạt bài này, chúng ta sẽ thực hiện (implement) tất cả các hàm của Snail Bait, cùng với một vài biến của nó, được sử dụng như biến toàn cục. Dĩ nhiên chúng ta sẽ không sử dụng biến toàn cục nếu như bạn chưa có kiến thức chắc chắn về các biến toàn cục đó. Hãy tham khảo phần Tài nguyên để có thêm những kiến thức về Javascript từ các chuyên gia như Douglas Crockford và Nicholas Zakas.

Bên cạnh việc sử dụng biến toàn cục, chúng ta có thể đóng gói (encapsulate) tất cả các hàm và biến của Snail Bait vào một đối tượng (object). Đối tượng đó sẽ được viết thành 2 phần, như bên dưới. (Xem phần Tải về để lấy toàn bộ đoạn mã (code) mẫu trong bài viết này).

Liệt kê 1 là hàm khởi tạo (constructor) của trò chơi, xác định các thuộc tính của đối tượng:

Liệt kê 1: Hàm khởi tạo của trò chơi (đã được rút gọn)
var SnailBait = function (canvasId) {
   this.canvas  = document.getElementById(canvasId);
   this.context = this.canvas.getContext('2d');

   // HTML elements

   this.toast = document.getElementById('toast'),
   this.fpsElement = document.getElementById('fps');

   //  Constants

   this.LEFT = 1;
   this.RIGHT = 2;
   ...

   // Many more attributes are defined in the rest of this function
};

Liệt kê 2 là định nghĩa các phương thức của đối tượng trong bản mẫu (prototype) của trò chơi:

Liệt kê 2. Bản mẫu của trò chơi (đã được rút gọn)
SnailBait.prototype = {
   // The draw() and drawRunner() methods were
   // discussed in the second article in this series.


   draw function (now) {
      this.setPlatformVelocity(); 
      this.setOffsets();
   
      this.drawBackground();
   
      this.drawRunner();
      this.drawPlatforms();
   },
   
   drawRunner: function () {
      this.context.drawImage(this.runnerImage,
         this.STARTING_RUNNER_LEFT,
         this.calculatePlatformTop(this.runnerTrack) - this.RUNNER_HEIGHT);
   },
   ...

   // Many more methods are defined in the rest of this object
};

Như tôi đã kết hợp các tính năng mới trong loạt bài này, tôi sẽ thêm, loại bỏ và chỉnh sửa một vài phương thức được thực hiện. Bảng 1 sẽ liệt kê những phương thức của Snail Bait được sử dụng cho đến hết bài viết này:

Bảng 1. Những phương thức của Snail Bait trong giai đoạn phát triển
Phương thứcChú giải
initializeImages()Khởi tạo ảnh của trò chơi. Sự kiện onload cho ảnh nền sẽ gọi phương thức start().
start()Bắt đầu trò chơi bằng cách gọi phương thức requestAnimationFrame(), và phương thức trên sẽ gọi tiếp phương thức animate() trong khi nó vẽ các khung hình (frame) đầu tiên.
splashToast() [1]Hiển thị thông báo tạm đến người chơi.
animate() [2]Nếu trò chơi không được tạm dừng, phương thức này sẽ vẽ tiếp khung hình tiếp theo bằng cách gọi phương thức requestNextAnimationFrame() để lập lịch và gọi tiếp đến phương thức animate(). Nếu trò chơi được tạm dừng, phương thức animate() sẽ chờ 200ms trước khi gọi phương thức requestNextAnimationFrame().
calculateFps()Tính toán tỷ lệ khung hình dựa trên thời gian trôi qua của khung hình trước.
draw()Vẽ khung hình tiếp theo.
setTranslationOffsets()Đặt lại hiệu số cho nền (background) và cảnh (platform) trong trò chơi
setBackgroundTranslationOffset().Đặt lại hiệu số cho nền (background) dựa trên thời gian hiện tại.
setPlatformTranslationOffset()Đặt lại hiệu số cho cảnh trong trò chơi dựa trên thời gian hiện tại.
setPlatformVelocity()Đặt lại tốc độ của cảnh trong trò chơi như bội số của tốc độ nền để tạo ra một hiệu ứng thị sai nhẹ.
drawBackground()Chuyển đổi tọa độ của hệ thống Canvas, vẽ nền hai lần và chuyển đổi tọa độ về vị trí ban đầu.
drawRunner() [3]Vẽ lại nhân vật với phương thức drawImage().
drawPlatforms() [3]Vẽ cảnh hình chữ nhật với phối cảnh 2D bằng phương thức strokeRect()fillRect().
calculatePlatformTop()Tính toán tọa độ Y phía trên của cảnh trong trò chơi, đánh dấu đường lại (cảnh trong trò chơi di truyển trên một trong ba đường ngang).
turnLeft()Cuộn nền (background) và cảnh (platform) sang bên phải.
turnRight()Cuộn nền (background) và cảnh (platform) sang bên trái.
togglePaused() [1]Chuyển trạng thái tạm ngừng của trò chơi.

[1] Đã được giới thiệu trong bài viết này
[2] Được gọi bởi trình duyệt
[3] Sẽ được thay thế trong phần sau của loạt bài này

Hàm (Functin) và Phương thức (Method)

Nếu hàm Javascript là một phần nhỏ của một đối tượng thì được xem như là một phương thức (method); trong khi hàm đứng độc lập để giải quyết chức năng nào đó thì được gọi đơn giản là hàm (function).

Tôi đã giới thiệu hầu hết các phương thức — mà lúc đó có thể xem như là các hàm — được liệt kê trong bảng 1 trong hai bài viết trước của loạt bài này. Trong bài viết này, chúng ta chỉ thảo luận đến 2 phương thức mới là: togglePaused()splashToast(), ngoài việc thay đổi thành các phương thức khác như animate().

Đoạn mã Javascript trong Liệt kê 1Liệt kê 2 định nghĩa hàm và bản mẫu nhưng chưa tạo nên đối tượng SnailBait . Tôi sẽ hoàn tất nó trong phần tiếp theo.


Bắt đầu trò chơi

Các biến toàn cục của SnailBait

Như Liệt kê 1Liệt kê 3 đã minh họa, Snail Bait chỉ có 2 đối tượng toàn cục: hàm SnailBait và đối tượng snailBait.

Liệt kê 3 là đoạn mã Javascript bắt đầu trò chơi. Phần đầu của Liệt kê cho ta thấy việc cài đặt ba phương thức của SnailBait là: animate(), start(), và initializeImages().

Liệt kê 3. Bắt đầu
SnailBait.prototype = {
   ...

   // The 'this' variable in the animate() method is
   // the window object, so the method uses snailBait instead 

   animate: function (now) { 
      snailBait.fps = snailBait.calculateFps(now); 
      snailBait.draw(now);

      requestNextAnimationFrame(snailBait.animate);
   },

   start: function () {
      this.turnRight();                     // Sets everything in motion
      this.splashToast('Good Luck!', 2000); // "Good Luck" is displayed for 2 seconds

      requestNextAnimationFrame(this.animate);
   },

   initializeImages: function () {
      this.background.src = 'images/background_level_one_dark_red.png';
      this.runnerImage.src = 'images/runner.png';
   
      this.background.onload = function (e) {

         // ...the 'this' variable is the window object,
         // so this function uses snailBait instead.
     
         snailBait.start();
      };
   },
}; // End of SnailBait.prototype


// Launch game

var snailBait = new SnailBait(); // Note: By convention, the object
                                     // reference starts with lowercase, but
                                     // the function name starts with uppercase.

snailBait.initializeImages();

Tầm quan trọng của đối tượng this trong Javascript

Nếu bạn đã từng sử dụng những ngôn ngữ hướng đối tượng cổ điển như Java, đối tượng của biến this luôn luôn tham chiếu đến đối tượng đã được gán cho phương thức.

Một khía cạnh trong Javascript là biến this có thể thay đổi. Trong Liệt kê 2, biến this trong phương thức animate() và trình xử lí sự kiện onload của hình nền thì được tham chiếu đến đối tượng window mà không phải là đối tượng snailBait, vì thế các phương thức đó có thể gọi đến đối tượng snailBait một cách trực tiếp.

Đoạn mã Javascript trong Liệt kê 3 khởi tạo đối tượng SnailBait và gọi phương thức initializeImages() của nó, được xử lý trong sự kiện onload. Khi tải hình ảnh, thì trình quản lý sự kiện sẽ gọi phương thức start().

Phương thức start() gọi turnRight() (phương thức định lại vị trí của nền và nền tảng hệ thống). Bên cạnh đó, nó còn gọi phương thức splashToast(), phương thức mà cho hiện chữ Good Luck! trong 2 giây. Cuối cùng, phương thức start() gọi phương thức requestNextAnimationFrame() polyfill (polyfill: là một khái niệm của Javascript cho phép các hàm của các trình duyệt mới chạy trên các trình duyệt cũ hơn - người dịch) — sẽ được thảo luận trong bài thứ hai của loạt bài này (xem bài viết requestAnimationFrame() phần polyfill) — cuối cùng gọi phương thức animate().

Phương thức animate() vẽ khung hình hiện tại và gọi phương thức requestNextAnimationFrame()— định nghĩa chính nó như một hàm callback (gọi lại) — để duy trì hình ảnh hoạt hình.

Đó là cách mà trò chơi được bắt đầu. Tiếp theo, tôi sẽ cho bạn thấy cái cách mà người ta tạm dừng trò chơi.


Tạm dừng trò chơi

Các trò chơi HTML5 — đặc biệt là những trò chơi video — phải có chức năng tạm dừng. Trong Liệt kê 4, tôi đã chỉnh sửa vòng lặp game của trò Snail Bait để cung cấp khả năng tạm dừng và tiếp tục trò chơi:

Liệt kê 4. Ngừng và tiếp tục trò chơi
var SnailBait = function (canvasId) {
   ...
   this.paused = false,
   this.PAUSED_CHECK_INTERVAL = 200; // milliseconds
   ...
};

SnailBait.prototype = {
   animate: function (now) { 
      if (snailBait.paused) {

         // Check again in snailBait.PAUSED_CHECK_INTERVAL milliseconds

         setTimeout( function () {

            requestNextAnimationFrame(snailBait.animate);

         }, snailBait.PAUSED_CHECK_INTERVAL);
      }
      else {

         // The game loop from Listing 1

         snailBait.fps = snailBait.calculateFps(now); 
         snailBait.draw(now);
         requestNextAnimationFrame(snailBait.animate);
      }
   },

   togglePaused: function () {
      this.paused = !this.paused;
   },
};

Phương thức togglePaused() chỉ đơn giản chuyển đổi giá trị của biến paused. Khi biến này mang giá trị true — , nghĩa là game đang ở trong trạng thái tạm dừng, — phương thức animate() không thực thi vòng lặp game.

Vì không nhất thiết — và cũng không mang tính hiệu quả — khi cứ phải kiểm tra 60 lần mỗi giây (giả sử trò chơi đang chạy với 60 khung hình trên giây) để kiểm tra xem đã đến lúc tiếp tục trò chơi chưa; Chính vì vậy, Phương thức animate() trong Liệt kê 4 sẽ đợi 200ms trước khi gọi hàm requestNextAnimationFrame(), chính hàm này sẽ quyết định thời điểm để gọi hàm animate() khi đến lúc cần phải vẽ khung hình tiếp theo.

Tự động tạm dừng trò chơi khi cửa sổ mất tiêu điểm (focus)

Trong đặc tả Timing control for script-based animations (Quản lý thời gian cho hình ảnh động dựa trên kịch bản) của W3C có nói về việc thực hiện hiệu ứng trong trò chơi với hàm requestAnimationFrame() như sau:

Nếu trang web đang không được thao tác bởi người chơi thì hiệu ứng trong trò chơi có thể được giảm tải để không phải cập nhật thường xuyên và sẽ chỉ tiêu hao một phần nhỏ tài nguyên hệ thống.

Thuật ngữ giảm tải (throttled heavily) nghĩa là trình duyệt sẽ gọi hàm thực thi hiệu ứng của bạn với tuần suất thấp nhất, thông thường nằm trong khoảng từ 1 đến 10 khung hình trên giây, như trong Hình 1, cho thấy tỷ lệ khung hình là 6 ngay sau khi cửa sổ được thao tác trở lại.

Hình 1. Snail Bait sau khi mất và lấy lại tiêu điểm
Hình chụp của Snail Bait sau khi lấy lại tiêu điểm

Giảm tải khung hình có thể gây ra ảnh hưởng đối với thuật toán nhận diện va chạm của trò chơi, vì thông thường để xác định một va chạm có xảy ra hay chưa, là dựa vào tỷ lệ khung hình. Bạn có thể tránh việc ảnh hưởng đó bằng cách thực hiện tạm dừng trò chơi ngay khi người chơi vừa chuyển đổi cửa sổ thao tác, và khởi động lại ngay khi người chơi quay lại thao tác trên cửa sổ. Cách thực hiện như trong Liệt kê 5:

Liệt kê 5. Tự động tạm ngừng
window.onblur = function () { // window looses focus
   if (!snailBait.paused) {
      snailBait.togglePaused();
   }
};

window.onfocus = function () { // window regains focus
   if (snailBait.paused) {
      snailBait.togglePaused();
   }
};

Bên cạnh việc tạm ngừng game khi cửa sổ mất thao tác, bạn cũng nên thực hiện "đóng băng" trò chơi ngay trong khi nó đang được tạm dừng.


"Đóng băng" trò chơi

Chức năng dừng trò chơi đòi hỏi nhiều thứ hơn là việc chỉ ngừng hiệu ứng. Khi trò chơi được tiếp tục yêu cầu nó phải đang ở trạng thái giống hệt như khi nó được tạm ngừng. Liệt kê 4 thỏa yêu cầu đó; bởi vì sau cùng thì, trò chơi ngừng, không có chuyện gì tiếp diễn, thì buộc nó phải trở về trạng thái ngay trước khi tạm ngừng là hoàn toàn hợp lý. Tuy nhiên, trong trường hợp này thì khác, bởi vì đại lượng chính cho tất cả hiệu ứng chuyển động trong mọi trò chơi, — cả Snail Bait cũng vậy — , chính là thời gian.

Và như tôi đã đề cập trong bài viết thứ hai (xem tại phần requestAnimationFrame()), requestAnimationFrame() truyền tham số thời gian cho một hàm mà bạn chỉ định; trong trường hợp của Snail Bait, hàm được gọi đó là phương thức animate() , hàm này sau đó sẽ truyền tham số thời gian cho phương thức draw().

Mặc dù hiệu ứng chuyển động sẽ không được thực thi khi trò chơi tạm dừng, tuy nhiên khác biệt về thời gian là không thể tránh khỏi. Và bởi vì phương thức draw() sẽ vẽ khung hình kế tiếp dựa vào thời gian mà nó nhận được từ animate(), và việc thực hiện hàm togglePaused() như ở Liệt kê 4 sẽ gây ra tình trạng lệch thời gian khi trò chơi được tiếp tục.

Liệt kê 6 chỉ ra cách Snail Bait tránh được việc lệch thời gian:

Liệt kê 6. "Đóng băng" trò chơi
var SnailBait = function (canvasId) {
   ...
   this.paused = false,
   this.pauseStartTime = 0,
   this.totalTimePaused = 0,
   this.lastAnimationFrameTime = 0,
   ...
};

SnailBait.prototype = {
   ...
   calculateFps: function (now) {
      var fps = 1000 / (now - this.lastAnimationFrameTime);
      this.lastAnimationFrameTime = now;
   
      if (now - this.lastFpsUpdateTime > 1000) {
         this.lastFpsUpdateTime = now;
         this.fpsElement.innerHTML = fps.toFixed(0) + 'fps';

      }

      return fps; 
   },

   togglePaused: function () {
      var now = +new Date();

      this.paused = !this.paused;
   
      if (this.paused) {
         this.pauseStartTime = now;
      }
      else {
         this.lastAnimationFrameTime += (now - this.pauseStartTime);
      }
   },
};

Trong Liệt kê 6, tôi đã thay đổi 2 phương thức togglePaused()calculateFps() để tính thời gian mà trò chơi được tạm ngừng, nếu có.

Để tính tỷ lệ khung hình cho khung hình, tôi lấy thời điểm hiện tại trừ đi thời điểm mà tôi đã vẽ khung hình cuối, rồi chia cho 1000, kết quả là tôi có tỷ lệ khung hình trên giây thay vì trên mili giây. (Xem mục Tính tỉ lệ khung hình trên giây về việc tính toán tỷ lệ khung hình.)

Khi trò chơi được tiếp tục, tôi cộng khoảng thời gian trò chơi tạm ngừng cho thời điểm vẽ khung hình cuối. Kết quả của phép cộng này là trạng thái trò chơi trước khi tạm dừng và sau khi tiếp tục là hoàn toàn giống nhau.


"Tan băng (Thawing)" trò chơi khi cửa sổ nhận được tiêu điểm (focus)

Khi trò chơi được tiếp tục, người chơi cảm nhận sự chuyển đổi trơn tru để trở lại hành động, cho họ thời gian để lấy lại sự điều khiển. Trong thời gian đó, ý tưởng tốt là cung cấp thông tin phản hồi liên quan đến tổng thời gian còn lại trước khi trò chơi tiếp tục. Snail Bail hiện thực phản hồi đó thông qua bộ đếm ngược được hiển thị trong một lời thông báo (toast), vì vậy, tôi sẽ bắt đầu thảo luận một cách tổng quan về toast.

Toasts (Lời thông báo)

Một toast là một lời thông báo mà trò chơi hiển thị tạm thời cho người chơi, giống như chữ Good Luck! trong hình 2:

Hình 2. Toasts
Hình chụp của toast Good Luck! trong Snail Bait

Giống như chính Snail Bait, các toast Snail Bait được thực hiện với sự kết hợp của HTML, CSS và JavaScript, như bạn có thể nhìn thấy trong ba Liệt kê dưới đây.

Liệt kê 7 hiển thị HTML cho một toast:

Liệt kê 7. Toasts: HTML
<!DOCTYPE html>
<html>
   <head>
      ...
   </head>

   <body>
      <div id='wrapper'>
         <!-- Toast...................................................-->

         <div id='toast'></div>
         ...
   
      </div>
      ...
  </body>
</html>

CSS thực hiện toast Good Luck! của Snail Bait trong Liệt kê 8:

Liệt kê 8. Toasts: CSS
#toast {
   position: absolute;
   ...

   -webkit-transition: opacity 0.5s;
   -moz-transition: opacity 0.5s;
   -o-transition: opacity 0.5s;
   transition: opacity 0.5s;

   opacity: 0;
   z-index: 1;
   display: none;
}

Liệt kê 9 thể hiện đoạn mã JavaScript cho toast Good Luck!:

Liệt kê 9. Toasts: JavaScript
var SnailBait =  function () {
   ...
   this.toast = document.getElementById('toast'),
   this.DEFAULT_TOAST_TIME = 3000, // 3 seconds
   ...
};

SnailBait.prototype = {
   ...
   start: function () {
      ...
      snailBait.splashToast('Good Luck!');
   },

   splashToast: function (text, howLong) {
      howLong = howLong || this.DEFAULT_TOAST_TIME;

      toast.style.display = 'block';
      toast.innerHTML = text;

      setTimeout( function (e) {
         toast.style.opacity = 1.0; // After toast is displayed
      }, 50);

      setTimeout( function (e) {
         toast.style.opacity = 0; // Starts CSS3 transition

         setTimeout( function (e) {
            toast.style.display = 'none'; // Just before CSS3 animation concludes
         }, 480);
      }, howLong);
   },
   ...
}

Như được thực hiện trong ba Liệt kê trên, toast chỉ là các DIV, như khi bạn thấy trong Liệt kê 7. Những thứ đó được làm rõ nét hơn trong Liệt kê 8, chứa CSS của DIV. Vị trí của DIVabsolute, nghĩa là nó có thể xuất hiện trên hoặc dưới các DIV khác thay vì trước hoặc sau chúng. DIVtoast cũng có z-index có giá trị là 1, nghĩa là nó luôn luôn được hiển thị trên khung nền ảnh của trò chơi, z-index của khung nền ảnh có giá trị mặc định là 0. Cuối cùng, CSS cho phần tử toast định nghĩa một sự chuyển tiếp 0.5 giây gắn liền với thuộc tính opacity; khi thuộc tính này thay đổi, CSS thay đổi một cách trơn tru độ trong suốt (opacity) của DIV từ giá trị cũ sang giá trị mới trong 0.5 giây.

Những thứ đó sẽ thú vị hơn trong phương thức splashToast() trong Liệt kê 9, hiển thị một toast trong một khoảng thời gian được chỉ định. Khi Snail Bait khởi chạy splashToast() với thời gian hiển thị mặc định là 3 giây, mờ dần trong 0.5 seconds, hiển thị ngắn gọn trong 2.5 giây, và sau đó biến mất trong 0.5 giây. Dưới đây là cách mà nó hoạt động:

Phương thức splashToast() bắt đầu bằng việc gán thuộc tính display của DIVtoast thành block, thông thường làm cho DIV hiển thị; tuy nhiên, bởi vì thuộc tính opacity được khởi chạy là 0, nên DIVtoast vẫn còn ẩn. Sau đó splashToast() gán văn bản HTML bên trong của DIVtoast mà bạn truyền vào phương thức, nhưng thiết đặt độ trong suốt vẫn còn, vì vậy việc gán văn bản cũng không làm cho DIV hiển thị.

Để làm cho DIVtoast hiển thị, tôi gán độ trong suốt của nó là 1.0. Thiết lập này gây nên kết quả là sự chuyển đổi hiệu ứng CSS3 mà tôi đã chỉ định trong Liệt kê 8, nhưng chỉ khi thuộc tính opacity được thực hiện sau đó (trong trường hợp này là 50ms) giống như kết quả mà setTimeout() chứa đựng. Đây là lý do tại sao:

Sự chuyển tiếp của CSS3 chỉ có thể được chỉ định cho các thuộc tính của các phần tử có trạng thái trung gian. Ví dụ, nếu bạn chuyển độ trong suốt từ 0.2 đến 0.3 (để lựa ra hai số ngẫu nhiên), có những độ trong suốt trung gian là 0.21, 0.22, và cứ thế.

Nó có ý nghĩa rằng quá trình chuyển đổi yêu cầu trạng thái trung gian, nếu không có chúng, không có cách nào để xác định sự chuyển động của quá trình chuyển đổi. Đó là lý do bạn không thể, ví dụ, chỉ định sự chuyển tiếp cho thuộc tính display, cái mà không có trạng thái trung gian. Không chỉ vậy, nếu bạn thay đổi thuộc tính display , CSS3 sẽ không đảm bảo bất kỳ sự chuyển tiếp mà bạn đã chỉ định cho bất kỳ thuộc tính khác. Đó là bởi vì bạn đang yêu cầu CSS3 làm hai điều xung đột: làm cho phần tử có thể nhìn thấy ngay lập tức bằng cách thay đổi thuộc tính display, và, từ từ mờ dần bằng thuộc tính opacity. CSS3 không thể làm cả hai thứ, vì vậy nó thay đổi thuộc tính display.

Độ trong suốt của DIV và các sự kiện

Sau phần thảo luận trước đây về splashToast(), bạn có thể tự hỏi tại sao phương thức này làm khó chịu khi thao tác thuộc tính display của DIVtoast. Tại sao không chỉ thao tác độ trong suốt của DIV để làm nó hiển thị hoặc ẩn mà thôi? Câu trả lời là trừ khi bạn có mục đích rõ ràng để làm như vậy, việc ẩn các DIV xung quanh không phải là một ý tưởng tốt, bởi vì chúng biết đến sự hiện diện của nhau, chẳng hạn như ngăn chặn sự kiện.

Vì CSS3 bỏ qua sự chuyển tiếp độ mờ nếu splashToast() gán thuộc tính displayopacity của DIVtoast một cách đồng thời, phương thức gán opacity thành 1.0 sau khi nó gán thuộc tính display— cụ thể là, khoảng 50ms sau đó.

Cuối cùng, sau khi thời gian bắt buộc hiển thị đã trôi qua, splashToast() thiết lập lại thuộc tính opacity của DIVtoast thành 0, một lần nữa tạo ra hiệu ứng CSS3 trong 0.5 giây. Hai giây sau khi bắt đầu hiệu ứng CSS3, phương thức splashToast() thiết lập lại thuộc tính display thành 0.

Tan Băng Snail Bait

Khi Snail Bait tiếp tục, nó cho người chơi thời gian để chuẩn bị với một bộ đếm ngược ba giây, được thể hiện trong Hình 3:

Hình 3. Bộ đếm ngược trong quá trình làm tan băng
Hình chụp của bộ đếm Snail Bait trong suốt quá trình tan băng

Liệt kê 10 thể hiện mã JavaScript cho bộ đếm ngược:

Liệt kê 10. Bộ đếm ngược JavaScript
var SnailBait = function (canvasId) {
   ...
   this.toast = document.getElementById('toast'),
};


window.onblur = function (e) {  // Pause if unpaused
   if (!snailBait.paused) {
      snailBait.togglePaused();
   }
};

window.onfocus = function (e) {  // unpause if paused
   var originalFont = snailBait.toast.style.fontSize;

   if (snailBait.paused) {
      snailBait.toast.style.font = '128px fantasy';

      snailBait.splashToast('3', 500); // Display 3 for one half second

      setTimeout(function (e) {
         snailBait.splashToast('2', 500); // Display 2 for one half second

         setTimeout(function (e) {
            snailBait.splashToast('1', 500); // Display 1 for one half second

            setTimeout(function (e) {
               snailBait.togglePaused();

               setTimeout(function (e) { // Wait for '1' to disappear
                  snailBait.toast.style.fontSize = originalFont;
               }, 2000);
            }, 1000);
         }, 1000);
      }, 1000);
   }
};

Khi cửa sổ Snail Bait lấy lại tiêu điểm, nó bắt đầu bộ đếm ngược bằng cách sử dụng phương thức splashToast(). Mỗi con số mờ trong 0.5 giây và sau đó biến mất trong 0.5 giây. Khi bộ đếm đếm về 0, trình xử lý onfocus thiết lập lại trò chơi.

Tuy nhiên, nếu người chơi bật cửa sổ khác hoặc tab trong suốt quá trình đếm ngược thì đoạn mã trong Liệt kê 10 có thể không làm việc một cách chính xác, bởi vì trò chơi sẽ tiếp tục vào cuối của bộ đếm ngược cho dù cửa sổ có tiêu điểm hay không. Dễ dàng khắc phục bằng cách thêm cờ windowHasFocus, được hiển thị trong Liệt kê 11:

Liệt kê 11. Tính toán cho việc mất tiêu điểm trong suốt quá trình đếm ngược
var SnailBait = function (canvasId) {
   ...
   this.windowHasFocus = true,
   ...
};
...

SnailBait.prototype = {
   ...

   splashToast: function (text, howLong) {
      howLong = howLong || this.DEFAULT_TOAST_TIME;

      toast.style.display = 'block';
      toast.innerHTML = text;

      setTimeout( function (e) {
         if (snailBait.windowHasFocus) {
            toast.style.opacity = 1.0; // After toast is displayed
         }
      }, 50);

      setTimeout( function (e) {
         if (snailBait.windowHasFocus) {
            toast.style.opacity = 0; // Starts CSS3 transition
         }

         setTimeout( function (e) { 
            if (snailBait.windowHasFocus) {
               toast.style.display = 'none'; 
            }
         }, 480);
      }, howLong);
   },
   ...
};
...

window.onblur = function (e) {  // pause if unpaused
   snailBait.windowHasFocus = false;
   
   if (!snailBait.paused) {
      snailBait.togglePaused();
   }
};

window.onfocus = function (e) {  // unpause if paused
   var originalFont = snailBait.toast.style.fontSize;

   snailBait.windowHasFocus = true;

   if (snailBait.paused) {
      snailBait.toast.style.font = '128px fantasy';

      snailBait.splashToast('3', 500); // Display 3 for one half second

      setTimeout(function (e) {
         snailBait.splashToast('2', 500); // Display 2 for one half second

         setTimeout(function (e) {
            snailBait.splashToast('1', 500); // Display 1 for one half second

            setTimeout(function (e) {
               if ( snailBait.windowHasFocus) {
                  snailBait.togglePaused();
               }

               setTimeout(function (e) { // Wait for '1' to disappear
                  snailBait.toast.style.fontSize = originalFont;
               }, 2000);
            }, 1000);
         }, 1000);
      }, 1000);
   }
};

Nhập liệu bàn phím (Keyboard input)

Người chơi sử dụng bàn phím để điều khiển nhân vật trong Snail Bait, vì thế tôi sẽ kết thúc bài này bằng cách mô tả ngắn gọn về cách trò chơi xử lý nhập liệu bàn phím. Phím dk dùng để di chuyển nhân vật sang trái và phải, trong khi jf để điều khiển nhân vật nhảy và rơi. Hình 4 thể hiện trạng thái nhân vật sau khi nhảy lên đường gạch thứ ba:

Hình 4. Nhân vật sau khi nhảy lên đường gạch thứ ba
Hình chụp của nhân vật sau khi nhảy lên đường gạch

Bạn chỉ có thể thêm bộ lắng nghe sự kiện bàn phím (keyboard event listeners) cho thành phần HTML có thể có tiêu điểm (focusable). Thành phần canvas thì không thể có tiêu điểm, vì thế Snail Bait thêm một trình xử lí sự kiện onkeydown tới đối tượng window, như trong Liệt kê 12:

Liệt kê 12. Tương tác với nhập liệu bàn phím
var runnerTrack = 1,
    BACKGROUND_VELOCITY = 42;

function turnLeft() {
   bgVelocity = -BACKGROUND_VELOCITY;
}

function turnRight() {
   bgVelocity = BACKGROUND_VELOCITY;
}

window.onkeydown = function (e) {
   var key = e.keyCode;

   if (key === 80 || (paused && key !== 80)) {  // p
      togglePaused();
   }

   if (key === 68 || key === 37) { // d or left arrow
      turnLeft();
   }
   else if (key === 75 || key === 39) { // k or right arrow
      turnRight();
   }
   else if (key === 74) { // j
      if (runnerTrack === 3) {
         return;
      }
      runnerTrack++;
   }
   else if (key === 70) { // f
      if (runnerTrack === 1) {
         return;
      }
      runnerTrack--;
   }
};

Việc nhận ra vòng lặp Snail Bait liên tục chạy là rất quan trọng. Hàm animate() liên tục được gọi bởi trình duyệt khi trình duyệt sẵn sàng để vẽ khung animation tiếp theo, và tới lượt hàm animate() liên tục gọi draw() (Liệt kê 2).

Bởi vì vòng lặp game liên tục chạy nên hàm xử lý sự kiện bàn phím chỉ đơn giản làm nhiệm vụ thiết lập giá trị cho các biến trong trò chơi. Ví dụ, khi bạn nhấn phím k để di chuyển nhân vật sang phải, trình xử lí sự kiện gán bgVelocity thành BACKGROUND_VELOCITY = 42 (pixel/giây), và khi bạn nhấn phím d để di chuyển nhân vật sang trái, trình xử lý sự kiện gán bgVelocity thành -42 pixel/giây. Cho đến sau này, những thiết lập đó vẫn có hiệu lực đối với các khung hoạt hình tiếp theo.


Lần tới

Trong bài viết kế tiếp của loạt bài này, tôi sẽ chỉ cho bạn cách để biến đồ họa tĩnh của Snail Bait thành các đối tượng hoạt hình được biết như là sprites (bóng lượn). Bạn sẽ biết làm thế nào để vẽ chúng trong những cách khác nhau, bao gồm vẽ chúng từ một spritesheet, bạn sẽ thấy làm thế nào để kết hợp chúng vào mã đã tồn tại của Snail Bait. Hẹn gặp bạn ở lần sau.


Tải về

Mô tảTênKích thước
Sample codej-html5-game3.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 dùng cho trò chơi video nhảy bậc thềm nguồn mở phổ biến này cho Android.

Thảo luận

  • Tham gia vào cộng đồng developerWorks. Kết nối với những người dùng khác trên developerWorks trong khi khám phá các blog, các diễn đàn, các nhóm và các wiki theo hướng nhà phát triển.

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ở
ArticleID=939471
ArticleTitle=Lập trình game 2D trên HTML5, Phần 3: Thiết lập các giai đoạn
publish-date=07312013