Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon

14. August 2012

 

This tutorial covers the process of creating and using a spritesheet.  A spritesheet is a collection of individual sprites grouped together into a single image.  I created the spritesheet for this tutorial using TexturePacker.  I already documented the process in this tutorial, the only difference is you set the Data Format to Cocos2D instead of Generic XML.  The spritesheet and all of the sprites that went in to making it are included in the archive at the end of this tutorial.

 

The cocos2d.js and main.js files are relatively unchanged:

 

cocos2d.js:

(function () {
    var d = document;
    var c = {
        COCOS2D_DEBUG:2,
        box2d:false,
        showFPS:false,
        frameRate:60,
        tag:'gameCanvas', 
        engineDir:'../cocos2d/',
        appFiles:['MyFifthApp.js'],
        mainFile:'main.js'
    };
    window.addEventListener('DOMContentLoaded', function () {
        var s = d.createElement('script');
        s.src = c.engineDir + 'platform/jsloader.js';
        d.body.appendChild(s);
        s.c = c;
        s.id = 'cocos2d-html5';
    });
})();

The only real different here is the file name passed in the appFiles array.

 

main.js:

var cocos2dApp = cc.Application.extend({
    config:document.querySelector('#cocos2d-html5')['c'],
    ctor:function (scene) {
        this._super();
        this.startScene = scene;
        cc.COCOS2D_DEBUG = this.config['COCOS2D_DEBUG'];
        cc.setup(this.config['tag']);
        cc.Loader.shareLoader().onloading = function () {
            cc.LoaderScene.shareLoaderScene().draw();
        };
        cc.Loader.shareLoader().onload = function () {
            cc.AppController.shareAppController().didFinishLaunchingWithOptions();
        };

        cc.Loader.shareLoader().preload([
            {type:"plist",src:"spritesheet.plist"}
        ]);
    },
    applicationDidFinishLaunching:function () {
        var director = cc.Director.getInstance();
        director.setDisplayStats(this.config['showFPS']);
        director.setAnimationInterval(1.0 / this.config['frameRate']);
        director.runWithScene(new this.startScene());

        return true;
    }
});
var myApp = new cocos2dApp(MyFifthAppScene);

 

There are only two changes here.  First we added spritesheet.plist to the preloader list.  plist files are short for Property List is a configuration format used in Mac/iOS development, but at the end of the day, it’s just data in an XML format.  This file was generated for us by TexturePacker and it describes the details and locations of each sprite within the spritesheet, indexed by the original file name.  The other difference is we pass MyFifthAppScene to our new cocos2dApp call.

 

Let’s take a look at ( a portion of ) the file spritesheet.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>frames</key>
        <dict>
            <key>Walk_left00.png</key>
            <dict>
                <key>frame</key>
                <string>{{0,0},{128,96}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{128,96}}</string>
                <key>sourceSize</key>
                <string>{128,96}</string>
            </dict>
            <key>Walk_left01.png</key>
            <dict>
                <key>frame</key>
                <string>{{128,0},{128,96}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{128,96}}</string>
                <key>sourceSize</key>
                <string>{128,96}</string>
            </dict>
            <key>spritesheet.png</key>
            <dict>
                <key>frame</key>
                <string>{{0,960},{512,1024}}</string>
                <key>offset</key>
                <string>{0,0}</string>
                <key>rotated</key>
                <false/>
                <key>sourceColorRect</key>
                <string>{{0,0},{512,1024}}</string>
                <key>sourceSize</key>
                <string>{512,1024}</string>
            </dict>
        </dict>
        <key>metadata</key>
        <dict>
            <key>format</key>
            <integer>2</integer>
            <key>realTextureFileName</key>
            <string>spritesheet.png</string>
            <key>size</key>
            <string>{512,2048}</string>
            <key>smartupdate</key>
            <string>$TexturePacker:SmartUpdate:873706d2c1ad9f5b33d90eefda3a7084$</string>
            <key>textureFileName</key>
            <string>spritesheet.png</string>
        </dict>
    </dict>
</plist>

 

The details of the file aren’t important, Cocos2D does all of the heavy lifting for us.  Just be aware that it describes the general layout and name of each sprite within your spritesheet. Now let’s take a look at MyFifthApp.js where the majority of our changes occurred:

 

