JavaScriptでゲーム作ろう

かつて人気のあったゲームエンジン「enchant.js」を使ってゲームを作っていたころに書いた過去の記事を全部まとめました。

過去の記事をそのまままとめているので下手くそなプログラムもあるとは思いますが参考にはなると思います。

 

筆者は2019年ごろまでenchant.jsを使ってゲームを作っていました。

enchant.jsでも頑張ればかなり本格的なゲームも作れます。

上のゲームはenchant.jsで作った60FPSの本格シューティングゲームです。4ステージまであるのでぜひ一度遊んでください。

 

この記事の一番最後に紹介しているテンプレートは私が実際にゲーム開発で使っていたものをそのまま公開しています。

すぐにゲームが作れる状態になっているのでぜひ使ってみてください。

 

公式サイトが消滅してしまいダウンロード場所が分からない方はこちらからダウンロードできます。

https://github.com/ghelia/enchant.js-builds

「build」フォルダにenchant.jsのファイルがあります。

 

ファイルを一括で読み込む

enchant.jsのファイルの読み込みをするpreload()という関数について、ちょっとでも楽になる方法の紹介です。

 

データの読み込みを本やサイトで調べるとだいたい

core.preload('aaa.png', 'bbb.png');

//使うとき
var sprite = new Sprite(64, 64); 
sprite.image = core.assets['aaa.png'];

のように書かれているけど、こうすると後で違うファイル名の画像にしたりするときに結構めんどくさい(-_-;)

 

で、このpreload()関数は配列で渡すことができるようなので連想配列でわかりやすい名前を付けて作っておく。

var ASSETS = {
    'start':'start.png',
    'title':'title.png',
};

読み込みは配列で一括。すごい楽ちん。

core.preload(ASSETS);

 

画像を使う場所で以下のように指定する。

title.image = core.assets['title'];
start.image = core.assets['start'];

と、まぁこんな感じでファイル名ではなくキーで指定するからわかりやすいし、ファイル名が変わっても修正は上の配列部分を変えるだけで済むので楽ちん。

 

クラスの作り方

enchant.jsには独自のクラスのようなものを作れる機能があります。

このクラスを上手く使えば処理を分割してわかりやすく書くことができます。さらに継承もできるのでプログラムを簡潔に書くことも出来るようになります。

 

書き方としては以下のようになります。

var Abc = enchant.Class.create({
    //処理
});

var abc = new Abc();

まずクラスを記述して、newで生成するって感じです。これでクラスとして扱うことができます。

後はこの中にメンバやメソッドなどを書いていきます。

 

メソッドは以下のように書きます

var ABC = enchant.Class.create({
    aaa: function(){//メソッドaaa
        //処理
    },//カンマが必要
    bbb: function(a, b){//メソッドbbb
        //処理
    }
});

各メソッドの間にはカンマが入ります。

 

使い方が決まってるメソッド

メソッドの中には使われ方が決まっているものもあります。

・initialize・・クラスをnewで生成したときに自動で行われる初期化処理です。初期値を設定しておきたいときなどに使います。もちろん後で呼び出して使うこともできます

・onenterframe・・フレームごとに行う処理を書きます。フレーム毎に勝手に処理してくれます。便利ですが個人的にはあまり使っていません。私はフレーム毎の処理はすべてメインループに書いています。その方が自分の考えた順番で処理させることができるし後で読み返したときに流れが分かるためです。ただし、基本的に流れに関係ないようなものはここに書くこともあります。(たぶんこれはSpriteなどにだけあるようです。なのでSpriteを継承しないとないみたい)

 

継承っていう機能

クラスには継承っていう別のクラスの機能を引き継いで新しいクラスを作る機能があります。たぶん最初はSpriteを継承してプレイヤークラスを作ったりすると思います。その際の書き方は以下のようになります

var Player = enchant.Class.create(enchant.Sprite, {
    initialize: function(w, h){
        enchant.Sprite.call(this, w, h);
        //処理
    }
});

 

これでSpriteクラスを持った新しいクラスを作ることができます。もちろん自作のクラスも継承できます(enchant.Spriteの部分を自作クラスに変える)。

さらに、継承して作ったクラスを継承してまた新しいクラスをつくることもできます。

継承すると継承元のプロパティやメソッドがそのまま引き継がれます。つまりよく似た内容のクラスが複数必要な場合、一つのクラスに共通の処理を作っておいてそれを継承してクラスを作成し、あとはそれぞれの処理を付け足すだけで済みます。

 

Groupクラスはとっても便利

enchant.jsでゲームを作っていて気付いたのがGroupクラスの便利さ。

実は最初は使い方が分からずあんまり使っていなかったんですが、何度か使っているうちに「そう使うものだったのか!」という発見がありました。

 

Groupは親になる

まずGroupとは何かって話ですが、簡単に言うと要素の親になります。スプライトを画面に表示する際にaddChild()を使っていますが、綴りの通り子をくっつけているわけです。つまりルートシーンにaddChildするとルートシーンが親になります。

で、Groupを使うときはGroupにaddChildします。そうすることでGroupが親になります。

 

ちょっと何言ってるかわからない、ってなってるかもしれませんがenchant.jsではaddChildされた要素は親要素に対して座標を持ちます。つまりシーンにaddChildされるとシーンに、GroupにaddChildされるとGroupに対しての座標になります。

 

Groupの座標を変えるだけで済む

enchant.jsではスプライトなどが持つ座標は画面に対しての絶対座標ではなく、相対座標になっています。つまり、

 

親が動けば子も動く

ということです。例えばこんな感じのメッセージボックスを表示させたい場合

グループクラスの例

Groupを使ってこのメッセージボックスを作ると後で表示位置を変えたくなった時にGroupの座標を変えるだけで済みます。Groupの座標を変えるだけで子の要素も移動します。なぜなら子の要素は親の座標を基準にした座標になるからです。

 

さらに便利なことはGroupを拡大・縮小すると子要素も同じくそうなります。

 

音楽再生のやり方

