Tutorial – Getting started with TypeScript and Cocos2d-x JS -Part 5 – Karma -TypeScript Definition Files – Hello World Scene – Asset Factory – Mock Assets

Tutorial – Getting started with TypeScript and Cocos2d-x JS -Part 5 – Karma -TypeScript Definition Files – Hello World Scene – Asset Factory – Mock Assets

Welcome to Part 5  of this Tutorial Series.

You will find the completed source for this tutorial in the project named CocosTsGame on the GitHub repository.

We we are going to create our first Cocos2d-x JS scene.

Common use for scenes within Cocos2d-x JS is to treat them as the distinct areas or pages (or screens) of your application for example :

  • Splash Page
  • Settings
  • Game Map
  • Game Play
  • Game Results

 

You can use the Cocos2d-x director to run or replace the current scene using the below command:

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

 

A scene manages a Scene Graph which is a typical pattern used to manage the display elements within a graphics or games engine.

In Cocos2dx-JS the  fundamental nodes of the scene graph are represented by cc.Node derived instances .

A scene can contain one or more cc.Nodes of which themselves can contain one or more cc.Nodes, and so on.

A special type of node is called a Layer, represented by the cc.Layer class.

A Layer is a special type of node that can receive  touch and accelerometer input. As derivative of node, a Layer can itself contain 1 or more nodes.

Please consult the Cocos2d-x documentation for further information regarding scenes, layers and nodes

We will come back to scenes and layers when we implement our first typescript based scene HelloWorldScene but first we must do some more configuration.

Third Party Javascript Libraries and Typescript Definition Files

As a TypeScript developer there will be times that you wish to utilise existing Javascript libraries that may not already have accompanying TypeScript Definition files.

In the case that there are no existing TypeScript definition files, you may want to create your own.

Creating your own TypeScript definition files will allow you to consume a given Javascript library while utilising  TypeScript’s inherent  type safety and the excellent tooling and productivity benefits it allows.

With this in mind we will create a folder  to contain our Typescript definition files.

note: We will also use this folder to house any other fully implemented TypeScript libraries that are not hosted by npm or typings, such as “in house” libraries etc.

Navigate to the project root and Create a folder named tslib.

Copy the contents of the tslib folder from the completed CocosTsGame project (see Git repository ) into your the folder you just created.

You tslib folder should now contain typescript definitions for the following libraries:

  • Signals  JS  – I created the typescript definitions for the indispensable signals library 
  • Cocos2dx JS – typing’s for Cocos2d-x modified from original source here  
  • dijon  – I created the typescript definitions for the simple but highly effective dijon IOC library (influenced by RobotLegs)

 

You will also find the fully implemented source code for  Moon CES a simple yet effective Typescript entity/component system

Now we have our typings and libraries,  we will ensure the relevant JavaScript files are loaded into the browser and available at run time.

Navigate to your project root, then navigate to the src folder.

Create a folder named jslib.

Copy the contents of the jslib folder from the completed CocosTsGame project (see Git repository ) into the folder you just created.

Your jslib should now contain at minimum:

note: we will install the typings for state-machine later using npm install –save-dev @types/javascript-state-machine

We are not quite finished yet, we have some last bits of configuration to ensure that our JavaScript files will be loaded and our TypeScript definitions and libraries will be considered by the transpiler.

Navigate to your project root:

Open up project.json and paste in the following code (changes have been highlighted)

{
    "project_type": "javascript",

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

    "modules" : ["cocos2d","extensions"],

    "jsList" : [
         "src/jslib/dijon.js",
         "src/jslib/signals.js",
         "src/jslib/state-machine.js",
            "src/resource.js",
            "src/applicationbundle.js",
            "src/app.js"
    ]
}

Open up tsconfig.json and ensure that it reads: (note some code has been removed, as well as highlighted changes made )

{
    
    "compilerOptions": {
        "noImplicitAny": true,
        "target": "es5",
        "module": "commonjs",
        
        "outDir":"tsdist/",

         "sourceMap": true,
    "jsx": "react",
    "allowJs": true
    },
     "include": [
         
        "tslib/**/*",
         "tssrc/**/*"
    ]

}

Note above that we replaced the files array with the include array.

 

Open up tsconfig.test.json and ensure that it reads: (note some code has been removed, as well as highlighted changes made )

 