var MyFifthApp = cc.LayerColor.extend({
    sprite:null,
    spriteFrameNamePrefix:"Walk_left",
    spriteFrameIndex:0,
    init:function()
    {
        this._super();
        this.initWithColor(new cc.Color4B(0,0,0,255));
        var size = cc.Director.getInstance().getWinSize();

        var cache = cc.SpriteFrameCache.getInstance();
        cache.addSpriteFrames("spritesheet.plist", "srcSprites/spritesheet.png");

        this.sprite = cc.Sprite.createWithSpriteFrameName(this.spriteFrameNamePrefix + "00.png");
        this.sprite.setPosition(new cc.Point(300,300));
        this.sprite.setScale(3);
        this.addChild(this.sprite);

        this.setKeyboardEnabled(true);
        return this;
    },
    onKeyUp:function(e){},
    onKeyDown:function(e){
        if(e == cc.KEY.left || e == cc.KEY.right){
            var prevPrefix = this.spriteFrameNamePrefix;
            if(e == cc.KEY.left)
                this.spriteFrameNamePrefix = "Walk_left";
            else
                this.spriteFrameNamePrefix = "Walk_right";
            if(prevPrefix !== this.spriteFrameNamePrefix)
                this.spriteFrameIndex = 0;


            if(this.spriteFrameIndex > 18)
                this.spriteFrameIndex = 0;
            var indexAsString;
            if(this.spriteFrameIndex < 10)
                indexAsString = "0" + this.spriteFrameIndex.toString();
            else
                indexAsString = this.spriteFrameIndex.toString();

            this.removeChild(this.sprite);
            this.sprite  = cc.Sprite.createWithSpriteFrameName(
                this.spriteFrameNamePrefix + indexAsString + ".png"
            );

            this.sprite.setPosition(new cc.Point(300,300));
            this.sprite.setScale(3);
            this.addChild(this.sprite);
            this.spriteFrameIndex++;
        }
    }
});


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

 

I wont bother going in to detail on MyFifthAppScene, we’ve done that enough times already.  In MyFifthApp we declare a sprite variable to point at the currently active sprite, then declare spriteFrameNamePrefix and spriteFrameIndex.  We access the sprites within the sprite sheet using the keys defined in the plist ( the file name ), of which there are 18 frames for walking left and right:

Walk_left00.png –> Walk_left18.png

Walk_right00.png –> Walk_right18.png

 

We will be building this key dynamically as we switch between sprites.

 

I am only going to cover the new stuff here, the rest should be pretty familiar at this point.  Cocos2D has a built in cache for Sprite frames ( in cocos2D a SpriteFrame is an individual sprite within the spritesheet, while a spritesheet is a SpriteFrames (<- note, plural ) ), the singleton SpriteFrameCache.  We add a new spritesheet to it with the call addSpriteFrames, passing in the name of the plist we preloaded earlier in main.js, as well as the path to the spritesheet graphic.

 

 

Speaking of which, this is spritesheet.png that TexturePacker generated:

spritesheet

We then get our sprite reference from the spritesheet with a call to createWithSpriteFrameName, passing in the key of the sprite specified in the plist.  In this case we are starting with Walk_left00.png.  We then position the sprite at 300,300, scale it to 3x it’s size and add it to our layer.  Finally we tell Cocos we want to handle keyboard events with a call to setKeyboardEnabled().

 

Next in the onKeyDown handler we check to see if the user has pressed ( and released ) the left or right arrow.  Depending on which direction we are walking, that determines which prefix to use for accessing the sprite sheet, left or right.  Next we check if the direction has changed since the last frame, if so, we start the frame index back to 0 to start the animation over.

 

Next we check to see if we’ve exceed our 18 frames, if we have we roll over to 0.  Next we check to see if our index is a single digit ( 0..9 ), in which case we pad it with a 0, so 1 becomes 01.  This is simply because that is how the source frames were named when I imported them to TexturePacker.  We remove our current ( about to be replaced ) animation frame from the layer. We then put our prefix, index and “.png” together to get the key to the next frame of animation, we access with using createWithSpriteFrameName again.  We position and scale the new sprite frame and add it to the scene to replace the previous frame.  Finally we advance to the next frame index.

 

 

Here is our game in action.  Be sure to click in the window to focus keyboard events, then use Left and Right arrows to walk:



This code wouldn’t run when I first deployed it live to my IIS server but worked fine in Apache!  The XML loader in CCSAXParser.js was failing, and it turned out to be a permissions issue.  When you deploy a new file extensions ( like plist ) to IIS, you need to be sure to add it as an allowed mime type!

 

For the record, there are actually better ways to handle animation that we will cover in a later tutorial. This tutorial does however illustrate how to load a sprite sheet and access individual sprites. You can download the entire archive here.

Programming , ,

12. August 2012

 