enchant.jsでは音を鳴らす基本的な方法はこんな感じになってます。

var sound = core.assets['bomb.mp3'].clone();
sound.play();

これで音が鳴ります。clone()は複製する処理で、なくても音は鳴らせますが同じ音を重ねて鳴らすことができません。

 

音を鳴らしたい場所に上の2行を書けばいくらでも音を鳴らせるんですが、同じ音を鳴らすならデータの引き当ては一回で済ませたいところです。

 

ということで、以下のようなクラスを作ってみました。

var SoundEffect = enchant.Class.create({
    //ファイルセット関数(引数:ファイル、オブジェクト作成数(※必ず1以上))
    set: function(data, max){
        this.sound = [];//サウンドオブジェクト保存用
        this.count = 0;//カウント用初期化
        this.max = max;//同時再生最大数
        for(var i = 0; i < this.max; i++){
            this.sound[i] = data.clone();    
        }
    },
    //再生関数(クローンを順繰りに再生) 
    play: function(){
        this.sound[this.count++].play();
        if(this.count >= this.max){
            this.count = 0;
        }
    }
});

 

set()で引数に音楽データとともに同時再生数の限度数も渡します。これで必要な数のクローンを先に作ってしまいます。

 

再生時は順繰りにデータを再生します。しかし、同時再生数が最大数になるとそれ以上再生できなくなり、鳴っている音が消えて再生可能な空きが出るまで次の再生が始まりません。

 

クローンを作る数は敵キャラの爆発など同時再生が多発するものは多く、あまり同時に発生しない効果音は少な目に作ると良いと思います。

 

ただし、一度再生するだけなら一番最初に説明した2行で十分です。必要に応じて使ってください(‘ω’)ノ

 

ループ再生のやり方

BGMのループ再生を可能にしてくれる方法がこちら。

var Bgm = enchant.Class.create({
    initialize: function(){
        this.data = null;
        this.isPlay = false;//プレイの状態フラグ
        this.isPuase = false;
    },
    //BGM用音楽ファイルのセット
    set: function(data){
        this.data = data;
    },
    //再生(再生のみに使う)
    play: function(){
        this.data.play();
        this.isPlay = true;
        if(this.data.src != undefined){//srcプロパティを持っている場合
            this.data.src.loop = true;
        }
    },
    //ループ再生(必ずループ内に記述すること) PCでのループ再生で使う
    loop: function(){
        if(this.isPlay == true && this.data.src == undefined){//再生中でsrcプロパティを持っていない場合
            this.data.play();
            this.isPuase = false;//ポーズ画面から戻った場合は自動的に再生を再開させるため
        }else if(this.isPuase){//srcあり場合でポーズ画面から戻ったとき用
            this.data.play();
            this.data.src.loop = true;//ポーズするとfalseになるっぽい(確認はしていない)
            this.isPuase = false;
        }
    },
    //再生停止(曲を入れ替える前は,必ずstop()させる)
    stop: function(){
        if(this.data != null){
            if(this.isPuase){
                this.isPlay = false;
                this.isPuase = false;
                this.data.currentTime = 0;
            }else if(this.isPlay){
                this.data.stop();
                this.isPlay = false;
            }
        }
    },
    //一時停止(ポーズ画面などの一時的な画面の切り替え時に音を止めたいときのみ使う)
    pause: function(){
        if(this.data != null){
            this.data.pause();
            this.isPuase = true;
        }
    }
});

srcというプロパティがある場合と無い場合で処理を変える必要があります。

srcプロパティを持っているブラウザでは

this.data.src.loop = true;

この一行を再生時に書いておくだけで自動でループ再生してくれます。問題はsrcプロパティが無い場合です。

 

srcプロパティが無い場合ではメインループの中にplay()メソッドを書いておく必要があります。ただし、直接メソッドを書いてしまうとトラブルを引き起こします(演奏停止したいけどplay()メソッドのせいで停止できない、とか)。

 

なので演奏状態を管理するフラグを用意して再生中のみにループ再生してくれるようにしました。

loop: function(){
    if(this.isPlay == true && this.data.src == undefined){
        this.data.play();
    }
}

名前もloopにして何をしてる処理か一目瞭然ですね。

 

あと、stop()メソッドなんですが、iPhoneでは演奏していない状態でstop()メソッドを実行するとエラーが出て止まってしまいます。それを避けるためにちょっと面倒臭いことになっています。

 

シーンの切り替え方

ゲームではメインのゲームを遊ぶ部分以外にもタイトル画面やポーズ画面など、いくつかの違う画面を表示する必要があります。

 

enchant.jsではそれらの画面のことをシーンといい、それぞれのシーンを必要に応じて切り替えて表示することができます。

 

シーンの切り替えには2種類あってreplaceSceneとpushScene &  popSceneがあります。それぞれ役割が違うので今回はそれについて説明します。

 

replaceSceneについて

replaceSceneはrootSceneの内容を指定されたシーンに書き換えます。なのでタイトル画面→メインゲーム→エンディングなど完全に違うシーンに移るときなどに使用します。

 

シーンは独立したクラスとして作成できます。Sceneクラスを継承して各シーンのクラスを作成します。

var MainGameScene = enchant.Class.create(enchant.Scene, {
    initialize: function(){
        enchant.Scene.call(this);
        core.replaceScene(this);//シーンを入れ替える

        this.addEventListener('enterframe', function(){
            //このシーンのループ処理はここに書く
        });
    }
});

上記のような感じにクラスを作るとクラスが生成された時点でroorSceneがこのシーンの内容に切り替わり表示されるようになります。

※便宜上rootSceneと書いてますが実際にはGameオブジェクトのrootSceneとは別になっているようです。

 

pushScene & popSceneについて

pushSceneとpopSceneは画面を一時的に切り替えたいときに使えます。

 

pushSceneはrootSceneの上に新しいシーンをのせるような感じになります。背景を指定しなければ下にrootSceneの内容がそのまま表示されます。ただし、rootSceneから切り替わっているのでrootSceneの動きは止まっています。

 

