Skip to content

続・Ruby×enchant.jsへの挑戦 / イベントドリブンモデルの実現

さて、昨日に引き続き、Rubyでenchant.jsを使えないかやってみた。
今回はHotRubyを解析しながらHotRuby.jsに手を加える形で進めてみた。

HotRuby.jsの中身を見ると、かなり荒っぽい実装というか、組み込みクラスがざっくりと実装されているだけだということに気づく。

このあたりの実装は、HotRuby.jsの1110行目付近

HotRuby.prototype.classes = {
    "Object" : {
        "==" : function(

のあたりから行われている。
enchant.jsに対応させるにはここらへんをいじるのが簡単そうだ。

そこで、とりあえずgame.preloadはともかくとして、Spriteクラスを実装してみることにした。

    "Game" : {
        "assets" : function(recver, args) {
            return args[0];
        }
    },
   
    "Sprite" : {
        "initialize" : function(recver, args) {
            var sprite = new Sprite(args[0],args[1]);
            game.rootScene.addChild(sprite);
            sprite.image=game.assets['andy.gif'];
            recver.__instanceVars.sprite = sprite;
            var obj = HotRuby.prototype.classes["Sprite"];

            //各パラメータへの演算子を手動で実装
            //かなりあらっぽいが、とりあえず動く
            'x y scaleX scaleY rotation frame'.split(' ').forEach(function(prop) {
              obj[prop] = function(recver,args){
                    return recver.__instanceVars.sprite[prop];};
              obj[prop+"="] = function(recver,args){
                    recver.__instanceVars[prop]=args[0];
                    return recver.__instanceVars.sprite[prop]=args[0];};
              obj["@"+prop+"="] = function(recver,args){
console.log("hoge");
                    recver.__instanceVars[prop]=args[0];
                    return recver.__instanceVars.sprite[prop]=args[0];};
              obj[prop+"+="] = function(recver,args){
                    return recver.__instanceVars.sprite[prop]+=args[0];};
              obj[prop+"-="] = function(recver,args){
                    return recver.__instanceVars.sprite[prop]-=args[0];};
              obj[prop+"*="] = function(recver,args){
                    return recver.__instanceVars.sprite[prop]*=args[0];};
              obj[prop+"/="] = function(recver,args){
                    return recver.__instanceVars.sprite[prop]/=args[0];};
              obj[prop+"++"] = function(recver,args){
                    return recver.__instanceVars.sprite[prop]++;};
              obj[prop+"--"] = function(recver,args){
                    return recver.__instanceVars.sprite[prop]--;};
              obj[prop+"<"] = function(recver,args){
                    return recver.__instanceVars.sprite[prop] < args[0] ? this.trueObj :  this.falseObj;};
              obj[prop+"<="] = function(recver,args){
                    return recver.__instanceVars.sprite[prop] <= args[0] ? this.trueObj :  this.falseObj;};
              obj[prop+">"] = function(recver,args){
                    return recver.__instanceVars.sprite[prop] > args[0] ? this.trueObj :  this.falseObj;};
              obj[prop+">="] = function(recver,args){
                    return recver.__instanceVars.sprite[prop] >= args[0] ? this.trueObj :  this.falseObj;};
              obj[prop+"=="] = function(recver,args){
                    return recver.__instanceVars.sprite[prop] == args[0] ? this.trueObj :  this.falseObj;};
            });
        },

        //imageプロパティへのアクセサは特別扱いして実装
        "image" : function(recver,args) {
            return  HotRuby.prototype.classes.
                    newNativeObject("String",
                            recver.__instanceVars.sprite.imageName);
        },
        "image=" : function(recver,args) {
            recver.__instanceVars.sprite.imageName=args[0].__native;
            return recver.__instanceVars.sprite.image=
                        game.assets[args[0].__native];
        },

        //今回のキモ、イベントリスナはブロック付きメソッド呼び出しで実装
        "addEventListener" : function(recver,args) {

            //ブロック部分をJavaScriptの関数として取り出す
            var func = function() {
                var hr = arguments.callee.hr;
                var proc = arguments.callee.proc;
                hr.runOpcode(
                    proc.__opcode,
                    proc.__parentStackFrame.classObj,
                    proc.__parentStackFrame.methodName,
                    recver,  // オブジェクトはrecver
                    hr.nativeAryToRubyObjectAry(arguments),
                    proc.__parentStackFrame,
                    true);
            };
            func.hr = this;
            func.proc = args[1]; //ブロック部分は第二引数として受け取る

            // enchant.jsのイベントリスナにつなぎ込み
            recver.__instanceVars.sprite.addEventListener(
                args[0].__native,
                func);

        },


        //クラス定義スタイルの時に使用する。後述
        "setupEventListener" : function(recver,args) {
            var classObj = ruby.__proto__.classes[recver.__className];
            var hr = this;
            'enterframe touchstart touchend touchmove'.split(
            ' ').forEach(function(eventName){
                if(classObj[eventName]){
                    var func = function() {
                        var hr = arguments.callee.hr;
                        var proc = arguments.callee.proc;
                        hr.runOpcode(
                            proc.__opcode,
                            proc.__parentStackFrame.classObj,
                            proc.__parentStackFrame.methodName,
                            recver,
                            hr.nativeAryToRubyObjectAry(arguments),
                            proc.__parentStackFrame,
                            true);
                    };
                    func.hr = hr;
                    func.proc={};
                    func.proc.__parentStackFrame=ruby.topSF;
                    func.proc.__opcode= classObj[eventName];
                    recver.__instanceVars.sprite.addEventListener(
                        eventName,func);
                }
            });

    },

今回のキモはコメントにもあるように、addEventListenerの実装だ。

enchant.jsがなぜ簡潔にゲームを記述できるかというと、イベントドリブンモデルを採用しているからだ。

それをRuby側でちゃんと記述できるようになれば、かなりの前進と言える。

前回はJavaScript側からかなり強引に呼び出していたが、今回はRuby側で素直に実装できるようにRubyの最大の特徴でもあるブロック付きメソッド呼び出しを使用している。

今回のHotRuby.js改造によってRuby側のコードはずいぶんenchant.jsっぽくなった。

bear = Sprite.new(32,32)
bear.image = Game.assets "chara1.gif"
bear.addEventListener('enterFrame'){|e|
    self.x+=1
}

これだけでクマを表示させて、右へ移動させるアニメーションを表現できる。
いかにもJavaScriptうまれのenchant.js流だ。

Rubyでは関数が第一級オブジェクトではないため、JavaScriptのように無名関数を渡すというわけにもいかないし、だいいち不自然だ。

また、本来はきっちりとしたクラス定義がある言語であるRubyとしては、クラスメソッドをオーバーライドする形でenterFrameなどのようなイベント処理を記述するのが筋のようにも思える。

例えば以下のような感じだ。

class Bear < Sprite
    def initialize w,h,image
        super(w,h)
        self.image = Game.assets(image)
        self.y=50
        self.setupEventListener(); //イベントリスナをセットアップする
    end
    def enterframe
        self.x+=1
    end
end

bear = Bear.new(32,32,"chara1.gif")

これはより解り易い書き方と言えるかもしれない。
まあ書き方を選ばないのがRubyのいいところでもあるが。

C++を前提としたMFC(Microsoft Fundation Class Library)や、Javaを前提としたAWT(Abstract Window Toolkit)では、イベント処理はオーバーライドで実装するスタイルが一般的だ。

ただ、これらの言語向けフレームワークでは、OSからやってくるイベントの受け取りの仕組みがそもそも直接的なメソッド呼び出しで来るわけではないので、ちょっと美しさはなくなってしまうが、initializeの中でsetupEventListenerを呼ぶことでJavaScript側のイベントを設定している。

僕がHotRubyに精通しているわけではないのでひょっとしたらもっと冴えたやり方があるかもしれない(superの中で処理するなど)。

ただ、これでJavaScriptではなく、敢えてRubyで書く意味が出て来た気がする。
ゲームに登場するオブジェクトを全てクラスで表現するとき、イベントリスナがクラスにオーバーライドの形で書かれているのは非常に美しいし、簡潔になる。

これでだいぶゴールが近づいて来た、という印象だ。
あとはいまはJavaScriptの処理で誤摩化しているSceneや、まだ対応していないGroupやLabelなどをRubyから使えるようにすれば、Rubyとenchant.jsでHTML5対応ゲームを作ることはそれほど難しくないかもしれない。

まだHotRubyのソースコードを読み始めたばっかりなので、もっとエレガントな実装方法があればぜひ教えていただきたい。

このエントリーをはてなブックマークに追加
はてなブックマーク - 続・Ruby×enchant.jsへの挑戦 / イベントドリブンモデルの実現
Post to Google Buzz
Share on GREE

Related posts:

  1. Rubyでenchant.jsを使える可能性(実験編)
  2. かんたんプログラミング #5 関数をもっと呼び出す
  3. enchant.js + node.jsで作る非同期型ネットゲームのプロトタイプ
  4. enchant.jsでTwitter連携ゲームを作ろう! (その1 プラグイン解説編)
  5. enchant.jsのサウンド機能で、ゲームをさらにカッコよく!

Facebook comments:

Post a Comment

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