HTMLとCSSだけで簡単なゲームを作ってみた
この記事はCSS Advent Calendar 2024の3日目の記事です。
先日(といっても9月ですが)、社内でのLT会とSendai Frontend Meetup #13で「HTMLとCSSだけで簡単なゲームを作ってみた」というネタでLTをしてきました。
LTではざっくりした説明になったので、技術的な詳細を記事にしておきます。
LTで使ったスライドはこちら。
完成品
動作しているものを見てもらうのが早いので、粗いですが完成品のgifアニメーションです。
Webページとして公開しているので良かったら遊んでみてください。
上からボールが流れてきてタッチすればクリアというシンプルなゲームです。
難易度が5段階あって、レベルが上がるごとにボールが小さくなり速くなります。
ソースコードはGitHubに上がっていますので良かったら見てください。
綺麗に書こうとしていないので色々と雑な部分はあります。
実装の詳細
このゲームの実装で重要なのは、CSSの :has()
擬似クラスです。
:has()
は2024年2月からすべてのモダンブラウザで使えるようになりました。
CSSの可能性を広げる革新的な機能だという評判でしたが、そこまで使えていなかったので実際に使ってどういうことができるかを試してみようと思いました。
使い方を調べていくとゲームのようなものを作るサンプルがあり、面白そうだったので自分でも何か作ってみようと考え作成しました。
タイトル画面 / ゲーム画面への遷移
タイトル画面は全画面で、タイトルとスタートボタンがあります。
このスタートボタンがHTMLの <input type="checkbox">
になっています。
<label class="button">
<input type="checkbox" name="start" /> Start
</label>
全体が .wrapper
で囲まれていて、以下のように書くことでスタートボタンを押す(チェックする)と :has([name='start']:checked)
の対象になり、タイトル画面が非表示になってゲーム画面が表示されます。
.wrapper:has([name='start']:checked) .titleView {
display: none;
}
同じようにスタートボタンがチェックされると、レベル1のボールが表示されてゲームが始まります。
.wrapper:has([name='start']:checked) .ball[value='lv1'] {
display: block;
}
下に流れるボールをタッチする
ボールは <input type="radio">
で作られていて、シンプルなCSSアニメーションの繰り返しで上から下に流れるようになっています。
<input type="radio" name="ball" value="lv1" class="ball" />
.ball {
position: absolute;
animation: slide-down var(--ball-animation-duration) linear
infinite var(--ball-animation-delay);
}
@keyframes slide-down {
from {
top: calc(0% - var(--ball-size));
}
to {
top: 100%;
}
}
タイトル画面のスタートボタンと同じように、チェックされると has(.ball[value='lv1']:checked)
の対象になってクリア画面が表示されます。
クリア画面は3秒で自動的に画面外にスライドされて次のレベルのゲームが始まります。
<div class="clear modal" data-lv="1">
...
</div>
.modal {
display: none;
}
.wrapper:has(.ball[value='lv1']:checked) .clear[data-lv='1'] {
display: grid;
animation: slide-out 0.4s ease-in-out forwards 3s;
}
ボールのレベル設定
ボールはレベルに合わせて要素を5つ用意しています。
元々はひとつの要素で実装していたんですが、チェックされているボールのレベルを :has()
で使えるようにしたくてこうなりました。
<input type="radio" name="ball" value="lv1" class="ball" />
<input type="radio" name="ball" value="lv2" class="ball" />
<input type="radio" name="ball" value="lv3" class="ball" />
<input type="radio" name="ball" value="lv4" class="ball" />
<input type="radio" name="ball" value="lv5" class="ball" />
以下のようにすることでチェックされているボールの次のレベルのボールだけが表示されます。
.ball {
display: none;
}
.ball:checked + .ball {
display: block;
}
難易度は5段階あって徐々にボールが速く小さくなっていき難しくなっていきます。
ここでも :has()
を利用して、チェックされているレベルによってCSSカスタムプロパティの値を変更して実現しています。
.wrapper {
--ball-size: 50%;
--ball-animation: slide-down;
--ball-animation-duration: 5s;
--ball-animation-delay: 1.5s;
&:has(.ball[value^='lv']:checked) {
--ball-animation-delay: 2.5s;
}
&:has(.ball[value='lv1']:checked) {
--ball-size: 40%;
--ball-animation-duration: 4s;
}
&:has(.ball[value='lv2']:checked) {
--ball-size: 30%;
--ball-animation-duration: 3s;
}
&:has(.ball[value='lv3']:checked) {
--ball-size: 25%;
--ball-animation-duration: 2s;
}
&:has(.ball[value='lv4']:checked) {
--ball-size: 20%;
--ball-animation-duration: 1s;
}
}
.ball {
width: var(--ball-size);
height: auto;
aspect-ratio: 1;
animation: var(--ball-animation) var(--ball-animation-duration) linear
infinite var(--ball-animation-delay);
}
ミス画面
ボールをタッチできないとミス画面が表示されます。
背景部分がすべて <input type="radio">
になっていて、ボールがタッチできないと背景部分がチェックされミス画面が表示されます。
<!-- Out area -->
<input type="radio" name="playableState" value="miss" class="out" />
<!-- Miss modal -->
<div class="miss modal">
…
</div>
.wrapper:has(.out:checked) .miss {
display: grid;
}
ミス画面からの復帰
ミス画面にあるretryボタンを押すとゲーム画面に戻ります。
これは背景と同じname値の <input type="radio">
になっていて、チェックされると背景のチェックが外れミス画面が非表示になります。
<label class="button -lv1">
<input
type="radio"
name="playableState"
value="lv1_retry"
class="retry"
/>
Lv1 Retry
</label>
すべてクリア
ミスなくすべてクリアすると特別なクリア画面が表示されます。
背景に紙吹雪のような演出を入れていて、こちらも当然HTMLとCSSだけで作っています。
紙吹雪はこちらの実装をほぼそのまま使わせていただきました。ありがとうございました!
まとめと後書き
実はLTの段階では、リトライからの復帰だけHTMLとCSSだけでは上手くいかずにJSを使っていて「ほぼHTMLとCSSだけで簡単なゲームを作ってみた」というネタでした。
ラジオボタン使ったらJS使わなくてもいけるのでは?とLTの質問でもらって、確かにと思ってやってみたらできました。
こういったフィードバックがもらえるのも人前で発表することの利点ですね。
実際に :has()
を使った実装をすることで、革新的な機能だということを実感することができました。なんとなく使い所も見えてきて実務でも、これは :has()
使えば簡単にできるかもという場面も増えてきました。
最近はCSSの進化がすごいので今後も新しい機能を積極的に使っていきます。
Discussion