ホームページ >ウェブフロントエンド >H5 チュートリアル >遊びながら学ぶHTML5 (9) ~テトリスのデータモデル~

遊びながら学ぶHTML5 (9) ~テトリスのデータモデル~

黄舟
黄舟オリジナル
2017-03-29 15:17:082257ブラウズ

1. データまたはインターフェイスから始めます

テトリス ミニゲームを作成するには、まず次の質問を検討してみましょう:

1. ブロックをどのように表現するか? 2.ブロックの色

3. ブロックの動かし方

4. ブロックの消し方

下を見続ける前に、少し考えてください。 。 。 。 。 。

上記の質問について考えると、それぞれの答えはインターフェイス、コントロール、プラットフォームに関連しています。つまり、.Net を使用している場合、それぞれの答えはコントロールの使用方法と使用方法を中心に展開することになります。 Windows のボディ、コントロールのどのイベントでどのプロパティを変更するかなどを考えている場合は、Microsoft の RAD 開発環境に毒されているということですので、すぐに Visual Studio を捨てて、他の軽量プログラミング言語に切り替えることをお勧めします。これにより、コントロールではなく問題そのものに集中できるようになります。

覚えておいてください: プログラム = データ構造 + アルゴリズム

インターフェイスはデータの外観にすぎませんが、データは問題の本質です。

以下では、テトリス ゲームのデータ モデルを段階的に構築していきます。モデル全体が確立されると、インターフェイスはありませんが、それでも完全に機能するテトリス ゲームであることがわかります。起こるすべてのこと 1 つ明らかなことは、私たちがそれを描いていないだけです。もちろん、操作しやすいインターフェイスは後で提供します。インターフェイスの問題については、次の記事で具体的に説明します。

2. 「形状」データ モデル

テトリスは永続的なミニゲームで、一般に 7 つの形状があります。つまり、

直線、S 字、Z 字、L 字、以下に示すように、L 字形、T 字形、正方形などを逆にします:

では、プログラム内でこれら 7 つの図形をどのように表現するのでしょうか?各形状は

4つの小さな正方形

で構成されており、4つの点で完全に表すことができることが分かりました。 しかし、質問はまた来ます、4 つの点の座標は何ですか?私が見つけた方法は次のとおりです:

各形状には独自の座標系があります (S 字型など)。これは次の図で表すことができます:

このようにして、S 字型のデータ モデルは次のようになります。 4 つの点で表される配列: [ [ 0, -1 ]、[ 0, 0 ]、[ -1, 0 ]、[ -1, 1 ] ] で構成されます。

同じ方法を使用して他の形状の配列モデルを作成し、これら 7 つの形状の配列モデルを組み合わせて大きな配列を形成できます。

また、各図形は単色にすることも、独自の色にすることもできます。色を追加するとプログラミングは複雑になりますが、それほど複雑ではないため、モデル内の色も考慮します。

最後に、形状配列と色の配列に簡単に適用できるように、各形状に番号を付けた方がよいでしょう。

上記の分析が完了したら、形状データ モデルのコードを与えることができます:

形状模型的代码 



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 ] ]
];

3. 形状の位置決めと回転

1. 位置決め

各形状について説明しました。形状を配置するために使用されるグローバル座標系もあります。そのため、形状の 4 つの点を独自の

座標系

からグローバル座標系に変換して、Shape の位置を指定するメソッドが必要です。 独自の座標系における S タイプの 4 つの点の座標が [ [ 0, -1 ]、[ 0, 0 ]、[ -1, 0 ]、[ -1, 1 ] ]

であるとします。

現在は にあります。 グローバル座標系の位置は [12,8] です

そして、グローバル座標系に変換された 4 点の座標は [ [ 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 を列に、y を行に変更してから、グローバル座標系に変換します。

2. 回転

回転は、形状の原点を中心として行われます。 式は、回転後の各点の座標と回転前の座標の関係は非常に簡単です。次のように (右に回転):

x' = y

y' = -x

注:

正方形の形状は回転しません。 上記の分析により、形状をグローバルに配置および回転するために使用される 2 つのグローバル メソッドが得られます:

全局定位和旋转的代码 



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&#39;=y, y&#39;=-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;
}

4. 空間の移動

前面我们说过,形状是由四个点组成的,而形状的移动空间也是由 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<h;row++)
        this.lines[row]=this.newLine();
}

//说明:间由 n 个格子组成的一行
Map.prototype.newLine=function(){
    var shapes=[];
    for(var col=0;col<this.width;col++)
        shapes[col]=NoShape;
    return shapes;
}

//判断一行是否全部被占用
//如果有一个格子为 NoShape 则返回 false
Map.prototype.isFullLine=function(row){
    var line=this.lines[row];
    for(var col=0;col<this.width;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<this.height;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<map.height;row++)
        for(var col=0;col<map.width;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次完成后,你才能看到最后一条直线,程序重新接受响应。这就是“算死”,解决的方法就是把绘制动作放在计时器或者线程里面,到下一篇,我们会解决这个问题。

 

以上が遊びながら学ぶHTML5 (9) ~テトリスのデータモデル~の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。