{
  
    "compilerOptions": {
        "noImplicitAny": true,
        "target": "es5",
        "module": "commonjs"
    },
      "include": [
        "tslib/**/*",
        "tssrc/**/*",
        "tstest/**/*"
    ]

}

 

Browser Based testing and Karma

As we are starting to use some of the features of Cocos2dx-JS it will be useful to be able to run our tests within the browser. This will allow us to  reference the Cocos2dx JS classes without our tests falling over.

For this we will introduce Karma and integrate it with gulp and Webpack.

Navigate to your project root and open package.json.

Ensure that it reads as below:

{
  "name": "CocosTsGame",
  "version": "1.0.0",
  "description": "base project for typescript based cocos2dx games ( external module )",
  "main": "./tsdist/main.js",
  "scripts": {
    "test": "mocha -w"
  },
  "keywords": [
    "typescript",
    "cocos2dxjs"
  ],
  "author": "Steven Daley - www.dalste.co.uk",
  "license": "ISC",
  "devDependencies": {
    "chai": "^3.5.0",
    "gulp": "^3.9.1",
    "gulp-mocha": "^4.3.1",
    "gulp-tslint": "^8.0.0",
    "gulp-typescript": "^3.1.6",
    "gulp-webpack": "^1.5.0",
    "karma": "^1.7.0",
    "karma-chai": "^0.1.0",
    "karma-chrome-launcher": "^2.1.1",
    "karma-cli": "^1.0.1",
    "karma-firefox-launcher": "^1.0.1",
    "karma-ie-launcher": "^1.0.0",
    "karma-mocha": "^1.3.0",
    "karma-mocha-reporter": "^2.2.3",
    "karma-phantomjs-launcher": "^1.0.4",
    "karma-safari-launcher": "^1.0.0",
    "karma-webpack": "^2.0.3",
    "source-map-loader": "^0.2.1",
    "ts-loader": "^2.1.0",
    "tslint": "^5.2.0",
    "typescript": "^2.3.2",
    "webpack": "^2.6.0",
    "webpack-stream": "^3.2.0"
  }
}

 

You will notice a number of new dependencies. We need to install them.

 

Open a command line at your projects root and execute the following command:

 

npm install

 

Ensure you are still on your project root and create a file called karma.config.js.

Copy the below code:

 

var webpackConfig = require('./webpack.config.js');
webpackConfig.entry ={};
module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['mocha', 'chai'],
    files: [
      'publish/html5/game.min.js',
      'tstest/*.ts'
    ],
    exclude: [
    ],
    preprocessors: {
      'tstest/**/*.ts': ['webpack']
    },
    webpack: {
      module: webpackConfig.module,
      resolve: webpackConfig.resolve
    },
    mime: {
      'text/x-typescript': ['ts','tsx']
    },
    reporters: ['mocha'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    concurrency: Infinity
  })
}

 

You will notice that on line 8 it references the file: publish/html5/game.min.js

We will now generate that file by using the cocos console to generate a build of our Cocos2dx-JS application skeleton, this build will include the portion of the Cocos2d-x JS framework that we reference in our project. (remember to rebuild as you add in more cocos2d-x dependencies such as chipmunk , or ccui)

Navigate to your project root and execute the command:

cocos deploy -p web -m release

For more info on the cocos console line please see this link.

We need to add our gulp tasks so that we may run our tests via karma.

Open up gulpfile.js and ensure that it reads (changes have been highlighted):

'use strict'
var gulp = require("gulp");
var ts = require("gulp-typescript");
var tsProject = ts.createProject("tsconfig.json");

var tsTestProject = ts.createProject("tsconfig.test.json");
const mocha = require('gulp-mocha');

var gulpTslint = require("gulp-tslint");
var tslint = require("tslint");

gulp.task('lint', function () {
    var program = tslint.Linter.createProgram("./tsconfig.json");

    gulp.src('tssrc/**/*.ts', { base: '.' })
        .pipe(gulpTslint({ program })).pipe(gulpTslint.report());
});


gulp.task('test', function () {

    return gulp.src(['./tslib/**/*.ts','./tssrc/**/*.ts','./tstest/**/*.spec.ts'],
        {
            base: '.'
        })
        /*transpile*/
        .pipe(tsTestProject())
        /*flush to disk*/
        .pipe(gulp.dest('tsdist'))
        /*execute tests*/
        .pipe(mocha())
        .on("error", function (err) {
            console.log(err)
        });
});