popSceneをするとrootSceneの上にあったシーンが消えてrootSceneに戻ります。

 

つまり一時的にしか画面の切り替えが必要のない、もしくはrootSceneの上に出したいシーン(ポーズ画面、会話イベントシーン)などに使うのに向いています。

var PuaseScene = enchant.Class.create(enchant.Scene, {
    initialize: function(){
        enchant.Scene.call(this);
        core.pushScene(this);//プッシュで上に出す
   
    this.addEventListener('enterframe', function(){

            //このシーンのループ処理

            if(//画面を戻す条件){
                removeScene(this);//このシーンの要素を削除
                core.popScene();
            }
        });
    }
});

 

実際の切り替えの仕方

で、実際にどう切り替えるかというと、私はこんな感じでSystemというゲーム管理用のクラスを作成し、そのクラスのメソッドにクラス切替の処理をさせてます。こんな感じで(‘ω’)ノ

var System = enchant.Class.create({
    
    //シーン切り替え
    changeScene: function(sceneNumber){
        switch(sceneNumber){
            case SCENE_TITLE:
                var title = new TitleScene();
                break;
            case SCENE_MAINGAME:
                var main = new MainGameScene();//メインゲームへ
                break;
            case SCENE_GAMEOVER:
                var gameover = new GameoverScene();//ゲームオーバーへ
                break;
        }
    },    
});

各シーンから切り替わるときにこのメソッドを使って切り替えています。

 

シーンの切り替え時には要素の削除を忘れずに

画面の切り替えをして前のシーンが画面上消えてもシーンで使った要素が残っています。これはたぶんHTML5ゲームの特徴で、プログラムの変数とは関係なくHTMLの要素として記録されているので、別にHTMLの要素を消す必要があるようです。

function removeScene(scene){
    while(scene.firstChild){
        scene.removeChild(scene.firstChild);
    }
}

この関数を使って忘れずに削除しておきましょう。

 

シーンの切り替え頻繁に行うと起こる問題

どうやらシーンの切り替えをかなりたくさん行うと(50回~100回くらい)iPhoneでフリーズを起こすようです。

私はiPhoneユーザーではないため気づかなかったのですが友達のiPhoneで試したところたぶんすべての機種で起こります(iPhone10まで試した)。ただし、新しい機種ほどフリーズするまでの間隔は長くなります。

なので頻繁にシーンを切り替えるゲームは避けて最低限必要な部分だけ(タイトルとかポーズ画面とか)に抑えて使った方が安全だと思います。

 

ロード画面を改造する

enchant.jsのゲームを起動すると一番最初に表示される画面。このバーが満たされていくことでロード状況を教えてくれるわけですが、何もない画面にこれだとなんか寂しいので改造してみましょう。

 

enchant.js内のloadingSceneの中身がこちら。

enchant.LoadingScene = enchant.Class.create(enchant.Scene, {
    initialize: function() {
        enchant.Scene.call(this);
        this.backgroundColor = '#000';
        var barWidth = this.width * 0.4 | 0;
        var barHeight = this.width * 0.05 | 0;
        var border = barWidth * 0.03 | 0;
        var bar = new enchant.Sprite(barWidth, barHeight);
        bar.disableCollection();
        bar.x = (this.width - barWidth) / 2;
        bar.y = (this.height - barHeight) / 2;
        var image = new enchant.Surface(barWidth, barHeight);
        image.context.fillStyle = '#fff';
        image.context.fillRect(0, 0, barWidth, barHeight);
        image.context.fillStyle = '#000';
        image.context.fillRect(border, border, barWidth - border * 2, barHeight - border * 2);
        bar.image = image;
        var progress = 0, _progress = 0;
        this.addEventListener('progress', function(e) {
            // avoid #167 https://github.com/wise9/enchant.js/issues/177
            progress = e.loaded / e.total * 1.0;
        });
        bar.addEventListener('enterframe', function() {
            _progress *= 0.9;
            _progress += progress * 0.1;
            image.context.fillStyle = '#fff';
            image.context.fillRect(border, 0, (barWidth - border * 2) * _progress, barHeight);
        });
        this.addChild(bar);
        this.addEventListener('load', function(e) {
            var core = enchant.Core.instance;
            core.removeScene(core.loadingScene);
            core.dispatchEvent(e);
        });
    }
});

なんとこの部分はjavascriptそのものではなく我々が慣れ親しんだenchant.jsのスタイルで書かれています!!これなら改造し放題!!

と、行きたいところですが、ここでは画像や音楽は使えません。だってロード前だから。読み込み状況を知らせてくれる画面だから。。

 

ただどうしても画像を出したいなら必要な画像だけ先に読み込んでしまえば使うことは可能です。

 

ちなみに私はこんな感じに作りました。

ラベルを追加して読み込み状況のバーの太さを変えました。barwidth、barHeight、borderあたりの値を変えてやれば簡単にできますよ(‘ω’)ノ

ロード画面

 

ゲーム作るときにあると便利なやつ

フェードアウト処理

フェードアウトのサンプル画像

ゲームを作っていて画面遷移時に絶対欲しいのがフェードアウト処理です。このフェードアウト処理がないと突然画面が切り替わり、あまりにも不自然に感じてしまいます。

 

フェードアウト処理はopacityという透明度のプロパティを使います。これは1が不透明(見える)で0が透明(見えない)です。なので黒い画像を用意してこれが透明から不透明に変わっていけばフェードアウトしていっているように見えるわけです。

var FadeOut = enchant.Class.create(enchant.Sprite, {
    initialize: function(w, h, color) {
        enchant.Sprite.call(this, w, h);
        
        // Surface作成
        var bg = new Surface(w, h);
        bg.context.fillStyle = color;
        bg.context.fillRect(0, 0, w, h);
        // Sprite作成
        Sprite.call(this, w, h);
        this.image = bg;
        this.x = 0;
        this.y = 0;
        this.opacity = 0;
        this.isStart = false;
    },
    //フェードアウト開始初期処理(引数にシーンが必要)
    start: function(scene){
        if(!this.isStart){
            scene.addChild(this);
            this.isStart = true;
        }
    },
    //実行処理(先にstart()で初期処理しないと作動しない)
    do: function(speed){//引数:フェードアウトの速さ0.01~0.5(大きいほど速い)
        if(this.isStart){
            this.opacity += speed;
            if(this.opacity >= 1){//終わったらtrueを返す
                return true;
            }
            return false;
        }
    }
});

