Fork me on GitHub

Hướng dẫn tạo trò chơi Space Invaders

Trong hướng dẫn này, chúng ta sẽ tạo một bản sao của trò chơi space invaders (bắn ong). Hướng dẫn này chủ yếu tập trung vào tạo thêm các yếu tố trò chơi thông qua mã, và dùng các API khác do MelonJS cung cấp mà hướng dẫn platformer không có nói đến.

Giới thiệu

Để thực hiện hướng dẫn này, bạn cần những công cụ sau:

  • melonJS boilerplate, mà chúng ta sẽ dùng như dự án mẫu mặc định cho hướng dẫn.
  • Các tập tin ảnh hướng dẫn, giải nén vào thư mục dữ liệu boilerplate. Khi bạn giải nén, bạn sẽ có:
  • data/img/player.png
    data/img/ships.png
    js/game.js
    etc
    
  • Thư viện melonJS. Nếu bạn đã tải về boilerplate, bạn sẽ có nó sẵn. Nó phải được sao chép vào thư mục /lib. Bạn có thể sao chép phiên bản đang phat triển, vì boilerplate cung cấp một tác vụ rút gọn.
  • Tài liệu melonJS để có thêm thông tin chi tiết

Kiểm thử/gỡ lỗi :

Cách tốt nhất là dùng một máy chủ web cục bộ, ví dụ cụ thể ở trong README melonJS boilerplate, bằng cách dùng công cụ `grunt connect`, và nó sẽ cho phép bạn thử nghiệm trò chơi trong trình duyệt của bạn bằng cách dùng url http://localhost:8000.

Nếu bạn chỉ muốn dùng hệ thống tập tin, vấn đề là bạn sẽ gặp lỗi bảo mật "cross-origin request". Với Chrome, bạn cần dùng thông số "--disable-web-security" hoặc tốt hơn là "--allow-file-access-from-files" khi gọi trình duyệt. Việc này phải được hoàn tất để kiểm thử bất kỳ nội dung cục bộ, hoặc trình duyệt sẽ phàn nàn khi cố gắng nạp các tài nguyên thông qua XHR. Dù phương thức này không được khuyến khích, bởi vì miễn là bạn bật tùy chọn, bạn đang thêm các lỗ hổng bảo mật vào môi trường của bạn.

Thiết lập tàu của bạn

Cấu trúc thư mục của bạn từ boilerplate sẽ trông như thế này:

data/
  img/
    player.png
    ships.png
js/
  game.js
  resources.js
  entities/
    HUD.js
    entities.js
  screens/
    play.js
    title.js
index.html
index.css

The boilerplate cung cấp một loạt mã mặc định. Đối với hướng dẫn này, sẽ có một số tập tin mà chúng ta sẽ không cần. Bạn có thể xóa tập tin screens/title.js, và xóa toàn bộ thư mục js/entities. Sau đó cập nhật tập tin index.html để nó không còn bao gồm các tập tin đó nữa, và xóa tham chiếu TitleScreen từ tập tin game.js.

var game = {
    // Run on page load.
    onload : function () {
        // Initialize the video.
        if (!me.video.init(640, 480, {wrapper : "screen", scale : 'auto'})) {
            alert("Your browser does not support HTML5 canvas.");
            return;
        }

        // add "#debug" to the URL to enable the debug Panel
        if (me.game.HASH.debug === true) {
            window.onReady(function () {
                me.plugin.register.defer(this, me.debug.Panel, "debug", me.input.KEY.V);
            });
        }

        // Initialize the audio.
        me.audio.init("mp3,ogg");

        // Set a callback to run when loading is complete.
        me.loader.onload = this.loaded.bind(this);

        // Load the resources.
        me.loader.preload(game.resources);

        // Initialize melonJS and display a loading screen.
        me.state.change(me.state.LOADING);
    },

    // Run on game resources loaded.
    loaded : function () {
        // set the "Play/Ingame" Screen Object
        this.playScreen = new game.PlayScreen();
        me.state.set(me.state.PLAY, this.playScreen);

        // start the game
        me.state.change(me.state.PLAY);
    }
};

Game.js là nơi trò chơi được khởi tạo. index.html gọi hàm game.onload sau khi nạp tất cả tập tin js trong sự kiện window sẵn sàng. Bit me.video.init tạo thẻ canvas và cài đặt video.

Dòng 12-16 là dành cho sử dụng bảng điều khiển gỡ lỗi.

Sau đó chúng ta khởi tạo cơ chế âm thanh, chỉ cho nó những định dạng chúng ta hỗ trợ đối với trò chơi này.

Chúng ta đặt một lệnh callback trên me.loader cho hàm đã nạp, và chỉ những tập tin nào cần nạp, thông qua một mảng.

Bước cuối cùng của quá trình này là thiết lập trạng thái của trò chơi thành đang nạp.

loaded : function () {
  this.playScreen = new game.PlayScreen();
  me.state.set(me.state.PLAY, this.playScreen);

  // start the game
  me.state.change(me.state.PLAY);
}

Hàm được nạp sẽ thiết lập màn hình chơi và bảo trò chơi dùng đối tượng màn hình đó cho trạng thái chơi.

Sau đó trạng thái trò chơi được đặt là PLAY (chơi).

Trở lại space invaders

Thứ đầu tiên cần thêm vào là hình ảnh vào tập tin resources.js

game.resources = [
    { name: "player", type: "image", src: "data/img/player.png" },
    { name: "ships", type: "image", src: "data/img/ships.png" }
];

