クソアプリ Advent Calendar 2022 1日目の記事です。
前置き
おはようございます。DE-TEIUです。
ポインセチアが美しい紅色を見せる時期となりましたが、皆様いかがお過ごしでしょうか。
ところでポインセチアって何ですかね。
過去にアドベントカレンダー用に作ったクソアプリ
- p5.jsで作ったダメなソリティア
- 絶対に作業できない作業用BGMサイト
- 社会に一石を投じるクソアプリ開発
- 鼻毛が生えるカメラアプリ
- 映画館のスクリーンでYouTubeが見られるクソアプリ
- 電卓の成長を体験できるクソアプリ
Jamstackについて
Jamstackとは、JavaScript、API、Markupを組み合わせたWeb開発アーキテクチャの一種です。
WebアプリをHTML、CSS、JavaScriptを用いて静的サイトとして開発し、動的コンテンツはすべてAPIを通じて取得する、という構成で開発する手法です。
また、静的サイトジェネレーターなどを用いて、ビルドのタイミングでAPIから動的コンテンツの取得を行い、表示させたいデータをあらかじめ埋め込んだ静的サイトを出力する、というやり方もあるようです。
余談(Jamstackの表記について)
Jamstackについて検索すると、「JAMstack」と「Jamstack」の二種類の表記が見つかります。
これは表記ゆれなどでは無く、どうやら2019年ぐらいまでは「JAMstack」、2020年頃から「Jamstack」と書かれるようになったらしいです。今後は「Jamstack」の方を使うのが良さそうです。
注意事項
この記事ではこれ以降、正しい意味でのJamstackの話はしません。ご了承下さい。
成果物
stackという単語には、積み重ねるという意味があります。
ということで、Jamをstackするゲームを作りました。
JAMが
STACKしたと思います。
15メートル以上積めたらプロです。
解説など
以下、今回使用したビルドツールやフレームワークの話などをします。
Vite
Vite(ヴィート)とは、フロントエンドのビルドツールの一種です。
以下のような特徴があります。
- わりと高速でビルドできる
- Vue、React、Preact、Lit、Svelteを組み込んだテンプレートがあり、これらのフレームワークを使った開発環境がすぐに作れる
- TypeScriptにも対応している
導入方法
はじめに | Vite に書いてある手順を実行するだけです。普通にnpmやyarnでいけます。
$ npm create vite@latest
あとは画面表示に従い、
- プロジェクト名の入力
- 使用するフレームワークの選択
- JavaScriptとTypeScriptのどちらで開発するか選択
を実施します。
その後、作成されたフォルダに移動し、
$ npm install
$ npm run dev
を実行すればローカルサーバーが起動します。これで導入は完了です。
ちなみに、今回のアプリ開発には使用していませんが、プロジェクトルート直下にvite.config.jsというファイルを作ると、ビルドオプションなどを細かく設定できます。
Phaser
Phaser(フェイザー)とは、JavaScriptで2Dゲームを開発するためのフレームワークの一種です。
- スプライトの管理
- 視点(カメラ)の移動
- シーンの管理
- リソース(画像、効果音など)の管理
- 物理演算(Matter.js)
など、ゲーム開発に必要な機能は一通り揃っています。
あと公式サイトで公開されているサンプルソースの量もそこそこ多いです。
導入方法
npmやyarnでインストールするだけです。
$ npm install phaser
使い方(ざっくり)
GameConfigクラスのインスタンスを作成し、ゲームの各種設定を定義しておきます。
設定可能な項目はAPIドキュメントに書いてあります。
const config: Phaser.Types.Core.GameConfig = {
parent: "canvas-parent",
type: Phaser.AUTO,
width: canvasWidth, //キャンバスの幅
height: canvasHeight, //キャンバスの高さ
backgroundColor: "#4488aa",
physics: {
default: "matter",
matter: {
debug: false,
gravity: { y: 0.9 },
},
},
audio: {
disableWebAudio: true,
},
scene: [TitleScene, MainScene], //この2つのシーンクラスは別途用意する
fps: {
target: 60,
forceSetTimeOut: true,
},
};
あとはGameクラスのインスタンス作成時にconfigを引数として渡してやれば、実行時にcanvasが生成されてゲーム画面が表示されます。
const game = new Phaser.Game(config);
シーンクラスについて
PhaserにはSceneクラスというものがあり、このクラスを継承することで1画面分の処理を実装できます。
Sceneクラスには主に
- init
- preload
- create
- update
の4つのメソッドがあり、これらに必要な処理を記述していきます。
(不要であれば省略しても良い)
class SampleScene extends Phaser.Scene {
constructor() {
super("titleScene");
}
/**
* シーン生成開始時、最初に実行される処理
*/
init() {
}
/**
* init実行後に実行される処理
* (画像や効果音などの各種リソースの読み込みはここで行う)
*/
preload() {
}
/**
* preload実行後(リソースの読み込み完了後に実行される処理
* (画面に最初に表示するスプライトの定義などはここで行う)
*/
create() {
}
/**
* フレーム更新時に毎回実行する処理
* @param time このシーンを開始してからの経過時間
* @param delta 前回のフレーム更新からの経過時間
*/
update(time: number, delta: number) {
}
}
実行中のシーンに画像やテキストを表示
例えば、画像を読み込んで表示させたい場合、
preloadメソッドに
this.load.image("jam", "/image/jam.png");
と書いておき、createメソッドに
const canvasWidth = this.sys.game.canvas.width; //キャンバスの幅
const canvasHeight = this.sys.game.canvas.height; //キャンバスの高さ
this.add.image(canvasWidth / 2, canvasHeight / 2, "jam");
みたいな記述をすれば、キャンバスの中央にジャムの画像を表示させたりできます。
ちなみに画像ファイルは、 プロジェクトのルートディレクトリ/public/ 以下に配置します。
(上記の例だと /public/image/jam.png となります)
他にも、テキストを表示させたい場合は、createメソッドに
this.add
.text(100, 100, "表示させるテキスト")
.setFont("32px Arial")
.setColor("#ffffff")
.setAlign("center")
.setLineSpacing(10)
.setOrigin(0.5, 0.5);
などと記述することで、フォントの色や大きさ、配置の設定も含めて実現できます。
また、これらの記述で生成したスプライトに対し、後ろに
.setInteractive({
useHandCursor: true, //ホバー時にマウスカーソルを手に変える
})
.addListener("pointerup", () => {
alert("タップしました")
});
と書いていけば、特定のインタラクションに対するイベントを持たせることもできます。
※setInteractive無しでaddListenerを付けても動かないので注意
効果音の再生
まずは画像と同様に、preloadメソッドでリソースを読み込みます。
this.load.audio("start", "/se/bell00.wav");
そして適当なメンバ変数を用意し、createメソッドで再生用のインスタンスを作成します。
this.bellSE = this.sound.add("start");
あとは何かしらのイベント処理やupdateメソッドの中などでplayメソッドを呼びます。
bellSE.play();
ちなみに音量の調節とかループ再生のOn/Offなども設定できます。
this.bgm = this.sound.add("bgm", {
volume: 0.5,
loop: true,
});
※ちなみに、スマートフォンで実行する場合、ユーザーが何かしらのインタラクション(画面タップとか)を1度実行した後でないと、効果音が再生されません。
というわけで、スマートフォン向けのブラウザゲームを作る際は、スタートボタンをタップしたタイミングで効果音の再生を開始する、などの実装にしておく必要があります。createメソッドにこんな感じで書いておくと良いでしょう。
this.add
.image(canvasWidth / 2, canvasHeight / 2 + 350, "start")
.setInteractive({ useHandCursor: true })
.addListener("pointerup", () => {
(this.sound as Phaser.Sound.HTML5AudioSoundManager).unlock(); //効果音の再生を許可する(iOSでの実行時に必須)
startSE.play(); //効果音再生
this.startGame();//ゲーム開始処理
});
あと環境によってはWeb Audio APIが使えず、HTML5 Audioで効果音の再生を行う必要があります。その場合は更に、GameConfigクラスのインスタンスを作る際に以下のオプションを追加しておきましょう。
audio: {
disableWebAudio: true,
},
ソースコード
Github で公開しています。
このクソアプリでは物理演算を行うスプライトの作成や、視点の移動処理なども行っているので、その辺りのコードが参考になれば幸いです。