🌕

三角関数を使って推しのブロック崩しを作った話

2022/05/20に公開

三角関数より金融経済を教えるべきなんて言ってる人がいるらしい。
まったくわかってない。

三角関数がわかると、思いついたときに推しの脱衣ブロック崩しが作れるんだよ。
だからみんな、三角関数を勉強しよう!

概要

ある日、ふと思い立ってYoutuberの餅月ひまりちゃんの脱衣ブロック崩しを検索したらなかったんだよね。
だから自分で作った。
(みんな遊んでみてね、絵師さんが素晴らしいよ)



ひまり崩し
https://himaribreak.pages.dev/

三角関数を利用してる所

色々使ってるけど…

まず基本、ボールの移動

ブロック崩しのボールの移動は、方位と移動距離を三角関数を利用してX成分とY成分に分解して計算してる。

const moveLen = BALL_MOVE_LEN * speed; // ボールの移動距離
const nextX = ball.x + moveLen * Math.cos(course); // 移動後のX
const nextY = ball.y + moveLen * Math.sin(course); // 移動後のY

指定方位/距離分移動した地点を求める

上のと似てるけど、指定した距離分だけ移動した点[x,y]を求める関数。
描画用オブジェクトを円形に並べるときなんかに使ってる。

function directPos(radiusX, radiusY, radian) {
    let x = radiusX * Math.cos(radian);
    let y = radiusY * Math.sin(radian);
    return [x, y];
}

 

こんな感じで三角関数を使うことで、ボールの移動や下のような演出が実現できる。
(画像はスペルカードが発動したシーン)

 
いや~三角関数って本当に便利だな~勉強しててよかったな~


以下、真面目な話

サイト構成

  • HTML/CSS/JavaScriptの静的サイト。
  • Cloudflare Pagesでホスティングしてるので、維持費は無料。
  • ゲーム部分はCreateJSでガリガリ手作り。

Cloudflare Pagesが最・高

  • ホスティングにCloudflare Pagesを初めて使ってみたけど、無料・早い・管理が楽でとても良い。
  • 転送量無制限がほんとうにえらい。
  • 前はFirebase使ってたけど、画像とか音が多いゲームだと無料枠(1日360MB)を超えそうで怖かった。
  • UIも、個人的にはCloudflare Pagesのほうがシンプルで使いやすい。簡単なアクセス解析とかも標準で用意されてる。
  • ソースはGithubのプライベートリポジトリで管理していて、コミットするとCloudflare Pages側で自動でビルドが走る。
  • ビルド時間は9秒程度(ビルドコマンドは設定してないので、ただファイル展開してるだけ)。

    Cloudflare Pagesのデプロイ状況画面

CreateJSでのゲーム作成の詰まりポイントと回避策

まず基本的な内容はCreateJS入門サイトに本当にお世話になった。

スマホ対応(レスポンシブ対応)

レトロ風の脱衣ブロック崩しがスマホで遊べたら面白いよな~と思ってスマホ対応を決めた。
ただ、今のスマホ、縦横比が無限にあって、どれターゲットにすればよいかわからなくて困った。
結局、canvas上のゲームは600/650の比率で作って、CSSで拡大縮小して表示することにした。

以下は試行錯誤の結果。
スマホは画面半分ぐらいのサイズで表示されて、PCだと画面の7~8割ぐらいのサイズで表示される。(たぶんもっとうまくやるほうほうがある)

<!-- html  -->
<div class="contents">
	<canvas id="gameArea" width="600" height="650"></canvas>
</div>
/* css */
.contents {
	width: 98%;
	height: 100%;
	max-width: 646px;
	max-height: 700px;
	margin-left: auto;
	margin-right: auto;
}

#gameArea {
	display: block;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	max-width: 646px;
	max-height: 700px;
}
//javascript、JQuery
let maxh = window.innerHeight * 0.9;
let maxw = maxh / 650 * 600;
$('.contents').attr('max-width', maxw);
$('.contents').attr('max-height', maxh);
$('#gameArea').attr('max-width', maxw);
$('#gameArea').attr('max-height', maxh);

ロード時間高速化

デプロイ後にサイトにアクセスしたらロードが爆遅だった(20秒ぐらいかかる)。
調べたらLoadQueueは、デフォルトだとリソースを1つずつしかロードしないらしい
setMaxConnectionsを設定して、6並列で読み込むようにしたらロード遅い問題が解消した。

this.loader = new createjs.LoadQueue();
this.loader.setMaxConnections(6);
this.loader.loadManifest(getManifest());

連続して効果音が鳴らせない

ボールが衝突したときの効果音をだすのにJavaScriptのAudioを利用している。
ただ、Audioは、音の再生中にもう1度再生しようとすると再生されないので、ボールがブロックに連続してぶつかっても音が1回しか鳴らない。
この問題の回避策として、Audioを4つ用意して、順番に鳴らすことで短時間に衝突が繰り返しても(4回までは)効果音が鳴るようにした。