Biến này được chuyển đến me.loader.preload trong game.js

Cấu trúc cho một tập tin là :

tên Tên của tập tin bạn muốn dùng trong trò chơi của bạn. Một chuỗi phím.
loại Loại tập tin. Các loại hợp lệ là: âm thanh, nhị phân, ảnh, json, tmx, tsx. Nhị phân là một giải pháp hay cho việc nạp văn bản thô, hoặc bất kỳ định dạng khác không được liệt kê. TMX & TSX là cho các định dạng tập tin chia ô. Bất kể nó là định dạng xml hay json.
src Đường dẫn đến tập tin, tương đối từ index.html. Đối với âm thanh bạn cần chỉ ra thư mục thay vì đường dẫn trực tiếp.

Mở js/screens/play.js và xóa trống mã của hai phương thức: onResetEvent và onDestroyEvent. Sau đó lưu lại, và mở lại trò chơi trong trình duyệt web của bạn.

Không có gì nhiều để thấy. Hãy thay đổi nó.

Việc đầu tiên là tạo một thực thể người chơi.

Thêm một tập tin mới dưới thư mục js, và gọi nó player.js. Xin đảm bảo thêm nó vào trong index.html.

game.Player = me.Sprite.extend({
  init : function () {
      var image = me.loader.getImage("player");
      this._super(me.Sprite, "init", [
          me.game.viewport.width / 2 - image.width / 2,
          me.game.viewport.height - image.height - 20,
          { image : image }
      ]);
  }
});

Những gì chúng ta đang làm là thêm một hàm vào đối tượng window.game, no sẽ mở rộng me.Sprite. Nó thiết lập một phương thức init sẽ lấy hình người chơi.

Đối với tọa độ x, chúng ta đơn giản chỉ lấy trung tâm chết, và trừ một nửa tàu, nó có thể được định vị ở trung tâm. và sau đó đặt thuộc tính y của nó là 20 pixel trên đáy. Sau đó cuối cùng là chuyển thể hiện ảnh đến nó.

Gọi this._super là cách chúng ta tham chiếu phương thức của lớp cha. Trong trường hợp này, chúng ta đang gọi constructor me.Sprite.

Hãy thiết lập người chơi trong kho thực thể. Mở game.js và thêm đoạn mã sau vào đầu phương thức đã nạp:

me.pool.register("player", game.Player);

Giờ hãy mở js/screens/play.js, và sửa phương thức onResetEvent, nó sẽ trông như thế này:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(me.pool.pull("player"));
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
  }
});

onResetEvent được gọi khi trạng thái này được nạp.


    me.state.change(me.state.PLAY);
    

Trong tập tin game.js, onResetEvent sau đó được gọi.

Yay, tàu đang nằm ở cuối màn hình!

Nhưng chúng ta vẫn có thể thấy thanh nạp, không hay lắm. Lý do cho việc này là MelonJS không muốn làm bất kỳ hành động mà nó không được chỉ dẫn. Đôi khi bạn sẽ có một ảnh nền được đồ họa lại, nó bao phủ thanh nạp ban đầu. Tuy nhiên, chúng ta không có một ảnh nền cho trò chơi này, vì thế cách chúng ta sẽ làm là thêm một lớp màu.

    me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
    

Thêm đoạn mã này vào màn hình trò chơi, trên dòng mà chúng ta đã thêm người chơi. Thông số đầu tiên đơn giản là tên của lớp, vì thế rất dễ để nạp nó từ thế giới trò chơi sau này nếu bạn cần.

Thông số thứ hai là màu để vẽ theo mã hex.

Thông số thứ hai được chuyển đến hàm addChild là z index. Chúng ta muốn vẽ nó trước, vì thế chúng ta đặt nó là zero (0).

Giờ thanh nạp phiền phức đã biến mất. Đến lúc thêm vào một kẻ thù. Tạo một tập tin mới trong thư mục js gọi là enemy.js, và thêm nó vào tập tin index.html.

Bởi vì kẻ thù sẽ phải va chạm với những thứ như đạn laser của người chơi, nó nên mở rộng me.Entity trái ngược với me.Sprite, hãy làm như sau:

game.Enemy = me.Entity.extend({
  init: function (x, y) {
      this._super(me.Entity, "init", [x, y, {
          image : "ships",
          width : 32,
          height : 32
      }]);
  },

  update: function (dt) {
      this._super(me.Entity, "update", [dt]);
      return true;
  }
});

Với kẻ thù, chugns ta sẽ cần đặt trong ở những vị trí khcas nhau, vì thế x & y sẽ được thêm vào constructor của nó, và sau đó chuyển nó đến constructor của me.Entity. Thông số thứ ba trong mảng là một băm các thiết lập. Các thiết lập chỉ định hình là "tàu", tham chiếu đến mảng game.resources. Chiều rộng và chiều cao được đặt là 32x32.

Chúng ta đang tạo một phương thức cập nhật tùy chỉnh, để chỉ cơ chế trò chơi vẽ lại. Khi melon đi qua vòng lặp trò chơi, nó làm một toán tử hoặc trên kết quả. Nếu không có thay đổi trong frame đã cho, nó sẽ không vẽ lại. Trả về true (đúng), và gọi siêu phương thức sẽ đảm bảo kẻ thù được render thực sự.

Trong game.js, thêm kẻ thù vào kho thực thể:

me.pool.register("enemy", game.Enemy);
    

