Cocos Creator Sự Lựa Chọn Tuyệt Vời Cho Người Mới Làm Game

Giới Thiệu

Thực tế thì mình cũng chả phải là một lập trình viên chuyên game hay gì cả, chỉ là lần này mình có được nhận làm một project game ngắn hạn từ anh sếp và được recomend sử dụng Cocos Creator. Sau một thời gian không quá dài tiếp xúc với nó thì mình thấy nó cũng khá hay nên muốn chia sẻ cho mọi người về engine này

Theo như mình có được biết thì trước đây khi chưa có Cocos Creator thì mọi người vẫn sử dụng Cocos2d-x code bằng C++. Nhược điểm của thằng này là không có giao diện người lập trình phải tự tưởng tượng code và rồi chạy lên mới thấy được kết quả. Để khắc phục điều này, làm tăng hiệu suất và bắt kịp được với Unity, thì Cocos Creator đã được ra đời. Hiện tại thì thằng này chỉ hỗ trợ trên MAC và Window nên anh em nào dùng Ubuntu thì chắc sẽ hơi buồn.

Vậy Cocos2d-x là gì thì Cocos2d-x là 1 Engine hỗ trợ lập trình Game đa nền tảng : Mobile ( IOS, ANDROID, Blackberry, TIZEN, WP) Window, MacOS, HTML5,.. đại loại là đủ cả. Theo wikipedia thì tác giả của Cocos2d-x là một người trung quốc có tên là Zhe Wang.

2012, at Zynga. With Rolando Abarca and Zhe Wang discussing Cocos2d-x’s and cocos2d-iphone’s roadmaps

Hiện tại thì nó vẫn được những bạn láng giếng phát triển khá là mạnh và không ngừng hoàn thiện để trở thành Unity phiên bản free, nhưng chặng đường chắc vẫn còn khá dài.

Thực Hành Tạo Và Deploy Game

Mình thì không thích dài dòng mà mình thấy các tốt nhất để học một cái gì là cứ cho vào làm luôn nó mới nhanh vỡ ra được, chứ cứ lý thuyết suông thì rất khó hình dung.

Đây là trang chủ để tải cocos creator : https://www.cocos.com/en/creator

Sau khi tải về và cài đặt (cài cái này khá là lâu nha) thì giao diện chính của creator sẽ như thế này

Giờ chúng ta sẽ đi luôn vào làm game đầu tiên như trong docs của cocos hướng dẫn. Dưới đây là nguồn assets làm game và game đã hoàn thành

Assets

Sau khi tải Assets ban đầu về chúng ta sẽ import nó vào creator, rất đơn giản chỉ cần kéo thả vào thôi

Phần Assets sẽ là nơi chứ các nguồn dữ liệu để ta dụng để tạo hình

Scene

Sau khi đã có Assets ta sẽ tạo các Scene. Scene là một bản bồ 2D và có thể mở chế độ 3D, giúp ta kéo tả, thay đổi tạo ra UI. Mỗi Scene sẽ là một màn hình kiểu như là trên web chúng ta sẽ có trang home, trang detail, trang about thì scene chính là các view đó. Ta có thể tạo nhiều scene và chuyển đổi giữa chúng trong game.

Thêm khác thực thể vào Scene

Ở đây ta có thể tùy kéo kéo chỉnh, thay đổi vị trí của các thực thể sao cho mình thấy ổn nhất. Có nhiều chế độ kéo thả

  • Di chuyển theo chiều X - Y đầu tiên chọn bên góc trái sau đó có thể di chuyển vị trí của thực thể

  • Thay đổi góc quay của thực thể. Chọn và ra scene thay đổi góc quay

  • Rsize kích thước thực thể chọn và thay đổi kích thước

Ở trên scene ta chỉ cần nắm thế thôi là có thể kéo thả, thay đổi kích thước và dic chuyển khắp bản đồ

Properties

Thì ngoài việc kéo thả ở scene chúng ta cũng có thể thay đổi vị trí, thay đổi độ trong suốt, thêm các script, thêm animation và rất nhiều thức khác ở trong phần này

  • Position chính là vị trí của thực thể đó trên scene tính theo trục tọa đọ X-Y
  • Rotaion là góc nghiêng
  • Scale là kiểu phóng to theo 2 chiều x hoặc y
  • Anchor là để thay đổi vị trí của thực thể so với gốc tọa độ của chính nó
  • Size chính là thay đổi kích thước width-Height của thực thể
  • Color là màu của thực thể
  • Opacity độ trong suốt
  • ...