まず、

var fadeout = new FadeOut(640, 960, 'black');

のように画面サイズとともに色を指定してFadeOutクラスを作成します(色を指定できるようにしているので白や赤など状況によって使い分けられます)。

 

initializeでは指定の色とサイズのコンテキストの作成などの初期化処理をします。

startメソッドはフェードアウトを開始したい場所に書きます。

doメソッドはメインループ内に書いておきます。isStartフラグがtrueになるとフェードアウトを開始します。ループ毎に指定されたスピードでフェードアウトします。終わったらtrueを返します。

以上の方法で簡単にフェードアウトさせることができます(‘ω’)ノ

 

残り時間のカウントダウンする

ゲームって制限時間が決まっているものがたくさんありますよね。一定時間にゴールしたり、敵全滅させたり、逃げたり、時間制限があるおかげでゲームが成り立っているものって結構あります。

 

というわけで制限時間というルールがあると作れるゲームが増えるだろうと思い、制限時間を設けたいときに使えるクラスを作ってみました。

画面に文字を表示させるのでLabelクラスを継承して作りました。

var TimeCountDown = enchant.Class.create(enchant.Label, {
    initialize: function(min, sec){//引数に分と秒を入れる
        enchant.Label.call(this);
        this.color = "white";
        this.font = '25px sens-serif';
        this.isTimeUp = false;
        var tick = 0;
        var minute = min;
        var second = sec; 
        this.addEventListener('enterframe', function(){
            if(tick++ >= core.fps){
                tick = 0;
                if(--second < 0){
                    second = 59;
                    if(--minute < 0){
                        this.isTimeUp = true;
                    }
                }
            }
            this.text = minute + ":" + ('00' + second).slice(-2);//時間
        });
    }
});

使うときは引数に分と秒をして使います。

var timer = new TimeCountDown(5, 0);//分、秒を指定
timer.moveTo(10, 10);
core.rootScene.addChild(timer);

残り時間が0になるとisTimeUpフラグがtrueになります。

 

tickがフレームごとに加算されてそれが設定されているfps以上になると1秒としています。

正確に1秒というわけではないですがJavaScriptのsetIntervalとか使うとポーズ画面とかのときなんかに面倒くさいことになりそうなのでこっちの方が良いかと思います。

 

円と四角形を簡単に書けるようにしておく

円と矩形を描くサンプル

ゲーム作ってるとけっこう円や四角形を使いたい、またはとりあえず円か四角形か表示させて開発を進めたいと思ったりすることないですか?

わたしは画像書くのが面倒なんで手抜きするために円や四角形をサクッと書けるクラスを作りました。面倒くさがりはプログラマーの資質だと思います(そうなのか?)

 

というわけで私はこんな感じにクラスを作って用意してあります。

//円を描く
var DrawCircle = enchant.Class.create(enchant.Sprite, {
    initialize: function(w, h, color){//w, hは幅と高さ
        enchant.Sprite.call(this, w, h);
        var surface = new Surface(w, h);	// サーフェス生成
        surface.context.beginPath();
        surface.context.arc(w*0.5, h*0.5, w*0.5, 0, Math.PI*2, false);//原点X、原点Y、半径、始点ラジアン、終点ラジアン
        surface.context.fillStyle = color;
        surface.context.fill();
        this.image = surface;	// サーフェスを画像としてセット
    },
});
//四角を書く
var DrawRectangle= enchant.Class.create(enchant.Sprite, {
    initialize: function(w, h, color){//w, hは幅と高さ
        enchant.Sprite.call(this, w, h);
        var surface = new Surface(w, h);	// サーフェス生成
        surface.context.beginPath();
        surface.context.fillStyle = color;
        surface.context.fillRect(0, 0, w, h);//X、Y、W、H
        this.image = surface;	// サーフェスを画像としてセット
    },
});

 

メッセージの表示のやり方

メッセージ表示サンプル

enchant.jsではLabelというクラスを使って文字を表示できるんですが、実はちょっとした問題があって、自動で改行させると文字が大きい場合重なってしまいます(訂正:どうやらこれは私の勘違いだったらしい(^^;))。

なのでその問題も解決しつつ、RPGやアドベンチャーゲームとかである「1文字ずつ表示しつつ画面押したら一括表示する」やつを作ってみました。

上の画像がそれなんですがGIFのフレームレートが低かったので数文字ごとに出ているように見えますがプログラムはちゃんと1文字ずつ表示します。

今回のプログラムでは画面のどこでも好きな場所にメッセージを表示できるように作りました。うまくやれば吹き出しのようにして出すこともできるかもね(‘ω’)ノ

