一、从数据出发还是从界面出发

要写一个俄罗斯方块小游戏,我们先来一块考虑一下下面几个问题:

1、用什么表示方块

2、怎么设置或者改变方块的颜色

3、怎么移动方块

4、怎么消除方块

请考虑一分钟后再继续向下看。。。。。。

如果你对上面几个问题思考,每一个答案都和界面、控件、平台有关的话,就是说假如你是用 .Net 的,你的每一个答案都是围绕着如何利用控件、如何使用窗体、在控件的哪个事件里面改变哪个属性等等,那么说明你被微软的 RAD 开发环境毒害的不浅,我建议你立刻扔掉 Visual Studio,改用其他轻量级的编程语言和开发平台,这样你可以更多的关注问题的本身,而不是控件。

记住:程序 = 数据结构 + 算法

界面只是数据的表象,而数据才是问题的本质。

下面,我们将一步一步建立一个俄罗斯方块小游戏的数据模型,当整个模型建立完毕后,我们会发现,虽然没有界面,仍然不妨碍这是一个功能完整的俄罗斯方块游戏,因为发生的每一件事情都很清楚,我们只是没把它画而已。当然,后面我们会给出一个操作简易的界面,等到下一篇,会专门探讨界面的问题。

二、“形状”的数据模型

俄罗斯方块是一个经久不衰的小游戏,最常见的版本中一般有七个形状,分别是:

直线型、S型、Z型、L型、反L型、T型、方形等,如下图:

那么我们在程序中如何表示这七个形状呢?我们发现每一形状都是四个小方块组成的,我们完全可以用四个点表示。

但是问题又来了,四个点的坐标分别是什么呢?我查到的方法是:每个形状都有一个自己的坐标系,比如S型,可以入下图表示:

这样,S型的数据模型可以表示为四个点组成的数组:[ [ 0, -1 ], [ 0, 0 ], [ -1, 0 ], [ -1, 1 ] ] 。

我们可以用同样的方法建立其他形状的数组模型,然后再将这七个形状的数组模型合起来组成一个大的数组。

另外,每个形状可以是单色,也可以有自己的颜色。增加颜色会增加编程的复杂度,但是也增加不了多少,所以我们的模型中也会考虑颜色。

最后,我们最好给每个形状一个编号,这样方便在形状数组和颜色数组中应用他们。

完成上面的分析后,我们就可以给出形状数据模型的代码了:形状模型的代码

Code highlighting produced by Actipro CodeHighlighter (freeware)

http://www.CodeHighlighter.com/

-->//各种形状的编号,0代表没有形状

NoShape=0;

ZShape=1;

SShape=2;

LineShape=3;

TShape=4;

SquareShape=5;

LShape=6;

MirroredLShape=7

//各种形状的颜色

Colors=["black","fuchsia","#cff","red","orange","aqua","green","yellow"];

//各种形状的数据描述

Shapes=[

[ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],

[ [ 0, -1 ], [ 0, 0 ], [ -1, 0 ], [ -1, 1 ] ],

[ [ 0, -1 ], [ 0, 0 ], [ 1, 0 ], [ 1, 1 ] ],

[ [ 0, -1 ], [ 0, 0 ], [ 0, 1 ], [ 0, 2 ] ],

[ [ -1, 0 ], [ 0, 0 ], [ 1, 0 ], [ 0, 1 ] ],

[ [ 0, 0 ], [ 1, 0 ], [ 0, 1 ], [ 1, 1 ] ],

[ [ -1, -1 ], [ 0, -1 ], [ 0, 0 ], [ 0, 1 ] ],

[ [ 1, -1 ], [ 0, -1 ], [ 0, 0 ], [ 0, 1 ] ]

];

三、定位和旋转形状

1、定位

我们上面说到每个形状都是在自己的坐标系里面描述的,另外还有一个全局坐标系,用来给形状定位,这样我们就需要一个方法将形状的四个点从自身坐标系转换到全局坐标系,从而给形状定位。

假如S型在自身坐标系中四个点的坐标为:[ [ 0, -1 ], [ 0, 0 ], [ -1, 0 ], [ -1, 1 ] ]

它当前在全局坐标系位置为:[12,8]

则,四个点转换为全局坐标系的坐标为:[ [ 0+12, -1+8 ], [ 0+12, 0+8 ], [ -1+12, 0+8 ], [ -1+12, 1+8 ] ]