var tsconfig = require(process.cwd() + '/tsconfig.json');

gulp.task('typescript', function() {
  var tsResult = tsProject.src() // instead of gulp.src(...) 
    .pipe(tsTestProject());
  
  return tsResult.js.pipe(gulp.dest(tsconfig.compilerOptions.outDir));
});

var gutil = require('gulp-util');
var webpack = require('webpack');
var webpackConfig = require(process.cwd() + '/webpack.config.js');
gulp.task('webpack', ['typescript'], function(callback) {
    // run webpack
    webpack(webpackConfig, function(err, stats) {
        if(err) throw new gutil.PluginError('webpack', err);
        gutil.log('[webpack]', stats.toString({
            // output options
        }));
        callback();
    });
});


//ES6
//import { Server } from 'karma';
 
// ES5
var karma = require('karma').Server; 
 

gulp.task('karma:watch', function(done) {
    karma.start({
        configFile: __dirname + '/karma.config.js',
        singleRun: false,
    autoWatch: true,
    }, function() {
        done();
    });
});

gulp.task('karma', function(done) {
    karma.start({
        configFile: __dirname + '/karma.config.js',
        singleRun: true
    }, function() {
        done();
    });
});

We now have two new gulp commands gulp karma and gulp karma:watch:

  • gulp karma – builds our tests using webpack – executes our mocha tests within chrome and exits immediately – printing the results to the console.

  • gulp karma:watch –does the same as gulp karma but continuously watches for code changes and processes again

 

Ok, that is it for our configuration. Let us now set up our  HelloWorldScene and Object Factories

Hello World Scene and Object Factories

We will now create the HelloWorldScene and HelloWorldLayer.

While developing games I have found it useful create a mock asset library containing throwaway assets  so that I can just get something  on the screen.

There is nothing more disruptive to creative flow, than having to leave my code IDE, draw some  graphics, export said graphics for multiple resolutions, create sprite sheets,  ensure said sprite sheets are loaded……

With this in mind we will create the beginnings of a mock asset library. I recommend any creative developer invest in a similar library, and expand upon it. You could have for example mock sounds or mock spine animations etc.

Its is  also wise to create asset factories, as they allow us to decouple the instantiation of assets from our main game code.

When we later add in Dependency injection you will be able to swap in and out whole suites of assets within a single line of code.

Lets get these classes down as quickly as possible, I will describe some of the TypeScript language features we are using as we progress through each file.

Navigate to your projects root folder and create the below highlighted files in their respective folders (if they don’t already exist):

│   Application1.ts
│   Application2.ts
│   Bootstrap.ts
│   IApplication.ts
│
├───component
├───controller
├───entity
├───factory
│   │   ICreationOptions.ts
│   │   IFactory.ts
│   │
│   ├───ccnode
│   └───view
│       │   CharacterAssetFactory.ts
│       │
│       └───characterassetfactory
│               MockAsset.ts
│
├───model
├───service
├───types
│       AssetTypes.ts
│
└───view
    └───scenes
        │   HelloWorldScene.ts
        │
        └───helloworld
                HelloWorldMainLayer.ts

Navigate to tssrc/factory/ICreationOptions.ts

Insert the following code:

export interface ICreationOptions<T> {
    getType():T
}

export default ICreationOptions;

The above interface uses a TypeScript generic type to describe an interface that has a function getType();  with return type T.   You will see this interface in use within the create functions of our games factories.

This interface is defined within a TypeScript external module which is exporting the interface as its default export.

Please read above provided links for further information on TypeScript’s  generic types  and module exports.

Navigate to tssrc/factory/IFactory.ts

Insert the following code:

export interface IFactory<T,U> {
    create(creationOptions: T): U;
}

export default IFactory;

Note the use of TypeScript’s Intersection Types to describe the parameter and return type for the create method

Navigate to tssrc/factory/view/CharacterAssetFactory.ts

Insert the following code:

import { IFactory } from "./../IFactory";
import { ICreationOptions } from "./../ICreationOptions";
import { CharacterAssetTypes } from "./../../types/AssetTypes";
import { MockAsset, MockAssetColours } from "./characterassetfactory/MockAsset";