Node Tree

Đây khi khu vực để ta xác định được đâu là thành phần cha con của nhau hoặc thằng nào ở lớp trên, thằng nào ở lớp dưới.

thằng nào càng ở dưới tức là nó đang trên lớp cao nhất. Kiểu như ở trong css có thuộc tính z-index khi ta sử dụng position vậy. Càng ở dưới thì z-index càng cao và có thể che lớp thằng kia. Giống như cái ground đang che đi cả thằng PurpleMonster.

Timeline

phần này dùng để tạo ra các animation đơn giảm chị cần thay đổi góc nghiêng hay thay đổi vị trí của thực thể. Đây là ví dụ về 1 animation nhảy

Tạo Hình Và Bắt Sự Kiện Cho Nhân Vật

Bây giờ sau khi đã hiểu hết các phần và chức năng của nó chúng ta sẽ đi vào làm game đầu tiên nha

  • Đầu tiên chúng ta cần kéo các thành phần của game vào để có một cái UI tổng quát

  • Sau khi sử dụng hết các kiến thức ở trên để kéo thả ra một UI như trong hình, thì bây giờ công việc cần làm là add thêm các script để bắt sự kiện trong game. Tạo một folder scripts --> và new một file Player.js.

Tạo folder

Tạo file js

  • Click double vào file Player ta sẽ thấy một khung js có sẵn
 // Player.js
 
    cc.Class({
        extends: cc.Component,

        properties: {
            // foo: {
            //     // ATTRIBUTES:
            //     default: null,        // The default value will be used only when the component attaching
            //                           // to a node for the first time
            //     type: cc.SpriteFrame, // optional, default is typeof default
            //     serializable: true,   // optional, default is true
            // },
            // bar: {
            //     get () {
            //         return this._bar;
            //     },
            //     set (value) {
            //         this._bar = value;
            //     }
            // },
        },

        // LIFE-CYCLE CALLBACKS:

        // onLoad () {},

        start () {

        },

        // update (dt) {},
    });
  • Giờ thêm các properties
 // Player.js
 
        //...
        properties: {
            // main character's jump height
            jumpHeight: 0,
            // main character's jump duration
            jumpDuration: 0,
            // maximal movement speed
            maxMoveSpeed: 0,
            // acceleration
            accel: 0,
        },
        //...
  • Tiếp đến ta sẽ gắn js này cho thằng thực thể PurpleMonster để lát nữa sẽ xử lý sự kiện trong file js này. Quay về màn hình vào phần Node Tree nãy mình đã nói, chọn thực thể PurpleMonster sau đó để ý sang phần properties của nó. Ta sẽ xuống button Add Component chọn Custom component và chọn Player

  • Sau khi đã add được js vào trong nó sẽ hiện ra các thuộc tính ta vừa định nghĩa trong file Player ở trên và ta sẽ điền các thông số vào đây.

  • Bây giờ ta sẽ viết một cái hàm nhảy cho thằng này
 // Player.js
 
        properties: {
            //...
        },

        setJumpAction: function () {
            // jump up
            var jumpUp = cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)).easing(cc.easeCubicActionOut());
            // jump down
            var jumpDown = cc.moveBy(this.jumpDuration, cc.v2(0, -this.jumpHeight)).easing(cc.easeCubicActionIn());
            //  repeat
            return cc.repeatForever(cc.sequence(jumpUp, jumpDown));
        },
  • Sau khi đã có hàm nhảy rồi ta sẽ set hàm đó vào onLoad(). Điều này có tác dụng là nó sẽ chạy ngay sau khi thực thể lại được load ra
// Player.js

        onLoad: function () {
            // initialize jump action
            this.jumpAction = this.setJumpAction();
            this.node.runAction(this.jumpAction);
        },
  • Ok test xem chạy không nha. Chọn vào nút ở trên cùng giữa màn hình