var MessageBox = enchant.Class.create(enchant.Group, {
    initialize: function(px, width, height){
        enchant.Group.call(this);
        
        //枠から少し間隔をあけるための数値
        var padding_right = 30;
        var padding_left = 30;
        var padding_top = 30;
        var padding_bottom = 30;
        
        //背景的なもの
        var surface = new Surface(width, height);// サーフェス生成
        surface.context.beginPath();
        surface.context.fillStyle = 'rgba(255, 0, 0, 0.5)';//色 
        surface.context.fillRect(0, 0, width, height);//X、Y、W、H
        var bg = new Sprite(width, height);
        bg.image = surface;
        this.addChild(bg);
        
        //初期化
        this.char_cnt = 0;//文字数カウント用
        this.line_char_cnt = 0;//一行の文字数カウント用
        this.line_cnt = 0;//行数カウント用 
        this.isNewLine = false;//改行フラグ
        this.isFull = false;//画面表示限界フラグ
        this.isEnd = false;//メッセージの最後まで来たフラグ
        this.isTouched = false;
        //メッセージ用配列。全角文字専用。
        this.lines = [];
        this.line_max = Math.floor((height - padding_top - padding_bottom) / (px+10));//最大行数を算出
        this.line_char_max = Math.floor((width - padding_right - padding_left) / px);//一行の最大文字数 
        for( var i = 0; i < this.line_max; i++){//
            this.lines[i] = new Label();   
            this.lines[i].x = padding_right;
            this.lines[i].width = width;
            this.lines[i].color = 'white';
            this.lines[i].font = px+'px sens-serif';
            this.lines[i].y = (px + 10) * i + padding_top; // 行の高さ
            this.lines[i].text = '';
            this.addChild(this.lines[i]);
        }
        //次のページへ行く合図点滅用
        this.btn = new Label();
        this.btn.x = width - 50; 
        this.btn.y = height - padding_bottom;
        this.btn.color = 'white';
        this.btn.font = px+'px sens-serif';
        this.btn.text = "▶";
        this.btn.visible = false;
        this.addChild(this.btn);
        
        this.addEventListener(enchant.Event.TOUCH_START, function(){
            this.isTouched = true;
        });
    },
    //メッセージ表示メソッド
    print: function(message){
        if(this.isFull){//画面いっぱいまで表示した?
            if(this.age % 5 == 0){//5フレーム毎に(点滅スピード)
                this.btn.visible = this.btn.visible ? false : true;//点滅表示
            }
            if(this.isTouched){//タッチした?
                this.isTouched = false;
                if(this.isEnd){//メッセージを最後まで表示していたら
                    //最後まで表示した後の処理(今回はとりあえず使った要素を削除しとく)
                    while(this.firstChild){//全要素を削除
                        this.removeChild(this.firstChild);
                    }
                    return;
                }
                this.line_char_cnt = 0;//一行の文字数カウント用
                this.line_cnt = 0;//行数カウント用 
                this.isNewLine = false;//改行フラグ
                this.isFull = false;//
                this.btn.visible = false;
                for( var i = 0; i < this.line_max; i++){//
                    this.lines[i].text = '';
                }
            }
            return;//もう用がないので抜ける
        }
      if(this.isTouched){//タッチした?
            this.isTouched = false;
            while(this.char_cnt < message.length){
                if(message.substr(this.char_cnt, 1) != '/'){//半角スラッシュは改行の合図
                    this.lines[this.line_cnt].text += message.substr(this.char_cnt, 1);
                }else{
                    this.isNewLine = true;//改行しますよ
                }
                this.char_cnt++;//文字を進める
                if(++this.line_char_cnt >= this.line_char_max || this.isNewLine == true){//最大文字数か改行フラグtrueのとき
                    this.line_char_cnt = 0;//改行したので行の文字カウントは0に
                    this.isNewLine = false;//フラグを戻す
                    if(++this.line_cnt >= this.line_max){//最大行数に達したら
                        this.isFull = true;//画面表示限界まで来たフラグ
                        return;
                    }
                }
            }
            this.isFull = true;//画面表示限界まで来たフラグ
            this.isEnd = true;//最後まで来たよフラグ
            return;//もう用がないので抜ける
        }
        //メッセージ表示処理
        if(this.char_cnt < message.length){ 
            if(message.substr(this.char_cnt, 1) != '/'){//半角スラッシュは改行の合図
                this.lines[this.line_cnt].text += message.substr(this.char_cnt, 1);
            }else{
                this.isNewLine = true;//改行しますよ
            }
            this.char_cnt++;//文字を進める
            if(++this.line_char_cnt >= this.line_char_max || this.isNewLine == true){//最大文字数か改行フラグtrueのとき
                this.line_char_cnt = 0;//改行したので行の文字カウントは0に
                this.isNewLine = false;//フラグを戻す
                if(++this.line_cnt >= this.line_max){//最大行数に達したら
                    this.isFull = true;//画面表示限界まで来たフラグ
                    return;
                }
            }
        }else{
            this.isFull = true;//画面表示限界まで来たフラグ
            this.isEnd = true;//最後まで来たよフラグ
            return;
        }
    }
});

使い方・内容説明

今回はGroupを継承してクラスを作りました。で、クラスを作る際に文字の大きさと文字を表示する場所の幅と高さを指定します。それをもとに計算して一行に表示する文字の数と行数を出してます。

このプログラムでは文字の大きさと表示域の幅から計算された文字数で改行を行います。それと「/(半角スラッシュ)」を改行文字として扱っています。表示したい文字列の改行したい部分に入れてください。

あと、全角専用です。半角文字を使うと文字の幅が違うので一行の長さに差が出てしまうので全角文字だけ使用してください。

プログラムの内容としてはjavascriptのsubstr()メソッドを使って一文字ずつ取得して、一行の限界文字数またはスラッシュで改行し、表示行の最大数まで来たら画面クリックを促す▶を点滅表示させます。クリックされたら表示に使っていた変数たちを初期化して続きを表示させます。

printメソッドをメインループの内に書き込むと動きます。

 

紙芝居風ストーリーシーンを作る