这样,我们就完成了 S型 的全局坐标转换。

这里需要注意一个问题,形状自身坐标系是用 (x,y) 描述的,而全局坐标系为了逻辑上更直观,是用 (row,col) 描述的,所以我们在实际编程中并不是向上面那样转换的,而是:

[ [ -1+12, 0+8 ], [ 0+12, 0+8 ], [ 0+12, -1+8 ], [ 1+12, -1+8 ] ]

即:先将 x 变为 col ,y 变为 row ,再转换为全局坐标系。

2、旋转

旋转是在形状的自身坐标系中,并围绕形状的原点完成的,公式很简单,每个点旋转后的坐标与旋转前坐标的关系如下(向右旋转):

x' = y

y' = -x

注意:方块形状不发生旋转。

有了上面的分析,我们就可以给出两个全局方法,他们用来对形状进行全局定位和旋转:全局定位和旋转的代码

Code highlighting produced by Actipro CodeHighlighter (freeware)

http://www.CodeHighlighter.com/

-->//将形状自身的坐标系转换为 Map 的坐标系,row col 为当前形状原点在 Map 中的位置

function translate(data,row,col){

var copy=[];

for(var i=0;i<4;i++){

var temp={};

temp.row=data[i][1]+row;

temp.col=data[i][0]+col;

copy.push(temp);

}

return copy;

}

//向右旋转一个形状:x'=y, y'=-x

function rotate(data){

var copy=[[],[],[],[]];

for(var i=0;i<4;i++){

copy[i][0]=data[i][1];

copy[i][1]=-data[i][0];

}

return copy;

}

四、移动空间

前面我们说过,形状是由四个点组成的,而形状的移动空间也是由 m * n 个点组成的一个二维数组。

这里为了更直观的描述,我将 n 个点组成一条线 Line,再将 m 条 Line 组成形状的移动空间,我把它叫做 Map 。

我们有了这 m * n 个点有什么用呢?用处很简单,就是保存形状的编号,如果一个点没有被形状占用,则编号为 NoShape。这就是前面给出形状编号的用处,同时也是为什么要有一个 NoShape 编号的原因。

Map 应该具有什么功能呢?下面我列举了一些:

1、构造函数:这不用说了,n 个点组成一行 Line, m 行 Line 组成Map,每个点初始化成 NoShape

2、newLine:生成新的一行。为什么需要这个方法呢,因为除了构造函数中,游戏运行过程中我们也需要用到它,当一行或者几行被消除以后,我们需要在顶部假如一行或者几行新的Line

3、isFullLine(row):这个方法用来判断第 row 行是否满了,每次一个形状落地后,就需要对每一行进行这个判断,满了当然是消除了。

4、isCollide(data): data 是一个定位后的形状数据,这样我们就可以检查这些数据是否超出移动空间的上下左右边界,另外还检查数据的四个点是否已经被占用,这就是碰撞检测。

5、appendShape(shape_id,data):当一个形状落地以后,我们就应该将运行空间中某些点的值改变为这个形状的编号,我把这称为占用。

6、消除操作:这个功能没有单独列为一个方法,我把它放在 appendShape 方法中了。消除操作也很简单,发现某一行 isFullLine 了以后,在 lines 数组中移除这一行,并在 lines 数组的顶部加入一个空行即可。

有了上面的分析,我们就可以给出移动空间的代码了:移动空间的代码

Code highlighting produced by Actipro CodeHighlighter (freeware)

http://www.CodeHighlighter.com/

-->/*

* 说明:由 m 行 Line 组成的格子阵

*/

