Skip to content

わずか166行の「アングリードロイド」で学ぶ、3Dゲームプログラミング

やあshi3zだ。
先週土曜日に国立情報学研究所(NII)で開催された「ゲームコミュニティサミット」では、enchant.js開発チームの高橋諒くんによるゲーム開発ワークショップが行われた。

ついでに今週末にも大阪でenchant.js meetupがあります。
enchant.js meetup! 大阪 vol.2
参加無料。まだぜんぜん空いてるのでぜひお誘い合わせの上ご来場ください

さて、NIIで使われていたサンプル「Angry Droid」が非常にわかりやすくコンパクトにまとまっていたので紹介しよう。

このゲーム「Angry Droid」は、ドロイド君を引っ張って飛ばすと、どこかで見たような木組みの建物が壊れて他のドロイド君も倒れる、というもの。

ソースコードは改行や空行込みでもたったの166行。

これくらいなら読んでいくのもそれほど苦痛にならないかもね。

重要な部分から軽く解説しよう

var Droid = Class.create(enchant.gl.physics.PhySprite3D, { //ドロイドクラス
    initialize: function() {
        var rigid = new RigidCylinder(0.4, 1, 2); //物理シミュレーション用に円柱を想定
        PhySprite3D.call(this, rigid);
        this.addChild(game.assets['droid.dae'].clone()); //ドロイド君を読み込む
        this.childNodes[0].y = -1;//座標を補正
    }
});

これはドロイド君クラス。
飛ばすドロイドも、当てられるドロイドも同じクラスだ。
重要なのは、PhySprite3Dクラスを継承しているところ。
RigidCylinderで、物理シミュレーション用のモデルを円柱として、実際に表示されるスプライトはdroid.daeとしている。

同じようにブロックを表現するクラスも作ることが出来る。

var Block = Class.create(enchant.gl.physics.PhyBox, { //ブロッククラス
    initialize: function(x, y, z) {
        var mass = x * y * z * 8; //質量を重めに設定
        PhyBox.call(this, x, y, z, mass);
        this.mesh.texture.src = game.assets['wood.png']; //テクスチャを設定
        this.mesh.texture.ambient = [ 1.0, 1.0, 1.0, 1.0 ];
        this.mesh.texture.diffuse = [ 0.0, 0.0, 0.0, 1.0 ];
        this.mesh.texture.specular = [ 0.0, 0.0, 0.0, 1.0 ];
    }
});

こちらは通常の物理シミュレーション用のPhyBoxをほぼそのまま使っているけど、木目のテクスチャを設定している。

また、physics.enchant.jsでは、質量は通常、x*y*zで自動的に決まるようになっている(大きいものほど重くなる)のがデフォルトだが、通常よりもさらに8倍重くするために8を乗算しているのだ。

さて、次にドロイド君をドラッグで引っ張って飛ばす処理だけど、enchant.jsらしくイベントリスナを使って以下のように表現されている。

        droid.addEventListener('touchstart', function(e) { //ドロイド君をドラッグ開始
            x = e.x;
            y = e.y;
            disp.x = e.x - 4;
            disp.y = e.y - 4;
            game.rootScene.addChild(disp);  //目安になるインジケータを表示
        });
        droid.addEventListener('touchmove', function(e) { //ドラッグ中
            disp.x = e.x - 4;
            disp.y = e.y - 4;
        });
        droid.addEventListener('touchend', function(e) { //ドラッグ終了=発射
            game.rootScene.removeChild(disp); //インジケータを消す
            game.rootScene.addChild(label);
            log(droid, 3);
            var dx = -(e.x - x); //元の位置からどのくらいドラッグしたか
            var dy = e.y - y;   
            if (state == 0) {
                this.applyImpulse(dx, dy, 0, 0, 0.075, 0); //ドロイド君に初速を与える
                state ++;
            }
        });

ドラッグ開始(touchstart)から、ドラッグ中(touchmove)までインジケータを表示して、どのくらい引っ張ってるのかわかるようにしている。

