【Html&Cssで作る四択クイズゲーム開発記録①】Sassのリストと@forループを使ったコード省力化の話
※ 当ページには【広告/PR】を含む場合があります。
2021/12/15

HTML(Javascript)とCSSを組み合わせながらミニゲームを開発していこうとすると、2つの言語でコードの役割分担を考えながらバランスよく進めて行く必要があります。
かといってCSSはもともとWebページデザインのための言語であり、あまり大規模なプロジェクトで用いるとコードが肥大化して非常に読みにくいメンテナンスに乏しいのが難点です。
もしもCSSでミニゲームを作りたい場合は、プロジェクト開発の早い段階から
今回は作成途中の
Sassのリストと@forの使い方
Sassの配列の基本は別ブログの以下の記事に色々と書いていましたので、詳しくはそちらをご確認ください。
そちらの記事でも説明はしましたが、Sassでは
より正確にいうとSassに配列(Array)はないのですが、この記事では極力配列ではなく「リスト」と呼ぶことにします。
Sassのリストはイテレーション(反復処理)に便利で、
@each <アイテム> in <リスト>
// 簡単なリスト
$list: [a, b, c, d];
$i: 0;
@each $item in $list {
.hoge-#{$item} {
piyo: '#{$item}';
index: $i;
}
$i: $i+1;
}
これを

というようにリストからCSSスタイルのブロックが展開されていることが分かります。
リストによる簡単な繰り返しなら
@each ~ in
そこで首題の通り、
@for
// 簡単なリスト②
$list: [a, b, c, d];
@for $i from 1 through length($list) {
.hoge-#{nth($list,$i)} {
piyo: '#{nth($list,$i)}';
index: $i - 1;
}
}
この
@for
@for <インデックス変数> from <初期値> through <終了値>
リストを@forでループさせる場合、初期値は1から始める必要があります(0ではないことに注意)。
これを試すと先程の@eachの場合と同じ結果を得ます。

さらにリストの面白い特徴がリストを使って多重配列的に入れ子にすることが出来ることです。
// 簡単なリスト③
$list: [
[a, [1, 2]],
[b, [5, 6]],
[c, [9, 10]],
[d, [13, 14]]
];
@for $i from 1 through length($list) {
.hoge-#{nth(nth($list,$i),1)} {
piyo: '#{nth(nth(nth($list,$i),2),1) + nth(nth(nth($list,$i),2),2)}';
index: $i - 1;
}
}

釘括弧
[]
()
先程のコードは以下のように置き換えできます。
// 簡単なリスト④
$list: (
a: [1, 2],
b: [5, 6],
c: [9, 10],
d: [13, 14]
);
@for $i from 1 through length($list) {
.hoge-#{nth(nth($list,$i),1)} {
piyo: '#{nth(nth(nth($list,$i),2),1) + nth(nth(nth($list,$i),2),2)}';
index: $i - 1;
}
}