Trở lại play.js, thêm một kẻ thù vào thế giới trò chơi. play.js giờ trông như thế này:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
      me.game.world.addChild(me.pool.pull("player"), 1);
      me.game.world.addChild(me.pool.pull("enemy", 50, 50), 2);
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
  }
});

Bạn có thể đặt kẻ thù ở bất kỳ x & y để thử nghiệm. Lưu & làm mới trang trong trình duyệt của bạn.

Bạn sẽ nhận thấy chiếc tàu liên tục thay đổi bề ngoài của nó. Nếu bạn mở tập tin ships.png trong thư mục data/img, bạn có teher thấy nó là một bảng sprite có chứa 4 tàu khác nhau. me.Entity for its renderable uses the me.AnimationSheet class. Bởi vì chúng ta đã không thêm và đặt bất kỳ chuyển động trên thuộc tính renderable, nó chỉ lặp qua mỗi & mọi frame. Hãy sửa nó.

Thêm một phương thức mới cho kẻ thù chúng ta:

chooseShipImage: function () {
    var frame = ~~(Math.random() * 3);
    this.renderable.addAnimation("idle", [frame], 1);
    this.renderable.setCurrentAnimation("idle");
}

Dòng đầu tiên đơn giản là tạo ngẫu nhiên số frame ta muốn. Tàu là 32x32, hình là 64x64, vậy ta có 4 frame. ~~ là một lối tắt cho Math.floor khi số lượng là 0 hoặc dương. Nếu số âm, nó hoạt động như Math.ceil

Dòng thứ hai truy cập thể hiện bảng chuyển động (this.renderable), và dùng hàm addAnimation để thêm một frame idle (không hoạt động). Vì thế chúng ta chỉ cần chỉ định tạo ngẫu nhiên index.

Với dòng cuối, chúng ta đặt chuyển động hiện tại là idle.

Bây giờ hãy gọi hàm ở cuối constructor, như sau:

game.Enemy = me.Entity.extend({
    init: function (x, y) {
        this._super(me.Entity, "init", [x, y, {
            image: "ships",
            width: 32,
            height: 32
        }]);
        this.chooseShipImage();
    },

    chooseShipImage: function () {
        var frame = ~~(Math.random() * 3);
        this.renderable.addAnimation("idle", [frame], 1);
        this.renderable.setCurrentAnimation("idle");
    },
});

Bây giờ hãy làm mới lại trang, và tàu của chúng ta sẽ chỉ xuất hiện như một trong số chúng. Hãy thử làm mới nó nhiều lần để xem sự thay đổi.

Áp dụng di chuyển

Bây giờ chúng ta đã có tàu trên màn hình, hãy tạo vài tương tác thật sự nào.

Trở lại play.js, hãy thêm vài gán phím:

game.PlayScreen = me.ScreenObject.extend({
  /**
   * action to perform on state change
   */
  onResetEvent : function () {
      me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
      me.game.world.addChild(me.pool.pull("player"), 1);
      me.game.world.addChild(me.pool.pull("enemy", 50, 50), 2);

      me.input.bindKey(me.input.KEY.LEFT, "left");
      me.input.bindKey(me.input.KEY.RIGHT, "right");
      me.input.bindKey(me.input.KEY.A, "left");
      me.input.bindKey(me.input.KEY.D, "right");
  },

  /**
   * action to perform when leaving this screen (state change)
   */
  onDestroyEvent : function () {
      me.input.unbindKey(me.input.KEY.LEFT);
      me.input.unbindKey(me.input.KEY.RIGHT);
      me.input.unbindKey(me.input.KEY.A);
      me.input.unbindKey(me.input.KEY.D);
  }
});

Phương thức được gọi ở đây rất đơn giản. Chúng ta gán một nhấn phim vào một tên hành động. Có thể gán nhiều phím vào một tên hành động.

Nó thường là một thực hành thiết kế trò chơi hay khi cấp nhiều gán phím. Thậm chí thực hành tốt hơn giúp nó có thể thiết lập được. Bạn luôn cần ghi nhớ người tay trái hoặc những người có bố cục khác biệt.

Bạn cũng có thể thấy là tôi đã thêm tùy chọn z index vào các lệnh gọi addChild. Đó là một thực hành khá tốt, bởi vì theo cách đó bạn đảm bảo thứ tự đồ họa của mình.

onDestroyEvent xóa các sự kiện khi trạng thái thay đổi. Không phải là cái chúng ta thật sự cần, bởi vì chúng ta chỉ có trạng thái chơi sau khi nạp. Nhưng là một kinh nghiệm thực hành đáng nhớ.

Bây giờ chúng ta có các gán phím, hãy thực hiện di chuyển của người chơi. Thêm hàm cập nhật sau vào lớp người chơi:

Sau đó thêm một thuộc tính velx vào người chơi trong phương thức init của nó, cũng như vị trí x xa nhất nó có thể đi trên màn hình (maxX):

init : function () {
    var image = me.loader.getImage("player");
    this._super(me.Sprite, "init", [
        me.game.viewport.width / 2 - image.width / 2,
        me.game.viewport.height - image.height - 20,
        image
    ]);
    this.velx = 450;
    this.maxX = me.game.viewport.width - this.width;
},

Sau đó chỉnh sửa phương thức cập nhật để kiểm tra các sự kiện phím, và di chuyển người chơi tương ứng theo.

