原生JavaScript实现打字游戏
写在最前面
本文主要针对的是原生JavaScript的编程能力培养,并采用了函数式编程的方法论。
1.页面的排版与布局
主要分成两个页面:a.初始呈现出来的界面;b.点击开始进入游戏的界面。
a界面: 比较丑,大家注重功能实现就好,忽略ui


a.并非是要绘制页面, 而是采用了一张简陋的图片作为背景, 使用简单的技术实现了这一功能。
b.在HTML代码中定义了三个DOM元素: 一个是起始键, 另一个是说明按钮; 当点击说明按钮时, 会显示相关信息。
// 初始呈现出来的界面
<div id="gameStart">
<!-- 1.开始游戏按钮 -->
<div id="start"></div>
<!-- 2.游戏说明按钮 -->
<div id="describe"></div>
<!-- 3.游戏说明内容 -->
<div id="des">
我是一段认认真真的游戏说明。
<div id="cl">关闭</div>
</div>
</div>
/* container 是最外层的包裹容器 */
#container{
/* 宽高就是背景图片的尺寸 */
width: 521px;
height: 342px;
margin: 50px auto;
/* 设置相对定位 */
position: relative;
/* 后续下落字母落到背景图以下时候,进行隐藏 */
overflow: hidden;
}
/* 把背景图片放进 gameStart 里 */
#gameStart{
background: url(./img/background.png) no-repeat;
/* 宽高百分之百,继承父级 */
width: 100%;
height: 100%;
/* display: none; */
}
/* 按钮的定位,两按钮除了距离底部高度不一致外其余都相同 */
#start, #describe{
width: 101px;
height: 30px;
/* border: 1px solid black; */
position: absolute;
left: 7px;
border-radius: 20px;
cursor: pointer;
}
#start{
bottom: 51px;
}
#describe{
bottom: 6px;
}
/* 游戏说明部分 */
#des{
width: 300px;
height: 100px;
position: absolute;
top: 100px;
left: 100px;
border: 7px solid skyblue;
background-color: #fff;
text-align: center;
display: none;
}
/* 关闭叉叉 */
#cl{
position: absolute;
top: 0;
right: 0;
cursor: pointer;
border: 1px solid #ccc;
font-weight: bold;
display: none;
}
b.界面: 点击开始按钮进入游戏界面
四个操作按钮:
- 启动按钮 —> 进入暂停模式
- 结束按钮
- 关闭游戏 —> 返回至游戏初始界面
- 配置 —> 调整游戏难度级别

点击设置,显示游戏难度选择。