その後、ついにドラッグが終わったとき(touchend)、インジケータを消し、もとの場所からどのくらいの距離ドラッグしたかを(dx,dy)として求め、最終的にapplyImpulseでドロイド君に初速を与えて飛ばしている。

たったこれだけのことで、あとは勝手にドロイド君がぶつかるのを見てれば良い。

肝心のスコアだけど、当たり判定で判定しているのではなく、この場合は敵ドロイドをどのくらいの距離吹き飛ばしたかで計算している。この辺りの手の抜き方はさすが9minコーディングバトルで鍛えた高橋君らしいアイデアだ。

      var getScore = function() { //元の座標(10,6,0)からどのくらいの距離離れたかでスコア化
            return parseInt(length3d(10, 6, 0, enemy.x, enemy.y, enemy.z) * 10);
        };

スコアの計算方法によってゲームの面白さ、もう一回遊んでみようと思わせる要素というのは大きく変わる。

たった9分でゲームを作るという、9min コーディングバトルでは常に3D物理シミュレーションゲームを出してくる高橋君。

彼によれば「物理シミュレーションのほうが手早くゲームを作るのは簡単」なのだという。

というのも、たいていのゲームは、本物っぽい動きを見せるためにいろいろな計算をするところでコード量がかかってきてしまうのだけど、物理シミュレーションはそうした複雑な動きや当たり判定といったものを全て自動的に処理してくれるから、なのだそうだ。

こんな感じで手軽に3Dゲームプログラミングが楽しめるenchant.js、以下のソースコード全文を掲載したのでぜひ参考にしてみて欲しい。

このゲームのカメラワークを書き換えるだけでも、ずいぶん違ったものに見えるハズだぞ!

enchant();
var game;

var length3d = function(sx, sy, sz, ex, ey, ez) { //3Dの距離を測る
    return Math.sqrt(Math.abs(Math.pow(ex - sx, 2) + Math.pow(ey - sy, 2) + Math.pow(ez - sz, 2)));
};

var log = function(sprite, dist) {
    if (!dist) dist = 10;
    sprite.addEventListener('enterframe', function(e) {
        var sp;
        if (sprite.age % dist == 0) {
            sp = new Sphere(0.1);
            sp.x = sprite.x;
            sp.y = sprite.y;
            sp.z = sprite.z;
            sprite.scene.addChild(sp);
        }
    });
};

var Droid = Class.create(enchant.gl.physics.PhySprite3D, { //ドロイドクラス
    initialize: function() {
        var rigid = new RigidCylinder(0.4, 1, 2);
        PhySprite3D.call(this, rigid);
        this.addChild(game.assets['droid.dae'].clone());
        this.childNodes[0].y = -1;

    }
});

var Block = Class.create(enchant.gl.physics.PhyBox, { //ブロッククラス
    initialize: function(x, y, z) {
        var mass = x * y * z * 8;
        PhyBox.call(this, x, y, z, mass);
        this.mesh.texture.src = game.assets['wood.png'];
        this.mesh.texture.ambient = [ 1.0, 1.0, 1.0, 1.0 ];
        this.mesh.texture.diffuse = [ 0.0, 0.0, 0.0, 1.0 ];
        this.mesh.texture.specular = [ 0.0, 0.0, 0.0, 1.0 ];
    }
});