cc.repeatForever sẽ giúp hành động xảy ra lặp lại mãi mãi. cc.sequence thực hiện hành động tuần tự. jumpUp, jumpDown là nhảy lên và nhảy xuống --> cứ như vậy cc.repeatForever(cc.sequence(jumpUp, jumpDown)) sẽ làm PurpleMonster nhảy mãi mãi và hàm được để ở onLoad() nên hàm sẽ được chạy ngay sau khi khi thực thể được load.

  • Đã tạo được hàm nhảy, tiếp đến chúng ta sẽ tạo hành động di chuyển sang trái, sang phải cho nhân vật.
// Player.js

        setJumpAction: function () {
            //...
        },

        onKeyDown (event) {
            // set a flag when key pressed
            switch(event.keyCode) {
                case cc.macro.KEY.a:
                    this.accLeft = true;
                    break;
                case cc.macro.KEY.d:
                    this.accRight = true;
                    break;
            }
        },

        onKeyUp (event) {
            // unset a flag when key released
            switch(event.keyCode) {
                case cc.macro.KEY.a:
                    this.accLeft = false;
                    break;
                case cc.macro.KEY.d:
                    this.accRight = false;
                    break;
            }
        },

onKeyDown là khi ta nhấm phím, cònonKeyUp là khi ta nhả phím ra. Sẽ có 1 switch ở trong để kiểm tra xem ta bấm phím nào ở keydown và nhả phím nào ở keyup. Sau đó set hướng cho nó ở keydown cho hướng đấy thành true và khi nhả phím thì set lại thành false chỉ đơn giản vậy thôi.

  • Nhưng bận muốn game nhận được ác hiệu lệnh đó của mình thì bạn cần khởi tạo bàn phím bằng cách như sau
// Player.js

        onLoad: function () {
            // Initialize the jump action
            this.jumpAction = this.setJumpAction();
            this.node.runAction(this.jumpAction);

            // Acceleration direction switch
            this.accLeft = false;
            this.accRight = false;
            // The main character's current horizontal velocity
            this.xSpeed = 0;

            // Initialize the keyboard input listening
            cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
            cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);   
        },

        onDestroy () {
            // Cancel keyboard input monitoring
            cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
            cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
        },

Khởi tạo lắng nghe bàn phím khi load và hủy lắng nghe khi thực thể đó bị phá hủy

  • Cuối cùng là phần cập nhật vị trí của nhân vật khi ta bấm nút
// Player.js

        update: function (dt) {
            // update speed of each frame according to the current acceleration direction
            if (this.accLeft) {
                this.xSpeed -= this.accel * dt;
            } else if (this.accRight) {
                this.xSpeed += this.accel * dt;
            }
            // restrict the movement speed of the main character to the maximum movement speed
            if ( Math.abs(this.xSpeed) > this.maxMoveSpeed ) {
                // if speed reach limit, use max speed with current direction
                this.xSpeed = this.maxMoveSpeed * this.xSpeed / Math.abs(this.xSpeed);
            }

            // update the position of the main character according to the current speed
            this.node.x += this.xSpeed * dt;
        },

Mình đã thử test thì thấy thằng update() này cứ 0,01 giây nó sẽ gọi một lần

      update(dt) {
        console.log("Run in function update() time : ", dt);
      }

Tức là nó sẽ gọi hàm gần như liên tục

        if (this.accLeft) {
            this.xSpeed -= this.accel * dt;
        } else if (this.accRight) {
            this.xSpeed += this.accel * dt;
        }

Phần này sẽ kiểm tra xem người dùng đang bắt nhân vật chạy về hướng nào. Sau đó sẽ thay đổi vị trí nhân vật theo hướng đó bằng cách trừ hay công tọa đọ X với một tốc độ tăng theo thời gian. Nhưng để phòng trường hợp tăng tốc độ quá nhanh thì một phần giới hạn tốc độ được thiết lập ở ngay phía dưới.

        if ( Math.abs(this.xSpeed) > this.maxMoveSpeed ) {
            // if speed reach limit, use max speed with current direction
            this.xSpeed = this.maxMoveSpeed * this.xSpeed / Math.abs(this.xSpeed);
        }

Phần cuối của hàm l à set vị trí cho nhân vật

        this.node.x += this.xSpeed * dt;

Tạo Hình Và Bắt Sự Kiện Cho Ngôi Sao