update : function (time) {
    this._super(me.Sprite, "update", [time]);
    if (me.input.isKeyPressed("left")) {
        this.pos.x -= this.velx * time / 1000;
    }

    if (me.input.isKeyPressed("right")) {
        this.pos.x += this.velx * time / 1000;
    }

    this.pos.x = this.pos.x.clamp(0, this.maxX);

    return true;
}

Các hàm cập nhật của các đối tượng trò chơi sẽ luôn nhận một thời gian delta (mili giây). Cần chuyển nó cùng với cập nhật lớp cha.

this._super(me.Sprite, "update", [time]);

Sau đó, kiểm tra sẽ gặp trục trặc nếu hành động left vẫn được nhấn. Dùng giá trị vận tốc đã đặt trước đó, chúng ta trừ giá trị vận tốc, nhân với delta theo giây.

if (me.input.isKeyPressed("left")) {
    this.pos.x -= this.velx * time / 1000;
}

Để di chuyển sang phải, chúng ta kiểm tra hành động right, và thêm giá trị vận tốc vào vị trí x của chúng ta.

if (me.input.isKeyPressed("right")) {
    this.pos.x += this.velx * time / 1000;
}

Chúng ta sau đó dùng clamp để đảm bảo giá trị x không vượt quá màn hình.

this.pos.x = this.pos.x.clamp(0, this.maxX);

Giá trị trả về nói với melon cần đồ họa lại hay không. Điều này có thể hữu ích khi chỉ định cho một bảng chuyển động cần chuyển động trên một khung đã cho, để chúng ta chỉ cần chỉ nó đồ họa lại.

return true;

Lưu tập tin & làm mới trình duyệt của bạn. Hãy thử dùng phím A/D hay phím mũi tên Trái & Phải để di chuyển.

Di chuyển của kẻ thù

Một đặc tính của trò chơi space invaders là tất cả tàu di chuyển theo một hướng, bay thẳng xuống và sau đó trở lại theo hướng khác. Tất cả chúng di chuyển cùng nhau. Chúng ta có thể lấy logic vận tốc mà chúng ta dùng cho người chơi, và áp dụng nó vào lớp kẻ thù. Nhưng chúng ta có thể tận dụng MelonJS làm việc này tốt hơn. Giờ hãy dùng lớp phụ riêng me.Container của chúng ta

Các đối tượng bên trong một container có liên quan đến cha của nó. Vì thế khi chúng ta di chuyển container, tất cả đối tượng bên trong sẽ di chuyển theo nó. Điều này áp dụng với cả các hoạt động xoay & căn chỉnh. Giờ hãy tạo một cái.

Tạo một tập tin mới: js/enemy_manager.js, và thêm nó vào index.html

game.EnemyManager = me.Container.extend({
  init : function () {
      this._super(me.Container, "init", [0, 32,
          this.COLS * 64 - 32,
          this.ROWS * 64 - 32
      ]);
      this.COLS = 9;
      this.ROWS = 4;
      this.vel = 16;
  }
});

Về cơ bản những gì chúng ta đang thiết lập ở đây là vị trí bắt đầu và chiều rộng cơ sở. Bắt đầu 32 pixel dưới, và ở 0 trái (hoặc x).

Chúng ta đang cấp 64 pixel chiều rộng & chieuf cao mỗi tàu. Sau đó trừ 32 pixel bởi vì hàng & cột cuối cùng không yêu cầu khoảng cách bê (side padding)n.

Để thêm kẻ thù vào container, chúng ta cần một phương thức khác:

createEnemies : function () {
  for (var i = 0; i < this.COLS; i++) {
      for (var j = 0; j < this.ROWS; j++) {
          this.addChild(me.pool.pull("enemy", i * 64, j * 64));
      }
  }
  this.updateChildBounds();
}

Tạo 9 cột, và 4 hàng: 36 tàu. Cũng lưu ý là lệnh gọi hàm "updateChildBounds" sẽ đảm bảo container đối tượng của chúng ta sẽ được thay đổi kích cỡ cho phù hợp để vừa với tất cả con đã thêm. Tương ứng chúng ta sẽ làm tương tự khi xóa một con

Giờ trong play.js, xóa addChild cho kẻ thù, và đặt một thuộc tính vào enemy manager. Bên dưới là lệnh gọi createEnemies, và thêm nó vào thế giới trò chơi.

onResetEvent : function () {
    me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
    me.game.world.addChild(me.pool.pull("player"), 1);

    this.enemyManager = new game.EnemyManager();
    this.enemyManager.createEnemies();
    me.game.world.addChild(this.enemyManager, 2);

    me.input.bindKey(me.input.KEY.LEFT, "left");
    me.input.bindKey(me.input.KEY.RIGHT, "right");
    me.input.bindKey(me.input.KEY.A, "left");
    me.input.bindKey(me.input.KEY.D, "right");
    me.input.bindKey(me.input.KEY.SPACE, "shoot", true);
}

Khi bạn đã lưu và làm mới, bạn sẽ thấy một đống tàu ngẫu nhiên.

Để di chuyển, hãy để nó đơn giản và cho container di chuyển môt lần mỗi giây. Đối với việc này, chúng ta có thể dùng một bộ tính giờ melonjs.

Thêm hai phương thức này vào the enemy_manager.js

onActivateEvent : function () {
    var _this = this;
    this.timer = me.timer.setInterval(function () {
        _this.pos.x += _this.vel;
    }, 1000);
},

onDeactivateEvent : function () {
    me.timer.clearInterval(this.timer);
}

Và sau đó đặt thuộc tính vel trong phương thức init thành 16:

this.vel = 16;

