原生JavaScript实现打字游戏

写在最前面

本文主要锻炼的是原生JavaScript的编程能力,运用了函数式编程的思想!

1.页面的排版与布局

主要分成两个页面:a.初始呈现出来的界面;b.点击开始进入游戏的界面。

a界面: 比较丑,大家注重功能实现就好,忽略ui
原生JavaScript实现打字游戏_第1张图片
原生JavaScript实现打字游戏_第2张图片
说明:
a.画页面不是我的目的,所以背景潦草的使用了一张图片简单实现了一下。
b.三个dom元素:两个按钮(开始、说明),点击说明按钮出现的说明内容。

// 初始呈现出来的界面

<div id="gameStart">
   
   <div id="start">div>
   
   
   <div id="describe">div>
   
   
   <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.界面: 点击开始按钮进入游戏界面
四个操作按钮:

  • 开始按钮 —> 游戏的暂停
  • 结束按钮
  • 退出游戏 —> 退出到游戏开始的界面
  • 设置 —> 设置当前游戏的难度
    原生JavaScript实现打字游戏_第3张图片
    点击设置,显示游戏难度选择。
    游戏难度设置

<div id="game">
   
   <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>0span>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)
}

需求d:进入游戏界面后的开始和暂停游戏
注意:该部分是位于需求2中的事件代理下的后续if操作。

// 定时器
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.getElementsByclassName方法,默认使用下面这个我们封装的方法。

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操作。

// 处理结束游戏
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的所有字母所在的元素
  • 实现游戏难度的设置
    游戏的默认难度是慢
    游戏在进行的过程中,是不允许设置游戏难度的
    在游戏暂停和开始之前,是允许设置游戏难度的
    游戏难度设置之后,是在关闭游戏难度设置之后生效的
  • 实现键盘打字,字母消失
    全局变量: 把当前游戏界面内出现的所有的字符放在该变量里
    根据键盘输入的字符,在全局变量字符串里,找到该字符的位置
    删除该字符所在的元素
  • 实现得分、正确率、速度
    得分: 输入与界面内出现的字符相同的时候,字母消失,得一份,累加
    正确率:游戏界面内出现的字符和按下的字母键不相符的时候,属于错误
    速度:一分钟之内输入正确的字符,不是一分钟的时候,按下的所有正确的字符的个数
  • 最后细节优化
    随着打字分数越来越高,让字母掉落的速度越来越快
    点击结束或者退出的时候,把正确率、得分、速度清零

以上便是全部的讲解
源码我放在了github上,欢迎下载!!!

你可能感兴趣的:(原生JavaScript,打字游戏,函数式编程)