JavaScriptでアニメーションエフェクト

今日もJavaScriptのお話です。
マリオのタル避けのゲーム(仮)の更新が滞っていますが、色んなことを平行してやるもので、あまり進んでいません、もうちょいお待ちを。

さて、前回、JavaScriptでゲームを作るなんて大してメリットないなーと言っていながら、今回はゲームに役に立ちそうなエフェクトが表示できるようになったよーってお話です。
では、実際にサンプルを試してみます。


実行ボタンを押した後、画面をクリックすると爆発のエフェクトを表示します。
(クリックしても表示されない場合は一度ブラウザで更新してみてください。)

なんかJavaScriptオンリーでこういうエフェクトって新鮮な気がしますが、ゲーム作っている人にはお馴染みのパラパラ漫画方式で爆発の画像を切り替えて表示しているだけです↓



使い方はこんな感じです。

// 作って
var effect = new Effect( "exp","img/effect.png", 14 );
// 場所を指定する
effect.start(x,y);

HTMLで画像の一部だけを切り出して表示するって方法がわからなかったので、(CSSを使えばいける?)枚数分画像を用意しないといけないのが面倒ですが、使うのは簡単だと思います。

最初はメインループでupdate関数を呼んでもらって、値のカウントでアニメーションを進める方法を取っていたのですが、簡単に使えるようにと後で内部でタイマーを持たす仕組みに変更しました。

確かに便利にはなったんですが、これってゲームで使う目的ならあまり良くないですね。試したことがないのでどれだけ負荷が違うか解りませんが、大量に表示した時あまりに処理落ちするならカウント式に戻さないと…。


JavaScriptかじったことがある人ならこの程度のコード書くのは楽勝だと思うので価値はないと思いますが、一応コードを載せておきます。

/**
 * 画像アニメーションクラス
 *
 * このクラスは画像の指定の仕方が少し特殊になっています。
 * 例えば、以下のように引数を指定した場合、
 *
 * var effect = new Effect("effect1", "image/effect.gif", 10);
 *
 * "image/effect.gif"という画像自体を読み込むのではなく、
 * 指定のパスに連番で画像が保存されていると想定して処理を行います。
 * (この場合はimageフォルダ内にある effect0.gif 〜 effect9.gif までの
 *  画像を使用するように指定した、ということになります。)
 *
 * @param {string}  name     エフェクトの名前(何でもOKですが、idとして使用するため完全に一意な名前を指定してください。)
 * @param {string}  filename 画像までのパス
 * @param {integer} all      アニメーションに使う画像の枚数
 */
var Effect = function(name,filename,all) {
    
    // 拡張子と連番を除いたファイルのパス
    var path = filename.substr(0,filename.lastIndexOf("."));
    // 画像の拡張子
    var extension = filename.substr(filename.lastIndexOf("."));
    
    /** 画像オブジェクト */
    this.images = new Array(all);
    for( var i = 0; i < this.images.length; ++i ){
        // 名前と番号を組み合わせて一意のIDとする
        this.images[i] = new this.EffectImage(name+i,path+i+extension);
    }
    /** 繰り返すかどうか */
    this.repeat = false;

    /** 現在描画している画像番号 */
    this.animIndex = 0;
    
    /** setInterval用のID */
    this.timerID = 0;
}

/**
 * アニメーションをスタートします。
 * @param {integer} x,y    描画位置
 * @param {number}  sec    何秒で1周アニメーションするか(省略すると1秒)
 * @param {boolean} repeat アニメーションを繰り返すか(省略すると繰り返しなし)
 * @return {boolean} 正常にスタートできれば true
 */
Effect.prototype.start = function(x,y,sec,repeat) {
    if( this.timerID ) return false;        
    if(typeof sec == 'undefined') sec = 1;
    this.repeat = (typeof repeat == 'undefined' ? false : repeat);
    var self = this;
    this.timerID = setInterval(function(){self.update()},sec*1000/this.images.length);
    for( var i = 0; i < this.images.length; ++i ){
        this.images[i].setPos(x,y);
    }
    this.images[0].setVisible(true);
    return true;
}

/** 
 * アニメーションを更新します。
 * 自動的に呼び出されます。
 */
Effect.prototype.update = function() {
    this.images[this.animIndex].setVisible(false);
    if( ++this.animIndex > this.images.length - 1 ){
        this.animIndex = 0;
        if( !this.repeat ){
            clearInterval(this.timerID);
            this.timerID = 0;
            return;
        }
    }
    this.images[this.animIndex].setVisible(true);
}

/**
 * アニメーションをリセットします。
 */
Effect.prototype.reset = function() {
    if( this.timerID ){
        clearInterval(this.timerID);
        this.timerID = 0;
    }
    this.animIndex = 0;
    for( var i = 0; i < this.images.length; ++i ){
        this.images[i].setVisible(false);
    }
}

 
/**
 * 画像一枚を管理するクラス
 */
Effect.prototype.EffectImage = function(name,path) {
    this.name = name;    
    document.write('<img id="'+this.name+'" src="'+path+'" STYLE="position:absolute;top:0;left:0;visibility:hidden">');
}
Effect.prototype.EffectImage.prototype.setPos = function(x,y) {
    document.getElementById(this.name).style.left = x;
    document.getElementById(this.name).style.top  = y;
}
Effect.prototype.EffectImage.prototype.setVisible = function(visible) {
    document.getElementById(this.name).style.visibility = (visible ? "visible":"hidden");
}