onActivateEvent được gọi là (nếu nó đã được định nghĩa) khi đối tượng được thêm vào thế giới trò chơi. Sự kiện này đối với bất kỳ đối tượng bạn chuyển đến addChild trên một container. Tương tự, onDeactivateEvent được gọi khi đối tượng bị xóa khỏi thế giới trò chơi.

Dùng phiên bản MelonJS của setInterval (được xây dựng thành vòng lặp trò chơi, nó không dùng window.setInterval), chúng ta có thể sau đó tăng vị trí x.

Lưu và làm mới trình duyệt. Giờ tất cả tàu kẻ thù sẽ di chuyển cùng nhau.

Sau đó thêm bản sao removeChildNow:

removeChildNow : function (child) {
    this._super(me.Container, "removeChildNow", [child]);
    this.updateChildBounds();
}

Lý do mở rộng removeChildNow thay cho removeChild, là removeChild được gọi sau khi khung hiện tại kết thúc. removeChildNow là phương thức thật sự xóa đối tượng, và chúng ta muốn thay đổi kích thước đường biên sau khi đối tượng bị xóa.

onActivateEvent : function () {
    var _this = this;
    this.timer = me.timer.setInterval(function () {
        var bounds = _this.childBounds;

        if ((_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width) ||
            (_this.vel < 0 && (bounds.left + _this.vel) <= 0)) {
            _this.vel *= -1;
            _this.pos.y += 16;
            if (_this.vel > 0) {
              _this.vel += 5;
            }
            else {
              _this.vel -= 5;
            }
        }
        else {
            _this.pos.x += _this.vel;
        }
    }, 1000);
}

That's a fair bit of code, so let's break it down.

Dùng giới hạn của con, chúng ta có thể nạp giá trị trái & phải vào tọa độ thế giới.

var bounds = _this.childBounds;

Phần đầu tiên của kiểm tra if là nếu container đang di chuyển về bên phái, và biên phải + vận tốc ở ngoài tầm nhìn.

(_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width)

Phần thứ hai kiểm tra nếu container đang di chuyển về bên trái, và biên trái của nó thấp hơn 0.

(_this.vel < 0 && (bounds.left + _this.vel) <= 0)

Trong khối, chúng ta đảo ngược vận tốc, di chuyển xuống 16 pixel và sau đó tăng vận tốc.

_this.vel *= -1;
_this.pos.y += 16;
if (_this.vel > 0) {
    _this.vel += 5;
}
else {
    _this.vel -= 5;
}

Cuối cùng, chúng ta tăng vận tốc nếu container đã không di chuyển về trái hoặc phải

else {
    _this.pos.x += _this.vel;
}

Lưu và làm mới lần này, nó giờ sẽ di chuyển qua lại trên màn hình, ngày càng gần với người chơi của chúng ta. Nhưng có một vấn đề! Nó không quay đầu lại khi nó đi đến biên! Chuyện gì sẽ xảy ra ở đây?

Lúc này, bạn nên thêm #debug vào URL: http://localhost:8000/#debug và chạm vào checkbox kế bên "hitbox" trong bảng điều khiển gỡ lỗi. Việc này sẽ bật render hitbot, để bạn có thể trực quan hóa cấu trúc bên trong các đối tượng.

Các hitbox hiển thị các biên con (hình chữ nhật màu hồng lớn chiếm vị trí ban đầu của EnemyManager) không di chuyển cùng với container. Đây là bởi vì các cập nhật biên con không được tính toán tự động. Bạn có thể dễ dàng tính toán lại các biên con bằng cách quá tải (overloading) phương thức cập nhật:

update : function (time) {
    this._super(me.Container, "update", [time]);
    this.updateChildBounds();
}

Hãy lưu và làm mới, và bạn sẽ thấy giờ nó hoạt động như mong đợi!

Thêm đạn Laser, pằng pằng!

Đã đến lúc thêm "trò chơi" thật sự vào trò chơi này.

Điều đầu tiên cần làm là mở tập tin play.js của bạn, và thêm một gán phím & bỏ gán phím mới:

me.input.bindKey(me.input.KEY.SPACE, "shoot", true);
me.input.unbindKey(me.input.KEY.SPACE);

Lý do cho biến boolean trong lệnh gọi bindKey là chỉ cho phép trên đăng ký trên mỗi nhấn phim. Để bắn hai viên đạn, người chơi phải nhấn phím cách, buông ra, và sau đó nhấn nó lại.

Trước khi chúng ta kết nối người chơi để bắn, chúng ta cần một đạn laser. Tạo tập tin laser.js, và thêm đoạn mã sau vào. Như mọi khi, xin đảm bảo thêm thẻ mã laser.js vào tập tin index.hmtml

game.Laser = me.Entity.extend({
    init : function (x, y) {
        this._super(me.Entity, "init", [x, y, { width: game.Laser.width, height: game.Laser.height }]);
        this.z = 5;
        this.body.setVelocity(0, 300);
        this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;
        this.renderable = new (me.Renderable.extend({
            init : function () {
                this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
            },
            destroy : function () {},
            draw : function (renderer) {
                var color = renderer.getColor();
                renderer.setColor('#5EFF7E');
                renderer.fillRect(0, 0, this.width, this.height);
                renderer.setColor(color);
            }
        }));
        this.alwaysUpdate = true;
    },

    update : function (time) {
        this.body.vel.y -= this.body.accel.y * time / 1000;
        if (this.pos.y + this.height <= 0) {
            me.game.world.removeChild(this);
        }

        this.body.update();
        me.collision.check(this);

        return true;
    }
});

