01219245/cocos2d-js/Sprites

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
This is part of 01219245.

In this tutorial, we will create a simple html5 program with Cocos2d-x.

Getting started

This tutorial is based on Cocos2d-JS version 3.10, however, it should work for other versions with some small changes.
If you installed the full Cocos2d-x framework, you should start by creating a new project. Please follow this old document first.

Make sure you follow instructions in the getting started guide to download and unzip the template. You should also try to run the game in a browser once to see if everything is ok.

Since the template is a hello world program, its directory is helloworld. Let's change it to tutorial1.

In this tutorial, we will use git to keep tracks of our work. Let's create a git repository in your project directory (in tutorial1).

git init

We shall commit regularly.

Running the game

Recall that to run the game you have to start the web server by running the following command

python -m SimpleHTTPServer

or, if you install Python 3:

python -m http.server

in the directory of your game.

Caching. Note that when you refresh the browser, it usually keeps old copies of files instead of reloading everything to speed up page loading. Therefore, it might fail to reload your new code. To avoid unexpected behaviors, make sure you disable JavaScript caching when you develop your games.

Inside the project: project starter

Let's take a look at what cocos created for us. At tutorial1, we shall see:

  • directories: frameworks/, src/, res/
  • and files: CMakeLists.txt, index.html, main.js, project.json

There are 3 main files for loading a Cocos2d-html5 game. In this section, we shall describe what should be in each file, and the changes we made from the original template. We also talk about a sample GameLayer code.

File: index.html

This is the only HTML page for the game. It contains a canvas element where the game would draw on. Note that we can set the canvas size (which would be the game screen size) with the width and height attributes of this element.

<canvas id="gameCanvas" width="800" height="600"></canvas>

File: main.js

This is the actual main program of our application. It defines function cc.game.onStart that would run when the application is started.

cc.game.onStart = function(){
    cc.view.adjustViewPort(true);
    cc.view.setDesignResolutionSize(800, 450, cc.ResolutionPolicy.SHOW_ALL);
    cc.view.resizeWithBrowserSize(true);
    //load resources
    cc.LoaderScene.preload(g_resources, function () {
        cc.director.runScene(new HelloWorldScene());
    }, this);
};

Important lines inside this function are:

    cc.LoaderScene.preload(g_resources, function () {
        cc.director.runScene(new HelloWorldScene());
    }, this);

They basically tells the framework to load resources (including JavaScript files and images as defined in resource.js) and then when it's done call

cc.director.runScene(new HelloWorldScene());

to start the first Scene of our game. In this sample program, it is called HelloWorldScene.

The last line in main.js

cc.game.run();

starts the application.

File: project.json

This file provides basic configuration of the framework as JavaScript object (in json format).