This post over on the gamedev forums at reddit caught my eye.  User @Mattrix has released a framework for creating 2D games using Unity with code only.  This effectively turns Unity in to a runtime only; it makes almost zero use of the tools provide with Unity and in many cases completely re-invents things Unity already does, such as texture packing.

 

At first glace you may be thinking “Why?  Isn’t that remarkably stupid?”

 

Well, not really.  To a certain degree, you are already cludging Unity a bit when you are working in 2D as it is, it certainly is designed as a 3D toolset.  Consider this though, if you want to create a game for Android and/or iOS and you want to use C#, what are your options?  Well, basically that leaves MonoGame or Unity.  MonoGame is certainly a good choice, but there is a major downside…

 

A 400$ price tag.  You see, in the end you either need monotouch or mono for android, which are 400$ a piece. ( God I wish Xamarin would reconsider their pricing! )

 

Or you can go with Unity, which is a licensed version of mono, with a complete game library built overtop and the exact same price tag.  Even better, just a few months back Unity made both Android and iOS versions completely free ( I know I got my licenses then ).  So, if you want to take a code heavy approach and avoid the higher level tools, but prefer to work in C#, Futile is certainly worth checking out.

 

He has included full source for a sample game, that you can see in the following video:

 

He has also provided a 10 minute as well as an hour long video tutorial on getting started.

 

 

I might play with this if I get a bit of time.  If you are interested in checking it out, this is the website and the source code is available on github.

 

Regardless to it’s final usefulness, it’s an interesting idea and it’s cool that he shared it.

News, Programming

12. August 2012

 

Just a quick note that all of the tutorials in the Cocos2D-HTML5 series have now been updated (text, downloads and source) to the most recent ( and hopefully stable ) API level.  Expect news of a beta release shortly.

 

 

If you spot any mistakes, please let me know!

 

 

Cocos2D-x HTML5 tutorial series

11. August 2012

 

I just finished updating all of the cocos2D source code to the newest beta code ( as of today anyways ).  You can read about what was involved here or you can download the source code below.  I will be updating the text of each tutorial shortly, but in the meanwhile, you should be able to puzzling things out using just the source, the process stays pretty similar over all, even though there were extensive changes to the code.

 

Project Source Code

 

I will post again when the text of the tutorials are updated.

 

A quick question to all of you, is there any value in keeping the old tutorials and link to the new ones, or should I just overwrite the existing tutorials?  I can’t really think of a use for outdated tutorials, unless of course someone wanted to keep working with their old code base I suppose.

Programming, General , ,

10. August 2012

 

I received a comment from a Cocos2D-HTML developer saying that the API is complete for the upcoming beta release, so that I could update the tutorials on this site.  So today I jumped in to see exactly what changed and the answer is quite a bit!  In the process of updating the code to work with the newest Cocos2D code, I went through a great deal of pain tracking down those changes.  This post is to illustrate what has changed, so you don’t have to go through the same process.

 

The code being ported is the example code from this tutorial.

Changes:

 

Filename changes

 

I encountered a few filename changes ( which JavaScript can make exceedingly annoying to find ).  First off, a few files have had their casing changed, which you won’t notice you are working in Windows, but under Linux you will.  Mostly it’s a matter of files being renamed from cc to CC.  Additionally the folder keypad_dispatcher is now keyboard_dispacter, and CCKeypadDelegate.js and CCKeypadDispatcher.cs have been renamed to CCKeyboardDelegate.js and CCKeyboardDispatcher.cs respectively.

 

Function changes

 

ccp is now Point

ccc2/3/4/ is now Color2B,Color3B,Color4B

Layer.setIsRelativeAnchorPoint has been removed.

Director.sharedDirector() is now Director.getInstance();

setIsTouchEnabled() is simply isTouchEnabled()

setIsKeypadEnabled() is now isKeyboardEnabled()

 

Events have been renamed, for example:

ccTouchesEnded is now onTouchesEnded

 

The Keyboard delegate have renamed events from:

keyDown and keyUp to onKeyDown and onKeyUp

cc.key.[name] is now cc.KEY.[name]

cc.Touch ( the value passed to touch handlers ) has changed from .locationInView() to .getLocation()

cc.AudioManager.sharedEngine() is now cc.AudioEngine.getInstance();

cc.Log is now cc.log

 

Bootstrap change

 

This is where the biggest changes occurred.  The startup process is completely different.  First of all, library loading is no longer the responsibility of the  application, it is instead handled by jsloader.js.

 

This changes cocos2d.js massively:

cocos2d.js

