Beezaro | Làm HTML canvas game bằng Create JS

37487b5ea686cb89e5a498774b80657c.png

Trong bài viết nay tôi sẽ hướng dẫn mọi người làm một game nhỏ nhỏ dùng CreateJS - một thư viện Javascript vô cùng thú vị.

Mọi người có thể thử chơi game tại http://heasygame.com/games/beezaro.html. Chắc hẳn ai ai cũng biết trò chơi cờ caro, ai xếp được 5 quân của mình thẳng hàng với nhau là thắng. Trò này cũng vậy, tuy nhiên thay vì bàn cờ ô vuông 8 hướng thì tôi làm bàn cơ hình tổ ong 6 hướng, và tôi gọi trò chơi của mình là Beezaro (yeah).

Create JS là gì?

Bạn có thể tìm thấy tất cả những thứ có liên quan đến Create JS từ trang chủ của nó http://www.createjs.com/.

Nói ngắn gọn nhất theo lời quảng cáo trên trang chủ, Create JS là một bộ thư viện Javascript bao gồm nhiều thư viện độc lập, nhưng có thể hoạt động cùng nhau để làm ra những ứng dụng HTML5 tuyệt vời.

4 thư viện hiện tại của Create JS là:

  • Easel JS: Một thư viện cho phép chúng ta thao tác trên HTML5 Canvas rất dễ dàng. Hình dung cái này là để vẽ hình rồi thiết lập các event trên các hình đó. Beezaro được viết hoàn toàn dùng Easel JS.
  • TweenJS: Cho phép tạo ra các ảnh động hay hoạt hình trên canvas.
  • SoundJS: Làm việc với âm thanh dễ dàng hơn rất nhiều.
  • PreloadJS: Quản lý và điều khiển việc loading của các assets.

Demo thì các bạn có thể xem tại đây. Các bạn cũng có thể tìm thấy một số lượng không giới hạn các bài hướng dẫn từ cơ bản đến nâng cao trên chính trang chủ của Create JS. Cộng đồng sử dụng Create JS cũng khá lớn, gần như cần cái gì là có thể tìm ngay ra câu trả lời 😄

Với một số mục đích bí mật, tôi có nhu cầu làm một HTML5 game, tiện thể thì viết luôn hướng dẫn. Hi vọng nó sẽ có ích với các bạn quan tâm.

Beezaro

Các bạn có thể tham khảo source code đầy đủ trên repo github của tôi https://github.com/bs90/bs90.github.io

Chuẩn bị

Tất cả những gì bạn cần làm là include 2 thư viện jquerycraete js vào header của file HTML.

  <script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
  <script src="https://code.createjs.com/createjs-2015.11.26.min.js"></script>

Do game trong bài này chỉ dùng Easel JS nên các bạn chỉ cần include mỗi nó thôi cũng được. Link CDN của các thư viện thì các bạn hãy dành 5 giây google nhé.

Tạo HTML canvas

Nội dung body chỉ vẻn vẹn như sau

<div style="text-align: center;">
  <canvas style="background-color: white; border-bottom: 10px solid #209900; border-top: 10px solid #209900" id="gameCanvas" width="950" height="550"></canvas>
</div>

(Hãy tha thứ cho việc tôi viết inline CSS)

Canvas có id là gameCanvas sẽ chính là nơi chúng ta dùng thư viện để vẽ vời lên.

Vẽ bàn thi đấu

    $(function(){
      init(); // Hàm init được gọi sau khi load xong trang
    });

    function init() {
      stage = new createjs.Stage("gameCanvas"); // Khởi tạo một stage của Create JS trên canvas có id là gameCanvas
      draw_game_table(stage); // Vẽ bàn thi đấu
      ...
    }

    function polygon_maker(x, y, size, color) {
      // Đây là hàm vẽ một hình lục giác đều với vị trí, kích cỡ, màu sắc tuỳ ý
      var polygon = new createjs.Shape();
      polygon.graphics.beginFill(color).drawPolyStar(x, y, size, 6, 0, 30);
      return polygon;
    }

    function draw_game_table(stage) {
      // Đây là hàm vẽ 160 hình lục giác theo đúng thứ tự
      var polygon;
      for (j=0; j<=5; j++) {
        for (i=1; i<=15; i++) {
          polygon = polygon_maker(40 + 54 * i, 35 + j * 96, 30, "#209900");
          ...
          stage.addChild(polygon);
        }
        if (j != 5) {
          for (i=1; i<=14; i++) {
            polygon = polygon_maker(67 + 54 * i, 83 + j * 96, 30, "#209900");
            ...
            stage.addChild(polygon);
          }
        }
      }
      stage.update(); // Update lại stage, lúc này các hình lục giác mới chính thức xuất hiện trên canvas
    }

