スタイルシート スカッシュ ゲーム の説明
戻る


JavaScript を使わず,スタイルシートだけでスカッシュのようなゲームを作ってみます.飛んでくるボールをパドルで打ち返すゲームです.
全くのお遊びです.これが何か実用の役に立つようなことは多分無いでしょう.スタイルシートは本来,動作を記述するものではありませんので,かなり力ずくで作っています.
画面
(これはキャプチャ画像です.これで遊ぶことはできません.)
使い方と仕組みについて説明します.


使い方

画面をクリックするとゲームを開始します.
前からボールが飛んできます.ボールは画面の左右の端に当たると跳ね返ります.
マウスを左右に動かすと画面下方にあるパドルが左右に動きます.パドルでボールを打ち返します.
打ち返した回数が画面左上に表示されます.


カーソル位置の取得

スタイルシートの機能では,直接マウス カーソルの位置を取得することはできません.どの HTML 要素の上にカーソルがあるかは :hover 擬似クラスで判定できますが,カーソルの座標は判りません.
このプログラム(?)では,幅 2px の要素を横に並べて敷き詰め,:hover 状態になった要素を調べることで,カーソルの X 座標を 2px 単位で取得します.
位置取得用要素
取得したカーソル位置にしたがってパドルを動かします.
後述のように,このようなカーソル位置取得のための要素を二組使っています.


打ち返しの処理

ボールをアニメーションで動かしています.横方向の動きは左右の端で移動方向を反転するようにします.縦方向の動きは画面の上端では常に移動方向を反転し,画面の下方では打ち返したとき(以下「続行」という)はパドルの位置で移動方向を反転し,打ち返せなかったとき(以下「終了」という)は移動方向を反転しない(そのまま後ろに飛んでいく)アニメーションを行ないます.
続行している間はずっとボールが画面の上端とパドルで跳ね返る動きを続けますから,ゲームを開始したら,ボールが前から飛んできてパドルの縦方向の位置で反転して戻るというアニメーション(以下「続行のアニメーション」という)を無限に繰り返すようにしておきます.終了したらボールが跳ね返らないアニメーション(以下「終了のアニメーション」という)に切り替えます.
見た目としては,打ち返したら移動方向を反転させているように見えますが,実際には,打ち返せなかったら移動方向が反転するのを中止するという処理になっています.

アニメーションを切り替える処理に少々工夫が必要です.ボールの位置とパドルの位置から続行か終了かを判定することはできますが,一旦終了を検出したらその状態を保持しておかなければなりません.たとえば,終了と判定したとき何かのプロパティを変更したとしても,その後ボールやパドルが動いて終了と判定されない状態になると,変更したプロパティも元に戻ってしまいます.
そのため,以下のような方法を使います.

終了を検出したら上述のカーソル位置取得のための要素(以下「プレイ中のカーソル位置要素」という)の前面に別の要素を配置します.そうするとプレイ中のカーソル位置要素は :hover 状態でなくなります.そのセレクタの変化でボールの位置とパドルの位置を固定します.
ボールのアニメーションは初期状態では停止状態にしておき,プレイ中のカーソル位置要素の :hover 状態で実行状態にします.プレイ中のカーソル位置要素が :hover 状態でなくなるとアニメーションが停止し,そのときの値がそのまま保持されます.
プレイ中のカーソル位置要素が :hover 状態でなくなるとカーソル位置が取得できなくなります.そのままだとパドルの位置は初期状態に戻ってしまうので,後述の「情報の保存」の方法でカーソル位置を保存します.
ボールとパドルの位置が終了を検出したときの値を保持していれば,終了の判定も保持されることになります.

プレイ中のカーソル位置要素の前面に別の要素を配置すると,今度は前面に配置した要素が :hover 状態になります.そのセレクタの変化で終了のアニメーションを実行状態にします.(終了のアニメーションも横方向の動きについては最初から実行状態にしています.これは,ボールの横位置が続行のアニメーションの続きになるようにするためです.)
プレイ中のカーソル位置要素の前面に配置する要素は,プレイ中のカーソル位置要素を :hover 状態でなくすだけならひとつの要素で全面を覆えばよいのですが,前面の要素でもカーソル位置を取得できるようにしたいので,プレイ中のカーソル位置要素と同じものをもう一組(以下「終了後のカーソル位置要素」という)用意して,それを使っています.
カーソル位置要素の順序変更
パドルの位置を固定しただけだと,終了した瞬間にパドルが動かなくなってしまいますので,終了したときのパドルの位置は保存しておいて,その後は終了後のカーソル位置要素の方でカーソル位置を取得してパドルを動かします.


打ち返した回数の表示

打ち返した回数は続行のアニメーションを繰り返した回数なので,続行のアニメーションと一緒に回数をカウントするアニメーションを実行します.終了したら回数のアニメーションも停止します.
打ち返した回数の表示には CSS カウンタを使っています.CSS カウンタの一般的な使い方は,counter-reset/counter-set で値を設定して counter-increment で値を増減させるというものだと思いますが,このプログラムでは counter-reset で値を設定しているだけです.
アニメーションでカウントした回数を counter-reset でカウンタに設定して,counter() を使って表示しています.つまり,CSS カウンタは単に数値を文字列に変換する機能として使っています.


ボールの方向/速度のランダム化

ボールの方向と速度がばらつくよう,以下のような処理をしています.