game.Laser.width = 5;
game.Laser.height = 28;

Hãy đọc qua nó. Ở cuối, chúng ta thiết lập hai thuộc tính chiều rộng & cao cho laser, để nó có thể dễ dàng được tái sử dụng.

game.Laser.width = 5;
game.Laser.height = 28;

Truyền thống đây. Cài đặt vị trí x & y từ các thông số của nó, và một thuộc tính rộng+cao. Một chút khác biệt so với các đối tượng khác, chúng ta đặt z index lên đối tượng một cách thủ công. Đây là một cách khác để chuyển z index trong lệnh gọi addChild.

this._super(me.Entity, "init", [x, y, { width: game.Laser.width, height: game.Laser.height }]);
this.z = 5;

Những phương thức kế tiếp này là để thiết lập một thân thể vật lý. Cái mà chúng ta sẽ dùng để di chuyển laser xuyên màn hình.

this.body.setVelocity(0, 300);
this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;

Theo mặc định, me.Body sẽ không thiết lập hình dạng cho bạn. Tuy nhiên me.Entity lại tạo một hình dạng dựa trên vị trí, chiều rộng và chiều cao.

Đầu tiên chúng ta thiết lập một vận tốc. Vận tốc là một vector, và chúng ta muốn laser di chuyển theo. Vì thế ta đặt vận tốc là 300. Lưu ý là vận tốc không bao giờ được âm theo hướng đã định.

this.body.setVelocity(0, 300);

Sau đó chúng ta đặt một kiểu va chạm. Việc này rất hữu ích trong lệnh gọi callback va chạm.

this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;

Đây là một đoạn mã ngắn nữa. me.Renderable là lớp vẽ cơ bản trong melon. Nó cung cấp bộ cài đặt tối thiểu cần thiết cho một đối tượng vẽ hợp lệ trong thế giới trò chơi.

this.renderable = new (me.Renderable.extend({
    init : function () {
        this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
    },
    destroy : function () {},
    draw : function (renderer) {
        var color = renderer.getColor();
        renderer.setColor('#5EFF7E');
        renderer.fillRect(0, 0, this.width, this.height);
        renderer.setColor(color);
    }
}));

Hàm init khá truyền thống, chúng ta đặt x & y thành 0, vì đây sẽ liên quan đến thực thể laser. Nên nó dùng cùng chiều rộng & chiều cao.

init: function () {
    this._super(me.Renderable, "init", [0, 0, game.Laser.width, game.Laser.height]);
},

Phương thức tiêu diệt rỗng là một callback khi renderable bị xóa. Chúng ta không cần định nghĩa logic callback, nhưng hàm vẫn cần được định nghĩa.

destroy : function () {},

Sau đó chúng ta chạy phương thức đồ họa. Các lớp chúng ta dùng đến đây đã cung cấp một phương thức đồ họa hoạt động cho các mục đích của chúng ta, giờ chúng ta sẽ dùng trình render để đồ họa gì đó bằng tay.

draw : function (renderer) {
    var color = renderer.getColor();
    renderer.setColor('#5EFF7E');
    renderer.fillRect(0, 0, this.width, this.height);
    renderer.setColor(color);
}

Trình render có thể là me.CanvasRenderer hoặc me.WebGLRenderer, tùy thuộc vào các cài đặt của bạn trong me.video.init. Trình render cung cấp các tác vụ vẽ cơ bản.

Chúng ta đầu tiên có tham chiếu của màu ban đầu. Giá trị trả về của getColor() là một thể hiện của me.Color.

var color = renderer.getColor();

Đặt màu thành một màu xanh laser đẹp.

renderer.setColor('#5EFF7E');

Sau đó dùng một con số chữ nhật đầy. Một lần nữa, 0, 0 có liên quan. Sau đó dùng chiều rộng & cao để chỉ định kích cỡ của hình chữ nhật chúng ta đang tô màu

renderer.fillRect(0, 0, this.width, this.height);

Sau đó đặt màu trở lại. Việc này rất cần để các lệnh đồ họa khác không bị tác động bởi đổi màu.

renderer.setColor(color);

Nói chung, bạn nên tạo trò chơi bằng cách dùng ảnh trên các lệnh gọi đồ họa canvas thuần khiết, nhưng biết cách làm và khi dùng canvas có thể hữu ích hơn.

Bước cuối cùng là cho phương thức init của đạn Laser:

init : function (x, y) {
    // ...
    this.alwaysUpdate = true;
}

Thuộc tính alwaysUpdate là né tránh càng nhiều càng tốt. Nó sẽ cập nhật một đối tượng khi nó nằm ngoài tầm nhìn. Lý do dùng nó trong trò chơi là bởi vì chúng ta không muốn xóa đạn laser cho đến khi nó nằm ngoài màn hình. Nếu chúng ta đợi cho đến khi nó khuất màn hình, và alwaysUpdate sai (false), nó sẽ không bao giờ bị xóa.

Phát biểu của phương thức cập nhật.

update : function (time) {
    this.body.vel.y -= this.body.accel.y * time / 1000;
    if (this.pos.y + this.height <= 0) {
        me.game.world.removeChild(this);
    }

    this.body.update();
    me.collision.check(this);

    return true;
}

Bit đầu tiên là cách chúng ta di chuyển laser. Tàu di chuyển bằng cách điều khiển trực tiếp vị trí. Bởi vì tàu này có một thân thể va chạm, chúng ta sẽ điều khiển vận tốc y, bằng cách trừ gia tốc y.