(function () {
    var d = document;
    var c = {
        COCOS2D_DEBUG:2, //0 to turn debug off, 1 for basic debug, and 2 for full debug
        box2d:false,
        showFPS:true,
        frameRate:60,
        tag:'gameCanvas', //the dom element to run cocos2d on
        engineDir:'../cocos2d/',
        appFiles:['MyFirstApp.js']
    };
    window.addEventListener('DOMContentLoaded', function () {
        //first load engine file if specified
        var s = d.createElement('script');
        s.src = c.engineDir + 'platform/jsloader.js';
        d.body.appendChild(s);
        s.c = c;
        s.id = 'cocos2d-html5';
        //else if single file specified, load singlefile
    });
})();

If you haven’t seen this syntax before, it is basically an anonymous function that will execute at the end of declaration ( because of the () it ends in ).  The majority of what is going on here is the creation of a configuration file ‘c’, where a number of settings are, um, set.  tag is the name of the canvas element in your html file, engineDir is where you installed cocos2D to, appFiles are additional files you write ( in addition to your main.js ).  Basically every file you create needs to be added to this array.  It then adds an event that will be fired once the DOM has been populated, which will then create a new <SCRIPT> tag in your HTML document, then pass in the path to the jsloader, which will then load all of the cocos2D libraries, as well as anything your specified in the appFiles array.  Finally it stores the configuration settings in ‘c’ in the newly created script tag.

 

In a change I disagree with, the framework now forces you to add a file named main.js, which will be called by jsloader at the end of the loading process.  IMHO, this should be added as part of the configuration you set above.  This file replaced AppDelegate.js from the tutorials.  Let’s take a look at the new main.js:

var cocos2dApp = cc.Application.extend({
    config:document.querySelector('#cocos2d-html5')['c'],
    ctor:function (scene) {
        this._super();
        this.startScene = scene;
        cc.COCOS2D_DEBUG = this.config['COCOS2D_DEBUG'];
        cc.setup(this.config['tag']);
        cc.Loader.shareLoader().onloading = function () {
            cc.LoaderScene.shareLoaderScene().draw();
        };
        cc.Loader.shareLoader().onload = function () {
            cc.AppController.shareAppController().didFinishLaunchingWithOptions();
        };
        cc.Loader.shareLoader().preload([
        ]);
    },
    applicationDidFinishLaunching:function () {
        // initialize director
        var director = cc.Director.getInstance();
        director.setDisplayStats(this.config['showFPS']);
        director.setAnimationInterval(1.0 / this.config['frameRate']);
        director.runWithScene(new this.startScene());

        return true;
    }
});
var myApp = new cocos2dApp(MyFirstAppScene);

Things have changed a fair bit here, but the process is pretty much the same.  First thing to notice is it pulls the configuration data ‘c’ and stores it in the variable config, which is then used to setup cocos2D.  The next major thing to notice is we now create an instance of our app, and pass in the scene to start with. 

 

Now let’s take a look at the changes to MyFirstApp.cs:

var MyFirstApp = cc.Layer.extend({
    init:function()
    {
        this._super();

        var s = cc.Director.getInstance().getWinSize();

        var layer1 = cc.LayerColor.create(new cc.Color4B(255, 255, 0, 255), 600, 600);
        //layer1.setPosition(new cc.Point(s.width/2,s.height/2));
        //layer1.setIsRelativeAnchorPoint(true);
        layer1.setAnchorPoint(new cc.Point(0.5,0.5));

        
        var helloLabel = cc.LabelTTF.create("Hello world", "Arial", 30);
        helloLabel.setPosition(new cc.Point(s.width/2,s.height/2));
        helloLabel.setColor(new cc.Color3B(255,0,0));
        var rotationAmount = 0;
        var scale = 1;
        helloLabel.schedule(function()
            {
                this.setRotation(rotationAmount++);
                if(rotationAmount > 360)
                    rotationAmount = 0;
                this.setScale(scale);
                scale+= 0.05;
                if(scale > 10)
                    scale =1;
            });

        layer1.addChild(helloLabel);
        this.addChild(layer1);

        
        return true;
    }

});

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

 

Things have changed a bit here.  First we now extend a scene to create a new object MyFirstAppScene.  This in turn creates a layer and adds it to itself.  The onEnter event is called when the scene is activated ( via RunWithScene ).  Otherwise all of our changes are those described above ( function renames, removal, etc… ).

 

 

I hope that helps you port your existing code to the newest codebase.  All of the changes are improvements ( except perhaps the whole forced main.js thing, I still don’t like that ), so it is worth the pain.  I hope to get the tutorials all updated shortly, but unfortunately, these changes more or less will result in complete re-writes of 3 tutorials, so it might take a bit! Sad smile

Programming , ,

Month List

Popular Comments