{
    "project_type": "javascript",

    "debugMode" : 1,
    "showFPS" : true,
    "frameRate" : 60,
    "id" : "gameCanvas",
    "renderMode" : 0,
    "engineDir":"frameworks/cocos2d-html5",

    "modules" : ["cocos2d"],

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

A few interesting parameters:

  • showFPS: You can hide the frame rate display here.
  • frameRate: This is the default framerate.
  • jsList: You should list all JavaScript files here. Currently, there are two files src/resource.js and src/app.js.
  • engineDir: This is where the framework code lies.

Let's clean up HelloWorld and start with our empty game

Plans: We will add two classes GameLayer and StartScene. Both classes will be in src/GameLayer.js. Therefore, we will have to change various old references to HelloWorldXXX. We will also remove unused images.

Removing files

  • Remove src/app.js - We will split our source codes based on classes. We shall not use HelloWorldScene any more.
  • Remove image files in res: CloseNormal.png, CloseSelected.png, and HelloWorld.png

Edit project.json and src/resource.js

Our main program will be in src/GameLayer.js; therefore, we have to change project.json to load that file. Change the jsList section in project.json to:

File: project.json
    "jsList" : [
        "src/resource.js",
        "src/GameLayer.js"
    ]

We also have no images to preload. We have to remove those image names from resource.js. Remove the removed image file names from resource.js.

File: src/resource.js
var res = {
};

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

Edit main.js: change the starting scene and adjust resolution

Our starting scene class will be StartScene. Fix the starting scene in main.js.

File: main.js
    cc.LoaderScene.preload(g_resources, function () {
        cc.director.runScene(new StartScene());
    }, this);

We also want to write a game with 800 x 600 screen. So update a resolution line in main.js to:

File: main.js
    cc.view.setDesignResolutionSize(800, 600, cc.ResolutionPolicy.SHOW_ALL);

(The previous resolution was 800 x 450.)

Our main program: src/GameLayer.js

This is a main game file. It defines the main GameLayer that would contains all our game objects. This class is a subclass of LayerColor. Class StartScene is also defined here. It is a scene that contains only the GameLayer object.

Save this as GameLayer.js in directory src.

File: src/GameLayer.js
var GameLayer = cc.LayerColor.extend({
    init: function() {
        this._super( new cc.Color( 127, 127, 127, 255 ) );
        this.setPosition( new cc.Point( 0, 0 ) );

        return true;
    }
});

var StartScene = cc.Scene.extend({
    onEnter: function() {
        this._super();
        var layer = new GameLayer();
        layer.init();
        this.addChild( layer );
    }
});

This is one pattern for writing class inheritance in JavaScript. It is introduced by John Resig, the creator of jQuery. In this pattern, you create an inherited class by calling method extend from the parent class passing an object with derived properties. Note that when writing a derived method, there is a special this._super that refers to the parent's method that you can call.

Note that GameLayer calls its parent init method (this._super(...)) with a Color object specifying the background color (gray (127,127,127)). You can try to change the background color by changing these RGB values.

In our early games, we will mostly modify GameLayer to co-ordinate various game objects' behaviors.

Git commit

We have an empty game running, so it is a good time to start committing to Git. Since this is probably the first time we commit in this project, don't forget to add files.

Debugging JavaScript programs in a browser

Debugging is a common task when developing programs as we try to find where things go wrong. Before we start creating games, let's see how we can do this when developing JavaScript application.

Logging with console.log

The most common task we would like to perform when debugging is to let the program print out status, so we can see what is happening inside our program. We once used alert to show a dialog, but it is very disruptive.

We can do so in the background through a JavaScript global object console by calling method log.

Let's try this by adding

console.log( 'Initialized' );

into method GameLayer.init before the method ends, and adding

console.log( 'GameLayer created' );

in method StartScene.onEnter right after a new GameLayer object is created.

These logs would appear in the JavaScript console. Try to refresh the game and look for these messages in the console screen. In Firefox, you can open the console by choosing Web Developer -> Browser Console. It should look like this:

Firefoxjsconsole.png

Notes: (from Manatsawin) To open Developer Console in Chrome, press ctrl+shift+i or right click and inspect element. The message log is accessed by pressing esc. In IE9+ the developer console is named F12 and can be accessed by the key of same name.

Don't forget to remove these logging lines before you move on.

Other debugging tools

Modern web browsers also provide various debugging capabilities. You can try using them while developing our game.

Sprites

We will create the simplest game object. A sprite is a 2d graphics that is a unit for various game animation. In this section, we will learn how to create a sprite with no animation and show it on the screen.

See the reference for cc.Sprite for what you can do with them.

Creating a sprite image file

Use a graphical software such as GIMP, Photoshop, Paint.NET to create a 64 pixel x 64 pixel image. Draw a simple spaceship whose head is in an upward direction. Make sure that the background is transparent.

Usually, when the image has transparent background, the graphic editor usually shows it like this:

219245-spaceship-sprite-transparent.png

If you background is not transparent, when you show the sprite you'll see it as an image in a white box.

We will create a directory images in tutorial1/res for keeping all our image assets. Save the image as ship.png

If you are lazy, you can just download ship.png.

Sprite class

A sprite should inherit from cc.Sprite (see reference). Create file src/Ship.js with this content:

var Ship = cc.Sprite.extend({
    ctor: function() {
        this._super();
        this.initWithFile( 'res/images/ship.png' );
    }
});

The code says that a ship is just a sprite that is initialized with our image. We can create many Ship objects from this class.

Including the file in jsList. Before you can use any JavaScript source, you should add the file names in project.json in section jsList. It should currently look like this:

    "jsList" : [
        "src/resource.js",
        "src/GameLayer.js",
        "src/Ship.js"
    ]

To show the ship, we have to create the sprite object. Let's add the code that create a ship inside GameLayer's init method. Put this before the line with return true.

        var ship = new Ship();
        this.addChild( ship );

The code creates a ship, and add it to the game layer object with method addChild.

Reload the game in the browser. You should see the top right of the spaceship at the lower left corner of the game. That's the co-ordinate (0,0). The screen co-ordinate originates from the bottom-left corner.

Cocos-screen-co-ordinates.png

Let's move the ship a bit so that we see the whole ship. Add the following line that set the ship's position to the code. The code portion becomes:

        var ship = new Ship();
        ship.setPosition( new cc.Point( 200, 200 ) );
        this.addChild( ship );

Try to reload the game again. You should see the ship at co-ordinate (200,200).

Gitmark.png Commit your work.

Notes: If you do not have transparent background, your space ship would look like the following picture.

219245-spaceship-sprite-broken-transparent.png

Moving sprites

Let's move the space ship. We shall create method Ship.update that updates the ship position. Add this method to class Ship

    update: function( dt ) {
	var pos = this.getPosition();
	this.setPosition( new cc.Point( pos.x, pos.y + 5 ) );
    }

Don't forget to add comma (,) after the end of method ctor.

Method update reads the sprite position and then sets the new position. Note that if we call method update regularly, the ship will move upwards.

Try to refresh the application now.

We would see that the ship remains still. This is indeed so because method update is not called. The question is: who will call this method?

The update method is a special method which you can schedule it to be called every frame using method scheduleUpdate. The sprite itself can call this, or, in this case, we will call it from GameLayer after we have add it as its child. Modify method init of GameLayer to be:

    init: function() {
        this._super( new cc.Color( 127, 127, 127, 255 ) );
        this.setPosition( new cc.Point( 0, 0 ) );

	var ship = new Ship();
	ship.setPosition( new cc.Point( 200, 220 ) );
	this.addChild( ship );
	ship.scheduleUpdate();
	
	return true;
    }

Refresh the application. Now you should see the ship starts moving.

Gitmark.png Commit the change.

Cycling

Right now our spaceship goes through the top of the screen and disappear. Let's make it cycling back to the bottom of the screen.

We must be able to figure out if our ship goes out of the frame. Recall that our frame height is 600 pixels; therefore, we can test with that. Change method update to:

    update: function( dt ) {
	var pos = this.getPosition();

	if ( pos.y < 600 ) {
	    this.setPosition( new cc.Point( pos.x, pos.y + 5 ) );
	} else {
	    this.setPosition( new cc.Point( pos.x, 0 ) );
	}
    }

While this approach works at this point, imagine what we have to do when we want to change the screen size. We refer to 600 here as a magic number, and it is a sign that if we change something in the program, we might have to change this constant. If we use 600 as the screen height every where in the program, changing the screen height to 480 would require so much work just to change 600 to 480 every where.

To avoid that problem, we create global variables screenWidth and screenHeight to keep these values. There are ways that we can find out the screen width and height, however, we shall use 800 and 600 for now. Add this code on top of main.js.

var screenWidth = 800;
var screenHeight = 600;

Then change the condition in Ship.update to:

if ( pos.y < screenHeight ) {
Gitmark.png If the ship moves nicely, commit the change.

Keyboard inputs

We will let the user change the ship's direction by pressing the space-bar. To do so, we need to handle keyboard events.

In this tutorial, we will follow a simple approach. First we implement methods onKeyUp and onKeyDown; add both functions to GameLayer

    onKeyDown: function( keyCode, event ) {
	console.log( 'Down: ' + keyCode.toString() );
    },
    onKeyUp: function( keyCode, event ) {
	console.log( 'Up: ' + keyCode.toString() );
    }

We have to register these function to an event listener. Add the following function addKeyboardListener to GameLayer:

    addKeyboardHandlers: function() {
        var self = this;
        cc.eventManager.addListener({
            event: cc.EventListener.KEYBOARD,
            onKeyPressed : function( keyCode, event ) {
                self.onKeyDown( keyCode, event );
            },
            onKeyReleased: function( keyCode, event ) {
                self.onKeyUp( keyCode, event );
            }
        }, this);
    }

Note that this function basically calls functions onKeyDown and onKeyUp of the game layer object. Also, add this line to GameLayer.init to call the function.

	this.addKeyboardHandlers();

Notes: At this point, just follow the instruction to add the listener. We will explain how function addKeyboardHandlers works and how event handling in Cocos2d-JS works later.

Then try the program. Open the console and hit a few keys. You might have to click the game screen once so that the game captures the keyboard input. The console should output messages like this:

Cocos-keyevent-logs.png

Note that methods onKeyDown and onKeyUp receive the key codes. (Space is 32, for example.) We usually do not refer to key code directly, but we will use constants defined in CCCommon.js.

Ship's direction

Let's add states to the ship. In the ship constructor, we shall add property direction to the ship's object. We can add a line like this to Ship.ctor.

	this.direction = 1;             // 1 => going up

We can refer to direction 1 as going up, but it will be very hard to remember later on. This makes our code hard to read. Let's try to add constants for ship directions and use them instead. We will have only two directions for now.

The code for class Ship changes to:

var Ship = cc.Sprite.extend({
    ctor: function() {
        // ..
	this.direction = Ship.DIR.UP;
    },
    // ..
});

Ship.DIR = {
    UP: 1,
    RIGHT: 2
};

EXERCISE: Modify method update so that the ship moves in the direction specified by this.direction. Make sure that if the ship moves rightward, it cycles back to the left side of the screen.

Notes: You can test this by changing the code that initialize the direction from Ship.DIR.UP to Ship.DIR.RIGHT

Controlling ship's direction

Let's add method switchDirection to Ship:

    switchDirection: function() {
	if ( this.direction == Ship.DIR.UP ) {
	    this.direction = Ship.DIR.RIGHT;
	} else {
	    this.direction = Ship.DIR.UP;
	}
    }

Notes: Don't forget to put appropriate comma's at the end of other methods.

We will change the ship's direction if the user hits spacebar. Clearly, we have to call method switchDirection of the Ship object in method onKeyDown. However if we look at the code where we create the Ship object, we only keep it in a local variable ship; this makes it impossible to refer to the object again in method onKeyDown. Thus, we have to change the portion of the code that creates the ship to the following.

	this.ship = new Ship();
	this.ship.setPosition( new cc.Point( 200, 220 ) );
	this.addChild( this.ship );
	this.ship.scheduleUpdate();

We can then refer to the Ship in onKeyDown method.

    onKeyDown: function( keyCode, event ) {
	if ( keyCode == cc.KEY.space ) {
	    this.ship.switchDirection();
	}
    }
Gitmark.png Try the game and commit the recent change.

Turning sprites

You can rotate a sprite with method setRotation. We can change method switchDirection to also rotate the sprite.

    switchDirection: function() {
	if ( this.direction == Ship.DIR.UP ) {
	    this.direction = Ship.DIR.RIGHT;
	    this.setRotation( 90 );
	} else {
	    this.direction = Ship.DIR.UP;
	    this.setRotation( 0 );
	}
    }

The sprite is rotate with middle point as the rotation center. If you want to use different center fro rotation, you specify that by method setAnchorPoint. In this method, you pass two real numbers representing the position of the center, when assuming that the bottom-left is at (0,0) and top-right is at (1,1). Note that initially, the center is at (O.5,O.5)

Exercise: Let's make a game

With all the know-how we have for manipulating sprites, we will try to create a simple game.

Let's create a goal

Moving a ship without any goal would not be fun. Let's put another object, a into the game screen. The goal is to move the ship to hit this object. Let's make the ship hit a golden bar.

Create an image of smaller size (e.g., 40 x 40) of a golden bar. Save it as res/images/gold.png. This is the gold image that I have.

Gold.png

We will create a new class Gold in Gold.js.

EXERCISE: Implement method randomPosition below.

var Gold = cc.Sprite.extend({
    ctor: function() {
        this._super();
        this.initWithFile( 'res/images/gold.png' );
    },

    randomPosition: function() {
        // --- your task is to write this method
    }
});

Notes: Don't forget to include this file src/Gold.js in project.json.

We also create the gold object in GameLayer.init. The order of two calls to addChild for the gold and the ship is important. You should try adding the ship first then the gold, and the gold first then the ship and see the difference.

	this.gold = new Gold();
	this.addChild( this.gold );
	this.gold.randomPosition();

Don't forget to add this file to the source file list in cocos2d.js.

Have you tried both addChild orders? Do you see the difference? Expand to see the explanation.

By default, the sprite is drawn on the screen in order they are added. If we add the ship before the gold, you'll see the ship flying underneath the gold.

Collision detection

Collision detection is a common task in game programming. In our case, we want to know if our ship hits the golden bar. There are many ways to perform collision detection, we will use a simple distance checking.

Add this method to the Gold class.

    closeTo: function( obj ) {
	var myPos = this.getPosition();
	var oPos = obj.getPosition();
  	return ( ( Math.abs( myPos.x - oPos.x ) <= 16 ) &&
		 ( Math.abs( myPos.y - oPos.y ) <= 16 ) );
    }

Note again that we are using another magic number here, i.e., 16 as the distance threshold. We can create a new constant for that, but for now, just leave it this way. (We might really need to change this constant later if we find it too difficult to hit the target.)

With this method we can check if the ship hits the gold, however, we need to run this check somewhere. We can let GameLayer checks this every frame. Let's add update method to GameLayer:

    update: function() {
	if ( this.gold.closeTo( this.ship ) ) {
	    this.gold.randomPosition();
	}
    }

You also need to add

this.scheduleUpdate();

in GameLayer.init to schedule the call to update every frame.

Try to run the game to see if you can hit the gold.

Gitmark.png Commit your work after it is done.

Notes: Our ship has update method and our GameLayer also has update method. Which one is called first? How can you figure this out?

Keeping the score

Let's show the current score.

EXERCISE: Add the scoring to the game.

Hints: You can use cc.LabelTTF to show the score. This is how you create it.

	this.scoreLabel = cc.LabelTTF.create( '0', 'Arial', 40 );
	this.scoreLabel.setPosition( new cc.Point( 750, 550 ) );
	this.addChild( this.scoreLabel );

If you want to change the score to 5, we can assign a new string to it; like this:

	this.scoreLabel.setString( 5 );
Gitmark.png Don't forget to commit after the game shows the score.

It's your turn: Make it more fun

Is it fun to play our game? There are many ways to improve it; here are a few:

  • Increase the ship speed as the player scores more gold. This progressively increase the challenge.
    • If you increase the speed the collision detection method that we use might not work as the ship might run through the gold in just one step. You will have to think about the distance between a point and a line segment.
  • Add objects that the player should avoid, e.g. a black hole or another ship. When hitting these undesirable objects, the player either die or the player's score is decreased.
  • Let the gold move or disappear after a while. (To do this, you might need to schedule timing event.)
  • Add the time limit to the game.
  • Make it a two-player game.
    • Both players try to hit the gold. They can leave a bomb to hit the other player.

EXERCISE: Try to make the game more fun to play.