/**
 * @class CharacterAssetCreationOptions
 * @description provides creation options to CharacterAssetFactory
 */
export class CharacterAssetCreationOptions implements ICreationOptions<CharacterAssetTypes>{
    private _type:CharacterAssetTypes; 
    constructor(type:CharacterAssetTypes){
            this._type= type;
    }
    getType():CharacterAssetTypes{
        return this._type;
    }
}

/**
 * @class CharacterAssetFactory
 * @param CharacterAssetCreationOptions
 * Uses the returned type from character creation options to create the appropriate cc.Node derived asset
 * 
 */
export  class CharacterAssetFactory implements IFactory<CharacterAssetCreationOptions,cc.Node> { 

    create(options:CharacterAssetCreationOptions):cc.Node{

        switch( options.getType()){
            case CharacterAssetTypes.NPC:
                return new MockAsset<CharacterAssetTypes,Object>(CharacterAssetTypes.NPC,{},50,MockAssetColours.PINK,"NPC");
 
            case CharacterAssetTypes.NPC_MOCK:
                return new MockAsset<CharacterAssetTypes,Object>(CharacterAssetTypes.NPC_MOCK,{},50,MockAssetColours.PINK,"NPC MOCK");
 
            case CharacterAssetTypes.PLAYER:
                return new MockAsset<CharacterAssetTypes,Object>(CharacterAssetTypes.NPC,{},50,MockAssetColours.GREEN,"PLAYER");

            case CharacterAssetTypes.PLAYER_MOCK:
                return new MockAsset<CharacterAssetTypes,Object>(CharacterAssetTypes.NPC,{},50,MockAssetColours.GREEN,"PLAYER MOCK");

        }

    }
}

Notes:

  • As this class defines multiple classes we have not defined a default export
  • The difference in import statements when importing modules that define, for example: a single class exported by default, multiple classes, and interfaces

Navigate to tssrc/factory/view/characterassetfactory/MockAsset.ts

Insert the following code:

declare var ccui: any;

/**
 * @description emum providing identifiable colour options for MockAsset
 */
export enum MockAssetColours {
    RED,
    BLUE,
    YELLOW,
    GREEN,
    PINK,
    NONE
};

/**
 * @class MockAsset
 * @description a cc.Node derived class for creating mock assets ,creates a circle with given radius, containing a label with optionalgiven text
 * Templater option T is for the Type used to describe type generally string | int | enumtype
 * 
 */
export class MockAsset<T, U> extends cc.Node {
    _visibleNode: cc.Node = null;
    _objecttype: T = null;
    _circleNode: cc.DrawNode;


    constructor(type: T, config: U, radius: number =20, COLOUR: MockAssetColours = MockAssetColours.BLUE, text: string ="Text") {
        super();
        this.ctor();

        this._objecttype = type;

        this.setContentSize(radius * 2, radius * 2);
        this.setAnchorPoint(0.5, 0.5);
        this._circleNode = new cc.DrawNode();
        this._circleNode.drawCircle(cc.p(radius, radius), radius, 0, 1, true, 8, this.getColour(COLOUR));
        this.addChild(this._circleNode);


        var textF = new ccui.Text();
        textF.boundingWidth = radius * 2;
        textF.boundingHeight = 30;
        textF.attr({
            textAlign: cc.TEXT_ALIGNMENT_CENTER,
            string: text,
            font: "20px Ariel",
            x: radius
        });
        textF.y = radius - textF.height / 8;
        this.addChild(textF);

    }

    getColour(colour: MockAssetColours) {

        switch (colour) {
            case MockAssetColours.RED:
                return new cc.Color(187, 56, 10, 255);
            case MockAssetColours.GREEN:
                return new cc.Color(12, 123, 2, 255);
            case MockAssetColours.BLUE:
                return new cc.Color(27, 68, 174, 255);
            case MockAssetColours.PINK:
                return new cc.Color(211, 62, 109, 255);
            case MockAssetColours.YELLOW:
                return new cc.Color(242, 171, 52, 255);
            case MockAssetColours.NONE:
                return new cc.Color(255, 255, 255, 255);
        }

    }

}

note:

  • Note the use of TypeScript’s Intersection Types to describe the mock asset constructor’s  type and config parameters.
  • If dealing with fixed sets of types, consider using TypeScripts Union Types 