ここでは紹介しませんが、この丸括弧でのリストは
@each ~ in
またSass組込モジュールの
Scssコードの改造
では先程の説明までで、@for構文の使い方のウォーミングアップを終えたところで、前回までの四択問題ミニゲームのソースコードから修正していきましょう。
改善前
まずは修正を加える前のソースコードを全載せしておきます。
index.html
<!DOCTYPE html>
<html lang="ja">
<meta charset="utf-8"/>
<head><link rel="stylesheet" type="text/css" href="style.css"></head>
<body>
<form id="game-wrapper">
<p class="game-header"><span id="gamestate">さあ4択クイズを始めましょう</span></p>
<input type="reset" id="reset"/>
<input type="checkbox" id="start"/>
<input type="checkbox" id="stage1"/>
<input type="checkbox" id="stage2"/>
<input type="checkbox" id="stage3"/>
<input type="checkbox" id="checkmark1"/>
<input type="checkbox" id="checkmark2"/>
<input type="checkbox" id="checkmark3"/>
<div id="stage---op" class="stage stage--op">
<label for="start">クリックしてスタート</label>
</div>
<div id="stage---1" class="stage stage--main stage--main--1">
<label for="stage1">
問題1:タコの足は何本?
<ul class="flexlist">
<li>5本</li>
<li id="correct-option-1">8本</li>
<li>10本</li>
<li>足はない</li>
</ul>
</label>
</div>
<div id="stage---2" class="stage stage--main stage--main--2">
<label for="stage2">
問題2:タコは何動物?
<ul class="flexlist">
<li>地球外生物</li>
<li>緩歩動物</li>
<li>実は植物</li>
<li id="correct-option-2">軟体動物</li>
</ul>
</label>
</div>
<div id="stage---3" class="stage stage--main stage--main--3">
<label for="stage3">
問題3:タコの水揚げ量が世界一の国?
<ul class="flexlist">
<li id="correct-option-3">中国</li>
<li>カナダ</li>
<li>モーリタニア</li>
<li>日本</li>
</ul>
</label>
</div>
<div id="stage---reset" class="stage stage--end">
<p class="score-board"><span id="score"></span></p>
<label for="reset">もう一度トライ</label>
</div>
</form>
<script>
document.getElementById("start").addEventListener("change", (e) => {
document.getElementById("gamestate").textContent = '成績: ';
});
document.getElementById("correct-option-1").addEventListener("click", (e) => {
document.getElementById("checkmark1").checked = true;
});
document.getElementById("stage1").addEventListener("change", (e) => {
const r_ = document.getElementById("checkmark1").checked;
if (r_) {
document.getElementById("gamestate").textContent += '⭕ ';
} else {
document.getElementById("gamestate").textContent += '❌ ';
}
});
document.getElementById("correct-option-2").addEventListener("click", (e) => {
document.getElementById("checkmark2").checked = true;
});
document.getElementById("stage2").addEventListener("change", (e) => {
const r_ = document.getElementById("checkmark2").checked;
if (r_) {
document.getElementById("gamestate").textContent += '⭕ ';
} else {
document.getElementById("gamestate").textContent += '❌ ';
}
});
document.getElementById("correct-option-3").addEventListener("click", (e) => {
document.getElementById("checkmark3").checked = true;
});
document.getElementById("stage3").addEventListener("change", (e) => {
const r_ = document.getElementById("checkmark3").checked;
if (r_) {
document.getElementById("gamestate").textContent += '⭕ ';
} else {
document.getElementById("gamestate").textContent += '❌ ';
}
let result = 0;
document.getElementById("checkmark1").checked && result++;
document.getElementById("checkmark2").checked && result++;
document.getElementById("checkmark3").checked && result++;
document.getElementById("score").textContent = `正解率は ${result}/3 でした。`;
});
document.getElementById("reset").addEventListener("click", (e) => {
document.getElementById("gamestate").textContent = 'さあ4択クイズを始めましょう';
});
</script>
</body>
</html>
以下は
style.css
#game-wrapper {
width: 100%;
height: 300px;
background-color: darkgray;
box-sizing: border-box;
position:relative;
input { display: none; }
.game-header {
position: absolute;
top: 0;
left: 0;
background: #220c0c;
color: #ceeece;
z-index: 1;
margin: 0;
font-size: 22px;
padding: 6px 0 6px 0;
width: 100%;
}
.stage {
position: absolute;
display: block;
width: 100%;
height: 100%;
font-size: 24px;
&--op {
display: flex;
align-items: center;
justify-content: center;
label {
display: block;
flex: 0 0 auto;
margin: 0 auto;
animation: flash 1s linear infinite;
}
}
&--main {
display: flex;
align-items: center;
justify-content: center;
label {
flex: 0 0 auto;
margin: 0 auto;
}
&--1 {
color: blue;
font-size: 22px;
}
&--2 {
color: rgb(99, 231, 169);
font-size: 22px;
}
&--3 {
color: rgb(235, 212, 85);
font-size: 22px;
}
}
&--end {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: darkgray;
label {
flex: 0 0 auto;
display: inline-block;
font-weight: bold;
padding: 0 0 0 0;
text-decoration: none;
color: #00BCD4;
background: #ECECEC;
margin: 0 auto;
animation: flash 1s linear infinite;
}
}
@keyframes flash {
0%,100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
}
.stage--main {
ul.flexlist {
border: 1px solid #666;
display: flex;
flex-wrap: wrap;
justify-content: center;
list-style-type: none;
margin: 10px auto 2px;
padding: 0;
width: 300px;
li {
border: 1px solid #aaa;
line-height: 110%;
margin: 5px 2px 5px 2px;
padding: 8px;
text-align: center;
width: 35%;
&:hover {
color: chartreuse;
}
}
}
}
#stage {
&---op {display: flex}
&---1,&---2,&---3,&---reset {display: none}
}
#start:checked ~ {
#stage---op {display: none}
#stage---1 {
display: flex;
opacity: 1;
animation: fadeIn 0.3s ease-in 0s forwards;
}
}
#stage1:checked ~ {
#stage---1 {display: none}
#stage---2 {
display: flex;
opacity: 1;
animation: fadeIn 0.3s ease-in 0s forwards;
}
}
#stage2:checked ~ {
#stage---2 {display: none}
#stage---3 {
display: flex;
opacity: 1;
animation: fadeIn 0.3s ease-in 0s forwards;
}
}
#stage3:checked ~ {
#stage---3 {display: none}
#stage---reset {
display: flex;
opacity: 1;
animation: fadeIn 0.6s ease-in 0s forwards;
}
}
@keyframes fadeIn {
0% {
display: none;
opacity: 0;
}
1% {
display: flex;
opacity: 0;
}
100% {
display: flex;
opacity: 1;
}
}
}
@forを使って改善する!
では@for構文使って無駄なコードを改善していきましょう。
先に改善後の
style.scss
#game-wrapper {
width: 100%;
height: 300px;
background-color: darkgray;
box-sizing: border-box;
position:relative;
input { display: none; }
.game-header {
position: absolute;
top: 0;
left: 0;
background: #220c0c;
color: #ceeece;
z-index: 1;
margin: 0;
font-size: 22px;
padding: 6px 0 6px 0;
width: 100%;
}
.stage {
position: absolute;
display: block;
width: 100%;
height: 100%;
font-size: 24px;
&--op {
display: flex;
align-items: center;
justify-content: center;
label {
display: block;
flex: 0 0 auto;
margin: 0 auto;
animation: flash 1s linear infinite;
}
}
&--main {
display: flex;
align-items: center;
justify-content: center;
label {
flex: 0 0 auto;
margin: 0 auto;
}
//👇修正ポイント①
@each $key, $val in (1:blue, 2:rgb(231, 185, 99), 3:rgb(211, 51, 203)) {
&--#{$key} {
color: $val;
font-size: 22px;
}
}
}
&--end {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: darkgray;
label {
flex: 0 0 auto;
display: inline-block;
font-weight: bold;
padding: 0 0 0 0;
text-decoration: none;
color: #00BCD4;
background: #ECECEC;
margin: 0 auto;
animation: flash 1s linear infinite;
}
}
@keyframes flash {
0%,100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
}
.stage--main {
ul.flexlist {
border: 1px solid #666;
display: flex;
flex-wrap: wrap;
justify-content: center;
list-style-type: none;
margin: 10px auto 2px;
padding: 0;
width: 300px;
li {
border: 1px solid #aaa;
line-height: 110%;
margin: 5px 2px 5px 2px;
padding: 8px;
text-align: center;
width: 35%;
&:hover {
color: chartreuse;
}
}
}
}
#stage {
&---op {display: flex}
//👇修正ポイント②
@each $item in (1, 2, 3, reset) {
&---#{$item} { display: none }
}
}
//👇修正ポイント③
$stages: (
'start' : 'op',
'stage1': '1',
'stage2': '2',
'stage3': '3'
);
@for $i from 1 through length($stages) {
##{nth(nth($stages, $i),1)}:checked ~ {
#stage---#{nth(nth($stages, $i),2)} {display: none}
@if $i == length($stages) {
#stage---reset {
display: flex;
opacity: 1;
animation: fadeIn 0.3s ease-in 0s forwards;
}
} @else {
#stage---#{nth(nth($stages, $i+1),2)} {
display: flex;
opacity: 1;
animation: fadeIn 0.3s ease-in 0s forwards;
label::before {
content: '問題#{$i}:';
}
}
}
}
}
@keyframes fadeIn {
0% {
display: none;
opacity: 0;
}
1% {
display: flex;
opacity: 0;
}
100% {
display: flex;
opacity: 1;
}
}
}
まずはソースコード内にコメントを挿入している箇所のうち、
@forループでリストから生成したcssのレンダリング結果は以下のようになります。