ボールは画面の上端中央から飛び始めますが,横方向の速度と左右どちらに向かって飛び始めるかのパラメータをアニメーションを使って決めます.縦方向の速度は一定です.
アニメーションで直接パラメータを変化させて,それをボールのアニメーションに使えれば楽なのですが,それはできません.アニメーションのプロパティをアニメーションすることはできないので,アニメーションでパラメータを決めても,それをボールのアニメーションに使うことはできません.
そのため,次のような方法で間接的にパラメータを決めます.

パラメータを 6種類用意してそれに 0 〜 5 の数値を対応させ,アニメーションを使ってその数値を選びます.ただし,同じ数値が続けて選ばれないよう,前回選ばれた数値を除く 5個の数値から選ぶような処理をしています.

6種類のパラメータに対応して 6個のラジオ ボタンを用意します.ラジオ ボタンは非表示にしておき,各ラジオ ボタンに対してラベル(LABEL タグ)を関連付けます.ラベルの幅は 2px にします.
ゲーム開始から終了までの間,パラメータを選ぶアニメーションを実行して数値を変化させます.その数値によってラベルの Z オーダーを変えて,選ばれた数値に対応するラベルが最前面になるようにします.
パラメータ選択用要素 1
ゲーム終了後はゲームが終了した時点で選ばれていた数値のラベルが最前面になった状態が続いています.
終了後のカーソル位置要素の :hover 状態でカーソル位置を取得しますが,その :hover 状態になった要素の上にラベルを重ねます.
パラメータ選択用要素 2
次のゲームを開始するためにマウス ボタンを押下すると,対応するラジオ ボタンがチェックされます.その :checked 状態でパラメータをセットして,次のゲームのボールのアニメーションに使います.
ゲーム開始から終了までの時間の長さによって選ばれるパラメータが変わります.

終了後のカーソル位置要素のさらに前面にラベルを配置するので,ラベルを重ねた時点で終了後のカーソル位置要素が :hover 状態でなくなります.:hover 状態でなくなってもカーソル位置を保持するために,後述の「情報の保存」の方法でカーソル位置を保存します.


情報の保存

カーソル位置の取得で,:hover 状態のセレクタで取得した情報をセレクタの条件が無効になった後も保存しておく必要があります.そのために transition の機能を流用します.
transition では,プロパティの設定値が変更されてから実際に値が変わり始めるまでのディレイを設定できます.情報を保存したいカスタム プロパティに,このようにディレイを設定します.
transition:--a 0s 10000s;
そうすると,セレクタが条件に該当しなくなっても,ディレイで指定した時間が経過するまでは設定した値が保持されます.ディレイに充分大きな値を指定すれば,事実上設定した値が保存されることになります.
ただし,値を設定するときにディレイが設定されていると,その時間が経過するまで値が変わらなくなってしまいますので,値を設定するときには,transition を無効にするかディレイをゼロにします.

カスタム プロパティに transition を適用するために,後述のように @ルールの @property でプロパティをアニメーション可能に設定しています.


カスタム プロパティの定義

このプログラムでは,いくつかのカスタム プロパティを @ルールの @property で定義しています.
@property の定義は,たとえばこのように書きます.
@property --a {
  syntax:'<integer>';
  inherits:true;
  initial-value:0;
}
上述したように,情報を保存するために transition を使っている他,本来の用途でカスタム プロパティにアニメーションを使っているところもあります.カスタム プロパティにアニメーションを使うためには,そのプロパティがアニメーション可能である必要があります.
@propertysyntax<integer> を指定することで,そのプロパティをアニメーション可能にしています.


条件分岐

スタイルシートでは,プログラミング言語のように if 文などで条件を判定して処理を分けるようなことができません.プロパティの設定はすべて式として書かなければなりません.また,直接数値の大小を比較するような機能もありません.
そのため,条件によって値を変えるところは,すべての条件の場合の値を計算しておいてから,そのうちのどれかが選ばれるような式を書くような形になります.C 言語の条件演算子(? :)や Visual Basic の If 演算子のようなものがあれば便利なのですが,今のところそのような機能は無いようです.

たとえば --x に,ある条件を満たす場合は --n1,満たさない場合は --n2 の値を設定するという場合,条件を満たす場合に 1,満たさない場合に 0 となるようなプロパティ --b を計算しておいて
--x:calc(var(--b) * var(--n1) + (1 - var(--b)) * var(--n2));
と書きます.
--b はプログラミング言語で言うブール変数に相当します.
--b の計算の仕方は,たとえば --v が整数を表すプロパティである場合,その値が 42 に等しいかどうかを表す --b はこのように計算できます.(小数の場合はもう少し面倒です.)
--w:calc(var(--v) - 42);
--b:calc(1 - min(var(--w) * var(--w), 1));
こんな風にも書けます.
--w:clamp(-1, var(--v) - 42, 1);
--b:calc(1 - var(--w) * var(--w));
abs() が実装されていれば,もう少し簡単に書けます.
--b:calc(1 - min(abs(var(--v) - 42), 1));
--b1--b2 を条件の真偽を表すプロパティ(1/0)とすると,論理演算は次のように計算できます.
論理積(AND)
  var(--b1) * var(--b2)
論理和(OR)
  min(var(--b1) + var(--b2), 1)
否定(NOT)
  1 - var(--b1)
このような考え方で,条件分岐的な処理を力業で書いています.


このおもちゃは Opera 80 以上用に作っていますが,Google Chrome 107 で動くことが確認できています.Google Chrome の他のバージョンでの動作は未確認です.
Firefox では,バージョン 105 の時点でまだ @property が未実装なので,動きません.


戻る