如何从零编写一个带虚拟键盘的俄罗斯方块游戏
score:00000000
俄罗斯方块是个好玩的小游戏,今天我们一起来编写一个网页游戏,程序很简单只要200多行javascript代码,想想是不是会有点小激动呢?
1、游戏布局设计
首先要画一个游戏方框“box”。我们使用DIV元素,是用来为HTML文档内大块(block-level)的内容提供结构和背景的元素。id="box"是它的名字,可以被document.getElementById("box")函数引用。style="……"中的内容定义了DIV的样式,包括宽度、字体、背景色、颜色、边界线条、文本阴影等参数。我们可以把这个DIV看作网页上的一个图层layer,在其中可以布置游戏中的各种方块。DIV的HTML代码如下:
<div id="box" style="width: 252px; font: 25px/25px 宋体; background: #000; color: #9f9; border: #999 20px ridge; text-shadow: 2px 3px 1px #0f0;"> </div>
我们在方框的下面添加一个记分板"scoreboard",可以用<h1>标签。HTML的<h1> - <h6> 标签可定义6种大小的标题。<h1> 定义最大的标题,<h6> 定义最小的标题。这是最大的标题格式,显示8位的游戏分数。
score:00000000
记分板的HTML代码如下:
<h1 id="scoreboard" style="text-align: left; margin-left: 30px;">score:00000000</h1>
下面再放两个图片按钮,一个用来调用游戏程序:
一个用来调出虚拟键盘:
到这里,一个最简的游戏布局设计就好了,下面我们编写javascript程序让它们动起来。
2、定义游戏对象
(1)“map”是23行3位16进制数组,用来定义游戏中的方块堆积图形,从0-21行初始值位0x801(0x表示16进制数),即二进制“100000000001”,两头的1代表左右边界,中间0代表空白区域,如果该位置被方块占了,就变成1,最后第22行为0xfff,即二进制“111111111111”,代表底部地面。0-21行的0构成一个10×22的长方形空间,就是方块下落的游戏空间。定义数组对象的代码如下,方括号中间用逗号分隔的23个数组元素值:
var map=[0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0x801,0xfff];
我们可以改写为更简练的代码如下:
var map=eval("["+Array(23).join("0x801,")+"0xfff]");
Array(23).join("0x801,")是Array对象的字符串连接函数,表示构造一个长度为23的空数组,再用"0x801,"作为分隔符,把数组元素(空字符串)连接成一个长字符串。"["+Array(23).join("0x801,")+"0xfff]"在连接的长字符串的前面连接"[",后面再连接"0xfff]",就得到一个方括号中间用逗号分隔的23个数组元素值的字符串。如果把这个字符串直接写在map= 等式的右边,map只能被赋值成为一个字符串变量。因此要使用eval()函数,可将字符串转换为代码执行,并返回值,这里map得到了一个23行3位16进制数组的赋值结果。
(2)用4个16进制数定义一个方块形状,代表4×4的网格,每个16进制数可转换为4位2进制数,代表网格中的一行。比如一个L形的方块形状,代码是0x4460,2进制表示如下:
0100
0100
0110
0000
这一个形状可以有4种方向变化,用一个长度4的数组表示:[0x4460,0x2e0,0x6220,0x740]
定义七种方块分别不同方向的数组tatris如下:
var tatris=[[0x6600],[0x2222,0xf00],[0xc600,0x2640], [0x6c00,0x4620],[0x4460,0x2e0,0x6220,0x740], [0x2260,0xe20,0x6440,0x4700], [0x2620,0x720,0x2320,0x2700]];
tatris是一个数组,它的元素也是数组,每个元素代表一个形状的各种方向变化。
(3)定义按键事件对象keycom。
这里使用的对象literals语法,封闭在花括号对{}中的一个对象的零个或多个“属性名:值”列表,以逗号分隔。例如:"38":"rotate(1)",属性名"38",代表按键号码,值为"rotate(1)",代表按键执行的函数。代码如下:
"38":"rotate(1)",up键—执行旋转90度函数
"40":"down()",down键—执行加速下落函数
"37":"move(2,1)",left键—执行左移函数
"39":"move(0.5,-1),right键—执行右移函数
var keycom={"38":"rotate(1)","40":"down()","37":"move(2,1)","39":"move(0.5,-1)"};//定义按键事件
var dia, pos, bak, run;
var score=0;
声明变量dia 方块数组,pos 运动位置对象,bak 运动映像对象,run 放映速度控制,score 游戏得分。
3、游戏物理学
即使最简单的游戏,也要为它设计一套物理系统,让这个迷你的世界有自己的物理定律,包括空间、随机、运动、控制和映射,7种方块按照这些规则运动变化。
(1)空间
游戏空间是在网页DIV元素“box”中的一个10×22矩形,构造的小小的方块世界。这个矩形使用一个map数组表示,其二进制初始值组成了一个被“1”U型包围起来的10×22全零矩阵,如下图:
(2)随机
随机是游戏的自由灵魂。在start()函数中,使用Math.random()函数从tatris数组的7种形状中随机生成1种方块,并在4个方向中随机选择一个初始方向。
bak和pos对象有4个属性,fk方块形状的4行12位二进制像素数组,y坐标,x坐标,s方向(s*90度旋转)。
function start(){ dia=tatris[~~(Math.random()*7)]; //~~去小数部分(取整)生成随机方块形状 bak=pos={fk:[],y:0,x:4,s:~~(Math.random()*4)};//fk方块形状,xy初始坐标,s随机方向 rotate(0); }
(2)运动
有旋转、下落、平移3种运动方式。
rotate(r)函数的参数r 代表旋转r*(90或180)度,从dia数组中按照下标(pos.s+r)%dia.length,得到方块的一个方向形状f。然后把f 通过右移位和左移位转换为4*12的二进制像素数组:如果f 的值为0x02e0,pos.x = 7,fk[] 表示平移的方块所在位置的四行像素点阵如下。update函数用来刷新fk在整个空间的显示。
fk[0] = 000000000000
fk[1] = 000100000000
fk[2] = 011100000000
fk[3] = 000000000000
updat(is())函数中is()作为条件参数,当方块碰到边界或底边或其他方块,则不移动且不更新显示。
function rotate(r){ var f=dia[pos.s=(pos.s+r)%dia.length];//f为移动方块形状的方向,r=1旋转90度 for(var i=0; i<4; i++) pos.fk[i]=(f>>(12-i*4)&15)<<pos.x;//生成移动方块的4行像素数组fk[] update(is()); }
下落down()函数每次pos.y+1,匀速下落运动。如果is(),表示方块下落时碰到了空间底边或者其他方块,则停止下落。这时判断是否一行全部排满(全一0xfff),如满则消除一行,再计算并显示奖励分数。数组的splice方法可以在指定位置移除一个或多个元素, unshift 方法将参数输入的元素插入数组开始位置,并返回该数组。
再判断第一行是否有方块,即 map[1] != 0x801,则游戏结束。否则继续产生新的方块。
update()执行一次无条件参数的显示刷新。
function down(){ ++pos.y;//方块下移 if(is()){ var prize = 0; for(var i=0; i<4 && pos.y+i<22; i++) if((map[pos.y+i]|=pos.fk[i])==0xfff) { map.splice(pos.y+i,1), map.unshift(0x801);//如果一行全为1,消除一行,添加空行 prize++;//消除1行奖励+1,2行+3,3行+6,4行+10 score+=prize; showscore();//显示新的分数 } if(map[1]!=0x801) return over();//第1行如果不空,则GameOver start(); } update(); }
平移move(t,k)函数的参数k是平移的距离。参数t 是像素左移(*2)或右移一位(*0.5)。is()函数判断平移时是否碰到边界。
function move(t,k){ pos.x+=k; for(var i=0; i<4; i++) pos.fk[i]*=t; update(is()); }
(4)控制
范围控制: is()函数使用&与运算判断方块是否碰到边界、底边或者其他已存在的方块,与运算的结果非0,说明fk与map对应的位置上都是1。方块不能再移动过去,pos = bak,即恢复前次保存的映像。
function is(){ for(var i=0; i<4; i++) if((pos.fk[i]&map[pos.y+i])!=0) return pos=bak; }
操纵控制:document.onkeydown按键事件定义了一个匿名函数function(e),参数e是侦测到的按键事件对象,e.keycode是按键号码。函数eval( codeString )将字符串codeString转换为代码执行,这里使用的是keycom[e.keycode],例如按键号码为"37",对应执行代码为左移move(2,1)。(e?e:event)是兼容写法,由于不同浏览器对JS的事件对象解析不同。主流浏览器中事件对象可以在事件的回调函数中通过参数传入,常规写法是简写为e。在低版本IE浏览器中,事件对象通过window对象中获取,即window.event。
document.onkeydown=function(e){ eval(keycom[(e?e:event).keyCode]);//执行按键事件代码对应的函数 };
放映速度控制run是setInterval()函数的返回值,每隔400毫秒执行down(),控制方块下落速度,同时调用update()函数刷新游戏空间的显示,如同电影胶片放映一样。
run=setInterval("down()",400);
(5)映射
网页游戏营造“轻”的美术风格,简单的美术元素对应简单的玩法,明亮的色调对应易用且轻松的体验。update(t)函数实现从游戏空间的二进制数字映射为直观的游戏画面输出。
首先把方块运动位置对象pos拷贝赋值给运动映像对象bak,相当于生成方块运动的一帧动画。判断条件参数t为true(用is()函数),则直接返回,不刷新显示。
map[i].toString(2)把map数组元素转换成二进制12位字符串,slice(1,-1)是切片函数,从第2位(下标为1)到最后1位(下标为-1,但不包括该位)截取中间10位字符串。<br/>是一个换行html标签,连接成为一个15字符22行的显示矩阵a2字符串。
function update(t){ bak={fk:pos.fk.slice(0),y:pos.y,x:pos.x,s:pos.s};//pos-->bak if(t) return; for(var i=0,a2=""; i<22; i++) a2+=map[i].toString(2).slice(1,-1)+"<br/>";//a2为22行显示矩阵 //toString(2)转为二进制(12位),slice()取第1位至第10位 for(var i=0,n; i<4; i++) if(/([^0]+)/.test(bak.fk[i].toString(2).replace(/1/g,"\u25a1"))) a2=a2.substr(0,n=(bak.y+i+1)*15-RegExp.$_.length-4)+RegExp.$1+a2.slice(n+RegExp.$1.length); //fk插入到a2正确的位置 document.getElementById("box").innerHTML=a2.replace(/1/g,"\u25a0").replace(/0/g,"\u3000"); //"\u25a1"空心块;"\u25a0"实心块;"\u3000"空白格 }
bak.fk[i].toString(2).replace(/1/g,"\u25a1"))将fk数组转为二进制数字符串,并把其中的1替换为空心方格□(\u25a1)。
([^0]+).test()是查找字符串中不是0字符的正则表达式。RegExp.input属性(或RegExp.$_属性)返回用于进行正则表达式查找的字符串。RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2,RegExp.$3,..RegExp.$99总共可以有99个匹配。
然后把RegExp.$1替换插入a2正确的位置n。
最后把a2中的1全部替换为实心方格■(\u25a0),0全部替换为空白符(\u3000),写入到游戏方框"box"的innerHTML属性值,显示在网页上。
这样我们就实现了一个简约风格的游戏画面的映射。
4、游戏管理学
一款游戏能否吸引人,从管理学的角度来看这个问题。管理归根到底是人的管理,以人性假设为前提。美国管理学家麦格雷戈(Douglas MC Gregor)于1957年提出了X-Y理论。麦格雷戈把传统管理学称为“X理论”,他自己的管理学说称为“Y理论”。
X理论
多数人天生懒惰,尽一切可能逃避工作;多数人没有抱负,宁愿被领导批评、怕负责任,视个人安全高于一切;对多数人必须采取强迫命令,软硬兼施的管理措施。
Y理论
一般人并不天生厌恶工作,多数人愿意对工作负责,并有相当程度的想象力和创造才能;控制和惩罚不是使人实现企业目标的唯一办法,还可以通过满足人对爱的需要、尊重的需要和自我实现的需要,使个人和组织目标融合一致,达到提高生产率的目的。
显然,一款吸引人的游戏应当以Y理论作为假设前提。
(1)目标
当一行排满了方块,就能消除一行得分。因为随机性,7种方块是不可能连续有利地出现,这时需要考虑如何放置的问题。如何适时适当地组合方块,最快地达至最有效的目标。然而假如出现失误,方块堆积造成难以弥补的空洞,一旦方块堆积到最顶行,游戏就将结束。尽可能地避免GAMEOVER的风险并获得更多的分数,是游戏的目标。在游戏结束后,页面上大大的开始游戏按钮,吸引玩家重复挑战。
function over(){ document.onkeydown=null; clearInterval(run); alert("GAME OVER"); }
(2)奖励
每次都得1分是让人安逸的,等待时机1次得3分是让人惊喜的,抓住时机1次得6分是会让人有成功感的,创造时机1次得10分是会让人有成就感的。
for(var i=0; i<4 && pos.y+i<22; i++) if((map[pos.y+i]|=pos.fk[i])==0xfff) { map.splice(pos.y+i,1), map.unshift(0x801);//如果一行全为1,消除一行,并在顶部增加一个空行 prize++;//消除1行奖励+1,2行+3,3行+6,4行+10 score+=prize; showscore();//显示新的分数 }
有时要故意堆积很高的方块保留很深的空洞,但一旦时机来到,这个大大的空洞会被一下消灭而得到高分。这是实现战略决策的战术技巧,同时还要冒方块堆积过高的风险,具有适当层次感的游戏难度。获取分数奖励并显示得分结果,与游戏的目标融合一致,得分的设计能提升玩家的优越感和些许粘稠度。
function showscore(){ var scoreboard=document.getElementById("scoreboard");//获得显示得分的网页对象 if (scoreboard) scoreboard.innerHTML='score:' + Array(8-(''+score).length+1).join(0)+score;//8位分数前面补零 }
(3)惯性
用户总要保持原来的使用习惯不变或者什么也不做,直到给他们的好处或者规则迫使他们改变这种使用习惯为止,这是用户行为的惯性定律。当玩家从电脑转移到手机上时,要为用户保留其操作惯性。在页面上设计一个虚拟键盘,通过一个醒目的图片按钮调出键盘,让用户在手机上也能流畅地玩游戏,不用学习新的操作,从而形成一定的用户粘性。
(function(exports){ var KeyBoard = function(keycom, options){ var body = document.getElementsByTagName('body')[0]; var DIV_ID = options && options.divId || '__w_l_h_v_c_z_e_r_o_divid'; if(document.getElementById(DIV_ID)){ body.removeChild(document.getElementById(DIV_ID)); } this.keycom = keycom; this.el = document.createElement('div');//生成网页键盘div显示 var self = this; var zIndex = options && options.zIndex || 1000;//键盘显示在最上层 var width = options && options.width || '100%';//键盘宽度 var height = options && options.height || '110px';//键盘高度 var fontSize = options && options.fontSize || '15px';//键盘字体大小 var backgroundColor = options && options.backgroundColor || '#fff';//键盘背景色 var opacity = options && options.opacity || '0.6';//键盘透明度 var TABLE_ID = options && options.table_id || 'table_0909099';//键盘表 var mobile = typeof orientation !== 'undefined';//手机屏幕方向 this.el.id = DIV_ID; this.el.style.position = 'fixed';//键盘固定在屏幕底部 this.el.style.left = 0; this.el.style.right = 0; this.el.style.bottom = 0; this.el.style.zIndex = zIndex; this.el.style.width = width; this.el.style.height = height; this.el.style.backgroundColor = backgroundColor; this.el.style.opacity = opacity;
程序中定义了一个立即执行函数(function(export){})(window),function外面添加的括号运算府就可以把匿名函数作为一个函数表达式进行执行。匿名函数的参数叫做 exports ,然后以 window 为实参调用了这个函数。
这样的好处是,除了在这个函数内显式地添加到 window 中的变量,其他的变量都是局部的,不会泄露到全局去。
在这个立即执行函数中,又定义了一个匿名函数:使用function声明函数,但未指定函数名,该匿名函数有两个参数,keycom和option,分别表示按键事件对象和虚拟键盘缺省外观参数。将此匿名函数赋予一个变量KeyBoard,作为虚拟键盘对象的构造器的方法。然后再输出这个变量KeyBoard成为window的全局对象:exports.KeyBoard = KeyBoard
虚拟键盘在网页上设计了一个DIV元素this.el,固定在浏览器底部,透明度0.6,飘浮在网页最上层(zIdex=1000)。
//CSS var cssStr = '<style type="text/css">'; cssStr += '#' + TABLE_ID + '{text-align:center;width:100%;height:80px;border-top:1px solid #CECDCE;background-color:#FFF;}'; cssStr += '#' + TABLE_ID + ' td{width:33%;border:1px solid #ddd;border-right:0;border-top:0;}'//每行3个键 if(!mobile){ cssStr += '#' + TABLE_ID + ' td:hover{background-color:#1FB9FF;color:#FFF;}';//非手机屏幕时,显示鼠标浮动变色 } cssStr += '</style>';
CSS层叠样式表(英文全称:Cascading Style Sheets)是一种用来修饰HTML或XML等网页外观样式风格的语言,定义在HTML元素的style属性中。
//Button 隐藏键盘 var btnStr = '<div style="width:60px;height:28px;background-color:#1FB9FF;'; btnStr += 'float:right;margin-right:5px;text-align:center;color:#fff;'; btnStr += 'line-height:28px;border-radius:3px;margin-bottom:2px;cursor:pointer;">hide</div>';
DIV元素也可以用来定义一个按钮hide,点击能够隐藏虚拟键盘,换成真实键盘操作模式。
//table var tableStr = '<table id="' + TABLE_ID + '" border="0" cellspacing="0" cellpadding="0">'; tableStr += '<tr><td id="save" style="background-color:#D3D9DF;">save</td><td id="38">▲</td>';//id是键盘事件号码 tableStr += '<td id="hide" style="background-color:#D3D9DF;">resume</td></tr>'; tableStr += '<tr><td id="37">◀</td><td id="40">▼</td><td id="39">▶</td></tr>'; tableStr += '</table>'; this.el.innerHTML = cssStr + btnStr + tableStr;//键盘网页代码
虚拟键盘共6个键,使用table元素定义一个2行3列的表格。每个单元格td的命名id表示按键号码,与真实按键号码一致。另外还定义了两个功能键,save和resume。
最后把CSS 、hide按钮和虚拟键盘table三段代码拼接在一起写入DIV元素的innerHTML中显示输出。
body.appendChild(this.el);//显示键盘
这条语句将在网页的文档(body)中添加一个定义好的DIV元素即this.el,显示出整个虚拟键盘。
(4)交互
达·芬奇说,“简单是最终的复杂性,而这个永恒的真理就像几个世纪以前也同样如此。”简单不代表空、原始或单一功能。相反,它意味着清晰,直观和有用。虚拟键盘与真实键盘符号相同,交互功能一致,产生熟悉的用户交互体验。用户可以随意选择两种不同的键盘方式操作。
function addEvent(e){ var ev = e || window.event; var clickEl = ev.element || ev.target; var value = clickEl.textContent || clickEl.innerText; var rb, sc; if (clickEl.tagName.toLocaleLowerCase() === 'td' && value !== "resume" && value !== "save") { var keycode; keycode = clickEl.id; if (self.keycom) eval(self.keycom[keycode]);//执行按键事件keycom } else if(clickEl.tagName.toLocaleLowerCase() === 'div' && value === "hide") { body.removeChild(self.el);//隐藏键盘 } }
addEvent(e)函数获取事件对象的网页元素,根据其id名字和value值,调用eval(self.keycom[keycode])执行相对应的按键事件,如果是隐藏键,则使用body.removeChild(self.ef)删掉虚拟键盘的DIV代码。
if(mobile){ this.el.ontouchstart = addEvent;//触屏事件 }else{ this.el.onclick = addEvent;//按键事件 }
mobile 变量是一个布尔类型值(typeof orientation !== 'undefined'),window.orientation是手机所特有的屏幕方向属性,如果window.orientation属性的类型不等于“未定义”,可以判断当前为手机浏览器,对this.el这个DIV元素,则调用触屏事件,否则调用鼠标按键事件。
(5)持久
当你玩累了关闭浏览器的时候你的游戏分数也随之消失了,这个体验会让玩家很懊恼。因此保存游戏记录,下次可以恢复前面的游戏继续玩,持久化用户体验,带来“微成长”的成就感。为什么是微成长?我们要的是“轻“,越简单越好。繁重的成长体系不会增加留存。用户选择小游戏的目的是偏于轻体验的减压,如果追求完整的成长体系,用户会选择有端的手游甚至PC端游戏。
resume键用于恢复游戏保存在cookie中的记录,map数组和score。save键则用于在cookie中保存游戏的这两个记录。其中map数组使用join方法,将数组元素通过“$”连接为一个字符串。
else if(clickEl.tagName.toLocaleLowerCase() === 'td' && value === "resume") { if (confirm('请确认恢复游戏保存的记录'))//恢复游戏保存的记录 { rb = getCookie('russiablocks').split('$');//读取map数组 sc = getCookie('score');//读取游戏分数 score = Number(sc); for(var i=0; i<map.length; i++) { map[i] = Number(rb[i]); } start();//从恢复点重新开始游戏 showscore();//重新显示游戏分数 } } else if(clickEl.tagName.toLocaleLowerCase() === 'td' && value === "save") { if (confirm('请确认保存游戏记录'))//保存游戏记录 { SetCookie('russiablocks',map.join('$'));//保存map数组,以$分隔数组元素 SetCookie('score',score);//保存游戏分数 } }
setCookie,getCookie,delCookie是三个cookie操作函数。设置cookie默认保存30天(expires参数)。cookie是 存于用户硬盘的一个文件,这个文件通常对应于一个域名,当浏览器再次访问这个域名时,便可以调用读写这个cookie文件。cookie是以键值对的形式保存的,即key=value的格式。各个cookie之间一般是以“;”分隔。
function SetCookie(name,value)//写cookies函数:两个参数,一个是cookie的名子,一个是值 { var Days = 30; //此 cookie 将被保存 30 天 var exp = new Date(); //new Date('December 31, 9998'); exp.setTime(exp.getTime() + Days*24*60*60*1000); document.cookie = name + '='+ escape (value) + ';expires=' + exp.toGMTString(); } function getCookie(name)//取cookies函数 { var arr = document.cookie.match(new RegExp('(^| )'+name+'=([^;]*)(;|$)')); if(arr != null) return unescape(arr[2]); return null; } function delCookie(name)//删除cookie { var exp = new Date(); exp.setTime(exp.getTime() - 1); var cval=getCookie(name); if(cval!=null) document.cookie= name + '='+cval+';expires='+exp.toGMTString(); }
在cookie 的名或值中不能使用分号(;)、逗号(,)、等号(=)以及空格。在cookie的名中做到这点很容易,但要保存的值是不确定的。如何来存储这些值呢?方 法是用escape()函数进行编码,它能将一些特殊符号使用十六进制表示,例如空格将会编码为“20%”,从而可以存储于cookie值中,而且使用此 种方案还可以避免中文乱码的出现。
当使用escape()编码后,在取出值以后需要使用unescape()进行解码才能得到原来的cookie值。
删除一个cookie ,可以将其过期时间设定为一个过去的时间。
最后创建KeyBoard对象实例:new KeyBoard(keycom)
使用new关键字后,意味着做了如下四件事情:
- 创建一个新的对象实例,这个对象的类型是object;
- 设置这个新的对象的内部、可访问性和[[prototype]]属性为构造函数(指prototype.construtor所指向的构造函数)中设置的;
- 执行构造函数KeyBoard(keycom),传入按键事件对象作为参数,初始化实例,关键字 this 被设定为该对象实例(instanceof KeyBoard);
- 返回新创建的虚拟键盘对象,显示在游戏网页上。
完整的游戏代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<div id="box" style="width: 252px; font: 25px/25px 宋体; background: #000; color: #9f9; border: #999 20px ridge; text-shadow: 2px 3px 1px #0f0;"> </div> <h1 id="scoreboard" style="text-align: left; margin-left: 30px;">score:00000000</h1> <p><img id="img1" onclick="javascript:var s=document.createElement('script');s.src='https://files.cnblogs.com/files/chris2002/russiablocks.js';document.body.appendChild(s);" src="http://images2015.cnblogs.com/blog/1011648/201608/1011648-20160820210401140-2008946463.jpg" alt="开始游戏" width="168" height="64" /><img id="img2" onclick="javascript:var s=document.createElement('script');s.src='https://files.cnblogs.com/files/chris2002/directionkey.js';document.body.appendChild(s);" src="https://img2018.cnblogs.com/blog/1011648/201903/1011648-20190315220403950-2039345945.jpg" alt="开启键盘" width="64" height="64" /></p>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var map=eval("["+Array(23).join("0x801,")+"0xfff]");//Array共23行3位16进制数 2 //alert("["+Array(23).join("0x801,")+"0xfff]"); 3 var tatris=[[0x6600],[0x2222,0xf00],[0xc600,0x2640],[0x6c00,0x4620],[0x4460,0x2e0,0x6220,0x740],[0x2260,0xe20,0x6440,0x4700],[0x2620,0x720,0x2320,0x2700]];//七种方块分别不同的方向数组 4 var keycom={"38":"rotate(1)","40":"down()","37":"move(2,1)","39":"move(0.5,-1)"};//定义按键事件 5 var dia, pos, bak, run; 6 var score=0; 7 8 function showscore(){ 9 var scoreboard=document.getElementById("scoreboard");//获得显示得分的网页对象 10 if (scoreboard) scoreboard.innerHTML='score:' + Array(8-(''+score).length+1).join(0)+score;//8位分数前面补零 11 } 12 function start(){ 13 dia=tatris[~~(Math.random()*7)]; //~~去小数部分(取整)生成随机方块形状 14 bak=pos={fk:[],y:0,x:4,s:~~(Math.random()*4)};//fk方块形状,xy初始坐标,s随机方向 15 rotate(0); 16 } 17 function over(){ 18 document.onkeydown=null; 19 clearInterval(run); 20 alert("GAME OVER"); 21 } 22 function update(t){ 23 bak={fk:pos.fk.slice(0),y:pos.y,x:pos.x,s:pos.s};//pos-->bak 24 if(t) return; 25 for(var i=0,a2=""; i<22; i++) 26 a2+=map[i].toString(2).slice(1,-1)+"<br/>"; 27 //a2为显示矩阵,取22行,toString(2)转为二进制(12位,第0位和第11位为1,中间为0),slice()取第1位至第10位 28 for(var i=0,n; i<4; i++) 29 if(/([^0]+)/.test(bak.fk[i].toString(2).replace(/1/g,"\u25a1"))) 30 a2=a2.substr(0,n=(bak.y+i+1)*15-RegExp.$_.length-4)+RegExp.$1+a2.slice(n+RegExp.$1.length); 31 //fk中的1替换为空心方块,([^0]+)是查找字符串中不是0的正则表达式,然后把fk插入a2正确的位置 32 //alert(RegExp.$_); 33 //alert(RegExp.$1); 34 //alert(a2); 35 //alert(a2.replace(/1/g,"\u25a0").replace(/0/g,"\u3000")); 36 document.getElementById("box").innerHTML=a2.replace(/1/g,"\u25a0").replace(/0/g,"\u3000"); 37 //"\u25a1"空心块;"\u25a0"实心块;"\u3000"空白格 38 } 39 function is(){ 40 for(var i=0; i<4; i++) 41 if((pos.fk[i]&map[pos.y+i])!=0) return pos=bak;//如果方块移动或下落的位置已有方块(或到达最后一行,或到达边界),则方块不能动 42 } 43 function rotate(r){ 44 var f=dia[pos.s=(pos.s+r)%dia.length];//f为移动方块形状的方向,r=1旋转90度 45 for(var i=0; i<4; i++) 46 pos.fk[i]=(f>>(12-i*4)&15)<<pos.x;//生成移动方块的4行像素数组fk[] 47 //alert(pos.fk[0].toString(2)+"|"+pos.fk[1].toString(2)+"|"+pos.fk[2].toString(2)+"|"+pos.fk[3].toString(2)) 48 update(is()); 49 } 50 function down(){ 51 ++pos.y;//方块下移 52 if(is()){ 53 var prize = 0; 54 for(var i=0; i<4 && pos.y+i<22; i++) 55 if((map[pos.y+i]|=pos.fk[i])==0xfff) 56 { 57 map.splice(pos.y+i,1), map.unshift(0x801);//如果一行全为1,消除一行 58 prize++;//消除1行奖励+1,2行+3,3行+6,4行+10 59 score+=prize; 60 showscore(); 61 } 62 if(map[1]!=0x801) return over();//第1行如果不空,则GameOver 63 start(); 64 } 65 update(); 66 } 67 function move(t,k){ 68 pos.x+=k; 69 for(var i=0; i<4; i++) 70 pos.fk[i]*=t; 71 update(is()); 72 } 73 74 document.onkeydown=function(e){ 75 eval(keycom[(e?e:event).keyCode]);//执行按键事件代码对应的函数 76 }; 77 78 start(); 79 run=setInterval("down()",400);//每400毫秒下落1行
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 (function(exports){ 2 var KeyBoard = function(keycom, options){ 3 var body = document.getElementsByTagName('body')[0]; 4 var DIV_ID = options && options.divId || '__w_l_h_v_c_z_e_r_o_divid'; 5 6 if(document.getElementById(DIV_ID)){ 7 body.removeChild(document.getElementById(DIV_ID)); 8 } 9 10 this.keycom = keycom; 11 this.el = document.createElement('div');//生成网页键盘div显示 12 13 var self = this; 14 var zIndex = options && options.zIndex || 1000;//键盘显示在最上层 15 var width = options && options.width || '100%';//键盘宽度 16 var height = options && options.height || '110px';//键盘高度 17 var fontSize = options && options.fontSize || '15px';//键盘字体大小 18 var backgroundColor = options && options.backgroundColor || '#fff';//键盘背景色 19 var opacity = options && options.opacity || '0.6';//键盘透明度 20 var TABLE_ID = options && options.table_id || 'table_0909099';//键盘表 21 var mobile = typeof orientation !== 'undefined';//手机屏幕方向 22 23 this.el.id = DIV_ID; 24 this.el.style.position = 'fixed';//键盘固定在屏幕底部 25 this.el.style.left = 0; 26 this.el.style.right = 0; 27 this.el.style.bottom = 0; 28 this.el.style.zIndex = zIndex; 29 this.el.style.width = width; 30 this.el.style.height = height; 31 this.el.style.backgroundColor = backgroundColor; 32 this.el.style.opacity = opacity; 33 34 //CSS 35 var cssStr = '<style type="text/css">'; 36 cssStr += '#' + TABLE_ID + '{text-align:center;width:100%;height:80px;border-top:1px solid #CECDCE;background-color:#FFF;}'; 37 cssStr += '#' + TABLE_ID + ' td{width:33%;border:1px solid #ddd;border-right:0;border-top:0;}'//每行3个键 38 if(!mobile){ 39 cssStr += '#' + TABLE_ID + ' td:hover{background-color:#1FB9FF;color:#FFF;}';//非手机屏幕时,显示鼠标浮动变色 40 } 41 cssStr += '</style>'; 42 43 //Button 隐藏键盘 44 var btnStr = '<div style="width:60px;height:28px;background-color:#1FB9FF;'; 45 btnStr += 'float:right;margin-right:5px;text-align:center;color:#fff;'; 46 btnStr += 'line-height:28px;border-radius:3px;margin-bottom:2px;cursor:pointer;">hide</div>'; 47 48 //table 49 var tableStr = '<table id="' + TABLE_ID + '" border="0" cellspacing="0" cellpadding="0">'; 50 tableStr += '<tr><td id="save" style="background-color:#D3D9DF;">save</td><td id="38">▲</td>';//id是键盘事件号码 51 tableStr += '<td id="hide" style="background-color:#D3D9DF;">resume</td></tr>'; 52 tableStr += '<tr><td id="37">◀</td><td id="40">▼</td><td id="39">▶</td></tr>'; 53 tableStr += '</table>'; 54 this.el.innerHTML = cssStr + btnStr + tableStr;//键盘网页代码 55 56 function SetCookie(name,value)//写cookies函数:两个参数,一个是cookie的名子,一个是值 57 { 58 var Days = 30; //此 cookie 将被保存 30 天 59 var exp = new Date(); //new Date('December 31, 9998'); 60 exp.setTime(exp.getTime() + Days*24*60*60*1000); 61 document.cookie = name + '='+ escape (value) + ';expires=' + exp.toGMTString(); 62 } 63 64 function getCookie(name)//取cookies函数 65 { 66 var arr = document.cookie.match(new RegExp('(^| )'+name+'=([^;]*)(;|$)')); 67 if(arr != null) return unescape(arr[2]); return null; 68 69 } 70 function delCookie(name)//删除cookie 71 { 72 var exp = new Date(); 73 exp.setTime(exp.getTime() - 1); 74 var cval=getCookie(name); 75 if(cval!=null) document.cookie= name + '='+cval+';expires='+exp.toGMTString(); 76 } 77 78 function addEvent(e){ 79 var ev = e || window.event; 80 var clickEl = ev.element || ev.target; 81 var value = clickEl.textContent || clickEl.innerText; 82 var rb, sc; 83 if (clickEl.tagName.toLocaleLowerCase() === 'td' && value !== "resume" && value !== "save") 84 { 85 var keycode; 86 keycode = clickEl.id; 87 if (self.keycom) eval(self.keycom[keycode]);//执行按键事件keycom 88 } 89 else if(clickEl.tagName.toLocaleLowerCase() === 'div' && value === "hide") 90 { 91 body.removeChild(self.el);//隐藏键盘 92 } 93 else if(clickEl.tagName.toLocaleLowerCase() === 'td' && value === "resume") 94 { 95 if (confirm('请确认恢复游戏保存的记录'))//恢复游戏保存的记录 96 { 97 rb = getCookie('russiablocks').split('$');//读取map数组 98 sc = getCookie('score');//读取游戏分数 99 score = Number(sc); 100 for(var i=0; i<map.length; i++) 101 { 102 map[i] = Number(rb[i]); 103 } 104 start();//从恢复点重新开始游戏 105 showscore();//重新显示游戏分数 106 } 107 } 108 else if(clickEl.tagName.toLocaleLowerCase() === 'td' && value === "save") 109 { 110 if (confirm('请确认保存游戏记录'))//保存游戏记录 111 { 112 SetCookie('russiablocks',map.join('$'));//保存map数组,以$分隔数组元素 113 SetCookie('score',score);//保存游戏分数 114 } 115 } 116 117 } 118 119 if(mobile){ 120 this.el.ontouchstart = addEvent;//触屏事件 121 }else{ 122 this.el.onclick = addEvent;//按键事件 123 } 124 body.appendChild(this.el);//显示键盘 125 } 126 127 exports.KeyBoard = KeyBoard; 128 129 })(window); 130 131 new KeyBoard(keycom);//调用虚拟键盘对象构造函数