Silverlight:手把手教你写俄罗斯方块(四)

  一.接下来,我们将创建俄罗斯方块的控制操作类。右键单击项目,选择“添加->类”,建立一个Control.cs类。在这个类中,我们先定义一些初始变量:
        public Rect[,] board;             //游戏画板
        public Rect[,] readyBoard;        //准备方块画板
        public Block runBlock;            //移动中的方块
        public Block readyBlock;          //准备方块
        public event EventHandler GameOver;    //游戏结束事件
        public GameStatus status;              //游戏状态

        private DispatcherTimer timer;         //计数器

        private TextBlock score;               //分数版
        private bool[,] staticRect;            //存储静态方块坐标
        private const double speed = 300;      //游戏速度
        private const int width = 10;
        private const int height = 20;
        private const int r_width = 4;
        private const int r_height = 4;
        private Color color = Color.FromArgb(255, 192, 192, 192);         //静态方块颜色

由于我们的画板大小是200*400,所以我们用10*20个Rect填充,这里的width和height变量实际上可以看做是横向和纵向Rect的个数。bool类型的数组staticRect用来存储画板中的Rect状态,当为false的时候,表示这一格子没有方块,为true表示已经有静态的方块,然后我们为静态的方块定义一个颜色变量。准备方块画板,是在俄罗斯方块游戏中的下一个要出现的方块的展示画板,我们为这个画板定义大小为4*4,接着我们定义构造函数:

        public Control(TextBlock s)
        {
            this.score = s;
            board = new Rect[width, height];
            readyBoard = new Rect[r_width, r_height];
            staticRect = new bool[width, height];


            for (int i = 0; i < width; i++)
            {
                for (int j = 0; j < height; j++)
                {
                    board[i, j] = new Rect(i, j);
                }
            }
            for (int i = 0; i < r_width; i++)
            {
                for (int j = 0; j < r_height; j++)
                {
                    readyBoard[i, j] = new Rect(i, j);
                }
            }
            timer = new DispatcherTimer();
            timer.Interval = TimeSpan.FromMilliseconds(speed);
            timer.Tick += new EventHandler(timer_Tick);
        }

在构造函数中, 我们先接收UI层传来的TextBlock控件,用于记录得分 ,然后用Rect填充2个画板,最后定义定时器,定时器主要是起到方块自动下移的作用。

  二.在游戏中俄罗斯方块的产生是随机的,所以我们写一个随机产生俄罗斯方块的方法,

        /// <summary>
        /// 取得俄罗斯方块
        /// </summary>
        /// <param name="startX">起始横坐标</param>
        /// <param name="startY">起始纵坐标</param>
        /// <returns></returns>
        private Block GetBlock(int startX, int startY)
        {
            Random rd = new Random();
            int index = rd.Next(0, 7);
            switch (index)
            {
                case 0: return new Block_I(startX, startY);
                case 1: return new Block_L(startX, startY);
                case 2: return new Block_L2(startX, startY);
                case 3: return new Block_O(startX, startY);
                case 4: return new Block_T(startX, startY);
                case 5: return new Block_Z(startX, startY);
                case 6: return new Block_Z2(startX, startY);
                default: return new Block_I(startX, startY);
            }
        }

方块有了,接下来是方块的移动了,但是在移动前我们需要判断方块是否能够移动,所以我们写一个判断的方法:

