HTML5
cocos2d-JS
EgretEngine
TinyUnity

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


パフォーマンス

ベンチマークのために、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ゲームエンジンの徹底評価日本語訳 ドキュメント編&実績編&最終結論