Navigate to tssrc/types/AssetTypes.ts

Insert the following code:

export enum CharacterAssetTypes{
    PLAYER,
    NPC,
    PLAYER_MOCK,
    NPC_MOCK
}

 

Navigate to tssrc/view/scenes/HelloWorldScene.ts

Insert the following code:

import HelloWorldMainLayer  from "./helloworld/HelloWorldMainLayer";
export default class HelloWorldScene extends cc.Scene{
    _mainLayer:HelloWorldMainLayer;
   constructor  () {
        // 1. super init first
        super();
        super.ctor();//always call this for compatibility with cocos2dx JS Javascript class system
   }
    onEnter () {
       super.onEnter();
       console.log("Hello World Scene");
       this._mainLayer = new HelloWorldMainLayer();
       this.addChild( this._mainLayer);
       
    }
}

 

Navigate to tssrc/view/scenes/hellowworldscene/HelloWorldLayer.ts

Insert the following code:

import { CharacterAssetTypes } from "./../../../types/AssetTypes";
import {CharacterAssetFactory, CharacterAssetCreationOptions} from "../../../factory/view/CharacterAssetFactory";
declare var res:any;
export default class HelloWorldMainLayer extends  cc.Layer{
    sprite:cc.Sprite;
    signal: signals.Signal;
    assetFactory:CharacterAssetFactory;
    constructor  () {
        //////////////////////////////
        // 1. super init first
        super();
        super.ctor(); // call the cocos super method in JS  this would be this._super()

        console.log("Hello World Layer");
        this.assetFactory  = new CharacterAssetFactory();
        /////////////////////////////
        // 2. add a menu item with "X" image, which is clicked to quit the program
        //    you may modify it.
        // ask the window size
        var size = cc.winSize;

        /////////////////////////////
        // 3. add your codes below...
        // add a label shows "Hello World"
        // create and initialize a label
        var helloLabel = new cc.LabelTTF("Hello World", "Arial", 38);
        // position the label on the center of the screen
        helloLabel.x = size.width / 2;
        helloLabel.y = size.height / 2 + 200;
        // add the label as a child to this layer
        this.addChild(helloLabel, 5);

        // add "HelloWorld" splash screen"
        this.sprite = new cc.Sprite(res.HelloWorld_png);
        this.sprite.attr({
            x: size.width / 2,
            y: size.height / 2
        });
        this.addChild(this.sprite, 0);

        var co = new CharacterAssetCreationOptions(CharacterAssetTypes.PLAYER);
        var ca = this.assetFactory.create(co);
        ca.setPosition(10,20);
        this.addChild(ca, 0);
        
    

    }
}

 

Note:

  • the instantiation and use of a player asset via our asset factory
  • the use of the declare statement declare res:any;  – this is used to declare the existence of the globally available res variable that is defined within our Cocos2dx-JS skeleton  application but not visible to the TypeScript compiler

 

That’s it for now.

You will find the completed source for this article in the project named CocosTsGame on the GitHub repository.

Ill see  you in the upcoming part 6 where we introduce an IOC and dependency injection driven MVCS pattern for managing our games scenes and game data.

 

References

https://stackoverflow.com/questions/32425580/karma-plugin-dependencies-not-being-found

http://mike-ward.net/2015/09/07/tips-on-setting-up-karma-testing-with-webpack/

https://www.npmjs.com/package/karma-mocha-reporter

https://templecoding.com/blog/2016/02/02/how-to-setup-testing-using-typescript-mocha-chai-sinon-karma-and-webpack/

http://www.tjcafferkey.me/setting-up-karma-and-mocha-with-gulp-and-webpack/

Command Line Quick Reference

#installs karma and karma utilities as project dependencies
npm i -D karma-chrome-launcher karma-firefox-launcher karma-ie-launcher karma-mocha karma-phantomjs-launcher karma-safari-launcher karma-webpack karma karma-cli karma-chai

#exits current command
ctrl + c 

#creates a release deployment of our cocos skeleton
cocos deploy -p web -m release

#creates a debug deployment of our cocos skeleton
cocos deploy -p web -m debug

#transpiles our typescript using webpack executes our tests within the browser, and reports to the console
gulp karma

#same as gulp karma but watches for changes 
gulp karma:watch

Leave a Reply

Your email address will not be published. Required fields are marked *