HTML5ゲームエンジンの徹底評価[日本語訳] (7)  パフォーマンス比較編


- [HTML5ゲームエンジンの徹底評価 \[日本語訳\] (1) 2D・3D対応とプログラミング言語の比較](https://qiita.com/motoyasu-yamada/items/f4e03f98442b3ec164be)
- [HTML5ゲームエンジンの徹底評価 \[日本語訳\] (2) Three.jsとPixi.js](https://qiita.com/motoyasu-yamada/items/0139a933ded6f478f92a)
- [HTML5ゲームエンジンの徹底評価 \[日本語訳\] (3) PhaserとEgretEngine](https://qiita.com/motoyasu-yamada/items/0df8489e6ad3c9c8cefc)
- [HTML5ゲームエンジンの徹底評価 \[日本語訳\] (4) enchant.jsとcrafty.js](https://qiita.com/motoyasu-yamada/items/0402bf1759f823405355)
- [HTML5ゲームエンジンの徹底評価 \[日本語訳\] (5) Turbulenz, cocos2d-js, PlayCanvas, melonJS](https://qiita.com/motoyasu-yamada/items/4265dab44bf82dc27b7b)
- [HTML5ゲームエンジンの徹底評価 \[日本語訳\] (6) 各エンジンのコンセプト・設計思想・機能比較のまとめ ](https://qiita.com/motoyasu-yamada/items/1549613ccd05d40f3182)

# パフォーマンス
ベンチマークのために、2Dゲームエンジン向けのレンダリングのみ負荷テストをしました。
同じ数の同じ内容の描画オブジェクトでテストをしました。公平にテストをするために、同じコンピュータ・同じバージョンのChromeブラウザでテストをしました。ゲーム画面のサイズは 800*600、画像も同じものを使いました。同じ画面で各エンジンに5,000、10,000、20,000個の描画オブジェクトのレンダリングをさせました。

craftyjsエンジンはレンダリングに問題があったために比較データをとれませんでした。QuintusはWebGLレンダリングに対応していないので、今回は比較データはとれませんでした。Phaserの描画コアはPixi.jsを利用しています。PhaserのデータはPixi.jsの結果のものです。各エンジン用に書いたテストコードはほぼ同一のものです。起動時に必要な量の描画オブジェクトを生成します。各フレームにて各オブジェクトを回転させています。

# テストコード
## pixi.js
```js
var renderer = PIXI.autoDetectRenderer(800, 600,{backgroundColor : 0x1099bb});
document.body.appendChild(renderer.view);
var stage = new PIXI.Container();
var texture = PIXI.Texture.fromImage('bunny.jpg');
var tnum = 5000;
console.log("render Object Number:",tnum);
var bunnys = [];
for(var i=0;i<tnum;i++)
{
    var bunny = new PIXI.Sprite(texture);
    bunny.position.x = Math.random()*800;
    bunny.position.y = Math.random()*600;
    stage.addChild(bunny);
    bunnys.push(bunny);
}

animate();

function animate() {
    requestAnimationFrame(animate);

    for(var i=0;i<tnum;i++)
    {
       bunnys[i].rotation += 0.1;
    }
    renderer.render(stage);
}
```

## Egret

```typescript
class Main extends egret.DisplayObjectContainer {

    public constructor() {
        super();
        this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this);
    }

    private tnum:number = 100000;
    private bunnys:egret.Bitmap[] = [];

    private onAddToStage(event:egret.Event) 
    {
        console.log("render Object Number:",this.tnum);
        this.stage.dirtyRegionPolicy = egret.DirtyRegionPolicy.OFF;
        RES.getResByUrl('resource/bunny.jpg',this.onComplete,this,RES.ResourceItem.TYPE_IMAGE);
    }

    private onComplete(event:any)
    {
        var img:egret.Texture = <egret.Texture>event;
        for(var i:number=0;i<this.tnum;i++)
        {
            var bunny = new egret.Bitmap(img);
            bunny.x = Math.random()*800;
            bunny.y = Math.random()*600;
            this.addChild(bunny);
            this.bunnys.push(bunny);
        }
        this.addEventListener(egret.Event.ENTER_FRAME, this.animate,this);
    }

    private animate(evt:egret.Event)
    {
        for(var i:number=0;i<this.tnum;i++)
        {
            this.bunnys[i].rotation += 1;
        }
    }
}
```

## enchant.js

```js
enchant();

window.onload = function () {
   var game = new Game(800, 600); 
   game.fps = 60;
   game.preload('bunny.jpg');
   game.onload = function() {
    var tnum = 100000;
    console.log("render Object Number:",tnum);
    var bunnys = [];
      var scene = new Scene();
      game.pushScene(scene);
      for(var i=0;i<tnum;i++)
        {
            var sprite = new Sprite(50, 50);
            sprite.image = game.assets['bunny.jpg'];
            sprite.x = Math.random()*800;
            sprite.y = Math.random()*600;
            scene.addChild(sprite);
            bunnys.push(sprite);
        }

        game.addEventListener('enterframe', function() {
          for(var i=0;i<tnum;i++)
            {
               bunnys[i].rotation += 1;
            }
        });
   };

   game.start();

};
```

## Turbulenz

```js
TurbulenzEngine = WebGLTurbulenzEngine.create({
    canvas: document.getElementById("canvas")
});

var graphicsDevice = TurbulenzEngine.createGraphicsDevice({});
var draw2D = Draw2D.create({
    graphicsDevice: graphicsDevice
});

var bgColor = [1.0, 1.0, 0.0, 1.0];
var tnum = 50000;

console.log("render Object Number:", tnum);
var bunnys = [];
for (var i = 0; i < tnum; i++) {
    var sprite = Draw2DSprite.create({
        width: 50,
        height: 50,
        x: Math.random() * 800,
        y: Math.random() * 600,
        color: [1.0, 1.0, 1.0, 1.0],
        rotation: Math.PI / 4
    });

    bunnys.push(sprite);
}

var texture = graphicsDevice.createTexture({
    src: "bunny2.jpg",
    mipmaps: true,
    onload: function (texture) {
        if (texture) {
            for (var i = 0; i < tnum; i++) {
                var sprite = bunnys[i];
                sprite.setTexture(texture);
                sprite.setTextureRectangle([0, 0, texture.width, texture.height]);
            }
        }
    }
});

var PI2 = Math.PI * 2;
var rotateAngle = PI2 / 360; // 1 deg per frame

function update() {
    if (graphicsDevice.beginFrame()) {
        graphicsDevice.clear(bgColor, 1.0);
        draw2D.begin();

        for (var i = 0; i < tnum; i++) {
            var sprite = bunnys[i];
            sprite.rotation += rotateAngle;
            sprite.rotation %= PI2; // Wrap rotation at PI * 2
            draw2D.drawSprite(sprite);
        }

        draw2D.end();

        graphicsDevice.endFrame();
    }
}



function render() {
    var tnum = 5000;
    console.log("render Object Number:", tnum);
    for (var i = 0; i < tnum; i++) {
        sprite.position.x = Math.random() * 800;
        sprite.position.y = Math.random() * 600;
    }
}
```

## cocos2d-js
```js
window.onload = function(){
    cc.game.onStart = function(){
        //load resources
        cc.LoaderScene.preload(["bunny.jpg"], function () {
            var tnum = 100000;
              console.log("render Object Number:",tnum);
              var bunnys = [];
            var MyScene = cc.Scene.extend({
                onEnter:function () {
                    this._super();
                    var batchNode = cc.SpriteBatchNode.create("bunny.jpg");
                    this.addChild(batchNode);
                    for(var i=0;i<tnum;i++)
                    {
                      var sprite = cc.Sprite.create("bunny.jpg");
                      sprite.setPosition((Math.random()*800), (Math.random()*600));
                      batchNode.addChild(sprite);
                      bunnys.push(sprite);
                    }
                    this.scheduleUpdate();
                },

                update:function () {
                  for(var i=0;i<tnum;i++)
                    {
                      bunnys[i].setRotation(bunnys[i].getRotation()+1);
                    }
                    this.scheduleUpdate();
                }
            });
            cc.director.runScene(new MyScene());
        }, this);
    };
    cc.game.run("gameCanvas");
};
```

## melonJS

```js
var PlayScreen = me.ScreenObject.extend( {
    onResetEvent: function() {
        me.game.world.addChild(new me.ColorLayer("background", "#5E3F66", 0), 0);
        for (var i = 0; i < 5000; i++) {
            me.game.world.addChild(new Smilie(i), 3);
        }
    }
});

var Smilie = me.Sprite.extend({
    init : function (i) {
        this._super(
            me.Sprite,
            "init",
            [
                (-15).random(800),
                (-15).random(600),
                {
                    image: me.loader.getImage(game.assets[0].name)
                    ,width : 50
                    ,height : 50
                }
            ]
        );
        this.rotation = 0;
        this.alwaysUpdate = true;
    },

    update : function () {
        this.rotation += 3/180*Math.PI;
        this.angle = this.rotation ;
        return true;
    },}  );
```

## Hilo

```js
function init(){
    var stage = new Hilo.Stage({
        renderType:'canvas',
        container: gameContainer,
        width: 800,
        height: 600
    });

    var sum = 5000;
    var bitmaps = [];

   var ticker = new Hilo.Ticker();
   ticker.addTick(stage);
   ticker.start(true);

    for(var i = 0; i < sum; i++) {
            var bmp = new Hilo.Bitmap({
            image: 'images/hero.jpg',
            rect: [0, 0, 50, 50],
            x: Math.random()*800,
            y: Math.random()*600
            }).addTo(stage);
            bitmaps.push(bmp);
    }

    function animate() {
        requestAnimationFrame(animate);
        for(var i = 0; i < sum; i++) {
                    bitmaps[i].rotation += 0.1;
        }
    }
    animate();
}
```

# テストの実行環境
![image.png](https://qiita-image-store.s3.amazonaws.com/0/302078/bb2a27bb-a7a7-a84d-8985-b8d9737abae8.png)

# テスト結果
![image.png](https://qiita-image-store.s3.amazonaws.com/0/302078/8236071c-aa5a-6b13-64ab-b7f052455cff.png)


# 結論
上記のテスト結果から、エンジンを性能順におおまかにランキングしてみました。

|順位|エンジン|
|----|----|
|1位| Pixi.js と Turbulenz|
|2位| Egret|
|3位| Cocos2d-js|
|4位|Hilo|
|5位|enchant.js|
|6位|melonJS|

# 描画テストのイメージ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/302078/da027225-d5d8-b26d-2dba-095e1a51bcc5.png)

# 訳者からの参考情報

TinyUnityのパフォーマンスを知りたい方はこちらの情報をご確認ください
[Tiny Unity (Unity Tiny Mode)のベンチマークを書いてみた](https://qiita.com/motoyasu-yamada/items/0297268db9af40a8044a)

ざっくりなイメージですが、TinyUnityはenchant.jsのようなレベルです。

これからコンパイラを作り替えることで速くなるという話ですが、これだけの差を詰めるのは相当難しいのではないでしょうか。またTypeScriptがC#になるということでオーバーヘッドも発生してよりリリース版ではさらに重くなる可能性もあります。

あとは、Adobe Animate CCのベンチマークにも期待したいところです。

---
つづく

[HTML5ゲームエンジンの徹底評価[日本語訳] (8) ドキュメント編＆実績編＆最終結論](https://qiita.com/motoyasu-yamada/items/8eded0199be291c0b8f5)