Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

パフォーマンス

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

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

テストコード

pixi.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

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

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

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

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

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

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

テスト結果

image.png

結論

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

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

描画テストのイメージ

image.png

訳者からの参考情報

TinyUnityのパフォーマンスを知りたい方はこちらの情報をご確認ください
Tiny Unity (Unity Tiny Mode)のベンチマークを書いてみた

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

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

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


つづく

HTML5ゲームエンジンの徹底評価日本語訳 ドキュメント編&実績編&最終結論

motoyasu-yamada
1999年にスパイシーソフト起業、iアプリ等のマーケット「アプリゲット」や、カジゲー「チャリ走・糸通し」等のゲーム配信を手がける。約20年近くたち、新しい事業に取り組むために、2018年より新会社「Liberapp」を立ち上げ。HTML5アプリのプラットフォームを手掛けるスタートアップです。
https://twitter.com/MotoyasuYamada
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away