Và đây là kết quả:

6f0039cceb9045944684553c92160c51.png

Khởi tạo mạng để lưu trạng thái của game

Phần này tôi tự cho mình là khá tinh tế, hay nói các khác là khôn vặt. Thay vì dùng mảng hai chiều 15x11 để lưu thì tôi dùng mảng 23x19 để lưu. Vì sau thì hồi sau sẽ rõ. Giá trị mặc định của các phần từ là 0, tức là trạng thái không có gì, 1 nếu có nước đi của người chơi số 1, 2 nếu có nước đi của người chơi số 2 ở ô tương ứng.

    function init_game_array() {
      for (j=0; j<=18; j++) {
        game_state[j] = new Array();
        for (i=0; i<=22; i++) {
          game_state[j][i] = 0;
        }
      }
    }

Gán event cho các hình lục giác khi ấn chuột vào

Ở phần code phía trên, các bạn có thể thấy có vài phần vẫn để .... Sau khi sinh ra mỗi hình lục giác, trước khi add chúng vào stage, chúng ta cần add hành động cho chúng.

          ...
          polygon = polygon_maker(40 + 54 * i, 35 + j * 96, 30, "#209900");
          polygon.addEventListener("click", move_click(j*2, i-1));
          stage.addChild(polygon);
          ...

    function move_click(j, i) {
      return function(event) {
        if (game_log.indexOf(j+" "+i) != -1){
          return
        } else {
          game_log.push(j+" "+i);
        }
        place_move(current_player, j, i);
        current_player = current_player == 1 ? 2 : 1;
        check_game_over();
      }
    }

    function place_move(player, x, y) {
      game_state[x+4][y+4] = player;
      draw_game_state();
    }

Các bạn hãy dành thời gian xem code trên repo github của tôi để hiểu rõ hơn nhé. Cơ bản là đây là khi click vào một ô chưa có nước đi nào, sẽ gán nước đi tiếp theo vào ô đó 😄

Kiểm tra trạng thái kết thúc của game và vẽ đường thẳng chiến thắng

