スタイルシート シューティング ゲーム の説明
戻る


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


使い方

画面上部を標的が左右に横切ります.それを画面下部にある砲で撃ちます.
マウスを左右に動かすと砲が左右に動きます.マウスの左ボタンで弾を発射します.
弾は全部で 10 発です.10 発すべて撃ち終えたらゲーム終了です.


カーソル位置の取得とボタン押下の判定

スタイルシートの機能では,直接マウス カーソルの位置を取得することはできません.どの HTML 要素の上にカーソルがあるかは :hover 擬似クラスで判定できますが,カーソルの座標は判りません.
このプログラム(?)では,幅 2px の要素を横に並べて敷き詰め,:hover 状態になった要素を調べることで,カーソルの X 座標を 2px 単位で取得します.
位置取得用/ボタン押下判定用要素 1
マウス ボタンの押下はラジオ ボタンのチェックで判定します.10 発の弾に対応した 10 個のラジオ ボタンを使います.
ラジオ ボタンは非表示にしておき,各ラジオ ボタンに対してラベル(LABEL タグ)を関連付けます.
ラベルの幅は 2px にします.位置取得用の要素が :hover 状態になったら,その上にラベルを重ねます.
位置取得用/ボタン押下判定用要素 2
その状態でマウス ボタンを押下すると,ラジオ ボタンがチェックされ,ラジオ ボタンの :checked 擬似クラスの条件が発生します.それによってマウス ボタンの押下を判定します.
ラベルは 1 番目のものから順に前面になるように並べ,対応するラジオ ボタンがチェックされたら Z オーダーを変えて背面に回します.そのようにして,順番にラジオ ボタンがチェックされるようにしています.

マウス ボタンの押下を 1 回判定するだけでよければ,直接ラベルを敷き詰めて,ラベル要素でカーソル位置の取得とボタン押下の判定を行えばよいのですが,10 発の弾に対応したラベルをそれぞれ敷き詰めると,ラベルの数が非常に多くなってしまいます.それを避けるため,位置取得用の要素は共用し,ラベルは各ラジオ ボタンにつき 1 個として,上記のような処理を行って判定しています.

なお,これだけだとカーソル位置の取得とボタン押下の判定の両方を同時に行うことができません.
ラベルが上に重なると,位置取得用の要素は :hover 状態ではなくなり,その時点で :hover 条件のセレクタは無効になります.ラジオ ボタンが :checked 状態になったときには位置取得用の要素は :hover 状態ではなくなっているので,位置を取得できません.
そのため,後述の「情報の保存」の方法で :hover 状態になった要素の位置を保存しています.


命中時の処理

標的や弾の移動,命中の表示などにアニメーションを使用しています.命中の判定は標的と弾の座標を比較して行っていますが,座標が命中の条件を満たしたタイミングではユーザー アクションが何も無いので,その時点でアニメーションを停止したり開始したりすることが(多分)できません.そのため,命中時の処理を以下のように行っています.

命中するかどうかにかかわらず,標的の移動以外のアニメーションは弾の発射時に開始しておきます.標的の移動のアニメーションはプログラム開始時に開始し,標的はずっと動かしておきます.
そして,命中したときに標的を消したり爆発を表示したりするのは,標的や爆発などの Z オーダーを変えることで行っています.命中しなかったときには Z オーダーを変えなければ,アニメーションは実行していても,表示には何も影響は与えません.

命中の判定は弾の発射時に行っています.標的や弾は一方向に等速度で動くので,それが命中するかどうかは弾を発射した時点で判ります.
命中してもアニメーションは続行するので,標的や弾は Z オーダーの変更により見えなくなってはいますが,移動を続けます.単に標的と弾の座標を比較しているだけだと,命中しても一旦命中の条件を満たした後,また条件を満たさなくなってしまいます.
そのため,弾の発射時に命中の判定を行って,その情報を後述の「情報の保存」の方法で保存しています.

また,続けて弾を発射した場合,すでに命中した(背面で動いている)標的に対してまた命中と判定してしまうことが無いよう,一度命中したら標的が一旦画面外に出るまで命中の判定を禁止します.


得点の表示

得点の表示には CSS カウンタを使っています.CSS カウンタの一般的な使い方は,counter-reset/counter-set で値を設定して counter-increment で値を増減させるというものだと思いますが,このプログラムでは counter-reset で値を設定しているだけです.
命中時の処理で命中したかどうかは保存されていますので,その値から命中の数は判ります.その値を counter-reset でカウンタに設定して,counter() を使って表示しています.つまり,CSS カウンタは単に数値を文字列に変換する機能として使っています.


情報の保存

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

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


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

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

また,計算結果の小数を整数に丸めるためにも @property を使っています.整数のプロパティを定義して,計算結果をそのプロパティに設定することで,整数への丸めを行っています.


条件分岐

スタイルシートでは,プログラミング言語のように 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 が未実装なので,動きません.


戻る