今回は私が使っている紙芝居風ストーリーシーンのプログラムを紹介しちゃいます(*´ω`)

これは私のおバカゲームに必ずあるアホなオープニングやエンディングに使われる紙芝居風のシーンのプログラムです。リンク張っておくので一度ゲームを遊んでください↓

▷▷▷ 秘密警察ギャボリン第3話

一枚の静止画を切り替えていくだけで大した機能もないですが、一応効果音やBGMも絵の切り替えに合わせて入れられるようになってます。

うまく使えば音付きのマンガみたいな感じにもできたりすると思うので挑戦してみてください

プログラムはこんな感じ

var FPS = 30;
//画面サイズ関係
var SCREEN_WIDTH = 640; //画面幅
var SCREEN_HEIGHT = 900;//画面高さ

//データはここへ
var ASSETS = {
    scn_001:'001.png',
    scn_002:'002.png',
    se_aaa:'aaa.mp3',
    se_bbb:'bbb.mp3',
    bgm_abc:'abc.mp3',
};
var story_data = [
     {
         image:'scn_001',//画像
         message:'あいうえお/かきくけこ/さしすせそ',//メッセージ
         se:'se_aaa',//効果音
         bgm:'bgm_abc'//BGM
     },
     {
         image:'scn_002',
         message:'たちつてと/なにぬねの/はひふへほ',
         bgm:'stop'//BGM停止
     },
];
enchant();
var core;
window.onload = function(){
    core = new Game(SCREEN_WIDTH, SCREEN_HEIGHT);    
    core.preload(ASSETS);
    core.fps = FPS;
    core.onload = function(){
         var storyShow = new StoryShowScene(story_data, 0);
    }; 
    core.start();
} 
//ストーリーシーン---------------------------------------------------------------
var StoryShowScene = enchant.Class.create(enchant.Scene, {
    initialize: function(data, page){
        enchant.Scene.call(this);
        core.replaceScene(this);//シーンを入れ替える
        this.backgroundColor = 'rgba(0, 0, 0, 1)';//背景色  
        var sound = null;
        var bgm = new Bgm();
        var FRAME_HEIGHT = 260;//会話フレームの高さ
        //画像
        var picture = new Sprite(SCREEN_WIDTH, SCREEN_HEIGHT-FRAME_HEIGHT);
        this.addChild(picture);
        
        //メッセージクラス
        var messageBox = new MessageBox(30, SCREEN_WIDTH, FRAME_HEIGHT);
        messageBox.y = SCREEN_HEIGHT-FRAME_HEIGHT;//画面下部に
        this.addChild(messageBox);
        
        var isTimeToLoad = true;//ページを読み込むか?フラグ
        var message;//メッセージ表示用変数
        //このシーンのループ処理--------------------------------------
        this.addEventListener('enterframe', function(){ 
            if(isTimeToLoad){//読み込みフラグtrueの時
                isTimeToLoad = false;//falseに変えとく
                
                if(data[page]['se']){//効果音があるとき
                    sound = core.assets[data[page]['se']];
                    sound.play();
                }
                if(data[page]['bgm'] == 'stop'){//BGM停止
                    bgm.stop();
                }
                if(data[page]['bgm']){//BGMがあるとき
                    bgm.data = core.assets[data[page]['bgm']];    
                    bgm.play();
                }
    
                if(data[page]['image']){//画像があるとき
                    picture.image = core.assets[data[page]['image']];
                }else{
                    picture.image = "";
                }
                message = "";
                if(data[page]['message']){//メッセージがあれば初期化
                    messageBox.erase();
                    message = data[page]['message'];
                }
            }
            bgm.loop();//BGMループ処理
            messageBox.print(message);//メッセージ表示処理
        });
        
        //画面タッチで次へ進む
        this.addEventListener('touchend', function(){ 
            if(data[page]['se']){//効果音があったら止める
                sound.stop();
            }
            if(++page < data.length){//dataの最後まで来ていない
                isTimeToLoad = true;//ページ読み込みフラグをtrueに
            }else{//ページの最後まで来た
                //ゲーム内のイベントとして使うときはここにシーンを入れ替える処理を書く
                //今回は最初に戻るようにしてある
                bgm.stop();
                page = 0;
                isTimeToLoad = true;//ページ読み込みフラグをtrueに
            }
        });
    },
});
//メッセージ表示クラス
var MessageBox = enchant.Class.create(enchant.Group, {
    initialize: function(px, width, height){
        enchant.Group.call(this);
        //枠がら少し間隔をあけるための数値
        var padding_right = 30;
        var padding_left = 30;
        var padding_top = 30;
        var padding_bottom = 30;
        
        //メッセージ表示場所の枠作成
        var surface = new Surface(width, height);// サーフェス生成
        surface.context.beginPath();
        surface.context.fillStyle = 'rgba(255, 255, 255, 1)';//色 
        surface.context.fillRect(0, 0, width, height);//X、Y、W、H
        var bg = new Sprite(width, height);
        bg.image = surface;
        this.addChild(bg);
        var surface2 = new Surface(width-10, height-10);// サーフェス生成
        surface2.context.beginPath();
        surface2.context.fillStyle = 'rgba(0, 0, 0, 1)';//色 
        surface2.context.fillRect(10, 10, width-10, height-10);//X、Y、W、H
        var bg2 = new Sprite(width-10, height-10);
        bg2.image = surface2;
        this.addChild(bg2);
        
        //初期化
        this.char_cnt = 0;//文字数カウント用
        this.line_char_cnt = 0;//一行の文字数カウント用
        this.line_cnt = 0;//行数カウント用 
        this.isNewLine = false;//改行フラグ
        this.isFull = false;//画面表示限界フラグ
        //メッセージ用配列。全角文字専用。
        this.lines = [];
        this.line_max = Math.floor((height - padding_top - padding_bottom) / (px+10));//最大行数を算出
        this.line_char_max = Math.floor((width - padding_right - padding_left) / px);//一行の最大文字数 
        for( var i = 0; i < this.line_max; i++){//
            this.lines[i] = new Label();   
            this.lines[i].x = padding_right;
            this.lines[i].width = width;
            this.lines[i].color = 'white';
            this.lines[i].font = px+'px sens-serif';
            this.lines[i].y = (px + 10) * i + padding_top; // 行の高さ
            this.lines[i].text = '';
            this.addChild(this.lines[i]);
        }
        //次のページへ行く合図点滅用
        this.btn = new Label();
        this.btn.x = width - 50; 
        this.btn.y = height - padding_bottom;
        this.btn.color = 'white';
        this.btn.font = px+'px sens-serif';
        this.btn.text = "▶";
        this.btn.visible = false;
        this.addChild(this.btn);
    },
    //メッセージ表示メソッド
    print: function(message){
        if(this.isFull){//画面いっぱいまで表示した
            if(this.age % 5 == 0){//5フレーム毎に(点滅スピード)
                this.btn.visible = this.btn.visible ? false : true;//点滅表示
            }
            return;
        }
        //メッセージ表示処理
        if(this.char_cnt < message.length){ 
            if(message.substr(this.char_cnt, 1) != '/'){//半角スラッシュは改行の合図
                this.lines[this.line_cnt].text += message.substr(this.char_cnt, 1);
            }else{
                this.isNewLine = true;//改行しますよ
            }
            this.char_cnt++;//文字を進める
            if(++this.line_char_cnt >= this.line_char_max || this.isNewLine == true){//最大文字数か改行フラグtrueのとき
                this.line_char_cnt = 0;//改行したので行の文字カウントは0に
                this.isNewLine = false;//フラグを戻す
                if(++this.line_cnt >= this.line_max){//最大行数に達したら
                    this.isFull = true;//画面表示限界まで来たフラグ
                }
            }
        }else{
            this.isFull = true;//画面表示限界まで来たフラグ
        }
    },
    //表示されていた内容を消す
    erase: function(){
        this.char_cnt = 0;
        this.line_char_cnt = 0;//一行の文字数カウント用
        this.line_cnt = 0;//行数カウント用 
        this.isNewLine = false;//改行フラグ
        this.isFull = false;//
        this.btn.visible = false;
        for( var i = 0; i < this.line_max; i++){//
            this.lines[i].text = '';
        }
    }
});

//************************************************************************
//BGM再生(ループ再生)、一時停止、停止
var Bgm = enchant.Class.create({
    initialize: function(){
        this.data = null;
        this.isPlay = false;//プレイの状態フラグ
    },
    //BGM用音楽ファイルのセット
    set: function(data){
        this.data = data;
    },
    //再生(再生のみに使う)
    play: function(){
        this.data.play();
        this.isPlay = true;
        if(this.data.src != undefined){//srcプロパティを持っている場合
            this.data.src.loop = true;
        }
    },
    //ループ再生(必ずループ内に記述すること) PCでのループ再生で使う
    loop: function(){
        if(this.isPlay == true && this.data.src == undefined){//再生中でsrcプロパティを持っていない場合
            this.data.play();
        }
    },
    //再生停止(曲を入れ替える前は,必ずstop()させる)
    stop: function(){
        if(this.data != null){
            this.data.stop();
            this.isPlay = false;
        }
    },
    //一時停止
    pause: function(){
        if(this.data != null){
            this.data.pause();
        }
    }
});

簡単に説明するよ

このプログラムは画面にタッチするたびにstory_data配列の内容を読み込んで画面に表示してます。

story_data配列のimageは画像、messageはメッセージ、seは効果音、bgmはBGMです。それぞれ必要な時だけ記述する。

メッセージで使えるのは全角のみ。半角スラッシュを改行文字にしてます。

そんだけ。あとはプログラムを読んで理解してくれ(^^;)

画像が切り替わるだけのショボいプログラムですがうまく使えば結構楽しめると思います。

うまく改造すればサウンドノベルゲームなんかも作れると思います、たぶん。

作ったことないから知らんけど(^^;)

 

簡単な画面エフェクトを作ってみた

最近ゲームのことを考えてて思ったのがエフェクト・演出です。自分のゲームにもっと演出が欲しいな、とちょっと思ったので作ってみることにしました。

で、とりあえず何から作ろうかなと考えて、簡単そうなやつから作ってみました。enchant.jsには拡大・縮小・回転のプログラムが最初から入っているのでそれを利用したものを考えてつくってみました。

それで出来上がったものがこちら

各クマさんをタッチするとそれぞれのエフェクトが作動します。

やっぱゲームって見た目の演出が大事やと思うんですよね。演出がちゃんとあるだけでペラペラのカスみたいな内容のゲームでも面白いと感じさせることができるんやないかと思うんですよ(^^;)逆にストーリーとかシステムとか超凝ってても演出なかったら絶対クソゲーやろうし。ゲームプログラムで一番大事なのはこういう演出やなかろうかと思うわけですよ(´ー`*)ウンウン