this.body.vel.y -= this.body.accel.y * time / 1000;

Nếu vị trí của laser cộng với chiều cao (cũng như đáy của laser) nhỏ hơn 0, chúng ta có thể xóa laser khỏi thế giới trò chơi. Một lần nữa, giờ tính năng sẽ hoạt động bởi vì alwaysUpdate được đặt thành true (đúng).

if (this.pos.y + this.height <= 0) {
    me.game.world.removeChild(this);
}

Cập nhật thân thể rất quan trọng, nó áp dụng vận tốc độ chúng ta đặt cho frame này.

this.body.update();

Sau đó chúng ta bảo MelonJS để kiểm tra va chạm đối tượng này với me.collision.check(this).

me.collision.check(this);

Bước kế tiếp cho tính năng này, là thêm laser vào kho thực thể. Thêm đoạn mã sau vào game.js, tương tự các đối tượng Người chơi & Kẻ thù.

me.pool.register("laser", game.Laser);

Sau đó trở lại tập tin player.js, thêm đạn laser vào phương thức cập nhật:

if (me.input.isKeyPressed("shoot")) {
    me.game.world.addChild(me.pool.pull("laser", this.pos.x - game.Laser.width, this.pos.y - game.Laser.height))
}

Nạp lại trò chơi, và thử bắn. Bạn nên xem đạn laser. Tuy nhiên chúng không va chạm với bất kỳ thứ gì.

Va chạm

Đầu tiên hãy cho Kẻ thù một thân thể vật lý. Nối lệnh này với phương thức init trong enemy.js

this.body.setVelocity(0, 0);
this.body.collisionType = me.collision.types.ENEMY_OBJECT;

Sau đó thêm một phương thức cập nhật để chúng ta có thể cập nhật thân thể:

update : function (time) {
    this._super(me.Entity, "update", [time]);

    this.body.update();

    return true;
}

Bây giờ hãy thêm một xử lý va chạm vào tập tin laser.js.

onCollision : function (res, other) {
    if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {
        me.game.world.removeChild(this);
        game.playScreen.enemyManager.removeChild(other);
        return false;
    }
}

Thông số res chúng ta không dùng, nó đơn giản chỉ là kết quả va chạm. Vì thế nó có chứa thông tin chi tiết về bao nhiêu lớp chồng lên, nơi của va chạm,...

Bởi vì chúng ta đặt kiểu va chạm lên thân thể Kẻ thù là một ENEMY_OBJECT, chúng ta có thể kiểm tra kiểu đó trên đối tượng mà laser va chạm đến.

