Nguyen Anh Tien

I'm a monkey coder who trying to be more creative.

Create HTML5 game with Phaser : Sushi Me

27 Dec 2016 » game, phaserjs

Create HTML5 game with Phaser

Intro và chém gió

Mình thích làm game (và cả chơi game nữa :D). Mơ ước từ nhỏ vẫn là trở thành một game developer và được vào làm việc cho những công ty game lớn như Nintendo, Konami,… (yaoming). Tuy nhiên, khi lớn lên thì mơ ước đó không còn được mạnh mẽ như trước nữa, nhưng mong muốn được tự mình làm game thì vẫn còn và giờ nó là một sở thích nho nhỏ mỗi khi có thời gian rảnh.

Mình đã từng thử lập trình game với nhiều ngôn ngữ khác nhau: Java, Python (với pygame), Godot,…Lần này, là với Javascript (framework PhaserJS). Để biết sơ qua về PhaserJS, mình đề nghị bạn đọc bài viết sau Lập trình game javascript sử dụng Phaser của tác giả @princebk . Đây là một bài viết rất cơ bản khi mới bắt đầu làm quen với PhaserJS. Còn nội dung bài viết này sẽ chỉ nói qua về các bước tổng quan để tạo được một game HTML5 đơn giản. Các hướng dẫn về cài đặt môi trường bạn có thể xem ở bài viết trên nhé :)

From GreenMe to SushiMe

Để làm quen với game thì có vẻ cách thông thường nhất đấy là clone lại một game khác. Giống như hiện tượng Flappy Bird, game này đã trở thành đối tượng clone của không biết bao nhiều người và đã trở thành “Hello Word” của giới làm game. Để clone game này đã có hướng dẫn How to make a Flappy Bird in HTML5 - Part 1 cực kỳ hữu ích nên ở đây mình sẽ clone 1 game khác: GreenMe

green_me.png

Get it here: https://play.google.com/store/apps/details?id=air.GreenMe

Được làm bởi @bs90, GreenMe có luật chơi rất đơn giản:

  • Bạn có một bảng 5 x 5 gồm các ô tròn có màu xanh, đỏ, vàng
  • Khi click vào một ô tròn thì 8 ô xung quanh sẽ đổi màu theo quy tắc sau: xanh -> đỏ -> vàng rồi lại xanh.
  • Nhiệm vụ của bạn là đưa tất cả về màu xanh (nên mới có tên là GreenMe)
  • Ban đầu bạn sẽ có sẵn một số điểm, click sẽ trừ điểm, khi qua level sẽ được cộng một số điểm kha khá.
  • Liệu bạn có thể đạt được bao nhiêu điểm ?

Quả là một trò chơi thích hợp để giết thời gian đúng ko :D. Mình sẽ sử dụng sushi thay cho các ô và nó như sau:

sushime.png

Let’s start

Đầu tiên là khởi tạo project:

[vigo@ubuntu ~/lab]$ mkdir sushi_me
[vigo@ubuntu ~/lab]$ cd sushi_me
[vigo@ubuntu ~/lab/sushi_me]$

và tạo các file ban đầu

[vigo@ubuntu ~/lab/sushi_me]$ yo phaser-official

     _-----_
    |       |
    |--(o)--|   .--------------------------.
   `---------´  |    Welcome to Yeoman,    |
    ( _´U`_ )   |   ladies and gentlemen!  |
    /___A___\   '__________________________'
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

You're using the fantastic Phaser generator.
? What is the name of your project? green_me
? Which Phaser version would you like to use? 2.4.3
? Game Display Width 800
? Game Display Height 600
   create Gruntfile.js
   create css/styles.css
   create bower.json
   create config.json
   create package.json
   create index.html
   create game/states/boot.js
   create game/states/preload.js
   create game/states/menu.js
   create game/states/play.js
   create game/states/gameover.js
   create assets/preloader.gif
   create assets/yeoman-logo.png
   create templates/_main.js.tpl
   create game/main.js
   create .bowerrc
   create .gitignore
   create .editorconfig
   create .jshintrc

I'm all done. Running bower install & npm install for you to install the required dependencies. If this fails, try running the command yoursef

xong xuôi ta chạy:

[vigo@ubuntu ~/lab/sushi_me]$ grunt

Để chạy server tự động load lại game khi có sự thay đổi. Generator cũng đã tạo sẵn cho ta một game đơn giản để chơi thử.

Tiếp theo là phần coding. Mình sử dụng editor Brackets cùng với plugin Brackets-Ternifictern-phaser để hỗ trợ auto-complete.

Source Code

Github: https://github.com/vigov5/sushi_me

Game States

Khi khởi tạo, ta đã có sẵn các state của game như sau:

  • boot : khởi động game, ta có thể thêm ảnh loading tại đây
  • preload : tại state này các assets cần thiết cho game sẽ được load vào cache
  • menu : hiện ra menu cho người chơi
  • play : chơi game
  • gameover : chơi thua sẽ đến state này.

Preload State

Hãy thay đổi file game/states/preload.js như sau:


'use strict';
function Preload() {
    this.asset = null;
    this.ready = false;
}

