Cocos2d-x

Simple Hockey Game with cocos2d-x by JavaScript – Android Version

 We are going to build a Simple Hockey Game to introduce you to main aspects of building a project with Cocos2d-x by JavaScript.
Oh, and you will need to call a friend. This is a two player game.

I use Mac so For Window, you can follow the same steps but beware your folder path.
Source Code: https://gitlab.com/tienthanh/simplehockey-cocos2d-x

1. Creating your Simple Hockey Project

  • open terminal and create a cocos2d-x project called SimpleHockey that uses JS as its main language. I saved mine in desktop , so command i had to enter looks like this:
    cocos new SimpleHockey -p com.iamkaiba.SimpleHockey -l js -d /Users/kaiba/Desktop/SimpleHockey

    undefined
  • Once the project is created, open project folder in your favorite IDE, with me is IntelliJ IDEA. You will see project structure like this
    undefined

  • Then, download resource which contain images and sounds for your Simple Hockey Game by this link
  • Once download finish copy two sounds, and hd folder to res folder of SimpleHockey project
    undefined
Update resource.js file by code below:
var res = {
    HelloWorld_png : "res/HelloWorld.png",
    court_png: "res/court.png",
    logo_png: "res/logo.png",
    mallet_png: "res/mallet.png",
    puck_png: "res/puck.png",
    hit_sound: "res/hit.wav",
    score_sound: "res/score.wav"
};

var g_resources = [];
for (var i in res) {
    g_resources.push(res[i]);
}

Open file main.js and change the resolution of project:

// Setup the resolution policy and design resolution size
cc.view.setDesignResolutionSize(1536, 2048, cc.ResolutionPolicy.SHOW_ALL);

2. Together Let’s Code!

First, by defualt cocos2d-x set our game orirentation is landscape so we need game orirentation to portrait.
undefined

In your project, open frameworks/runtime-src/proj.android/app/AndroidManifest.xml file and change the line:

android:screenOrientation="landscape" to android:screenOrientation="portrait"
undefined

Your Custom Sprite

No, there is nothing wrong with build in Sprite, but we need a bit more information from our Sprite like velocity, radius, next position… So we create GameSprite class.
Right Click src folder in your IDE and create a new file and name it by GameSprite.js

Select the GameSprite.js file and let’s start on the instantiation logic of
the class:

  1. We store GameSprite’s velocity, radius, next position by corresponding properties and create getter, setter method.
  2. We create radius method, retrieve the radius of our sprite, which we determine to be half its texture’s width
  3. And finally, Next we need to override the Node method setPosition. We need to make sure that whenever we change the position of the sprite, the new value is also used by _nextPosition.

So here is the GameSprite.js file:

var GameSprite = cc.Sprite.extend({
ctor: function (res) {
this._super(res);
this._nextPosition = null;
this._vector = cc.p(0, 0);
this._touch = null;
},
getTouch: function() {
return this._touch;
},
setTouch: function(touch) {
this._touch = touch;
},
getNextPosition: function() {
return this._nextPosition;
},
setNextPosition: function(point) {
this._nextPosition = point;
},
setVector: function(vec) {
this._vector = vec;
},
getVector: function() {
return this._vector;
},
setPosition: function(newPosOrxValue, yValue) {
this._super(newPosOrxValue);
if (!cc.pointEqualToPoint(this.getNextPosition(), newPosOrxValue)) {
this.setNextPosition(newPosOrxValue);
}
},
radius: function() {
return this.getContentSize().width*0.5;
}
});

Your Game Scene

Similarly, you create GameLayer.js file, GameLayer is our game. It contains references to all the sprites we need to control and update, as well as all game data

In the class implementation, all the logic starts inside the ctor method.

ctor: function () {
    this._super();
    this._player1 = null; // GameSprite for player1
    this._player2 = null; // GameSprite for player2
    this._players = [];
    this._ball = null;    // GameSprite for ball

    this._player1ScoreLabel = null; //player1's score label
    this._player2ScoreLabel = null; //player2's score label

    //init data
    this._player1Score = 0; //data of player1's score
    this._player2Score = 0; //data of player2's score

    //get screen size
    this._screenSize = cc.director.getWinSize();
    this.createGameUI();
    this.createListener();

},

we created GameLayer properties, variables for player’s data and game UI in screen.

Next, we implement the createGameUI method:

//1. add court image
var court = new cc.Sprite(res.court_png);
court.setPosition(cc.p(this._screenSize.width * 0.5, this._screenSize.height * 0.5));
this.addChild(court);

//2. add players
this._player1 =  new GameSprite(res.mallet_png);
this._player1.setPosition(cc.p(this._screenSize.width * 0.5, this._player1.radius() * 2));
this.addChild(this._player1);
this._players.push(this._player1);