if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {

Sau đó chúng ta xóa kẻ thù khỏi laser, cùng với kẻ thù từ container enemyManager.

me.game.world.removeChild(this);
game.playScreen.enemyManager.removeChild(other);

Kết quả sai trả về trong trường hợp này không quá cần thiết, nhưng nó cần được nêu ra. Khi bạn trả về sai từ xử lý va chạm trong MelonJS, đối tượng sẽ đi xuyên qua. Nếu bạn trả về đúng, nó sẽ bị dừng cứng.

Lưu các thay đổi, và tải lại trình duyệt của bạn. Giờ bạn có thể xóa sổ tàu kẻ thù.

Bước kế tiếp, là thêm các điều kiện thắng và thua.

Điều kiện thắng & thua

Bước cuối cùng của trò chơi này là thêm điều kiện thắng & thua. Điều kiện sẽ rất dễ hiểu. Khi các chiến tàu đến gần phạm vi của người chơi, người chơi thua. Khi người chơi tiêu diệt tất cả tàu kẻ thù, họ thắng.

Điều gì xảy ra khi trò chơi kết thúc? Rất nhiều lần bạn muốn hiển thị một màn hình mà người chơi thua hoặc thắng. Để giữ việc này đơn giản và cho bạn thấy một mẹo nhỏ khác, chúng ta sẽ chỉ reset trò chơi. Đẻ nó bắt đầu trở lại.

Đầu tiên, chúng ta sẽ đặt điều kiện thua

Mã giả cho điều kiện này sẽ là:

if enemy manager overlaps player
  then end game
else
  continue
end

playScreen đang là trạng thái chơi hiện tại của chung ta. Nó giữ tham chiếu đến người chơi, và nó có khả năng reset trạng thái. Vì thế hãy thêm logic kiểm tra điều kiện thua ở đó.

checkIfLoss : function (y) {
    if (y >= this.player.pos.y) {
       this.reset();
    }
},

Thêm đoạn mã đó ở trên phương thức onResetEvent. Nó chấp nhận giá trị Y, và kiểm tra nếu nó vượt qua người chơi. Sau đó gọi phương thức reset của nó. Reset sẽ xóa sạch mọi đối tượng khỏi thế giới trò chơi, và nạp lại trạng thái. Nó sẽ gọi lại onResetEvent, sắp xếp lại kẻ thù và người chơi.

Giờ hãy gọi kiểm tra điều kiện này, đơn giản chỉ cần thêm phương thức gọi đến interval trong enemy manager:

this.timer = me.timer.setInterval(function () {
    var bounds = _this.childBounds;

    if ((_this.vel > 0 && (bounds.right + _this.vel) >= me.game.viewport.width) ||
        (_this.vel < 0 && (bounds.left + _this.vel) <= 0)) {
        _this.vel *= -1;
        _this.pos.y += 16;
        if (_this.vel > 0) {
          _this.vel += 5;
        }
        else {
          _this.vel -= 5;
        }
        game.playScreen.checkIfLoss(bounds.bottom); // <<<
    }
    else {
       _this.pos.x += _this.vel;
    }
}, 1000);

Bởi vì chúng ta đang kiểm tra trong phương thức checkIfLoss nếu số được chuyển đến lớn hơn vị trí Y của người chơi, chúng ta cần chuyển biên dưới của container, là bounds.bottom

Vấn đề bây giờ là, nếu bạn chạy nó, this.player sẽ không được định nghĩa. Đó là bởi vì chúng ta chưa thiết lập thuộc tính này trên playScreen. Bạn có thể thiết lập thuộc tính bằng cách làm như sau:

Thay thế dòng ở trong onResetEvent:

me.game.world.addChild(me.pool.pull("player"), 1);

Với:

this.player = me.pool.pull("player");
me.game.world.addChild(this.player, 1);

Lưu và làm mới trình duyệt. this.player giờ sẽ được thiết lập phù hợp, giờ lệnh gọi phương thức mới của chúng ta sẽ hoạt động. Hãy để kẻ thù di chuyển vòng quanh trong một phút, và xem trò chơi reset.

Điều Kiện Thắng

Tương tự như vậy, chúng ta sẽ reset trò chơi một lần khi người chơi thắng. Bởi vì chúng ta muốn tạo ra chiến thắng khi tất cả tàu biến mất, chúng ta có thể kiểm tra độ dài của con trên enemy manager.

Đầu tiên thêm biến boolean này vào cuối phương thức createEnemies :

this.createdEnemies = true;

Thêm phương thức cập nhật sau vào enemy manager:

update : function (time) {
    if (this.children.length === 0 && this.createdEnemies) {
        game.playScreen.reset();
    }
    this._super(me.Container, "update", [time]);
    this.updateChildBounds();
}

Cái này rất đơn giản. Con là một mảng, vì thế chúng ta kiểm tra độ dài của nó là 0, và đảm bảo nó đã tạo kẻ thù trước. Không có kiểm tra boolean, trò chơi có thể tiếp tục reset chính nó bởi vì nó chưa có con.

Lưu và làm mới trình duyệt. Thử loại bỏ tất cả tàu trong trong thời gian cho phép, và xem trò chơi reset.

Thách thức

Chúng ta đã bỏ qua một số phần của hướng dẫn này, để bạn có thể tự khám phá. Đây là một phần quan trọng của lập trình và phát triển trò chơi.

Nếu bạn gặp khó khăn ở bất kỳ thách thức hoặc phần nào của hướng dẫn, xin tìm theo chủ đề sự cố, hoặc hỏi chúng tôi tại diễn đàn @html5gamedevs

Thách thức #1

Thêm một màn hình thắng & thua phù hợp

  1. Những màn hình này có thể tạo ra bằng cách thêm ScreenObjects bổ sung vào trò chơi, đăng ký chúng trong game.js, và sau đó thay đổi trạng thái.Đối với trạng nào sẽ dùng cho chiến thắng & màn hình, hãy xem các trạng thái có sẵn: http://melonjs.github.io/melonJS/docs/me.state.html
  2. Màn hình thắng và thua có chứa một sprite, hoặ văn bản, hoặc cả hai. Bất kể bạn muốn gì thực sự. Hãy xem qua me.Font và me.Sprite. Để hiển thị một đối tượng me.Font, dùng một thể hiện của me.Renderable có chứa một thể hiện của me.Font, và thực thi hàm vẽ để gọi me.Font#draw.
  3. Điều chỉnh phương thức checkIfLoss để hiển thị màn hình thua mới của bạn thay vào.
  4. Điều chỉnh khối if trong phuowng thức cập nhật trên EnemyManager, để thay đổi trạng thái sang screenobject chiến thắng của bạn.
  5. Thậm chí vui hơn, thêm một màn hình menu sẽ chỉ người chơi cách chơi.

Thách thức #2

Thêm một UI (giao diện người dùng)

  1. Thêm một bộ đếm kẻ thù, và vận tốc kẻ thù vào góc trên cùng bên phải/trái của màn hình. Những thuộc tính này có thể được nạp thông qua: game.playScreen.enemyManager.children.length game.playScreen.enemyManager.vel
  2. Một lần nữa xem lại me.Font, và chạy một renderable để vẽ văn bản. Thử chỉ dùng một lớp mở rộng renderable có thể dùng cho tất cả các phần UI.
  3. Thêm một yếu tố điểm. Theo dõi điểm số trên màn hình chơi. Cập nhật nó mỗi lần một kẻ địch bị tiêu diệt. Nhớ rằng kẻ thù bị xóa khỏi bộ xử lý va chạm trên laser.

Thách thức #3

Thêm khái niệm màn chơi

  1. Sau khi bạn tiêu diệt một đợt kẻ thù, thay vì làm mới cùng đợt, hãy bắt đầu một đợt mới nhanh hơn. Logic chính ở đây sẽ giữ số lượng đợt trên game.js, và tăng nó sau mỗi lần thắng. Sau đó dùng số điểm trong trình quản lý kẻ thù để thiết lập vận tốc.
  2. Để mỗi đợt tiến triển nhanh hơn (+ 8 mỗi lần tăng Y thay vì + 5 cho ví dụ). Chơi với các con số một chút cho đến khi bạn cảm thấy đúng.