Sau khi đã thực hiện xong cho nhân vật tiếp đến chúng ta sẽ thực hiện cho ngôi sao. Do ngôi sao sẽ xuất hiện và mất đi khi nhân vật chạm tới nó. Tức là nó sẽ được tạo ra và phá hủy đi liên lục do vậy ở đây ta sẽ động chạm đến một khái niệm Prefab. Prefab là những node những thực thể ta đã tạo ra nhưng cần sử dụng đi sử dụng lại nhiều lần, nó giống như khái niệm component ở trong các framework React hay Vue vậy.

  • Thì bây giờ ta sẽ tạo ra các Prefab như vậy bằng cách kéo ngôi sao vào Scene và tạo một file script rồi add vào cho nó.

// Star.js

    properties: {
        // When the distance between the star and main character is less than this value, collection of the point will be completed
        pickRadius: 0,
    },

Rồi sau khi đã add script cho nó ta biến nó thành 1 một Prefab bằng cách. Kéo nó từ Scene xuống ô Assets vậy là xong

  • Tiếp đến là tạo sự kiện để set vị trí cho ngôi sao khi nó xuất hiện. Tạo một file Game.js
// Game.js
    
        properties: {
            // this property quotes the PreFab resource of stars
            starPrefab: {
                default: null,
                type: cc.Prefab
            },
            // the random scale of disappearing time for stars
            maxStarDuration: 0,
            minStarDuration: 0,
            // ground node for confirming the height of the generated star's position
            ground: {
                default: null,
                type: cc.Node
            },
            // player node for obtaining the jump height of the main character and controlling the movement switch of the main character
            player: {
                default: null,
                type: cc.Node
            }
        },

starPrefab ta để type là Prefab. bây giờ add script này cho thằng Canvas kéo lên phần Node Tree --> chọn Canvas --> rồi Properties chọn Add Component và chọn file script Game

  • Rồi giờ đến phần Generate sao bằng cách random vị trí
// Game.js

        onLoad: function () {
            // obtain the anchor point of ground level on the y axis
            this.groundY = this.ground.y + this.ground.height/2; // this.ground.top may also work
            // generate a new star
            this.spawnNewStar();
        },

        spawnNewStar: function() {
            // generate a new node in the scene with a preset template
            var newStar = cc.instantiate(this.starPrefab);
            // put the newly added node under the Canvas node
            this.node.addChild(newStar);
            // set up a random position for the star
            newStar.setPosition(this.getNewStarPosition());
        },

        getNewStarPosition: function () {
            var randX = 0;
            // According to the position of the ground level and the main character's jump height, randomly obtain an anchor point of the star on the y axis
            var randY = this.groundY + Math.random() * this.player.getComponent('Player').jumpHeight + 50;
            // according to the width of the screen, randomly obtain an anchor point of star on the x axis
            var maxX = this.node.width/2;
            randX = (Math.random() - 0.5) * 2 * maxX;
            // return to the anchor point of the star
            return cc.v2(randX, randY);
        },

Test nào

  • Rồi giờ gắn sao vào với tổng thể game
// Game.js

        spawnNewStar: function() {
            // ...
            // Staging a reference of Game object on a star component
            newStar.getComponent('Star').game = this;
        },
// Star.js

        getPlayerDistance: function () {
            // judge the distance according to the position of the player node
            var playerPos = this.game.player.getPosition();
            // calculate the distance between two nodes according to their positions
            var dist = this.node.position.sub(playerPos).mag();
            return dist;
        },

        onPicked: function() {
            // When the stars are being collected, invoke the interface in the Game script to generate a new star
            this.game.spawnNewStar();
            // then destroy the current star's node
            this.node.destroy();
        }, 
        
        update: function (dt) {
        // judge if the distance between the star and main character is less than the collecting distance for each frame
        if (this.getPlayerDistance() < this.pickRadius) {
            // invoke collecting behavior
            this.onPicked();
            return;
        }
    },

Add score