this._player2 =  new GameSprite(res.mallet_png);
this._player2.setPosition(cc.p(this._screenSize.width * 0.5, this._screenSize.height - this._player1.radius() * 2));
this.addChild(this._player2);
this._players.push(this._player2);

//3. add puck
this._ball = new GameSprite(res.puck_png);
this._ball.setPosition(cc.p(this._screenSize.width * 0.5, this._screenSize.height * 0.5));
this.addChild(this._ball);


//4. add score display
this._player1ScoreLabel = new cc.LabelTTF("0", "Arial", 60);
this._player1ScoreLabel.setPosition(cc.p(this._screenSize.width - 60, this._screenSize.height * 0.5 - 80));
this._player1ScoreLabel.setRotation(90);
this.addChild(this._player1ScoreLabel);

this._player2ScoreLabel = new cc.LabelTTF("0", "Arial", 60);
this._player2ScoreLabel.setPosition(cc.p(this._screenSize.width - 60, this._screenSize.height * 0.5 + 80));
this._player2ScoreLabel.setRotation(90);
this.addChild(this._player2ScoreLabel);

undefined

We created sprites and label then arrange them in screen.

Then we turn GameLayer into a multitouch event listener and tell the Director event dispatcher that GameLayer wishes to listen to those events. And we finish by scheduling the game’s main loop as follows:

createListener: function() {

    //listen for touches
    cc.eventManager.addListener({
        swallowTouch: true,
        event: cc.EventListener.TOUCH_ALL_AT_ONCE,
        onTouchesBegan: this.onTouchesBegan.bind(this),
        onTouchesMoved: this.onTouchesMoved.bind(this),
        onTouchesEnded: this.onTouchesEnded.bind(this)
    }, this);


    //create main loop
    this.scheduleUpdate();
},

There are three methods we need to implement in this game to handle touches. Each method receives, as one of its parameters, a vector of Touch objects:

So add our onTouchesBegan method:

onTouchesBegan: function(touches, event) {
for (var i = 0; i < touches.length; i++) {
if (touches[i] != null) {
var tap = touches[i].getLocation();


for (var player of this._players) {
var rect = player.getBoundingBox();
if (cc.rectContainsPoint(rect, tap)) {
player.setTouch(touches[i]);
}

}
}
}

},

Each GameSprite, if you recall, has a _touch property. So we iterate through the touches, grab their location on screen, loop through the players in the vector, and determine if the touch lands on one of the players. If so, we store the touch inside the player’s _touch property (from the GameSprite class).
A similar process is repeated for onTouchesMoved and onTouchesEnded, so you can copy and paste the code and just replace what goes on inside the _players array for loop.

In TouchesMoved, when we loop through the players, we do this:

onTouchesMoved: function(touches, event) {
    //loop through all moving touches
    for (var touch of touches) {
        if (touch != null) {
            var tap = touch.getLocation();

            for (var player of this._players) {
                if (player.getTouch() != null && cc.pointEqualToPoint(player.getTouch().getLocation(), touch.getLocation())) {
                    var nextPosition = tap;

                    //keep player inside screen
                    if (nextPosition.x < player.radius())
                        nextPosition.x = player.radius();
                    if (nextPosition.x > this._screenSize.width - player.radius())
                        nextPosition.x = this._screenSize.width - player.radius();
                    if (nextPosition.y < player.radius())
                        nextPosition.y  = player.radius();
                    if (nextPosition.y > this._screenSize.height - player.radius())
                        nextPosition.y = this._screenSize.height - player.radius();

                    //keep player inside its court
                    if (player.getPositionY() < this._screenSize.height* 0.5) {
                        if (nextPosition.y > this._screenSize.height* 0.5 - player.radius()) {
                            nextPosition.y = this._screenSize.height* 0.5 - player.radius();
                        }
                    } else {
                        if (nextPosition.y < this._screenSize.height* 0.5 + player.radius()) {
                            nextPosition.y = this._screenSize.height* 0.5 + player.radius();
                        }
                    }

                    player.setNextPosition(nextPosition);
                    player.setVector(cc.p(tap.x - player.getPositionX(), tap.y - player.getPositionY()));
                }

            }
        }
    }
},

We check to see if the _touch property stored inside the player is the being moved now. If so, we update the player’s position with the touch’s current position, but we check to see if the new position is valid: a player cannot move outside the screen and cannot enter its opponent’s court. We also update the player’s vector of movement; we’ll need this when we collide the player with the puck. The vector is based on the player’s displacement.

In onTouchesEnded, we add this:

onTouchesEnded: function(touches, event) {
//loop through all ending touches
for( var touch of touches) {
if(touch != null) {
for (var player of this._players) {
if (player.getTouch() != null && cc.pointEqualToPoint(player.getTouch().getLocation(), touch.getLocation())) {
//if touch ending belongs to this player, clear it
player.setTouch(null);
player.setVector(cc.p(0,0));
}
}
}
}
}

