本文详细介绍了如何使用纯原生JavaScript和CSS构建一个完整的浏览器游戏,包含数据结构处理、DOM操作、CSS动画、模块化编程和游戏逻辑实现,无需任何外部库或框架。
使用原生JS和CSS构建浏览器游戏
构思
我们将构建一个国旗猜谜游戏。玩家会看到一个国旗图案和一个多项选择式的答案列表。
第一步:基础结构
首先需要准备国家和对应国旗的列表。幸运的是,我们可以使用表情符号来显示国旗,这意味着我们不需要自己寻找或创建它们。我已经以JSON格式准备好了这些数据。
最简单的界面将显示一个国旗表情符号和五个按钮:
1
/* 使用网格居中所有元素,并使用相对尺寸确保在不同屏幕尺寸上良好显示 */
项目文件结构如下:
1
2
3
4
5
6
7
8
9
step1.html
step2.html // 等等
js/
data.json
// 更多js文件
helpers/
// 第三步会用到
css/
i/
第二步:简单原型
首先获取data.json文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
async function loadCountries ( file ) {
try {
const response = await fetch ( file );
return await response . json ();
} catch ( error ) {
throw new Error ( error );
}
}
loadCountries ( './js/data.json' )
. then (( data ) => {
startGame ( data . countries )
});
启动游戏逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function startGame ( countries ) {
shuffle ( countries );
let answer = countries . shift ();
let selected = shuffle ([ answer , ... countries . slice ( 0 , 4 )]);
document . querySelector ( 'h2.flag' ). innerText = answer . flag ;
document . querySelectorAll ( '.suggestions button' )
. forEach (( button , index ) => {
const countryName = selected [ index ]. name ;
button . innerText = countryName ;
button . dataset . correct = ( countryName === answer . name );
button . onclick = checkAnswer ;
})
}
答案检查逻辑:
1
2
3
4
5
6
7
8
9
10
function checkAnswer ( e ) {
const button = e . target ;
if ( button . dataset . correct === 'true' ) {
button . classList . add ( 'correct' );
alert ( 'Correct! Well done!' );
} else {
button . classList . add ( 'wrong' );
alert ( 'Wrong answer try again' );
}
}
Fisher-Yates洗牌算法:
1
2
3
4
5
6
7
8
9
10
function shuffle ( array ) {
var m = array . length , t , i ;
while ( m ) {
i = Math . floor ( Math . random () * m -- );
t = array [ m ];
array [ m ] = array [ i ];
array [ i ] = t ;
}
return array ;
}
第三步:类结构
使用模块化保持代码结构:
1
< script type = "module" src = "./js/step3.js" ></ script >
在js/step3.js中导入辅助函数:
1
2
import loadCountries from "./helpers/loadCountries.js" ;
import shuffle from "./helpers/shuffle.js" ;
创建Game类封装游戏逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Game {
constructor ( countries ) {
this . masterCountries = countries ;
this . DOM = {
flag : document . querySelector ( 'h2.flag' ),
answerButtons : document . querySelectorAll ( '.suggestions button' )
}
this . DOM . answerButtons . forEach (( button ) => {
button . onclick = ( e ) => {
this . checkAnswer ( e . target );
}
})
}
start () {
this . countries = shuffle ([... this . masterCountries ]);
const answer = this . countries . shift ();
const selected = shuffle ([ answer , ... this . countries . slice ( 0 , 4 )]);
this . DOM . flag . innerText = answer . flag ;
selected . forEach (( country , index ) => {
const button = this . DOM . answerButtons [ index ];
button . classList . remove ( 'correct' , 'wrong' );
button . innerText = country . name ;
button . dataset . correct = country . name === answer . name ;
});
}
checkAnswer ( button ) {
const correct = button . dataset . correct === 'true' ;
if ( correct ) {
button . classList . add ( 'correct' );
alert ( 'Correct! Well done!' );
this . start ();
} else {
button . classList . add ( 'wrong' );
alert ( 'Wrong answer try again' );
}
}
}
第四步:计分和游戏结束界面
更新Game构造函数处理多轮游戏:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Game {
constructor ( countries , numTurns = 3 ) {
this . numTurns = numTurns ;
// DOM元素引用
this . DOM = {
score : document . querySelector ( '.score' ),
play : document . querySelector ( '.play' ),
gameover : document . querySelector ( '.gameover' ),
result : document . querySelector ( '.result' ),
flag : document . querySelector ( 'h2.flag' ),
answerButtons : document . querySelectorAll ( '.suggestions button' ),
replayButtons : document . querySelectorAll ( 'button.replay' ),
}
}
}
添加计分和游戏结束逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
start () {
this . countries = shuffle ([... this . masterCountries ]);
this . score = 0 ;
this . turn = 0 ;
this . updateScore ();
this . showCountries ();
}
nextTurn () {
const wrongAnswers = document . querySelectorAll ( 'button.wrong' ). length ;
this . turn += 1 ;
if ( wrongAnswers === 0 ) {
this . score += 1 ;
this . updateScore ();
}
if ( this . turn === this . numTurns ) {
this . gameOver ();
} else {
this . showCountries ();
}
}
gameOver () {
this . DOM . play . classList . add ( 'hide' );
this . DOM . gameover . classList . remove ( 'hide' );
this . DOM . result . innerText = ` ${ this . score } out of ${ this . numTurns } ` ;
}
CSS样式:
1
2
3
4
button . correct { background : darkgreen ; color : #fff ; }
button . wrong { background : darkred ; color : #fff ; }
. score { position : absolute ; top : 1 rem ; left : 50 % ; font-size : 2 rem ; }
. hide { display : none ; }
第五步:添加动画效果
使用CSS关键帧动画:
1
2
3
4
5
6
7
8
9
10
11
. slide-off { animation : 0.75 s slide-off ease-out forwards ; animation-delay : 1 s ;}
. slide-on { animation : 0.75 s slide-on ease-in ; }
@ keyframes slide-off {
from { opacity : 1 ; transform : translateX ( 0 ); }
to { opacity : 0 ; transform : translateX ( 50 vw ); }
}
@ keyframes slide-on {
from { opacity : 0 ; transform : translateX ( -50 vw ); }
to { opacity : 1 ; transform : translateX ( 0 ); }
}
动画事件监听:
1
2
3
4
5
6
7
8
this . DOM . play . addEventListener ( 'animationend' , ( e ) => {
const targetClass = e . target . classList ;
if ( targetClass . contains ( 'slide-off' )) {
this . showCountries ();
targetClass . remove ( 'slide-off' , 'no-delay' );
targetClass . add ( 'slide-on' );
}
});
第六步:最终完善
添加开始界面:
1
2
3
4
5
6
< section class = "intro fade-in" >
< h1 > Guess the flag</ h1 >
< p class = "guess" > 🌍</ p >
< p > How many can you recognize?</ p >
< button class = "replay" > Start</ button >
</ section >
评分系统:
1
2
3
const ratings = [ '💩' , '🤣' , '😴' , '🤪' , '👎' , '😓' , '😅' , '😃' , '🤓' , '🔥' , '⭐' ];
const percentage = ( this . score / this . numTurns ) * 100 ;
const rating = Math . ceil ( percentage / ratings . length );
星星动画效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
button :: before , button :: after {
content : ' ' ;
background : url ( ../i/star.svg );
height : 32 px ; width : 32 px ;
position : absolute ;
opacity : 0 ;
}
button . correct :: before { animation : sparkle .5 s ease-out forwards ; }
button . correct :: after { animation : sparkle2 .75 s ease-out forwards ; }
@ keyframes sparkle {
from { opacity : 0 ; bottom : -2 rem ; scale : 0.5 }
to { opacity : 0.5 ; bottom : 1 rem ; scale : 0.8 ; left : -2 rem ; transform : rotate ( 90 deg ); }
}
总结与扩展想法
在不到200行(带有详细注释)的JavaScript代码中,我们完成了一个功能完整、移动友好的游戏,而且没有任何依赖或库!
可以添加的改进功能:
为正确和错误答案添加基本音效
使用Web Workers使游戏可离线使用
在localStorage中存储游戏次数、总体评分等统计数据并显示
添加分享分数和在社交媒体上挑战朋友的功能
Licensed under CC BY-NC-SA 4.0