これでリストを追加していくだけで、いちいち一つのステージずつスタイルを追加していかなくても自動でスタイルが定義されていきます。

共通したcssスタイルのテンプレートさえ見極められれば、リストと@forから効率的にスタイルを生成できています。
今回もっとも難易度の高い@forとリストの使いこなしが

まずよく理解していただきたいのは、
nth
nth関数は深いネストをもつリストから要素を取り出す際に、
nth(nth(nth(nth(...
$stages: (
'start' : 'op',
'stage1': '1',
'stage2': '2',
'stage3': '3'
);
//以下はnth関数の使い方の例
nth($stages,1) //👉'start' : 'op'
nth($stages,4) //👉'stage3': '3'
nth(nth($stages,2),1) //👉'stage1'
nth(nth($stages,4),2) //👉'3'
nth関数の第2引数で取り出したい要素の位置を指定しますが、この指定番号のルールにも気をつけましょう。
他にちょっとした修正点ですが、
問題...
//....
label::before {
content: '問題#{$i}:';
}
//....
これで
index.html
問題...
...
<div id="stage---1" class="stage stage--main stage--main--1">
<label for="stage1">
タコの足は何本?
<ul class="flexlist">
<li>5本</li>
<li id="correct-option-1">8本</li>
<li>10本</li>
<li>足はない</li>
</ul>
</label>
</div>
<div id="stage---2" class="stage stage--main stage--main--2">
<label for="stage2">
タコは何動物?
<ul class="flexlist">
<li>地球外生物</li>
<li>緩歩動物</li>
<li>実は植物</li>
<li id="correct-option-2">軟体動物</li>
</ul>
</label>
</div>
<div id="stage---3" class="stage stage--main stage--main--3">
<label for="stage3">
タコの水揚げ量が世界一の国?
<ul class="flexlist">
<li id="correct-option-3">中国</li>
<li>カナダ</li>
<li>モーリタニア</li>
<li>日本</li>
</ul>
</label>
</div>
...
ということで今回の修正は以上です。
Sassコードのリファクタリングには積極的にリストと@forループを利用することを考えてください。