function Map(w,h){

//游戏区域的长度和宽度

this.width=w;

this.height=h;

//生成 height 个 line 对象,每个 line 宽度为 width

this.lines=[];

for(var row=0;row

this.lines[row]=this.newLine();

}

//说明:间由 n 个格子组成的一行

Map.prototype.newLine=function(){

var shapes=[];

for(var col=0;col

shapes[col]=NoShape;

return shapes;

}

//判断一行是否全部被占用

//如果有一个格子为 NoShape 则返回 false

Map.prototype.isFullLine=function(row){

var line=this.lines[row];

for(var col=0;col

if(line[col]==NoShape)

return false

return true;

}

/*

* 预先移动或者旋转形状,然后分析形状中的四个点是否有碰撞情况:

* 1:col<0 || col>this.width 超出左右边界

* 2:row==this.height ,说明形状已经到最底部

* 3:任意一点的 shape_id 不为 NoShape ,则发生碰撞

* 如果发生碰撞则放弃移动或者旋转

*/

Map.prototype.isCollide=function(data){

for(var i=0;i<4;i++){

var row=data[i].row;

var col=data[i].col;

if(col<0 || col==this.width) return true;

if(row==this.height) return true;

if(row<0) continue;

else

if(this.lines[row][col]!=NoShape)

return true;

}

return false;

}

//形状在向下移动过程中发生碰撞,则将形状加入到 Map 中

Map.prototype.appendShape=function(shape_id,data){

//对于形状的四个点:

for(var i=0;i<4;i++){

var row=data[i].row;

var col=data[i].col;

//找到所在的格子,将格子的颜色改为形状的颜色

this.lines[row][col]=shape_id;

}

//========================================

//形状被加入到 Map 中后,要进行逐行检测,发现满行则消除

for(var row=0;row

if(this.isFullLine(row)){

//将满的那一行替换成新的空,这一步主要是为了显示效果,可以不要!

//this.lines[row]=null;

//重绘 Map 消除效果

//onClearLine(row);

//将满行删除

this.lines.splice(row,1);

//第一行添加新的一行

this.lines.unshift(this.newLine());

//重绘 Map 整行下落效果

onDraw(this.lines);

}

}

}

五、游戏模型

我们有了游戏的数据模型,我们就可以读写他们了。所谓读好理解,所谓写就是改变他们,改变的方法当然是用户的操作了。

下面给出 GameModel 类,他维护三个主要的数据:

1、一个形状的编号,就是用户可以操作移动的那个形状

2、形状的全局位置,用 row col 表示

3、一个 Map,用它完成碰撞检测,添加等操作

另外,还抽象出几个用户的操作动作:

1、left:左移。将形状的全局坐标 col 减少 1 。请思考一下,这样就可以了吗?当然不行,我们还需要进行碰撞检测,如果已经在最左边,则放弃处理。

2、right:右移。同上。

3、rotate:旋转。同上。

4、down:下落。同上。下落过程中的碰撞检测有所不同,一旦发生碰撞,我们不能再放弃处理了,而是要将当前形状加入到空间中。

5、GameOver:下落过程中还需要进行一个检测就是游戏是否结束。如果当前形状在出生地点刚一下落就发生碰撞,说明已经到顶部了,则游戏结束。

有了上面的分析,我们就可以给出 GameModel 的代码:GameModel 代码

Code highlighting produced by Actipro CodeHighlighter (freeware)

http://www.CodeHighlighter.com/

-->/*

* 说明:GameModel 类

*/

function GameModel(w,h){

this.map=new Map(w,h);

this.born();

}

//出生一个新的形状

GameModel.prototype.born=function(){

//随机选择一个形状

this.shape_id=Math.floor(Math.random()*7)+1;

this.data=Shapes[this.shape_id];

//重置形状的位置为出生地点

this.row=1;

this.col=Math.floor(this.map.width/2);

//通知绘制移动效果,传回数据为形状的四个点在 Map 中的位置

onMove(this.shape_id,this.map,translate(this.data,this.row,this.col));

}

//向左移动

GameModel.prototype.left=function(){

this.col--;

var temp=translate(this.data,this.row,this.col);

if(this.map.isCollide(temp))

//发生碰撞则放弃移动

this.col++;

else

//通知绘制移动效果,传回数据为形状的四个点在 Map 中的位置

onMove(this.shape_id,this.map,temp);

}

//向右移动

GameModel.prototype.right=function(){

this.col++;

var temp=translate(this.data,this.row,this.col);

if(this.map.isCollide(temp))

this.col--;

else

onMove(this.shape_id,this.map,temp);

}

//旋转

GameModel.prototype.rotate=function(){

//正方形不旋转

if(this.shape_id==SquareShape) return;

//获得旋转后的数据

var copy=rotate(this.data);

//转换坐标系

var temp=translate(copy,this.row,this.col);

//发生碰撞则放弃旋转

if(this.map.isCollide(temp))

return;

//将旋转后的数据设为当前数据

this.data=copy;

//通知绘制移动效果,传回数据为形状的四个点在 Map 中的位置

onMove(this.shape_id,this.map,translate(this.data,this.row,this.col));

}

//下落

GameModel.prototype.down=function(){

var old=translate(this.data,this.row,this.col);

this.row++;

var temp=translate(this.data,this.row,this.col);

if(this.map.isCollide(temp)){

//发生碰撞则放弃下落

this.row--;

//如果在 1 也无法下落,说明游戏结束

if(this.row==1) {

//通知游戏结束

//onGameOver();

alert("Game Over")

return;

}

//无法下落则将当前形状加入到 Map 中

this.map.appendShape(this.shape_id,old);

//出生一个新的形状

this.born();

}

else

//通知绘制移动效果,传回数据为形状的四个点在 Map 中的位置

onMove(this.shape_id,this.map,temp);

}

六、一个简单的操作界面

虽然到现在为止,我们没有给出一行和界面有关的代码,但是整个游戏在逻辑上已经完全可以运行起来了,只是我们没有把他画出来而已,要想把他画出来也很简单。

注意上面给出的代码中很多地方调用了两个全局函数:onDraw 和 onMove ,这两个函数就是用来进行绘制的。

绘制的代码其实只占很少的一部分,其中一些绘图函数我为了方便对 HTML5 的 2D 函数进行了简单的封装,您完全可以用原生的 HTML5 函数,或者用您自己平台的绘图函数,因为他们本身不是太复杂。

另外有一个全局变量 Spacing ,他表示一个格子的宽度。

下面给出操作界面的代码:界面操作代码

Code highlighting produced by Actipro CodeHighlighter (freeware)

http://www.CodeHighlighter.com/

-->//每一格的间距,也即一个小方块的尺寸

Spacing=20;

//在内存中绘制一个小方块

function drawRect(color){

var temp=new Surface(Spacing,Spacing,"rgba(255,255,255,0.2)");//背景色

temp.fillRect(1, 1, Spacing-2, Spacing-2, color);//前景色

return temp;

}

var display= Display.attach(document.getElementById("html5_09_1"));

var model = new GameModel(display.width/Spacing,display.height/Spacing);

function onDraw(map){

//清屏

display.clear();

var lines=map.lines;

//依次绘制每一个非空的格子

for(var row=0;row

for(var col=0;col

var shape_id=lines[row][col];

if(shape_id!=NoShape){

var rect = drawRect(Colors[shape_id]);

var y=row * Spacing;

var x=col * Spacing;

display.draw(rect, x, y);

}

}

}

function onMove(shape_id,map,data){

onDraw(map);

//绘制当前的形状

for(var i=0;i<4;i++){

var y=data[i].row * Spacing;

var x=data[i].col * Spacing;

var rect = drawRect(Colors[shape_id]);

display.draw(rect, x, y);

}

}

function down(){

model.down();

}

function left(){

model.left();

}

function right(){

model.right();

}

function rotate_click(){

model.rotate();

}

七、如何改进

到现在为止,程序已经基本能运行起来了,但是还没有加入键盘操作,另外还有一个很大的问题就是:程序有时候会“算死”。为什么会出现这个现象呢?

做个实验,不管你用什么平台,你用绘图函数,先清屏,然后随机绘制一条直线,连续循环1000次。你会发现,前面999次,并看不到清屏和绘制效果,而且程序都会失去响应,等到1000次完成后,你才能看到最后一条直线,程序重新接受响应。这就是“算死”,解决的方法就是把绘制动作放在计时器或者线程里面,到下一篇,我们会解决这个问题。

4维俄罗斯方块 java_HTML5边玩边学(九)-俄罗斯方块之数据模型篇相关推荐

  1. python边玩边学_边听边学数据科学

    python边玩边学 Podcasts are a fun way to learn new stuff about the topics you like. Podcast hosts have t ...

  2. 乐高小颗粒履带机器人_乐高搭建+乐高编程…快带小朋友来漳州这家培训中心,边玩边学...

    厦门科技馆 培训中心 漳州校区 厦门科技馆培训中心漳州校区 如果你还以为乐高只是玩具 那就大错特错啦 乐高启蒙.搭建.编程 边玩边学边成长 亲手创建属于你的乐高小王国! 88元=3节乐高课程 前方一大 ...

  3. 朋友圈集赞,简洁壁纸,玩游戏学git

    骚操作来了 2.优品ppt http://www.ypppt.com/ 一个有情怀的免费PPT模板下载网站! PPT素材库,精美PPT素材大全,包括3D小人.商务人物图片.目录模板.箭头素材等各类PP ...

  4. 推荐28个网站,让你边玩边学

    今天我给大家推荐28个辅助你学习巩固知识的网站,让你边玩边学边记! 本文大致的目录结构如下: CSS相关的学习网站 1 xxxx 2 xxxx n xxxx JavaScript相关的学习网站 1 x ...

  5. 少儿学单词软件android,推荐4款免费的自然拼读APP,孩子在家可以边玩边学!

    原标题:推荐4款免费的自然拼读APP,孩子在家可以边玩边学! 昨天有家长留言问,有没有适合孩子使用的英语学习APP,Professor从网上搜罗了4款评价还不错的APP,供大家选择使用-- 壹 End ...

  6. 苏州运维交流研讨会-走进玩友时代顺利举办

    11月30日,由玩友时代.蜗牛游戏.新钛云服共同发起的苏州运维交流研讨会-走进玩友时代顺利举办,活动在玩友时代举行,活动围绕游戏运维,分享了五个主题演讲,并且进行了一个多小时的自由交流. 虽然天气有些 ...

  7. 编玩边学——高新科技启蒙教育,让程序编写铸就高新科技能手

    孩子教育一直以来全是一个关键的话题讨论,上千年之前的孟母,就为了更好地孩子教育难题而连搬了三次家,最后才给了孔子一个优良的生长发育自然环境,将孔子变成了一代圣贤.上千年之后,爸爸妈妈针对孩子教育仍然高 ...

  8. python边玩边学_边学边学

    python边玩边学 So you've decided to download and check out this newfangled "Unity" thing. You ...

  9. ironbot智能编程机器人_边玩边学,编程启蒙,IronBot机器人套件视频图文评测

    前言 当今世界充满竞争和挑战,你的对手可能不是人.早在2015年,教育部就颁发了指导意见,鼓励以编程为核心的,包括科学(S).技术(T).工程(E).艺术和航空(A),以及数学(M)五个方面的&quo ...

最新文章

  1. Jpeglib读取jpg文件
  2. 用Java线程获取优异性能(II)——使用同步连载线程访问关键代码部份
  3. python字典 items函数
  4. 9张图带你领略AI态势!
  5. PHP面向对象中new self( )和 new static( ) 的区别
  6. Err:ClassNotFoundException: org.apache.tomcat.util.log.SystemLogHandler
  7. 查看exe代码_【安全风险通告】Windows Type 1字体解析远程代码执行漏洞安全风险通告...
  8. python是开源的.它可以被移植_python是开源的,它可以被移植到许多平台上,是对的吗?...
  9. Eclipse之智能提示 actionscript javascript java开发环境智能提示
  10. 路由交换技术vlan、trunk、单臂路由、三层交换、链路聚合、STP
  11. note2 android4.3,三星N7100/note2官方4.3原版线刷版N7100ZCUENB1
  12. 局域网文档服务器搭建,局域网服务器的搭建.pdf
  13. 面向对象化(封装,继承,多态)
  14. 自动化测试(定位元素的方式,浏览器操作)
  15. eclipse官网下载安装教程
  16. 快排及其优化(C语言)
  17. 注解与APT、JavaPoet
  18. teamviewer 5分钟断线解决办法
  19. 【Unity】获取免费可商用的中文像素字体
  20. uni-app写小程序音乐播放器

热门文章

  1. oracle 的导入导出,Oracle 导入导出详细介绍
  2. 夏天来了,来吃鹅厂新瓜,小马哥已吃
  3. 华强北airpods三代连接安卓手机没声音_安卓手机体验华强北的顶配AirPods,“翻车”还是真香?...
  4. QQ小程序开发者工具及官网
  5. 电商系统购物车设计思路
  6. Unity-黑暗之魂复刻-翻滚、后跳功能
  7. html+css+javascript满屏雪花爱心520表白网站 (含音乐)520告白/七夕情人节/生日礼物/程序员表白必备
  8. 简单的集装箱号码识别
  9. 虚幻4引擎开发的手游_虚幻4引擎开发 《神佑》手游首次公开
  10. MIT 6.824 Raft论文精读