Preload.prototype = {
    preload: function() {
        this.asset = this.add.sprite(this.game.width/2, this.game.height/2, 'preloader');
        this.asset.anchor.setTo(0.5, 0.5);

        this.load.onLoadComplete.addOnce(this.onLoadComplete, this);
        this.load.setPreloadSprite(this.asset);
        // load required assets
        this.load.spritesheet('foods', 'assets/foods.png', 56, 56, 3);
        this.load.bitmapFont('carrier_command', 'assets/carrier_command.png', 'assets/carrier_command.xml');
    },
    create: function() {
        this.asset.cropEnabled = false;
    },
    update: function() {
        if(!!this.ready) {
            this.game.state.start('play');
        }
    },
    onLoadComplete: function() {
        this.ready = true;
    }
};

module.exports = Preload;

Ta có thể thấy là mỗi state đều có các function:

  • preload: load các assets cần thiết cho state này
  • create: được chạy 1 lần khi bước vào state, khởi tạo và thiết lập cấu hình
  • update: sẽ được chạy sau một khoảng delta cố định để cập nhật thay đổi

Ở state này ta chỉ có nhiệm vụ load vào các assets cần dùng:

Sau khi load xong ta chuyển luôn sang state play this.game.state.start('play');

Play State

Đây là phần nội dung chính của game.

Create

Ta thiết lập màu nền cho game và mảng 2 chiều this.grid để chứa các ô của game. Cùng xem phần khởi tạo:

this.grid[i][j] = new Cell(this.game, this.game.width/2 - 60*5/2 + i*60, 150 + j*60, j, i, 0);
this.game.add.existing(this.grid[i][j]);
this.grid[i][j].inputEnabled = true;
this.grid[i][j].events.onInputDown.add(this.clickListener, this);

mỗi ô đều là một đối tượng Cell, ta thiết lập vị trí hiển thị và vị trí của cell đó trong bảng, chấp nhận click vào cell thông qua this.grid[i][j].inputEnabled = true; và định nghĩa hàm callback là this.clickListener khi cell bị click vào.

clickListener: function(object) {
    this.clickCell(object.row, object.col, true);
},
clickCell: function(col, row, changeScore){
    for (var i = 0; i < this.deltas.length; i++) {
        var targetCol = col + this.deltas[i][0];
        var targetRow = row + this.deltas[i][1];
        if (targetCol >= 0 && targetCol < 5 && targetRow >= 0 && targetRow < 5) {
            this.grid[targetRow][targetCol].flip();
        }
    }
    if (changeScore) {
        this.score -= 5;
    }
},

Tại đây ta kiểm tra xem cell nào nào cell xung quanh của cái bị click và tiến hành flip() nó. Cùng xem qua filp()game/prefabs/cell.js nhé:

'use strict';

var Cell = function(game, x, y, row, col, frame) {
    Phaser.Sprite.call(this, game, x, y, 'foods', frame);
    this.col = col;
    this.row = row;
    this.game.physics.arcade.enableBody(this);
};

Cell.prototype = Object.create(Phaser.Sprite.prototype);
Cell.prototype.constructor = Cell;

Cell.prototype.update = function() {
    // write your prefab's specific update code here
};

Cell.prototype.flip = function() {
    this.frame = (this.frame + 1) % 3;
}

module.exports = Cell;

this.frame sẽ chỉ định hình ảnh món ăn nào đang được hiển thị cho ô đó. Thay đổi thuộc tính này sẽ thay đổi món ăn được hiển thị.

Update

update: function() {
    this.scoreText.setText('' + this.score);
    this.scoreText.x = this.game.width / 2 - this.scoreText.textWidth / 2;

    var done = true;
    for (var i = 0; i < 5; i++) {
        for (var j = 0; j < 5; j++) {
            if (this.grid[i][j].frame != 0) {
                done = false;
                break;
            }
        }
    }
    if (done) {
        this.score += this.BASE_SCORE * this.level;
        this.level += 1;
        this.levelText.setText('level:' + this.level);
        this.generateLevel(this.level);
    }
}

Kiểm tra toàn bộ các cell, nếu tất cả đều là sushi thì ta sẽ chuyển qua level mới.

resetLevel: function() {
    for (var i = 0; i < 5; i++) {
        for (var j = 0; j < 5; j++) {
            this.grid[i][j].frame = 0;
        }
    }
},
generateLevel: function(level) {
    this.resetLevel();
    for (var i = 0; i < level; i++) {
        var col = this.game.rnd.integerInRange(0, 4);
        var row = this.game.rnd.integerInRange(0, 4);
        this.clickCell(row, col, false);
    }
}

Trước khi tạo level mới ta đưa toàn bộ các ô về trạng thái ban đầu rồi tiến hành click ngẫu nhiên vào các ô để sinh ra level mới. Easy

Ngoài ra còn các phần xử lý khác liên quan đến bitmap font, bạn có thể xem thêm trong source code nhé :D

Demo

Game: http://vigov5.github.io/sushi_me/index.html

The End

Việc tạo một game HTML5 với PhaserJS cực kì đơn giản và nhanh do có bộ thư viện rất mạnh mẽ. Mình đã hoàn thành cơ bản game này chỉ trong 1 buổi sáng. Lần tới mình sẽ thử sức với một game khó hơn. See ya !

References