HTML5のcanvas上で画像を高速に処理できるWebGLという技術が使えるJavaScriptのライブラリ「PixiJS」をベースに簡易ゲームエンジンを作成&公開しました。
enchant.jsを真似て作ってあるので初心者でも扱いやすいと思います。
必要最低限の機能に絞っているので最初に学ぶことはすごく少なくJavaScriptさえ分かっていればすぐ作れます。
かなり簡潔に作っているので改造もゴリゴリできると思います。
※ちなみにこれを使って作った最新のゲームがコレ↓です。
気の短い人が多いようなのでcodepenに投稿しているサンプルを見てください(この投稿の内容はタップゲームの記事で説明してます。あとこのブログのトップページにもゲーム埋め込んでます)。
See the Pen
p94e.jsサンプル by いんわん (@inwan78)
on CodePen.
※このゲームエンジンの作成ではSmith氏の「HTML5ゲーム開発の教科書」という本のプログラムを一部参考にさせていただきました。とてもいい本なので興味のある方は読んでみてください。(enchant.js風に気軽に使えるようにしたくて改造しまくったので最終的にできたものはだいぶ違いますけど。。)
PixiJSの基本的な使い方についてはこちらの記事に書いてあるので興味ある方は読んでみてください。
プログラムのダウンロード
とりあえずどんな感じのものができるか気になるところだと思うので先にサンプルを触ってみてください。
▷▷▷ サンプルプログラムを遊ぶ
サンプルは画面をタッチするとマスクのキャラがジャンプ&方向転換するだけです。あと左上のポーズボタンでポーズできます。
サンプルは非常に簡素ですが必要最低限にしてるから簡素なだけですからね!!
作り込めばすごいのだって作れるんだから!!
先にソースだけ見てみたい人はこっちをどうぞ(‘ω’)ノ
▷▷▷ ソースを見る
※ブラウザでの表示はコメントが文字化けしてると思いますが気にしないでください。
ゲームエンジンのファイルの内容について
ダウンロードしたフォルダには以下のようなファイルが入っています。
pixi.min.jsとpixi.min.js.mapがPixiJSのファイルです。バージョンはv5.3.7です。
pixi-sound.jsはpixi.jsの音楽再生プログラムです。バージョンはv4.0.4です。
上記のファイル以外は私が作ったものです。その中で「p94e.js」(ピくしイーってゴロ合わせね(^^;))が今回私が作ったゲームエンジンです。他はサンプルゲームのためのプログラムです。
※注意事項:今回公開しているプログラムに関しては好きに使っていただいて構いませんが一緒に添付している画像・音楽は他では使わないでください。
PixiJSでゲームを作るために必要なもの
テキストエディタを用意
プログラムでゲームを作っていくにはプログラムを書く専用のエディタがあった方が作業が楽になります。
最近はVisual Studio Codeというのが人気らしいので流行りに乗って使いましょう。人気のあるものは情報も多くて何かと便利ですからね!
サーバー機能を追加
PixiJSで作ったゲームを動作させるにはサーバーが必要です。Visual Studio Codeではこのサーバーの役割をしてくれる機能を拡張で入れることができます。
上のリンクからインストールできます。
インストールすると画面右下端あたりに下の画像のようなものが表示されます。
これをクリックするとブラウザが開いてプログラムが実行されます。
index.htmlについて
サンプルでは以下のようになっています。
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <script src="pixi.min.js"></script> <script src="pixi-sound.js"></script> <script src="p94e.js"></script> <script src="tilemap.js"></script> <script src="main.js"></script> <title></title> <style> * { margin: 0; padding: 0; } body { position: relative; background-color: #222; text-align: center; } </style> </head> <body> </body> </html>
ここで重要なのは<script src=”○○.js”></script>部分です。<script src=”○○.js”></script>がプログラムを読みこんでる部分です。他はとりあえず気にしないでください。
で大事なのはこの順番で、先に読み込む必要のあるものから書いていきます。試しにmain.jsを上にするとエラーになってゲームができません。
これは他のファイルの内容をmain.jsで使っているからで、使う前に必要なファイルが読まれていないからです。
プログラムをファイルで分ける場合は順番に気を付けて下さい。
p94e.jsのプログラム解説
p94e.jsはゲームを動かすための基幹となるプログラムが入っています。
ちなみにクラスやプロパティの名前などはけっこうenchant.jsと同じにしてあります。昔使ってた人は名前だけで内容が分かるかと思います。
Gameクラス
最初のGameクラスがいろんなものを管理しています。一番重要な部分ですがpixi.jsのおかげでだいぶコンパクトに出来ています。
Gameクラスの中身については難しいのでここでは解説しません。
ContainerクラスとSpriteクラス
その下に出てくるContainerクラスとSpriteクラスはpixi.jsのクラスを拡張したものです。この2つにupdateという更新機能をつけてゲームを作りやすくしています。
pixi.jsでは画面に何か表示する際は基本的にこのContainerクラスとSpriteクラスという2つのクラスを使います。
Containerクラスは画面に表示するものを入れる入れ物みたいなものであとで説明するシーンもContainerクラスから出来ています。
SpriteクラスはContainerに画像表示機能が追加されたようなものです。なのでContainerのように他のSpriteをaddChildすることもできます。
ContainerとSpriteのプロパティ
SpriteはContainerから拡張して作られているのでプロパティがほとんど同じです。
座標などよく使うプロパティを説明します。
- x、y・・・表示する座標です。addChildしたContainer上の座標になります
- width、height・・・幅、高さ
- scale・・拡大縮小率。1が基準。scale.xが幅、scale.yが高さです。マイナスにすると反転
- rotation・・・回転。単位はラジアン
- alpha・・・透明度。1で見える、0で見えなくなる
- visible・・・trueで表示、falseで非表示
- pivot・・・位置座標の基準となる場所。座標で設定
- anchor・・・位置座標の基準となる場所(Spriteのみ)。(0, 0)だと画像の左上、(1, 1)で右下、中心なら(0.5, 0.5)
- interactive・・・trueでタッチイベントが使えるようになる
基本的に使うのはこのくらいだと思います。
座標は
player.x = 1o; player.y = 2o;
と入力したり
player.position.set(10, 20)
というふうに一度に設定もできます。
scaleもscale.set(1, 1)といった感じにできます。
pivotとanchorはx、yの座標の基準となる場所を設定します。基本は画面と同じで左上端が設定されています。これを例えば真ん中に変更したい場合に使います。
pivotは自身の左上を原点としたときにそこからどの位置化を座標で指定します。anchorは割合で決めます。anchorはSpriteだけがもっています。この基準位置は回転や反転の基準になります。
interactiveはtrueにするとタッチやマウスのクリックなどの処理が使えるようになります。ただし、Containerはtrueにしても上にSpriteなどが無いと反応しません。
もっと詳しく知りたい方はpixi.jsの公式サイトを見てください。
▷▷▷ PIXI.Sprite
▷▷▷ PIXI.Container
EnchantSpriteクラス
EnchantSpriteはenchant.js風のSpriteにしています。
pixi.jsではスプライトのアニメーションにjsonファイルを使うんですがjsonファイルを作るのが面倒なのと初心者には向いていないと思うのでenchant.jsのように使えるようにしました。詳しい内容については下の記事に書いています。
ただ、この方法には「座標に小数点以下の数字があると画像にノイズが入る」という欠点があります。一応小数点以下を分ければノイズはでなくなりますが拡大・縮小・回転には効果が無いです。
ちなみにサンプルでは真ん中で動いてるデカいキャラがjsonを使ったやり方、ふんどし一丁のキャラがenchant.js風のスプライトになっています。
Graphicsクラス
PIXI.Graphicsにupdate機能を追加しています。
また、円や四角や星型などの図形を簡単に描けるメソッドを追加しています。
- line(x, y, x2, y2,線の太さ, 色)・・線を引きます
- rect(x, y, 幅, 高さ, 線の太さ, 色)・・四角系の枠線を描きます
- rectFill(x, y, 幅, 高さ, 色)・・指定の色で塗潰した四角形を描きます
- circ(x, y, 半径, 線の太さ, 色)・・円の枠線を描きます
- circFill(x, y, 半径, 色)・・指定の色で塗潰した円を描きます
- star(x, y, 頂点の数, 外側の半径, 内側の半径, 線の太さ, 色)・・頂点の数の形の星形の枠線を作ります
- starFill(x, y, 頂点の数, 外側の半径, 内側の半径, 色)・・頂点の数の形の星形を指定の色で塗潰します
- regPoly(x, y, 頂点の数, 半径, 線の太さ, 色)・・頂点の数の正多角形の枠線を作ります
- regPolyFill(x, y, 頂点の数, 半径, 色)・・頂点の数の正多角形を指定の色で塗潰します
LoadingSceneクラス
LoadingSceneクラスはゲーム起動時に表示される画面です。
ローディング状況を%で教えてくれます。
ロードが完了するとゲームが始まります。
main.jsのプログラム解説
main.jsはサンプルゲームの処理が書かれているファイルです。改造するときはこのファイルを書き換えてください。
Config
設定用の変数を入れておくオブジェクトです。
サンプルでは画面の解像度くらいしか書いてないですが、音量とかストレージの名前とか、なんなら言語とかそういうのも入れて管理すると便利です。
Resource
このサンプルゲームで使う画像や音楽、マップデータなどのアドレスが入っています。
これはアドレスに名札を付けてるようなもので使うときにResource.○○○みたいな感じで使うことができます。直接アドレスを書くよりわかりやすくなります。
このResourceオブジェクトの内容を
const assets = []; for (let key in Resource) { assets.push(Resource[key]); }
でurlだけ取り出してcore.preload(assets)でデータをゲームに読み込んでいます。
window.onload
ブラウザが読み込みを完了するとwindow.onload()という関数を実行します。ブラウザゲームはここから起動します。
let core; //ブラウザロード完了からスタート window.onload = () => { core = new Game(Config.Screen.Width, Config.Screen.Height, Config.Screen.BackGroundColor); core.preload(assets); core.onload = () => { core.replaceScene(new MainScene()); } }こ
ここでまずcore = new Game()でゲームに必要ないろんなもの(画面とか)が作られています。
で、core.preload(assets)で画像とか音楽とかを読み込んでいます。この読み込みが終わるとcore.onloadが実行されます。
ゲームはシーン単位で作ります
core.replaceScene(new MainScene())で新しいシーンが作られて画面が切り替わります。
ゲームはこのシーン単位で作っていくと作りやすく管理も楽です。
タイトル画面、ゲームオーバー、ポーズ画面、コンフィグ画面などなど完全に独立して作れるものはシーンで分けて作ります。
私のこのサンプルではタイトル画面が無いのでいきなりゲーム画面になってますがタイトル画面が欲しい場合はTitleSceneを作ってnew TitleScene()てすればタイトル画面が出ます(名前はTitleSceneでなくても好きに付けていいよ)。
replaceScene()
このシーンを切り替えるのにはcore.replaceSceneを使います。これは現在のシーンを破棄して新しいシーンと入れ替えます。
ゲーム開始時では実はLoadingSceneというシーンになってるんですがこいつを破棄してMainSceneに切り替わっています。
replaceSceneを使うと前のシーンは自動的に破棄されます。
pushScene()とpopScene()
replaceScene()と違って破棄せずに今のシーンの上に新しいシーンを作って表示するのがpushScene()です。ポーズ画面とかメニュー画面とか今のシーンを破棄したら困るときに使います。
今までのシーンは一時停止状態になって新しいシーンが動きます。
また元のシーンに戻したいときはpopScene()を使います。popScene()を使うと上にあったシーンが破棄されて一時停止していた元のシーンがまた動き始めます。
シーンの作り方
シーンはContainerを拡張して作ります。Containerは大雑把に言うと入れ物みたいなもです。
シーンの作り方はこんな感じです。
class MainScene extends Container { constructor(){ super(); //処理を書く } update(delta){ super.update(delta); //処理を書く } }
constructor()はクラスが作られるときに最初に実行される部分です。サンプルではここでマップやキャラが作られたり音楽が再生されたりしています。
update(delta)は更新処理を書くところです。サンプルでは空ですがここにキャラクターの操作や敵キャラクターのことなどを書いたりできます。引数のdeltaは更新スピードの情報みたいなやつでラグとかの帳尻を合わせたりするのに使ったりするようです(私は使ってないですけど)。
constructorのsuper()とupdateのsuper.update()は拡張するContainerがもともと持っている処理を引き継いて使うことができるようにしてくれる処理です。とりあえず書いておけばOKです。
タイルマップ用プログラムのtilemap.jsについて
RPGやアクションゲームを作りたい人のためにタイルマップ用のクラスを作っておきました。ただみんなが使うものでもないしゲームにもよると思うので別ファイルに分けました。
今回のマップデータはTiledMapEditorというマップエディターを使って作っていますが、1次元配列であれば別のエディターで作っても使えます。
このタイルマップを動かしたい場合はちょっと問題があって、「小数点以下を切り離す&戻す」処理を行わないと画面がノイズだらけになるので注意してください。
音の再生方法について
ゲームに使うデータはcore.preload()で読み込まれた後はcore.resourcesというところに入っています。
core.resources[Resource.MapTile]といった感じで指定すると取り出せます。
音楽ファイルは
core.resources[Resource.Bgm].sound.play();
といった感じで、読み込んでる音データの後ろに.sound.○○をつけることで操作できるようになります。
- play()・・・再生
- stop()・・・停止
- pause()・・・一時停止
- resume・・・再開
- loop・・・trueでループ再生
- volume・・・再生音量
volumeはConfig.Volumeとか作っておいてvolume = Config.Volumeとかにして使うと良いと思います。
再生などは他に
core.sound.play(Resource.Bgm)
という書き方もできます。
また、ポーズ画面など一時停止時に再生中の音を全部止めてくれるcore.pausePlayingSounds()とそれを再開するcore.resumePausedSounds()という処理を作っておきました。
音の再生方法についてさらに詳しく知りたい方はpixi-sound.jsの公式サイトを見てください。
▷▷▷ PIXI.sound
ゲームの操作入力について
ゲームではタッチ以外の操作方法も使いたい方がいるかと思います。
もちろんそういうのは自力で実装していく必要があります。
入力についてはこちらに記事があるので参考にしてください(‘ω’)ノ
Typescriptの記事なのでちょっと書き方が違いますがjavascriptでも同じようにできると思います。ただ、残念ながらゲームパッドだけはこの方法ではできません。
試しにゲームを作ってみる
簡単なゲームの作り方の説明を書いてみたのでお試しください。
開発ファイルをダウンロード
開発の準備ができたのでpixi.jsとp94e.jsなどのファイル一式をダウンロードしてください。
下のお好きな方からダウンロードしてください。
▷ Github
zipファイルがダウンロードされるのでそれを展開するとデスクトップにフォルダが作られると思います。
開いたフォルダの中身は上の画像のようになっていると思います。
audioフォルダは音ファイル、imageは画像などを入れるフォルダです。pixi.○○はPixiJS関係のファイルです。p94e.jsは私の作ったゲームエンジンのプログラムです。
main.jsがゲームのプログラムを書くファイルです。このmain.jsにこれから追加したり変更したりしてゲームを作っていきます。
ではこのすべてのファイルが入っているフォルダを起動しているVisual Studio Codeにドラッグしてください。そうすると全部のファイルをエディタで見ることができるようになります。
Visual Studio Codeで開いたら上記にあるように画面右下にある「Go Live」をクリックするとブラウザが起動してプログラムが実行されます。
上のような画面が表示されていればOKです。画面をタッチしてぐりぐりするとキャラクターが動きます。
今回のこのプログラム一式はここからゲーム開発を始めやすいようにキャラクター以外のものは何もありません。新しくゲームを作るときはフォルダごとコピーしてゲームを作ってください。
ゲームプログラムを見てみよう
main.jsがゲームのプログラムを書くファイルです。
サンプルのmain.jsの中身の説明をここに書くのはかなり大変になりそうだったのでプログラム内にコメントでかなり書き込んでいます。とりあえずコメントを読んでその部分が何をしている所なのか何となくイメージしてください。
以下の記事にもPixiJSの説明を書いているので交互に読んだりして理解を深めてください。
main.jsを改造してゲームを作ってみよう!
では実際にmain.jsを少し改造してゲームっぽいものを作ってみましょう!!
サンプルとしてこんな感じのものを作ってみたので下の記事を読みながら頑張って作ってみてください。
▷ サンプルゲーム
ボールを追加
まずはこんな画像を用意してください。
ボールです。大きさは128×128です。面倒くさい方は上の画像を保存してください。そして画像をimageフォルダに入れてください。で下記のようにResourceに追加します。
const Resource = { Actor: 'image/player.png', PauseBtn: 'image/pausebutton.png', Bgm: 'audio/rescue.mp3', Pause: 'audio/pause.mp3', Ball: 'image/ball.png'//追加 }
次にMainSceneの上の所のplayer変数があるところに
let player;//プレイヤーの変数 let ball;//追加
ball変数を追加してください。
そしてMainSceneの「player = new Player(128, 128);」と書いている所の上に
ball = new EnchantSprite(128, 128); ball.image = core.resources[Resource.Ball].texture; ball.anchor.set(0.5, 0.5); ball.position.set(Config.Screen.Width * 0.5, 128); this.addChild(ball);
を追加してみてください。すると画面にボールが表示されるはずです。
もし何も表示されなかったら何か間違いがあります。エラーについては次のセクションに書いているのでエラーが出た場合はそちらを先に読んでください。
ballはEnchantSpriteというクラスで出来ています。このEnchantSpriteというクラスはPixiJSのSpriteをenchant.js風に使えるように拡張したものです。内容についてはこちらの記事で詳しく解説しています ▷ PixiJSのSpriteでJSON無しでフレームアニメーションさせるやり方
Spriteは簡単に言うと画像を画面に表示してくれるクラスです。EnchantSpriteはそれにenchant.js風の機能を持たせたものです。
ballの表示プログラムをplayerの前に書きましたが、playerの後に書いても起動時の見た目は同じになります。この順番が何に影響するかというと重なったときに上になるものが違ってきます。
プログラムでは後で画面に描画された方が上になります(後にaddChildした方が上になる)。ゲームを作るときに重なったときにどっちが上になった方が良いのかを考えて書く順番を決めます。
もちろんプログラムで重なり順を変える方法もあるんですが動的に変更するようなことでもなければ基本的に描画順でやった方が楽です。
ボールとのあたり判定
次はゲームらしく当たり判定をつけます。MainSceneのupdateに当たり判定を追加します。
updateは1秒間に60回実行される特殊な関数です。このupdateに書いたプログラムは1/60秒毎に実行されるのでキャラを動かしたりなどゲームらしいことをするのに使います。当たり判定も毎回チェックしたいのでここに書きます。
update(delta){ super.update(delta); const dx = player.x - ball.x; const dy = player.y - ball.y; if(dx * dx + dy * dy < (64 + 48) * (64 + 48)){ console.log('hit'); } }
これで当たったらコンソールに「hit」と表示されます(コンソールはエラーメッセージとか表示されるところね。下のエラーの説明読んでね)。
上の例では円の当たり判定を使っています。プレイヤーもボールも128ドットの画像ですがプレイヤーの分は若干小さくしたいのでプレイヤー分の半径を小さめの48として計算しています。
当たり判定についてはこちらの記事で解説しています ▷ ゲームで使われる基本的な当たり判定
やられた処理
当たったらやられた演出が欲しいのでそれっぽく作ってみましょう。
まずplayerクラスにフラグを追加します。
class Player extends EnchantSprite { constructor(width, height){ super(width, height); this.anchor.set(0.5, 0.5); this.walkAnime = [0, 1, 2, 1]; this.animeCount = 0; this.image = core.resources[Resource.Actor].texture; this.isMove = false; this.isDead = false;//フラグ追加 }
でupdateにやられた演出を追加します。
update(delta){ super.update(delta); if(this.isDead){//やられた! this.rotation += 0.2; //ぐるぐる回転 this.y += 10;//下に飛んでいく return;//ここで処理から抜ける } //略
MainSceneのupdateの当たり判定でフラグを変更します。
update(delta){ super.update(delta); if(player.isDead)return;//もう死んでたらここから出る const dx = player.x - ball.x; const dy = player.y - ball.y; if(dx * dx + dy * dy < (64 + 48) * (64 + 48)){ player.tint = 0xFF00000;//プレイヤーを赤くする player.isDead = true;//死んだ!! } }
すでに死んでいる場合はもう当たり判定の必要は無いのでif(player.isDead)returnで処理から抜けています。
tintは色を変えられる便利なプロパティです(厳密には「変える」とは違うんだけど)。
で、死んだら操作できないようにタッチの処理も抜けるように追加します。
this.on("pointerdown", (e) => { if(player.isDead)return; //略 }); this.on("pointermove", (e) => { if(player.isDead)return; //略 });
ゲームオーバー画面
ゲームが終了したらゲームオーバー画面をだしたいので作ります。
ここは楽してポーズ画面をコピーしてちょっと修正すると良いです。
//ゲームオーバーシーンのクラス class GameoverScene extends Container { constructor(){ super(); this.interactive = true; const bg = new PIXI.Graphics(); bg.beginFill(0xFF0000);//赤にした bg.drawRect(0, 0, Config.Screen.Width, Config.Screen.Height); bg.endFill(); const texture = PIXI.RenderTexture.create({width: Config.Screen.Width, height: Config.Screen.Height}); core.app.renderer.render(bg, texture); const backGround = new PIXI.Sprite(texture); backGround.alpha = 0.4; this.addChild(backGround); //GAME OVERの文字表示 const text = new PIXI.Text('GAME OVER', new PIXI.TextStyle({ fontFamily: 'sans-serif', fontSize: 96, fill: 0xff0000, })); text.anchor.set(0.5, 0.5); text.position.set(Config.Screen.Width*0.5, Config.Screen.Height*0.5); this.addChild(text); } }
で、これをプレイヤーが死んで画面外に出たときに表示されるようにMainSceneのupdateを変更します。
update(delta){ super.update(delta); if(player.isDead){ if(player.y > Config.Screen.Height){//画面外まで飛んでった!! core.resources[Resource.Bgm].sound.stop();//BGM停止 core.pushScene(new GameoverScene());//ゲームオーバーシーンを表示 } return; } //略
ボールを動かす
最後にボールを動かしましょう。止まったままじゃゲームらしさが足りないですからね。
ボールは画面端で跳ね返るようにします。
ballに移動量用の変数を追加します。
ball = new EnchantSprite(128, 128); ball.image = core.resources[Resource.Ball].texture; ball.anchor.set(0.5, 0.5); ball.position.set(Config.Screen.Width * 0.5, 128); ball.vx = 2;//追加 ball.vy = 2;//追加 this.addChild(ball);
次にMainSceneのupdateで移動させて画面端に当たった場合は跳ね返るようにします。壁に当たるときは座標がボールの真ん中にあるので半径分外側になります。
update(delta){ super.update(delta); ball.x += ball.vx;//移動 if(ball.x - 64 < 0 ||ball.x + 64 > Config.Screen.Width){//端に当たった!! ball.vx *= -1;//プラスマイナスを変える } ball.y += ball.vy; if(ball.y - 64 < 0 ||ball.y + 64 > Config.Screen.Height){ ball.vy *= -1; } if(player.isDead){ if(player.y > Config.Screen.Height){ core.resources[Resource.Bgm].sound.stop(); core.pushScene(new GameoverScene()); } return; } //略
跳ね返るのは単純にプラスマイナスを変えて移動する向きを変えているだけです。
エラーがあった場合の対処の仕方
プログラムを入力して動かなかった場合や途中で止まった場合どこかにおかしなところがあります。
エラーなどを確認するにはChromeの場合は右上にあるメニューからデベロッパーツールを開いて確認できます(Edgeも似たところにあります)。
デベロッパーツールを開いたら上にあるメニューの「console」をクリックします。
するとエラーメッセージなどが表示される画面が下に出ます。
赤字でこんな感じのメッセージが表示されていたらエラーです。
右上にファイル名と行番号が表示されているのでそのあたりをよく見てエラーを見つけましょう。
エラーやバグというのは嫌なものですが何度も経験するとある程度原因の予測がつくようになってくるので挫けずに頑張って対処してください。
おしまい
ミニゲーム作るなら高機能なゲームエンジンより簡単だと思うので使ってちょんまげ。