window.onload = function() {
    game = new Game(640, 480);
    game.keybind(32, 'a');
    game.preload('sky.png', 'wood.png', 'droid.dae', 'ground.png');
    game.onload = function() {
        var scene = new PhyScene3D();
        var camera = scene.getCamera();
        camera.y = 5;
        camera.z = 20;
        camera.projMat = mat4.perspective(90, 1, 1, 1000);

        var sky = new Sphere(900); //空を作る
        sky.mesh.reverse();
        sky.mesh.texture.src = game.assets['sky.png'];
        sky.mesh.texture.ambient = [ 1.0, 1.0, 1.0, 1.0 ];
        sky.mesh.texture.diffuse = [ 0.0, 0.0, 0.0, 1.0 ];
        sky.mesh.texture.specular = [ 0.0, 0.0, 0.0, 1.0 ];
        scene.addChild(sky);

        var ground = new PhyPlane(0, 1, 0, 0, 900); //地面を作る
        ground.mesh.texture.src = game.assets['ground.png'];
        ground.mesh.texture.ambient = [ 1.0, 1.0, 1.0, 1.0 ];
        ground.mesh.texture.diffuse = [ 0.0, 0.0, 0.0, 1.0 ];
        ground.mesh.texture.specular = [ 0.0, 0.0, 0.0, 1.0 ];
        scene.addChild(ground);

        var lblock = new Block(0.5, 2.0, 0.5); //ブロックを作る
        lblock.x = 8;
        lblock.y = 2;
        scene.addChild(lblock);

        var rblock = new Block(0.5, 2.0, 0.5);
        rblock.x = 12;
        rblock.y = 2;
        scene.addChild(rblock);

        var ublock = new Block(2.5, 0.5, 0.5);
        ublock.x = 10;
        ublock.y = 4.5;
        scene.addChild(ublock);

        var enemy = new Droid(); //敵ドロイドを作る
        enemy.x = 10;
        enemy.y = 6;
        enemy.rotateYaw(Math.PI/2);
        scene.addChild(enemy);

        var droid = new Droid(); //自分ドロイドを作る
        droid.x = -10;
        droid.y = 1;
        droid.rotateYaw(-Math.PI/2);
        scene.addChild(droid);
        var state = 0;
        var time = 0;

        var x = 0, y = 0;
        var disp = new Sprite(8, 8);
        disp.backgroundColor = '#aa99ff';

        var label = new Label(0);
        label.font = '24pt monospace';
        label.color = 'white';
        label._style.textAlign = 'center';
        label.x = game.width / 2 - label.width / 2;
        label.y = 48;

        var getScore = function() {
            return parseInt(length3d(10, 6, 0, enemy.x, enemy.y, enemy.z) * 10);
        };

        droid.addEventListener('touchstart', function(e) {
            x = e.x;
            y = e.y;
            disp.x = e.x - 4;
            disp.y = e.y - 4;
            game.rootScene.addChild(disp);
        });
        droid.addEventListener('touchmove', function(e) {
            disp.x = e.x - 4;
            disp.y = e.y - 4;
        });
        droid.addEventListener('touchend', function(e) {
            game.rootScene.removeChild(disp);
            game.rootScene.addChild(label);
            log(droid, 3);
            var dx = -(e.x - x);
            var dy = e.y - y;
            if (state == 0) {
                this.applyImpulse(dx, dy, 0, 0, 0.075, 0);
                state ++;
            }
        });

        var score;
        game.rootScene.addEventListener('enterframe', function() {
            score = getScore();
            label.text = score;
            if (score > 100) {
                label.font = '36pt monospace';
                label.color = 'red';
            }
            if (state > 0) {
                time++;
                camera.x = camera.centerX = Math.min(droid.x, 10);
            }
            if (time > 10 * game.fps) {
                game.stop();
                game.end(score, 'score is ' + score);
            }
        });

        game.rootScene.addEventListener('abuttondown', function() {
            if (scene.isPlaying) {
                scene.stop();
            } else {
                scene.play();
            }

        });
        scene.play();
    };
    game.start();
};

このエントリーをはてなブックマークに追加
はてなブックマーク - わずか166行の「アングリードロイド」で学ぶ、3Dゲームプログラミング
Post to Google Buzz
Share on GREE

Related posts:

  1. たった99行でシューティングゲームを作る方法
  2. Ruby on enchant.js : Groupクラスの追加
  3. 3D野郎は寄ってたかれ!WebGLでグリグリ遊べるgl.enchant.jsがついにβ公開!
  4. こんなにカンタンでいいの!? enchant.jsがついに3D物理シミュレーションに対応!
  5. WebGL+enchant.jsでモグラ叩き!ゲームをつくる!(221行)

Facebook comments:

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*