RPGの敵出現風エフェクト

左のクマさんを押すと拡大縮小を利用したRPGの敵に遭遇した時の演出みたいなものが起こります。音と組み合わせればなんとなくそんな感じしなくもないでしょ?

var ExtendEffect = function(){
    this.isPlay = false;
    this.count;//繰り返し用カウント
    //エフェクトスタートと初期化処理
    this.start = function(){
        if(!this.isPlay){
            this.isPlay = true;
            this.count = 0;
        }
    }
    //一瞬の画面の揺れエフェクト(ダメージを受けたときなど)
    this.do = function(data){//data:エフェクトを掛ける対象
        if(!this.isPlay){
            return;
        }
        var a = [1.3, 1, 1.3, 1, 1.3, 1, 3,];
        if(this.count < a.length){//配列の数まで
            if(this.count % 2 == 0){//偶数回は拡大
                if(data.scaleX < a[this.count]){//a[this.count]の大きさまで
                    data.scaleX += 0.2;
                    data.scaleY += 0.2;
                }else{
                    this.count++;
                }
            }else{//奇数回は縮小
                if(data.scaleX > a[this.count]){//a[this.count]の大きさまで
                    data.scaleX -= 0.2;
                    data.scaleY -= 0.2;
                }else{
                    this.count++;
                }
            }
        }else{
            this.isPlay = false;
            data.scaleX = 1;
            data.scaleY = 1;
        }
    }
}

クマのイベントリスナーにstart()を入れといて触るとスタート、メインループ内のdo()がisPlayがtrueで作動って感じです。dataはSceneオブジェクトです。Groupでやってみたら拡大の中心点が違って位置が思いっきりずれます(;’∀’)

※GroupのoriginX、originYを変更すると大丈夫です。Groupは初期設定で左上が原点になってるようです。

ダメージ演出みたいなやつ

真ん中のクマさんを押すと何か衝撃を受けて画面が揺れてる感じになります。ダメージを受けたときなんかに使えます。