Điểm sẽ bắt đầu từ 0 khi trò chơi bắt đầu. 1 điểm sẽ được thêm khi 1 sao được thu thập. Để hiển thị điểm số, trước tiên chúng ta nên tạo một node Label. Chọn Canvas trong Node Tree , nhấp chuột phải và chọn Create -> Create Renderer Nodes -> Node With Label. Một node label mới sẽ được tạo dưới node Canvas. Tiếp theo, chúng tôi sẽ sử dụng các bước sau để thiết lập node label này:

  1. Thay đổi tên node thành score
  2. Chọn node score và thiết lập position thành (0, 180)
  3. Chỉnh sửa thuộc tính String thành Score: 0
  4. Đặt Font Size50
  5. Kéo assets/mikado_outline_shadow(chú ý! Biểu tượng là ) vào thuộc tính Font của Label

Thêm logic ghi điểm vào script Game

// Game.js

    properties: {
        // ...
        // reference of score label
        scoreDisplay: {
            default: null,
            type: cc.Label
        }
    },

Tiếp đến là khởi tạo điểm trong onLoad()

// Game.js

    onLoad: function () {
        // ...
        // initialize scoring
        this.score = 0;
    },

Sau đó thêm một phương thức mới được đặt tên gainScore :

// Game.js

    gainScore: function () {
        this.score += 1;
        // update the words of the scoreDisplay Label
        this.scoreDisplay.string = 'Score: ' + this.score;
    },

Invoke ghi điểm của trò chơi trong Star

// Star.js

    onPicked: function() {
        // when the stars are being collected, invoke the interface in the Game script to generate a new star
        this.game.spawnNewStar();
        // invoke the scoring method of the Game script
        this.game.gainScore();
        // then destroy the current star's node
        this.node.destroy();
    },

Kéo các thành phần vào logic game

Chọn Canvas nhìn sang Properties và để ý phần Game sẽ có các trường nhu sau

Đây chính là các properties ta đã định nghĩa trong file Game.js. Tiếp theo ta cần kéo các thành phần cần thiết vào đây.

Sau khi đã kéo các thành phần vào properties thì ta cần xóa node star trên Node Tree đi. Vì về sau ta sẽ generate các star bằng Prefab nên thằng node này sẽ không cần thiết nữa. Nếu không xóa đi nó sẽ bị hiện tượng lúc nào cũng có 2 ngôi sao và một ngôi sao không bao giờ biến mất như thế này. À tiện thể test xem kết quả luôn nhá

Xóa node star đi

Kết quả đã ok

Game Over

Ta sẽ thiết lập thời gian xuất hiện của ngôi sao để làm sao từ khi ngôi sao được xuất hiện mà 60s sau chẳng hạn, mà nó không được thu thập thì sẽ game over. Vây đầu tiên cần thêm biến đếm thời gian

// Game.js

    onLoad: function () {
        // ...
        // initialize timer
        this.timer = 0;
        this.starDuration = 0;
        // generate a new star
        this.spawnNewStar();
        // initialize scoring
        this.score = 0;
    },

Sau đó thêm logic đặt lại bộ định thời vào cuối spawnNewStar phương thức, trong đó this.minStarDurationthis.maxStarDuration là các thuộc tính của Game được khai báo ở đầu. Chúng được sử dụng để quy định tỷ lệ ngẫu nhiên của thời gian sao:

// Game.js

    spawnNewStar: function() {
        // ...
        // reset timer, randomly choose a value according the scale of star duration
        this.starDuration = this.minStarDuration + Math.random() * (this.maxStarDuration - this.minStarDuration);
        this.timer = 0;
    },

Thêm logic cập nhật bộ đếm thời gian và phán đoán vượt quá thời lượng cho phương thức update:

// Game.js

    update: function (dt) {
        // update timer for each frame, when a new star is not generated after exceeding duration
        // invoke the logic of game failure
        if (this.timer > this.starDuration) {
            this.gameOver();
            return;
        }
        this.timer += dt;
    },

Cuối cùng, thêm phương thức gameOver

// Game.js

    gameOver: function () {
        this.player.stopAllActions(); //stop the jumping action of the player node
        cc.director.loadScene('game');
    }

Để người chơi biết rằng ngồi sao sắp biến mất ta cần có cho nó một hiệu ứng bằng cách giảm opacity của nó xuống làm nó mờ dần đi.

// Star.js

    update: function() {
        // ...
        // update the transparency of the star according to the timer in the Game script
        var opacityRatio = 1 - this.game.timer/this.game.starDuration;
        var minOpacity = 50;
        this.node.opacity = minOpacity + Math.floor(opacityRatio * (255 - minOpacity));
    }