Phần nay vận dụng cái "tinh tế" phía trên tôi nói. Để kiểm tra game kết thúc hay chưa tất nhiên phải kiểm tra xem đã có 5 nước đi nào của cùng 1 user thẳng 1 hàng hay chưa. Và để không phãi nghĩ đến trường hợp biên, tôi cộng vào một chiều 4 đơn vị nữa (thế mới thành 23x19) để khi cộng hay trừ đi 1 2 3 4 thì không bị lỗi =)). Việc còn lại đơn giản như sau:

    (function(){
      Array.prototype.check_same = function(){
        for(var i=1;i<this.length;i++){
          if(this[0]==0 || this[i]!=this[0]) return false;
        }
        return true;
      }
    })();

    function check_game_over() {
      var win_line = new createjs.Shape();
      win_line.name = "win_line";
      win_line.graphics.setStrokeStyle(3);
      win_line.graphics.beginStroke("#e74c3c");
      for (j=4; j<=14; j++) {
        for (i=4; i<=18; i++) {
          if ([game_state[j][i-4],game_state[j][i-3],game_state[j][i-2],game_state[j][i-1],game_state[j][i]].check_same()
            ||[game_state[j][i+1],game_state[j][i-3],game_state[j][i-2],game_state[j][i-1],game_state[j][i]].check_same()
            ||[game_state[j][i+1],game_state[j][i+2],game_state[j][i-2],game_state[j][i-1],game_state[j][i]].check_same()
            ||[game_state[j][i+1],game_state[j][i+2],game_state[j][i+3],game_state[j][i-1],game_state[j][i]].check_same()
            ||[game_state[j][i+1],game_state[j][i+2],game_state[j][i+3],game_state[j][i+4],game_state[j][i]].check_same()) {
            if (j%2 == 0) {
              win_line.graphics.moveTo(40 + 54 * (i-3) - 30, 35 + (j-4)/2 * 96).lineTo(40 + 54 * (i-3) + 30, 35 + (j-4)/2 * 96);
            } else {
              win_line.graphics.moveTo(67 + 54 * (i-3) - 30, 83 + (j-5)/2 * 96).lineTo(67 + 54 * (i-3) + 30, 83 + (j-5)/2 * 96);
            }
            // console.log("["+(j-4)+","+(i-4)+"]");
          }
          var ya = j%2 == 0 ? [i-2,i-2,i-1,i-1,i,i+1,i+1,i+2] : [i-2,i-1,i-1,i,i+1,i+1,i+2,i+2];
          if ([game_state[j-4][ya[0]],game_state[j-3][ya[1]],game_state[j-2][ya[2]],game_state[j-1][ya[3]],game_state[j][i]].check_same()
            ||[game_state[j+1][ya[4]],game_state[j-3][ya[1]],game_state[j-2][ya[2]],game_state[j-1][ya[3]],game_state[j][i]].check_same()
            ||[game_state[j+1][ya[4]],game_state[j+2][ya[5]],game_state[j-2][ya[2]],game_state[j-1][ya[3]],game_state[j][i]].check_same()
            ||[game_state[j+1][ya[4]],game_state[j+2][ya[5]],game_state[j+3][ya[6]],game_state[j-1][ya[3]],game_state[j][i]].check_same()
            ||[game_state[j+1][ya[4]],game_state[j+2][ya[5]],game_state[j+3][ya[6]],game_state[j+4][ya[7]],game_state[j][i]].check_same()) {
            if (j%2 == 0) {
              win_line.graphics.moveTo(40 + 54 * (i-3) - 17, 35 + (j-4)/2 * 96 - 30).lineTo(40 + 54 * (i-3) + 17, 35 + (j-4)/2 * 96 + 30);
            } else {
              win_line.graphics.moveTo(67 + 54 * (i-3) - 17, 83 + (j-5)/2 * 96 - 30).lineTo(67 + 54 * (i-3) + 17, 83 + (j-5)/2 * 96 + 30);
            }
            // console.log("["+(j-4)+","+(i-4)+"]");
          }
          ya = j%2 == 0 ? [i+2,i+1,i+1,i,i-1,i-1,i-2,i-2] : [i+2,i+2,i+1,i+1,i,i-1,i-1,i-2];
          if ([game_state[j-4][ya[0]],game_state[j-3][ya[1]],game_state[j-2][ya[2]],game_state[j-1][ya[3]],game_state[j][i]].check_same()
            ||[game_state[j+1][ya[4]],game_state[j-3][ya[1]],game_state[j-2][ya[2]],game_state[j-1][ya[3]],game_state[j][i]].check_same()
            ||[game_state[j+1][ya[4]],game_state[j+2][ya[5]],game_state[j-2][ya[2]],game_state[j-1][ya[3]],game_state[j][i]].check_same()
            ||[game_state[j+1][ya[4]],game_state[j+2][ya[5]],game_state[j+3][ya[6]],game_state[j-1][ya[3]],game_state[j][i]].check_same()
            ||[game_state[j+1][ya[4]],game_state[j+2][ya[5]],game_state[j+3][ya[6]],game_state[j+4][ya[7]],game_state[j][i]].check_same()) {
            if (j%2 == 0) {
              win_line.graphics.moveTo(40 + 54 * (i-3) + 17, 35 + (j-4)/2 * 96 - 30).lineTo(40 + 54 * (i-3) - 17, 35 + (j-4)/2 * 96 + 30);
            } else {
              win_line.graphics.moveTo(67 + 54 * (i-3) + 17, 83 + (j-5)/2 * 96 - 30).lineTo(67 + 54 * (i-3) - 17, 83 + (j-5)/2 * 96 + 30);
            }
            // console.log("["+(j-4)+","+(i-4)+"]");
          }
        }
      }
      stage.addChild(win_line);
      stage.update();
    }

Dù trông hơi vô học nhưng mà thực sự là nó vẫn chạy (tin tôi đi !). Nếu bạn có cách check nào hay hơn hãy chỉ cho tôi với (khoc)

Hết rồi (tl;dr)! Chúc bạn có những giờ phút vui vẻ với Create JS!