#region 判断能否移动或变形
        /// <summary>
        /// 能否移动
        /// </summary>
        /// <param name="ps">方块</param>
        /// <param name="offsetX">偏移x,左偏移为负数</param>
        /// <param name="offsetY">偏移y,下偏移为正数</param>
        /// <returns></returns>
        private bool CanMove(Point[] ps, int offsetX, int offsetY)
        {
            for (int i = 0; i < 4; i++)
            {
                int x = (int)ps[i].X + offsetX;     //横向移动后的新x轴坐标
                int y = (int)ps[i].Y + offsetY;     //纵向移动后的新y轴坐标
                if (x < 0) return false;            //超出左边界
                if (x >= width) return false;       //超出右边界
                if (y < 0) return false;            //超出上边界
                if (y >= height) return false;      //超出下边界
                if (staticRect[x, y]) return false; //新坐标已经有方块
            }
            return true;
        }
        /// <summary>
        /// 能否移动
        /// </summary>
        /// <param name="offsetX">偏移x,左偏移为负数</param>
        /// <param name="offsetY">偏移y,下偏移为正数</param>
        /// <returns></returns>
        private bool CanMove(int offsetX, int offsetY)
        {
            for (int i = 0; i < 4; i++)
            {
                int x = (int)runBlock[i].X + offsetX;
                int y = (int)runBlock[i].Y + offsetY;
                if (x < 0) return false;
                if (x >= width) return false;
                if (y < 0) return false;
                if (y >= height) return false;
                if (staticRect[x, y]) return false;
            }
            return true;
        }
        #endregion

原理很简单,就是先取得方块移动或变形后新的坐标,然后通过一系列得判断,最后决定能否操作。关键是如何取得新坐标,方向移动很好判断,但是不同方块变形之后的坐标可能毫无规律,所以我们写一个重载方法,直接获得新坐标的数组。总之,如果传递了point[]参数(该数组大小一定为4,因为一开始我们就规定了所有的俄罗斯方块只可能由4个小方块组成)则根据传递进来的点数组判断,否则就按照runBlock的索引器来判断。

  三.有了判断方法,接下来,左右和下移动以及变形方法就好办了(俄罗斯方块可没有上移操作哦):

        /// <summary>
        /// 左移
        /// </summary>
        public void MoveLeft()
        {
            if (status != GameStatus.Play) return;
            if (CanMove(-1, 0))
            {
                runBlock.Hidden(board);
                runBlock.posX--;
                runBlock.Show(board);
            }
        }

        /// <summary>
        /// 右移
        /// </summary>
        public void MoveRight()
        {
            if (status != GameStatus.Play) return;
            if (CanMove(1, 0))
            {
                runBlock.Hidden(board);
                runBlock.posX++;
                runBlock.Show(board);
            }
        }

        /// <summary>
        /// 下移
        /// </summary>
        /// <returns></returns>
        public bool MoveDown()
        {
            if (status != GameStatus.Play) return false;
            if (CanMove(0, 1))
            {
                runBlock.Hidden(board);
                runBlock.posY++;
                runBlock.Show(board);
                return true;
            }
            return false;
        }

        /// <summary>
        /// 变形
        /// </summary>
        public void Change()
        {
            if (status != GameStatus.Play) return;
            Point[] vitualPoint = runBlock.GetChangedPoint();
            if (CanMove(vitualPoint, 0, 0))
            {
                runBlock.Hidden(board);
                runBlock.Change();
                runBlock.Show(board);
            }
        }

有的俄罗斯方块还拥有Drop效果,即方块直接落到最下端,这个实现效果实现起来并不困难,先停止计时器,然后不断下移,知道不能移动为止,这也就是为什么前面MoveDown方法要返回bool值的原因了。代码如下:

        public void Drop()
        {
            if (status != GameStatus.Play) return;
            timer.Stop();
            while (MoveDown()) ;
            timer.Start();
        }

好了,在这之前,我们再定义一个枚举变量,如果游戏并不是在进行中,那么所有的操作都是无效的:

        public enum GameStatus
        {
            Ready,
            Play,
            Pause,
            Over
        }