We clear the _touch property stored inside the player if this touch is the one just ending. The player also stops moving, so its vector is set to 0. Notice that we don’t need the location of the touch anymore; so in TouchesEnded you can skip that bit of logic.

Finally, the most important method is update
Collisions are checked through the distance between ball and players.
Two conditions will flag a collision, as illustrated in the following diagram:

update: function(dt) {
//update puck
var ballNextPosition = this._ball.getNextPosition();
var ballVector = cc.pMult(this._ball.getVector(), 0.98);

ballNextPosition.x += ballVector.x;
ballNextPosition.y += ballVector.y;

//test for puck and mallet collision
var squared_radii = Math.pow(this._player1.radius() + this._ball.radius(), 2);

for (var player of this._players) {

var playerNextPosition = player.getNextPosition();
var playerVector = player.getVector();

var diffx = ballNextPosition.x - player.getPositionX();
var diffy = ballNextPosition.y - player.getPositionY();

var distance1 = Math.pow(diffx, 2) + Math.pow(diffy, 2);
var distance2 = Math.pow(this._ball.getPositionX() - playerNextPosition.x, 2) + Math.pow(this._ball.getPositionY() - playerNextPosition.y, 2);

if (distance1 <= squared_radii || distance2 <= squared_radii) {

var mag_ball = Math.pow(ballVector.x, 2) + Math.pow(ballVector.y, 2);
var mag_player = Math.pow(playerVector.x, 2) + Math.pow(playerVector.y, 2);

var force = Math.sqrt(mag_ball + mag_player);
var angle = Math.atan2(diffy, diffx);

ballVector.x = force* Math.cos(angle);
ballVector.y = (force* Math.sin(angle));

ballNextPosition.x = playerNextPosition.x + (player.radius() + this._ball.radius() + force) * Math.cos(angle);
ballNextPosition.y = playerNextPosition.y + (player.radius() + this._ball.radius() + force) * Math.sin(angle);

cc.audioEngine.playEffect(res.hit_sound);
}

//check collision of ball and sides
if (ballNextPosition.x < this._ball.radius()) {
ballNextPosition.x = this._ball.radius();
ballVector.x *= -0.8;
cc.audioEngine.playEffect(res.hit_sound);
}

if (ballNextPosition.x > this._screenSize.width - this._ball.radius()) {
ballNextPosition.x = this._screenSize.width - this._ball.radius();
ballVector.x *= -0.8;
cc.audioEngine.playEffect(res.hit_sound);
}
//ball and top of the court
if (ballNextPosition.y > this._screenSize.height - this._ball.radius()) {
if (this._ball.getPosition().x < this._screenSize.width* 0.5 - GOAL_WIDTH* 0.5 || this._ball.getPosition().x > this._screenSize.width* 0.5 + GOAL_WIDTH* 0.5) {
ballNextPosition.y = this._screenSize.height - this._ball.radius();
ballVector.y *= -0.8;
cc.audioEngine.playEffect(res.hit_sound);
}
}
//ball and bottom of the court
if (ballNextPosition.y < this._ball.radius() ) {
if (this._ball.getPosition().x < this._screenSize.width* 0.5 - GOAL_WIDTH* 0.5 ||
this._ball.getPosition().x > this._screenSize.width* 0.5 + GOAL_WIDTH* 0.5) {
ballNextPosition.y = this._ball.radius();
ballVector.y *= -0.8;
cc.audioEngine.playEffect(res.hit_sound);
}
}

//finally, after all checks, update ball's vector and next position
this._ball.setVector(ballVector);
this._ball.setNextPosition(ballNextPosition);


//check for goals!
if (ballNextPosition.y < -this._ball.radius() * 2) {
this.playerScore(2);

}
if (ballNextPosition.y > this._screenSize.height + this._ball.radius() * 2) {
this.playerScore(1);
}

//move pieces to next position
this._player1.setPosition(this._player1.getNextPosition());
this._player2.setPosition(this._player2.getNextPosition());
this._ball.setPosition(this._ball.getNextPosition());
}
},

We must update file project.json to build project, so update project.json like code below:

{
    "project_type": "javascript",
    "isLandscape": false,
    "isPortrait":true,
    "debugMode" : 1,
    "showFPS" : true,
    "frameRate" : 60,
    "noCache" : false,
    "id" : "gameCanvas",
    "renderMode" : 0,
    "engineDir":"frameworks/cocos2d-html5",

    "modules" : ["cocos2d"],

    "jsList" : [
        "src/resource.js",
        "src/GameLayer.js",
        "src/GameSprite.js",
        "src/app.js"
    ]
}

Build SimpleHockey Project

Open terminal, change directory to SimpleHockey project and type cocos script below to corresponding version you want:

Web version: cocos run -p web

Android version: cocos compile -p android

Source Code: https://gitlab.com/tienthanh/simplehockey-cocos2d-x

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s