あの大人気ゲームをHTMLとjsで再現してみる
こんにちは、株式会社カドベヤの幅です
前回の投稿から滅茶苦茶間が空いてしまいました
この記事を書いている現在は「そろそろ次の書いてください」って言われてから2か月くらい経っています…本当にすみません…
実は別の記事を書いていたのですが難産となっていまして、一旦代わりに簡単に書けそうな記事でお茶を濁そうと思っています(書きかけのやつも次にちゃんと完成させたいです)
さて、自分はVtuber大好き人間なのですが、今Vtuber界隈で一番熱いゲームと言えば何かご存知でしょうか?そうです、スイカゲームです
2021年にswitchで販売開始したこのゲームは紆余曲折あって2023年9月の中頃から爆発的に流行っていますね
単純明快なルール・かわいいUI・完全な実力ゲームにならない絶妙な運要素などが人気の鍵でしょうか?
自分はハイスコア3197を一回だけ出せましたが正直全然安定しません
さて、このゲームの配信を見たり自分でプレイしたりしながら、あるとき(二日前)自分は思いました
「これ物理演算ゲームのチュートリアルに良さそうだし自分でそれっぽいの作ってみるか(そしてそれを投稿のネタにしよう)」
というわけでやってみようと思います
7年目のエンジニアが作ったとは思えないお粗末な出来になると思いますが、1人日クオリティなのでご容赦ください
多分世の中的にはインターン生とかがこういうの作らされるんだろうな…
あと、今回の記事はバリバリにコード書くので、そのあたり何もご存知無い方からするとかなりつまらない内容かもです。ごめんなさい…
ふわっと要件定義と技術選定
ゲーム名までパクると良くないので、とりあえずバブルゲームという仮名で進めます。球体のものでくっつく感じが泡っぽいので…
さてバブルゲームを作る上で満たすべき項目を考えます。今回は参考元があるのでそこから要素を抽出しつつある程度簡略化していきます
箱型のフィールドがある
フィールドをクリックしてゲーム開始
クリックでフィールド上部からバブルを落とすことができる
同じ大きさのバブルが2つくっつくと消滅し、一回り大きい新しいバブルがその場に出来る
バブルが一定の高さ以上に積まれるとゲームオーバー
クリックしてリスタートできる
とりあえずこんなところでしょうか?多分開発してたらもっと決めるべきことが出てきそうですがとりあえず進めます
技術ですがシンプルにHTML+jsでやります。お遊びプロジェクトなのでビルドとかサーバー構築とか絶対にやりたくないです。できるだけシンプルにします
さてこのゲームを作成する上で一番大事なのは物理演算をどうやるのかですが、これも真面目に考えると時間がいくらあっても足りないので世にあるライブラリを使います。
検索してみたらMatter.jsというライブラリが人気だったので素直にそれに乗っかることにしました
コーディング開始
とりあえず適当にhtmlを書きます。というかテンプレをコピーして適当に弄ります
<!DOCTYPE html>
<html>
<head>
<title>Bubble Game</title>
<meta charset="UTF-8" />
<script src="
https://cdn.jsdelivr.net/npm/matter-js@0.19.0/build/matter.min.js
"></script>
<script src="./main.js"></script>
<link rel="stylesheet" type="text/css" href="./index.css" media="screen" />
</head>
<body>
<div class="main">
<div class="container">
</div>
</div>
</body>
</html>
Matter.jsはCDNで配布されているのでそれをscriptタグで読み込んでいます。これでmain.js側でMatter.jsのコードを使えるはずです
bodyにはとりあえずmainのdivとゲーム画面を配置するためのcontainerのdivを作りました
cssはとりあえずcontainerを真ん中に置いてます。ゲーム画面のサイズは適当に縦長の長方形にしてます。relativeなのは多分そうした方があとでゲーム画面に重ねてゲームオーバーのメッセージとかをabsoluteで出しやすいだろうなという予想です
.container {
position: relative;
width: 420px;
height: 700px;
margin: 100px auto;
}
クラスを作る
こういうのを書くときに大枠からやる人と細部からやる人と2パターンいると思いますが、自分は前者のタイプです。なのでまず「こうだったらいいな」を書きます
// ゲームを管理するクラス
class BubbeGame{
constructor(){}
init(){}
}
window.onload = () => {
// とりあえずゲーム作成
const game = new BubbeGame()
// とりあえず初期化すると思う
game.init()
}
コンストラクタと初期化メソッドは役割が被っているようにも見えますが、ゲームを描画する場所の指定やゲームエンジンの初期化・登録はコンストラクタで行い、initでは画面描画の初期化や得点のリセットなどを行う予定です
ではコンストラクタを書きます。Matter.jsのチュートリアルを見ましたが、どうやらEngine,Render,Runnerあたりの登録が必要そうで、Renderの初期化時に描画先のHTML要素を指定してあげるみたいです。なのでそんな感じで書きます
const { Engine, Render, Runner } = Matter; // Matter.jsからとってくる
const WIDTH = 420; // 横幅
const HEIGHT = 700; // 鷹さ
class BubbeGame {
engine;
render;
runner;
constructor(container) {
this.engine = Engine.create();
this.render = Render.create({
element: container,
engine: this.engine,
options: {
width: WIDTH,
height: HEIGHT,
},
});
this.runner = Runner.create();
Render.run(this.render);
Runner.run(this.runner, this.engine);
}
init() {}
}
window.onload = () => {
const container = document.querySelector(".container");
// とりあえずゲーム作成
const game = new BubbeGame(container);
// とりあえず初期化すると思う
game.init();
}
はい、この時点で何もないゲーム画面がページ上に出現しました
デフォルトで黒で塗りつぶしなようです。面倒なのでこのままにします
initの中身はいったん置いといて、ではクリックで球体を召喚できるようにしてみましょう
球体をつくる
Matter.jsではオブジェクトの作成はBodyクラスのメソッドから行いますが、四角形や円形などの簡単な形はBodiesにショートカットがあるみたいです
作成したオブジェクトはCompositeというクラスのaddメソッドで描画先とオブジェクトを指定することで可能になるようです
class BubbeGame {
constructor(container) {
/* 略 */
// クリック時の制御メソッドを登録
container.addEventListener("click", this.handleClick.bind(this));
}
handleClick() {
// 描画位置のX座標、y座標、円の半径を渡す
const bubble = Bodies.circle(WIDTH / 2, 30, 20);
Composite.add(this.engine.world, [bubble]);
}
}
これでページを開き、画面をクリックしたらこうなりました
(背景が青っぽくなってるのはgif化とか縮小とかしたせいなので実際は黒一色です)
はい、球体が描画され、重力に従って落ちていくようになりました!
この時点で目標の3分の1くらい達成していると思います
壁を作る
このままだと落ちてしまうので画面上に地面と壁を作りましょう。
この処理は多分init内で書くといい感じになりそうです
init() {
// リセット時も使うので一旦全部消す
Composite.clear(this.engine.world);
// 地面と壁作成
// 矩形の場合X座標、Y座標、横幅、高さの順に指定、最後にオプションを設定できる
const ground = Bodies.rectangle(
WIDTH / 2,
HEIGHT - WALL_T / 2,
WIDTH,
WALL_T,
{
isStatic: true,
label: "ground",
}
);
const leftWall = Bodies.rectangle(WALL_T / 2, HEIGHT / 2, WALL_T, HEIGHT, {
isStatic: true,
label: "leftWall",
});
const rightWall = Bodies.rectangle(
WIDTH - WALL_T / 2,
HEIGHT / 2,
WALL_T,
HEIGHT,
{
isStatic: true,
label: "rightWall",
}
);
// 地面と壁を描画
Composite.add(this.engine.world, [ground, leftWall, rightWall]);
}
WALL_Tは壁の厚さで10を設定しています
壁は固定オブジェクトなのでisStatic: trueを渡します、これで壁が落ちることはありません
エディタのフォーマットで改行がそろってませんがとりあえずこれで壁が召喚出来てバブルが底にたまるようになります
衝突させる
さて、いよいよこのゲームで一番大事な部分となる衝突時の処理です
とはいってもMatter.jsはデフォルトのエンジンでいい感じに衝突や跳ね返りの挙動はやってくれるので基本はそのままで大丈夫そうです
なので、「同じ大きさのバブルが衝突したら合体して大きくなる」というルールについてだけ追加していきたいと思います
まず、バブル作成時の処理でラベルを追加します。これは衝突したのが壁なのかバブルなのかを区別できるようにするためです
handleClick() {
const bubble = Bodies.circle(WIDTH / 2, 30, 20, {
label: "bubble_0", // この0はサイズ識別子
});
Composite.add(this.engine.world, [bubble]);
}
Matter.jsではEventsクラスのメソッドで衝突判定や描画更新といったイベントの際のハンドラを設定できますのでこれをコンストラクタで登録します
constructor(container) {
/* 略 */
// 衝突判定のタイミングごとに行ってほしいメソッドを登録
Events.on(this.engine, "collisionStart", this.handleCollision.bind(this));
}
handleCollisionの中身はややこしいのですが、まず引数としてpairsという名前の変数が与えられます。これはその判定時に得られた衝突の組み合わせすべての情報が入っている配列です。
各pair内には衝突した物体Aと物体Bについての情報があります。座標や速度、作成時に付与したラベルなどです
というわけでやることは以下の通りです
pairsを全部forで回し、衝突がどのオブジェクトによるものなのかをチェックする
衝突がバブル同士かつ同じサイズの場合、2つのバブルを消す
2つのバブルがあった中間の座標に、一回り大きいバブルを作る
これをコードにしたのが以下の通りです
handleCollision({ pairs }){
for (const pair of pairs) {
const { bodyA, bodyB } = pair;
if (bodyA.label === bodyB.label && bodyA.label.startsWith("bubble_")) {
const currentBubbleLevel = Number(bodyA.label.substring(7)); // この辺めっちゃ雑
const newLevel = currentBubbleLevel + 1;
const newX = (bodyA.position.x + bodyB.position.x) / 2;
const newY = (bodyA.position.y + bodyB.position.y) / 2;
const newRadius = newLevel * 10 + 20;
const newBubble = Bodies.circle(newX, newY, newRadius, {
label: "bubble_" + newLevel,
});
Composite.remove(this.engine.world, [bodyA, bodyB]);
Composite.add(this.engine.world, [newBubble]);
}
}
}
さてこれで画面がどうなるかというと…
はい、バブルが合体して大きくなりました!
ちゃんと2段階目同士も合体して3段階目になっていますね
これでもう7~8割くらい出来てると言っても過言ではありません
作りこむ
ゲーム性を高め、要件を満たすために、他にやるべきこととして以下が挙げられます
バブルの出現位置のX座標を移動可能にする
出現するバブルの大きさをランダムにする
バブル消滅時にスコアを加算する
ゲームオーバーの判定を追加する
また、雑とはいえ多少なりともUI・UXをよくするためにできることもありそうです
バブルのサイズ毎に色を設定する
壁をわかりやすくする
スコアを表示する
ゲーム開始前、ゲームオーバー時にメッセージを表示する
で、これらを全部こうやって実現しますって書いていくとキリが無いので省略して、出来上がったものがこちらです…をしたかったのですがgifが長いからかnoteに直接貼れませんでした
というわけでGithub Pagesで公開しました
こんなもん公開してどうするんだ
↓のページ飛んだら遊べます
クソダサいメッセージとボタン表示は気にしないでください
本家と比べて物理演算による挙動の感覚は違いますが概ね再現できているのではないでしょうか?
ちょいちょい怪しいですがちゃんとデバッグしてないので許してください…
コードは全体はこちらから
https://github.com/A-Haba/BubbleGame
まとめ
というわけで大人気スイカゲームっぽいものをhtmlとjsで作ってみました
普段の仕事ではもっとお堅いWEBアプリばかりを作っているので、今回のような物理演算を利用したゲームを作るというのはほぼ初めてでとても面白かったです
とはいえ今回書いたコードで自分の頑張りは1割で残り9割はMatter.jsがとにかく頑張ってくれています。初めて触ったけどドキュメントとサンプルコード読めばだいたい使い方わかりましたし、とにかく素晴らしいライブラリでした
ネクスト表示機能など再現できていないものがたくさんあったり挙動が気に入らなかったりといろいろ心残りはあるのですが、一旦時間切れということで今回はこれで終わりにします
ちなみにこういうの作ってたら本家スイカゲームも上手くならないかなという期待も1ミリくらいあったのですが、全然そんなことはありませんでしたね
とりあえずこんなパチモンのことはいいので皆さんスイカゲームを買って遊んでください!