var ShakeEffect = function(){
    this.isPlay = false;
    this.max = 5;//エフェクトの繰り返し数の最大値
    this.count;//繰り返し用カウント
    //エフェクトスタートと初期化処理
    this.start = function(){
        if(!this.isPlay){
            this.isPlay = true;
            this.count = 0;
        }
    }
    //一瞬の画面の揺れエフェクト(ダメージを受けたときなど)
    this.do = function(data){//data:エフェクトを掛ける対象
        if(!this.isPlay){
            return;
        }
        if(this.count++ < this.max){
            if(data.age % 2 == 0){
                data.x += random(15);
                data.y += random(15);
            }else{
                data.x -= random(15);
                data.y -= random(15);
            }
        }else{
            this.isPlay = false;
            data.x = 0;
            data.y = 0;
        }
    }
}

シーンをランダムに一定時間動かしてるだけです。イージーですね(´∀`)

画面全体を回転

今回難しかったのがこれ。どうやらenchant.jsにはspriteにあるrotate()関数はGroupやSceneにはついていないみたいです。なのでいくつもの画像を同時に回転させようとすると自分でプログラムを組まないといけないみたいです( ;∀;)

で、右のクマさんをタッチすると背景とクマさんが回転します。クマさんが背景の定位置にくっついて回ることがこのプログラムの重要なポイントです。

var ShakeEffect = function(){
    this.isPlay = false;
    this.max = 5;//エフェクトの繰り返し数の最大値
    this.count;//繰り返し用カウント
    //エフェクトスタートと初期化処理
    this.start = function(){
        if(!this.isPlay){
            this.isPlay = true;
            this.count = 0;
        }
    }
    //一瞬の画面の揺れエフェクト(ダメージを受けたときなど)
    this.do = function(data){//data:エフェクトを掛ける対象
        if(!this.isPlay){
            return;
        }
        if(this.count++ < this.max){
            if(data.age % 2 == 0){
                data.x += random(15);
                data.y += random(15);
            }else{
                data.x -= random(15);
                data.y -= random(15);
            }
        }else{
            this.isPlay = false;
            data.x = 0;
            data.y = 0;
        }
    }
}

先に言っときますが、私高校時代全く勉強しなかったのでサインコサインて実はよくわかっていない(;’∀’)しかしプログラムなら読めば何とかなる!円の動きのプログラムを検索していくつか読めば何とかなるもんですよ(笑)

簡単に説明するとinitialize()で原点を設定してクマさんまでの半径を取得して謎の数学関数を使えば動きます(適当)

 

簡単にゲームが作れるテンプレート

enchant.jsテンプレート公開

enchant.jsを使って初心者でも簡単にゲームが作れるテンプレートを公開します。

初心者でもこのテンプレートをベースに私の記事やネットで作り方を調べればなんとかなるんじゃないかと思います。

 

今回公開しているテンプレートには2種類あって一つは普通のカジュアルゲームを作るシンプルなバージョン、もう一つはゲームボーイ風コントローラーのついたバージョンです。

またenchant.jsも少しだけ改造してあります。

 

以下のリンクからダウンロードできます。

▷▷▷ カジュアルゲーム版

▷▷▷ ゲームボーイスタイル版

 

ついでに各テンプレートを使って実際に作ったゲームがこちらです。

カジュアルゲーム ▷▷▷ へのへのもへピ

ゲームボーイスタイル ▷▷▷ Cosmo Fighter

 

がんばればenchant.jsだってけっこうなクオリティーのゲームが作れます。

 

テンプレートファイルの使い方

enchant.jsのゲームはシンプルなjavascriptファイルなのでindex.htmlをダブルクリックするだけでゲームを起動できます。

プログラミングを改造したい場合は各javascriptファイルをエディタで直接変更してください。

 

音楽ファイルは添付していないので音楽再生部分のプログラムはすべてコメントアウトしてあります。音楽を使いたい場合はご自分で音楽ファイルを用意してコメントアウトを外してください。

 

テンプレートのファイル構成について

このテンプレートはいくつかのファイルで構成されています。

  • enchant_kai.jsは私がちょっと改造したenchant.jsファイル
  • inwan_base.jsは私の自作クラスや関数が入ったファイル
  • inwan_keypad.jsはゲームボーイコントローラーのプログラム

となっています。これらのファイルはゲームプログラムを書くmain.jsより前に読み込む必要があるのでindex.htmlでは必ずmain.jsより前に書いてください。

 

公開するにあたってプログラムを読み返したりしていないので変な部分もあるかと思いますが気づいたら「プププッ」と笑って修正or削除てください(;^ω^)

 

テンプレートのプログラムについて

ファイルの読み込み

画像や音楽ファイルはASSETSオブジェクトに記述して一括で読み込みます。読み込まれたものはcore.assetsに保存されています。

 

シーンの構成と切り替え

このテンプレートではタイトル・ゲーム・ポーズ・ゲームオーバーの4つのシーンで構成されています。シーンごとに役割が分かれています。

 

基本的にはこのシーンを切り替えることでタイトル画面やゲーム画面に切り替わります。シーンの切り替えはsystemクラスのchangeScene()が行っています。

 

ポーズシーンだけは特別でゲームシーンの上に出るようになっています。pushScene()というメソッドで現在のシーンの上に表示されるようになっています。上に表示している間は下のシーンは停止しています。

 

テンプレートを使ったゲームの作り方

シーンの切り替えや音楽再生機能など必要な部分はすでにできているので作るのはゲーム部分だけです。

 

ゲームはMainGameSceneにプログラムを書いていきます。とりあえずプレイヤーだけ表示しています。後は自分で作ってください。

ゲーム制作に必要な情報は私のブログの記事、もしくはネットで検索して頑張ってください。enchant.jsの記事はネットにかなりたくさんあります。

 

プログラムを作っていく際、クラスという概念がわかるようになるととても効率よくプログラミングできるようになるので初心者の方はクラスの使い方を勉強すると良いと思います。