<!-- 进入游戏界面 -->
<div id="game">
<!-- 四个操作按钮,使用了事件委托进行js交互 -->
<div id="oprate">
<span class="start">开始</span>
<span class="exit">退出</span>
<span class="finish">结束</span>
<span class="set">设置</span>
</div>
<!-- 点击设置弹出游戏难度选择 -->
<div id="select">
<!-- 一开始是隐藏的 -->
<select name="" id="">
<option value="3">慢</option>
<option value="2">中</option>
<option value="1">快</option>
</select>
<!-- 关闭按钮 -->
<span id="close">关闭</span>
</div>
<!-- 提示打字的得分、正确率和速度 -->
<div id="tip">
<p>得分: <span>0</span> 分</p>
<p>正确率:<span>0 %</span></p>
<p>速度:<span>0 </span>个 / 分</p>
</div>
</div>
/* 一开始游戏界面是隐藏的 */
#game{
display: none;
width: 100%;
height: 100%;
border: 1px solid #666;
background-color: #ccc;
}
/* 四个选项:开始、退出、设置、结束 */
#oprate{
position: absolute;
bottom: 0;
left: 0;
width: 100px;
height: 100px;
}
#oprate span{
display: inline-block;
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
border-radius: 50%;
cursor: pointer;
position: absolute;
background-color: skyblue;
color: #fff;
font-weight: bolder;
}
#oprate span:hover{
background-color: green;
}
#oprate span:nth-child(1) {
left: 0;
top: 30px;
}
#oprate span:nth-child(2) {
left: 30px;
top: 0;
}
#oprate span:nth-child(3) {
left: 30px;
bottom: 0;
}
#oprate span:nth-child(4) {
top: 30px;
right: 0;
}
/* 点击设置弹出游戏难度选择 */
#select{
display: none;
width: 140px;
height: 60px;
border: 7px solid skyblue;
position: absolute;
top: 50px;
left: 50px;
background-color: #fff;
}
#select select{
width: 80px;
height: 30px;
text-indent: 22px;
font-size: 19px;
margin-left: 30px;
margin-top: 15px;
}
#close{
position: absolute;
top: 0;
right: 0;
cursor: pointer;
display: none;
}
#close:hover{
background-color: pink;
}
/* 提示打字的得分、正确率和速度 */
#tip{
position: absolute;
top: 0;
right: 0;
width: 150px;
line-height: 30px;
padding: 5px 10px;
letter-spacing: 2px;
color: red;
opacity: 0.5;
}
END: 至此,结构样式部分全部结束
JavaScript部分:
1.封装函数拿到所有需要的DOM节点。
function $(idName) {
return document.getElementById(idName);
}
var gameStart = $('gameStart');
var start = $('start');
var describe = $('describe');
var des = $('des');
var cl = $('cl');
var game = $('game');
var oprate = $('oprate');
var close = $('close');
需求A:点击开始游戏,隐藏游戏初始界面,显示进入游戏界面。
// 开始游戏的按钮的 id=start
// gameStart是初始界面最外层的id
// game是游戏开始界面最外层的id
// 思路很简单: 让页面最外层包裹元素display=none/block;即可
start.onclick = function() {
gameStart.style.display = "none";
game.style.display = "block";
}
需求B:进入游戏界面后点击"退出"按钮使页面返回到起始界面。同时设置按钮。
// 使用了事件委托,将事件全部委托到父元素上实现功能
// oprate是包裹四个操作按钮元素的父容器id
// 事件委托,将子元素的点击事件全部赋到父元素上,点击父元素触发子元素的点击事件
oprate.onclick = function(event) {
// 事件兼容处理,兼容 IE
var e = event || window.event;
var target = e.srcElement || e.target;
// 退出
if(target.className === 'exit') {
gameStart.style.display = "block";
game.style.display = "none";
}
// 设置,出现游戏难度
if(target.className == 'set') {
select.style.display = 'block';
}
}
需求C:初始页点击游戏说明按钮显示游戏说明及关闭。游戏难度
// 点击说明按钮显示游戏说明
// describe是游戏说明按钮的 id
// des是说明内容的 id
// cl是关闭文字的 id
// select是设置游戏难度最外层的 id
// close是关闭文字的 id
describe.onclick = function() {
des.style.display = 'block';
}
// 鼠标经过游戏说明区域时,显示关闭按钮
des.onmouseover = function() {
cl.style.display = 'block';
}
// 鼠标移除时候隐藏
des.onmouseout = function() {
cl.style.display = 'none';
}
// 关闭游戏说明内容
cl.onclick = function() {
des.style.display = 'none';
}
// 游戏难度的关闭按钮
select.onmouseover = function() {
close.style.display = 'block';
}
select.onmouseout = function() {
close.style.display = 'none';
}
// 点击关闭设置游戏难度的按钮
close.onclick = function() {
select.style.display = "none";
// 当我们进行游戏难度设置以后,关闭游戏难度设置之后,level数值生效!
level = selFir.value;
}
2.封装函数,获取到元素使用样式的最终值,保证兼容。
function getStyle(ele, attr) {
// 定义变量,用以保存最终获取到的值
var res = null;
// 判断当前浏览器是否支持 currentStyle 这个属性
if(ele.currentStyle) {
// 有这个属性的话,使用ele对象的currentStyle属性来获取 attr 元素属性,并储存
res = ele.currentStyle[attr];
}else {
// 否则
res = window.getComputedStyle(ele, null)[attr];
}
// 将储存的值返回出去
return parseFloat(res);
}
3.封装运动函数。
// 获取游戏界面的高度和宽度
var gameH = getStyle(gameStart, "height");
var gameW = getStyle(gameStart, "width");
// 运动的元素 运动到的最终值 运动元素哪个属性在变化
function startMove(ele, end, attr) {
// 控制字母下落速度
// 随着分数越来越高,让速度越来越快
var speed = 0.5 + score / 100;
// 将定时器赋值给一个变量,以便后续去清除
ele.timer = setInterval(function() {
// 获取当前元素的运动值
var moveVal = getStyle(ele, attr);
if(moveVal >= end) {
clearInterval(ele.timer);
// 删除元素,防止长时间页面卡死
game.removeChild(ele);
// 当ele 目标元素达到清除的时候,就让目标元素 ele 里面的内容清除,最后再删除掉
letters = letters.replace(ele.innerHTML, '');
}else {
ele.style[attr] = moveVal + speed + 'px';
}
}, 10)
}
在游戏界面切换后进行启动并控制游戏流程中的切换与停止作为需求2中事件代理的一部分在发生时触发相应的操作
// 定时器
var c;
// 动态创建的所有字母元素,我怎么去获取呢?去到创建字母的函数中看看
var letterEles;
// 如果用户点击了开始按钮
if(target.className === 'start') {
target.innerHTML = target.innerHTML == "开始" ? "暂停" : '开始';
// 游戏的暂停
if(target.innerHTML == '开始') {
// 当前状态是暂停的时候,游戏的设置功能开启
oprate.lastElementChild.style.cursor = 'pointer';
clearInterval(c);
// 重置 c
c = undefined;
// 清除所有字母元素上的定时器
clearAllLetters();
}else {
// 当前状态是开始的时候,不允许点击游戏设置按钮
oprate.lastElementChild.style.cursor = 'not-allowed';
// 游戏的开始
// 注意: 当我们反复点击开始暂停按钮的时候,需要判断当前是否已经存在定时器了,已经存在就不再开启了,防止开启多个定时器之后页面卡死。
if(c) {
return;
}
// 定义开始时间,用以统计打字速度
startTimeStamp = new Date() * 1;
// 设置定时器,每隔0.5s下落一个文字
c = setInterval(function() {
// 定义结束时间,用以统计打字速度
endTimeStamp = new Date() * 1;
// 不满1分钟按1分钟进行计算
if(endTimeStamp - startTimeStamp <= 60 * 1000) {
// tip.children[2].firstElementChild 找到速度
tip.children[2].firstElementChild.innerHTML = score;
}else {
// 超过1分钟不足两分钟
// Math.ceil((endTimeStamp - startTimeStamp)/(60*1000)) 得到分钟
tip.children[2].firstElementChild.innerHTML = Math.ceil(score/Math.ceil((endTimeStamp - startTimeStamp)/(60*1000)));
}
// 下面创建字母的封装函数
createLetter();
console.log(letters);
// 拿到所有字母所在的元素,看下面封装函数4,发现每个创建的元素的class类名都是 active
// 防止通过 className 获取到的方式不兼容所有浏览器,下面进行兼容处理
letterEles = document.getElementsByClassName('active');
// 通过改变 level 的数值改变游戏进行的快慢
}, level * 1000)
// 暂停之后的开始游戏
gameStarts();
}
}
兼容性处理:如果用户浏览器无法执行document.everyByClassName方法,则采用我们自定义的解决方案。
if(!document.getElementsByClassName) {
document.getElementsByClassName = function(clsName) {
// 获取所有标签元素
var all = document.all;
// 放数组容器,进行遍历筛选
var all = [];
for(var i = 0;i < all.length;i ++) {
Array.push(all[i]);
}
return all;
}
}
4.封装函数,创建下落字母。
.active{
position: absolute;
top: -30px;
width: 30px;
height: 30px;
border-radius: 50%;
text-align: center;
line-height: 30px;
color: #fff;
font-weight: bolder;
}
function createLetter() {
var span = document.createElement('span');
// 赋予样式
span.className = 'active'
// 随机创建一个字母
var l = randLetter();
// 将字母插入到 span 里面
span.innerHTML = l;
// letters全局变量是存放所有随机产生的容器,需求g中有定义
letters += l;
// left数值 = 游戏界面的宽度 - 一个字母的宽度30
span.style.left = Math.floor(Math.random() * (gameW - 30)) + 'px';
// 使用下面封装的随机颜色函数
span.style.background = randBg();
// 创建完成之后追加到游戏界面中
game.appendChild(span);
// 字母运动
startMove(span, gameH, "top");
}
5.封装函数,随机产生字母。
function randLetter() {
var str = "abcdefghijklmnopqrstuvwxyz";
// 将大写的字母也包含进去
str += str.toUpperCase();
return str.charAt(Math.floor(Math.random() * str.length));
}
6.封装函数,生成16进制随机颜色值。
function randBg() {
var str = '0123456789abcdef';
var colorVal = '#';
for(var i = 0;i < 6;i ++) {
colorVal += str.charAt(Math.floor(Math.random() * str.length));
}
return colorVal;
}
7.封装函数,清除掉所有字母所在元素的定时器。
function clearAllLetters() {
for(var i = 0;i < letterEles.length;i ++) {
clearInterval(letterEles[i].timer);
}
}
8.封装函数,暂停之后点击开始按钮,继续开始游戏
function gameStarts() {
// 因为在调用这个函数的时候由于定义的letterEles还没有赋值,是undefined,所以我们这边进行排除一下
if(!letterEles) return;
for(var i = 0;i < letterEles.length;i ++) {
startMove(letterEles[i],gameH,"top");
// 在游戏开始的时候调用
}
}
9.封装函数,结束游戏。
function finished() {
// 清除单位时间内产生字母的定时器
clearInterval(c);
c = undefined;
// 当点击结束按钮的时候,将得分,速度,正确率都清零
score = 0;
s = 0;
accu = "0%";
// 清零完成后的重新加载数据
tip.children[0].firstElementChild.innerHTML = score;
tip.children[1].firstElementChild.innerHTML = accu;
tip.children[2].firstElementChild.innerHTML = s;
// 删除所有字母
for(var i = letterEles.length - 1;i >= 0;i --) {
// 从父元素开始查找
game.removeChild(letterEles[i]);
}
// 此时游戏画面已经清空,但是按钮不知道停留在什么状态。下面进行一步判断,将按钮状态调整为待开始状态。
if(oprate.firstElementChild.innerHTML == '暂停') {
oprate.firstElementChild.innerHTML = '开始'
}
}
需求E:完成退出游戏流程 注意 :该部分是位于需求2中的事件代理下的后续条件判断逻辑
// 处理结束游戏
if(target.className == 'finish') {
finished();
}
// 处理退出游戏
if(target.className == 'exit') {
// 首先处理结束游戏
finished();
// 显示游戏开始的界面,隐藏进入游戏的界面
game.style.display = 'none';
gameStart.style.display = 'block';
}
需求f:设置游戏难度
<div id="select">
<!-- 一开始是隐藏的 -->
<select name="" id="">
<option value="3">慢</option>
<option value="2">中</option>
<option value="1">快</option>
</select>
<!-- 关闭按钮 -->
<span id="close">关闭</span>
</div>
var selFir = select.firstElementChild;
// 默认游戏难度是慢的
var level = 3;
需求g:实现键盘打字,字母消失。
// 声明一个全局变量容器,用来存放 createLetter 函数所创建出来的字母
var letters = ''
需求h:键盘事件,敲击键盘,实现dom节点的消失
// 如果使用`onkeypress`的话,会区分大小写,这里我们不需要去区分
document.onkeyup = function(evt) {
// 兼容处理
var e = evt || window.event;
var codeVal = e.keyCode;
console.log(codeVal);
// 统计用户一共按了多少次规定范围下的按键,用于后面统计正确率
if(codeVal >= 65 && codeVal < 90) {
count ++;
}
// 根据键值找到对应的字符
var char = keyVal[codeVal];
if(char) {
var index = letters.search(eval('/' + char + "/gi"));
// index != -1; 说明找到了
if(index != -1) {
// 将对应元素的 dom 节点干掉
game.removeChild(letterEles[index]);
// 全局匹配 + 忽略大小写
var exp = eval('/' + char + '/gi');
// 将 exp 匹配成 ''
letters = letters.replace(exp, '');
// 成功消灭了一个 dom 元素,得一分
tip.firstElementChild.firstElementChild.innerHTML = ++ score;
endTimeStamp = new Date() * 1;
if(endTimeStamp - startTimeStamp <= 60 * 1000) {
tip.children[2].firstElementChild.innerHTML = score;
}else {
tip.children[2].firstElementChild.innerHTML = Math.ceil(score/Math.ceil((endTimeStamp - startTimeStamp)/(60*1000)));
}
}
// 实现正确率,toFixed(2)保留两位小数位
// 将数值插入到 tip 下的第二个子元素的第一个元素内容部分
tip.children[1].firstElementChild.innerHTML = (score / count * 100).toFixed(2) + '%'
}
// console.log(char)
}
需求i:找到键值
// 新建一个 js 文件,命名为 keyValue.js
var keyVal = {};
var str = 'abcdefghijklmnopqrstuvwxyz';
// a键的键值是65,往后加26个字母
for(var i = 65;i < 90;i ++) {
// 将对应字符以及其键值相对应。
keyVal[i] = str.charAt(i - 65);
}
console.log(keyVal)
需求j:计算并获得得分值、准确率和响应时间等关键指标。为此需设置全局变量并明确实现步骤以满足上述各项要求
// count 键盘一共按下多少次
var score = 0,accu = '0%',s = 0,count = 0;
// 定义开始时间和结束时间
var startTimeStamp = null,endTimeStamp = null;
总体实现思路总结:
- 在进入游戏界面后启动游戏流程
通过点击开始按钮触发字母下落的效果
当游戏处于暂停状态时,可执行暂停操作
通过清除定时器的方式实现游戏暂停
清除所有正在掉落字母的定时器
移除所有用于控制字母掉落速度的定时器
当退出游戏时,系统会结束当前游戏并展示初始界面
当结束游戏时,系统会清除所有相关于第1步和第2步中提到的内容
-
配置游戏难度
游戏的预设难度是缓慢运行的状态
当游戏处于运行中时是不支持调整难度的操作
当游戏处于暂停或准备状态时是允许进行难度设置的操作
难度设置会在调整后的时间点生效 -
通过键盘操作使字母消失
在全局变量中存储当前游戏界面内的所有字符
在全局变量字符串中定位输入的每一个字符的位置
删除对应于输入键位的目标元素 -
实现得分
当操作成功与显示窗口中的字符完全一致时,则会使得对应字母消失并获得一份积分,并将积分进行累加。 -
实现正确率
当游戏界面内的字符与按下键盘键不一致时,则视为错误操作。 -
实现速度
在规定时间内完成尽可能多的正确输入,在超出时间限制的情况下,则计算所完成的正确字符数量。 -
最后细节调优
当打字分数逐渐提升时,在字母掉落的速度上也取得了显著进展
在游戏界面的结束按钮或退出选项被点击时,请确保将正确率、得分以及当前速度全部归零
以上部分即为全部讲解