// 効果音の初期化
const soundList = [['wall', 'sound/wall.mp3', 0.1],
['wall2', 'sound/wall.mp3', 0.1],
['wall3', 'sound/wall.mp3', 0.1],
['wall4', 'sound/wall.mp3', 0.1],
// 省略
];
soundList.forEach((s) => {
	const au = new Audio(s[1]);
	au.volume = Number(s[2]);
	au.load();
	this.sound[s[0]] = au;
});
this.wallsound.push('wall');
this.wallsound.push('wall2');
this.wallsound.push('wall3');
this.wallsound.push('wall4');
// 効果音を鳴らす処理
// 0~3でインデックスをループ
this.wallsoundIndex = this.wallsoundIndex < (this.wallsound.length - 1) ? this.wallsoundIndex + 1 : 0;
// インデックスに対応するテキスト取得
const target = this.wallsound[this.wallsoundIndex] 
// 再生
this.sound[target].play(); 

中央寄せで表示

CreateJSで中央寄せしようと思ったら標準メソッドになくて困った。
"getBounds()"でオブジェクトの縦横サイズが取れるので、キャンバスサイズの半分から、オブジェクトの縦(横)サイズの半分を引けば中央寄せができる。

function center(obj,canvasWidth,canvasHeight){
	const bounds = obj.getBounds();
	obj.x = (canvasWidth / 2) - (bounds.width / 2);
	obj.y = (canvasHeight / 2) - (bounds.height / 2);
};

sourceRectの指定はscale反映前の元画像

getBoundsと同じように、sourceRect(画像の一部切り取り)もscale反映前の状態のサイズで指定する必要があるので注意。
scale反映後の値で指定したい場合は、下例のようにscaleで割り戻す。

const scale = 1.4;
const beforeImg = new createjs.Bitmap(loader.getResult('xxx')); 
beforeImg.scale = scale;
beforeImg.sourceRect = {
	x: 100 /scale,
	y: 100 / scale,
	width: 50 / scale,
	height: 50 / scale
}

setChildindexで表示オブジェクトが最前面にならない

オブジェクトをsetChildindexで最前面にしようとしたができなかった。(第二引数のIndexが実在するIndexじゃないと動かないから?)

// 期待どおり動かないコード
this.stage.setChildindex(this.ball,30);

結局、表示オブジェクトを削除⇒追加で無理やり最前面にした(力技)。

// ballを最前面に表示
this.stage.removeChild(this.ball);
this.stage.addChild(this.ball);

当たり判定を手作り

当たり判定は、最初はCreateJSのhittestを利用しようとしたけど、それだと画像同士がかなり重ならないと判定されなかった。
ブロック崩しなので、ボールとブロックの画像が重なる前に反発してほしいので、仕方なく判定を手作り。

ボールとブロックのgetBoundsを取得して重なってるか判定する処理で当たり判定してる。

setTimeOut/setIntervalを使わないこと

手元の低スペック端末で動作確認したときに、一部の演出が動かないことがあった。
原因は演出の実行にsettimeout/setIntervalを使っていたから。

CreateJS関連の描画処理は、Tickerで動かしてるんだけど、setIntervalを使うとTickerの処理と同期しないので、バラバラのタイミングで実行されてしまう。(スペックが高い端末だとこの問題はおきにくい)

//描画処理のupdateの初期化部分
createjs.Ticker.timingMode = createjs.Ticker.RAF;
createjs.Ticker.addEventListener("tick", update);

なので、setTimeOut/setIntervalの処理はすべてTweenのwait内で実施するように修正した。
こうすることで、スペックが遅い端末で描画が遅れた場合、演出も合わせて遅れるようになる。

createjs.Tween.get(specialContainer).wait(1000).call(function () {
// setTimeOut/setIntervalで実行する処理
});

おわりに

今までゲーム作ったことなくて、ブロック崩しぐらい簡単に作れるだろ、と思って始めたけど、意外とハマりポイントが多くて苦労した。

あとは絵師さんに使用許可もらうのも始めてだったので、メッセージ送るの緊張した。
推しを応援したいんです!ってダメ元で連絡したら、みんな快く許可をくれた。
あったけぇ・・・

色々苦労はしたけど、イメージしたとおりに演出が動くと楽しいし、ゲームを遊んでくれた人が感想をツイートしてくれるのも嬉しい。
こんなゲームが作れたのも、三角関数のおかげです。

ゲームを作ってよかった!三角関数学んでて良かった!!

追記(2024/1/18)

このゲームは作って放置してたんだけど、なんか最近妙に流入あるなと思ったら、Googleで「脱衣ブロック崩し」で検索すると1ページ目に表示されるようになってるっぽい。

餅月ひまりは引退したのに脱衣ブロック崩しだけ残ってる。
こんなのを無料でホスティングし続けてくれているCloudflareさんへの感謝しかない。

Discussion