共有4种状态:准备就绪,进行中,暂定,结束。

  四.在俄罗斯方块游戏中,当方块落到最底部,或者碰到下方的方块时,这个方块就会停住并归为静态方块,如果有满行的情况,会消除这一行,然后准备方块变成了下一个移动方块,这是俄罗斯方块的游戏规则,基于这个逻辑,我们用下面的方法实现:

        /// <summary>
        /// 检查方块是否到底
        /// </summary>
        /// <param name="runBlock"></param>
        public void CheckAndOverBlock()
        {
            if (status != GameStatus.Play) return;
            bool over = false;
            for (int i = 0; i < 4; i++)
            {
                int x = (int)runBlock[i].X;
                int y = (int)runBlock[i].Y;
                if (y >= height - 1)//是否超出下边界,已经到达最低端
                {
                    over = true;
                    break;
                }
                if (staticRect[x, y + 1])//方块下面是否已存在别的方块
                {
                    over = true;
                    break;
                }
            }
            if (over)//如果确定当前方块已经结束
            {
                for (int i = 0; i < 4; i++)//把当前砖块归入静态方块类
                {
                    staticRect[(int)runBlock[i].X, (int)runBlock[i].Y] = true;
                }
                //检查是否有满行的情况,如果有则删除满行
                CheckFullAndDelRow();
                //重新绘制背景
                this.PaintBack();

                //产生新方块                
                runBlock = readyBlock;
                runBlock.posX = 4;
                runBlock.posY = 0;
                for (int i = 0; i < 4; i++)//游戏结束(当产生的新方块的位置已经包含有静态方块)
                {
                    if (staticRect[(int)runBlock[i].X, (int)runBlock[i].Y])
                    {
                        this.OnGameOver(null);
                        return;
                    }
                }
                runBlock.Show(board);
                //绘制准备方块背景
                this.PaintReadyBack();
                readyBlock = GetBlock(1, 1);
                readyBlock.Show(readyBoard);
            }
        }

在这个方法中,我们有使用了如下几个私有的方法,其中最重要的是消除满行的方法,代码如下:

    /// <summary>
        /// 检查是否满行,如果满行则重画
        /// </summary>
        /// <param name="runBlock"></param>
        private void CheckFullAndDelRow()
        {
            int low = (int)runBlock[0].Y;
            int high = (int)runBlock[0].Y;
            for (int i = 0; i < 4; i++)//获得该方块在画板中的最大和最小纵坐标
            {
                int y = (int)runBlock[i].Y;
                if (y < low)
                    low = y;
                if (y > high)
                    high = y;
            }
            for (int j = low; j <= high; j++)
            {
                bool rowfull = true;//判断是否为满行
                for (int i = 0; i < width; i++)
                {
                    if (staticRect[i, j] == false)
                    {
                        rowfull = false;
                        break;
                    }
                }
                if (rowfull)
                {
                    this.score.Text = (Int32.Parse(this.score.Text) + 1).ToString();
                    for (int k = j; k > 0; k--)
                    {
                        for (int i = 0; i < 10; i++)
                        {
                            staticRect[i, k] = staticRect[i, k - 1];
                        }
                    }
                    for (int i = 0; i < width; i++)//清除第0行
                    {
                        staticRect[i, 0] = false;
                    }
                }
            }

        }

以及重新绘制背景与重新绘制准备方块背景的方法,之所以要重新绘制是为了避免新方块与旧方块产生重叠效果,具体代码如下:

        /// <summary>
        /// 刷新背景
        /// </summary>
        private void PaintBack()
        {
            for (int i = 0; i < width; i++)
            {
                for (int j = 0; j < height; j++)
                {
                    if (staticRect[i, j])
                    {
                        board[i, j].Color = color;
                    }
                    else board[i, j].Color = null;
                }
            }
        }

        private void PaintReadyBack()
        {
            foreach (Rect r in readyBoard)
            {
                r.Color = null;
            }
        }

  五.那么我们前面提到的定时器效果就是不断检查方块是否结束,并且不断让方块下移:

        void timer_Tick(object sender, EventArgs e)
        {
            CheckAndOverBlock();
            MoveDown();
        }

  到此为止,我们已经完成了大部分逻辑,在下一节中,我将介绍逻辑层与UI层的交互,敬请关注。

posted @ 2011-05-22 16:32  疯狂的戴夫  阅读(766)  评论(1编辑  收藏  举报