Thêm hiệu ứng âm thanh cho Player

Game mà im lặng thì rất khô khan, bây giờ chúng ta sẽ thêm cho nó hiệu ứng âm thanh khi nó thực hiện các hành động.

  • Đầu tiên, thêm hiệu ứng âm thanh nhảy. Mở Player.js và thêm thuộc tính tham jumpAudio
// Player.js

    properties: {
        // ...
        // jumping sound effect resource
        jumpAudio: {
            default: null,
            type: cc.AudioClip
        },
    },

Sau đó viết lại phương thức setJumpAction chèn cuộc gọi lại để phát hiệu ứng âm thanh và phát âm thanh bằng cách thêm phương thức playJumpSound

// Player.js

    setJumpAction: function () {
        // jump up
        var jumpUp = cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)).easing(cc.easeCubicActionOut());
        // jump down
        var jumpDown = cc.moveBy(this.jumpDuration, cc.v2(0, -this.jumpHeight)).easing(cc.easeCubicActionIn());
        // add a callback function to invoke other defined methods after the action is finished
        var callback = cc.callFunc(this.playJumpSound, this);
        // repeat unceasingly, and invoke callback to play sound after landing each time
        return cc.repeatForever(cc.sequence(jumpUp, jumpDown, callback));
    },

    playJumpSound: function () {
        // invoke sound engine to play the sound
        cc.audioEngine.playEffect(this.jumpAudio, false);
    },

Hiệu ứng âm thanh khi ghi điểm

Thêm thuộc tính âm thanh ghi điểm vào Game.js

// Game.js

    properties: {
        // ...
        // scoring sound effect resource
        scoreAudio: {
            default: null,
            type: cc.AudioClip
        }
    },

Sau đó chèn vào phương thức gainScore

// Game.js

    gainScore: function () {
        this.score += 1;
        // update the words of the scoreDisplay Label
        this.scoreDisplay.string = 'Score: ' + this.score.toString();
        // play the scoring sound effect
        cc.audioEngine.playEffect(this.scoreAudio, false);
    },

Tiếp đến làm tương tự như kéo cá thành phần, giờ ta sẽ kéo các file audio vào ô thuộc tính tương ứng.

Đã hoàn tất giờ chúng ta thử trả nghiệm thôi

Build và Deploy

Build

Phần Build của nó rất đơn giản. Chọn Project -> chọn Build

Tiếp đén chọn Build và chờ thanh Build... hiện conpleted là được

Vậy là đã build xong giờ có thể test kết quả build bằng cách chọn Play

Deploy

Để đơn giản hóa việc deploy chúng ta sẽ sử dụng surge: https://surge.sh/

  • insttall rất đơn giản
    npm install --global surge

sau khi đã cài đặt ta sẽ vào thư mục mà vừa nãy ta build ra và mở terminal

Chạy lệnh surge

Sau đó thiết lập tên domain là xong

Và đây là kết quả : http://gamestar.surge.sh/

Tổng kết

Đến đây chắc hẳn mọi người đã biết cách làm sao để làm những game đơn giản với Cocos Creator rồi. Còn nếu muốn tạo những game phức tạp hơn thì cần phải đọc docs và tham khảo từ những sản phẩm đi trước rất nhiều nữa. Cảm ơn các bạn đã quan tâm đến bàì viết, rất vui và hẹn gặp lại ở những bài viết tiếp theo.

Nếu các bạn có hứng thú với game socket có thể tham khảo game mà mình và bạn trong team đã làm. Nó có tích hợp sử dụng cả websocket để chơi và blockchain để lưu kết quả nên, tuy hơi sơ sài nhưng chắc là sẽ giúp được ai đó nếu cần. Và đặc biệt là thằng này chưa hỗ chợ SocketIO nếu muốn tích hợp sẽ khá khó khăn theo như docs của nó có nói vậy, nên mình đành ngậm ngùi sử dụng Websocket. Đây là link game và cần có ví metamask mới chơi được nha: https://github.com/ngovannghia1997kma/HeadBall2

Nguồn:

https://docs.cocos2d-x.org/creator/manual/en/