zoukankan      html  css  js  c++  java
  • 练手WPF(三)——扫雷小游戏的简易实现(下)

    十四、响应鼠标点击事件
        (1)设置对应坐标位置为相应的前景状态

    /// <summary>
    /// 设置单元格图样
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="state"></param>
    private void SetCellFore(int x, int y, ForeState state)
    {
        if (state > ForeState.QUESTION || state < ForeState.NONE)
            return;
    
        _foreData[y, x] = (int)state;
    
        if (ForeCanvas.Children.Contains(_foreImage[y, x]))
            ForeCanvas.Children.Remove(_foreImage[y, x]);
    
        if (state == ForeState.NONE)
            return;
    
        _foreImage[y, x].Source = ImageHelper.CutImage(_bmpForeground,
            new Int32Rect((_foreData[y, x] - 1) * _cellSize.Width, 0, _cellSize.Width, _cellSize.Height));
        ForeCanvas.Children.Add(_foreImage[y, x]);
    }

    如果当前坐标位置设置的前景状态为允许值范围,则将其赋给相应的_foreData元素,并删除原来的图形。如果设置状态为问号或小红旗,则重新设置该图形。

    (2)鼠标点击空白区域时,自动打开附近连片的空白区域。使用了以下递归方法。

    /// <summary>
    /// 自动打开附近空白区域
    /// </summary>
    /// <param name="y"></param>
    /// <param name="x"></param>
    private void OpenNearToSpace(int y, int x)
    {
        if (y < 0 || y >= _gameLevel._colGrid || x < 0 || x >= _gameLevel._rowGrid)//越界
            return;
    
        if (_backData[y, x] == (int)BackState.BLANK && _foreData[y, x] == (int)ForeState.NORMAL)
        {
    
            _foreData[y, x] = (int)ForeState.NONE;        //已打开
            if (ForeCanvas.Children.Contains(_foreImage[y, x]))
                ForeCanvas.Children.Remove(_foreImage[y, x]);
    
            if (y - 1 >= 0 && x - 1 >= 0)
            {
                OpenNearToSpace(y - 1, x - 1);
            }
    
            if (y - 1 >= 0)
            {
                OpenNearToSpace(y - 1, x);
            }
    
            if (y - 1 >= 0 && x + 1 <= _gameLevel._rowGrid - 1)
            {
                OpenNearToSpace(y - 1, x + 1);
            }
    
            if (y + 1 <= _gameLevel._colGrid - 1 && x - 1 >= 0)
            {
                OpenNearToSpace(y + 1, x - 1);
            }
    
            if (y + 1 <= _gameLevel._colGrid - 1)
            {
                OpenNearToSpace(y + 1, x);
            }
    
            if (y + 1 <= _gameLevel._colGrid - 1 && x + 1 <= _gameLevel._rowGrid - 1)
            {
                OpenNearToSpace(y + 1, x + 1);
            }
            if (x + 1 <= _gameLevel._rowGrid - 1)
            {
                OpenNearToSpace(y, x + 1);
            }
    
            if (x - 1 >= 0)
            {
                OpenNearToSpace(y, x - 1);
            }
    
            Open8Box(y, x);     // 打开周围8个方格
        }
    
        return;
    }
    
    /// <summary>
    /// 打开周围8个方格
    /// </summary>
    /// <param name="y"></param>
    /// <param name="x"></param>
    private void Open8Box(int y, int x)
    {
        if (y - 1 >= 0 && x - 1 >= 0)
        {
            _foreData[y - 1, x - 1] = (int)ForeState.NONE;
            if (ForeCanvas.Children.Contains(_foreImage[y - 1, x - 1]))
                ForeCanvas.Children.Remove(_foreImage[y - 1, x - 1]);
        }
    
        if (y - 1 >= 0)
        {
            _foreData[y - 1, x] = (int)ForeState.NONE;
            if (ForeCanvas.Children.Contains(_foreImage[y - 1, x]))
                ForeCanvas.Children.Remove(_foreImage[y - 1, x]);
        }
        if (y - 1 >= 0 && x + 1 <= _gameLevel._rowGrid - 1)
        {
            _foreData[y - 1, x + 1] = (int)ForeState.NONE;
            if (ForeCanvas.Children.Contains(_foreImage[y - 1, x + 1]))
                ForeCanvas.Children.Remove(_foreImage[y - 1, x + 1]);
        }
        if (x - 1 >= 0)
        {
            _foreData[y, x - 1] = (int)ForeState.NONE;
            if (ForeCanvas.Children.Contains(_foreImage[y, x - 1]))
                ForeCanvas.Children.Remove(_foreImage[y, x - 1]);
        }
        if (x + 1 <= _gameLevel._rowGrid - 1)
        {
            _foreData[y, x + 1] = (int)ForeState.NONE;
            if (ForeCanvas.Children.Contains(_foreImage[y, x + 1]))
                ForeCanvas.Children.Remove(_foreImage[y, x + 1]);
        }
        if (y + 1 <= _gameLevel._colGrid - 1 && x - 1 >= 0)
        {
            _foreData[y + 1, x - 1] = (int)ForeState.NONE;
            if (ForeCanvas.Children.Contains(_foreImage[y + 1, x - 1]))
                ForeCanvas.Children.Remove(_foreImage[y + 1, x - 1]);
        }
        if (y + 1 <= _gameLevel._colGrid - 1)
        {
            _foreData[y + 1, x] = (int)ForeState.NONE;
            if (ForeCanvas.Children.Contains(_foreImage[y + 1, x]))
                ForeCanvas.Children.Remove(_foreImage[y + 1, x]);
        }
        if (y + 1 <= _gameLevel._colGrid - 1 && x + 1 <= _gameLevel._rowGrid - 1)
        {
            _foreData[y + 1, x + 1] = (int)ForeState.NONE;
            if (ForeCanvas.Children.Contains(_foreImage[y + 1, x + 1]))
                ForeCanvas.Children.Remove(_foreImage[y + 1, x + 1]);
        }
    }

    (3)添加鼠标左键事件
    编辑xaml文件,在ForeCanvas元素内添加MouseLeftButtonDown="ForeCanvas_MouseLeftButtonDown"。后台代码如下:

    private void ForeCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (_gameState == GameState.NONE)
            return;
    
        // 获取鼠标点击的格子位置
        Point mousePoint = e.MouseDevice.GetPosition(ForeCanvas);
        int dx = (int)(mousePoint.X / _cellSize.Width);
        int dy = (int)(mousePoint.Y / _cellSize.Height);
    
        // 已打开区域
        if (_foreData[dy, dx] == (int)ForeState.NONE)
            return;
    
        if (_backData[dy, dx] > (int)BackState.BLANK)
        {
            if (ForeCanvas.Children.Contains(_foreImage[dy, dx]))
            {
                ForeCanvas.Children.Remove(_foreImage[dy, dx]);
                _foreData[dy, dx] = (int)ForeState.NONE;
            }
        }
    
        if (_backData[dy, dx] == (int)BackState.BLANK)
        {
            OpenNearToSpace(dy, dx);
        }
    
        if (_backData[dy, dx] == (int)BackState.MINE)
        {
            if (ForeCanvas.Children.Contains(_foreImage[dy, dx]))
            {
                ForeCanvas.Children.Remove(_foreImage[dy, dx]);
                _foreData[dy, dx] = (int)ForeState.NONE;
            }
    
            // FriedMine(dy, dx);等待后补
        }
    
        // 是否胜利判断
    }

    实现了主要状态判断和动作,踩雷和胜利判断的情况因为要使用动画效果,所以这里先留空,待后再做。

    (4)添加鼠标右键功能
    编辑xaml文件,在ForeCanvas元素内添加MouseRightButtonDown="ForeCanvas_MouseRightButtonDown",后台代码:

    private void ForeCanvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (_gameState == GameState.NONE)
            return;
    
        // 获取鼠标点击的格子位置
        Point mousePoint = e.MouseDevice.GetPosition(ForeCanvas);
        int dx = (int)(mousePoint.X / _cellSize.Width);
        int dy = (int)(mousePoint.Y / _cellSize.Height);
    
        // 已打开区域
        if (_foreData[dy, dx] == (int)ForeState.NONE)
            return;
    
        if (ForeCanvas.Children.Contains(_foreImage[dy, dx]))
        {
            // 循环前景数据
            _foreData[dy, dx]++;
            if (_foreData[dy, dx] > 3)
            {
                _foreData[dy, dx] = 1;
            }
    
            // 设置相应的图片(原始、红旗、问号)
            _foreImage[dy, dx].Source = ImageHelper.CutImage(_bmpForeground,
                new Int32Rect((_foreData[dy, dx]- 1) * _cellSize.Width, 0, _cellSize.Width, _cellSize.Height));
    
            // 计算地雷数
            int num = 0;
            for (int y=0; y<_gameLevel._colGrid; y++)
            {
                for (int x=0; x<_gameLevel._rowGrid; x++)
                {
                    if (_foreData[y, x] == (int)ForeState.FLAG)
                        num++;
                }
            }
            textBlockMineNum.Text = (_gameLevel._mineNum - num).ToString();
    
            // 待加胜利检测
        }
    }

    每单击一次右键,将相应的单元格的图片从原始-红旗-问号-原始,循环递增,并重新计算显示的地雷数。

    十五、添加判断是否胜利
    看注释的判断。

    private bool IsWin()
    {
        bool flag = true;
        for (int y = 0; y < _gameLevel._colGrid; y++)
        {
            for (int x = 0; x < _gameLevel._rowGrid; x++)
            {
                // 地雷未被红旗标记
                if (_backData[y, x] == (int)BackState.MINE && _foreData[y, x] != (int)ForeState.FLAG)
                {
                    flag = false;
                    break;
                }
    
                // 存在未打开格子或标记为问号的格子
                if (_foreData[y, x] == (int)ForeState.NORMAL || _foreData[y, x] == (int)ForeState.QUESTION)
                {
                    flag = false;
                    break;
                }
            }
    
            if (!flag)
                break;
        }
    
        return flag;
    }

    将该方法添加到前景ForeCanvas控件的左、右键事件中进行调用。

    if (IsWin())
    {
        WinProcess();
    }

    这是胜利后的处理方法:先停止计时,然后重新覆盖前景图片,启用计时动画事件,从下往上逐消去前景图片:

    private void WinProcess()
    {
        // 停止计时
        _stopWatchGame.Stop();
        _timerSetTimeText.Stop();
        _gameState = GameState.NONE;
    
        // 重新覆盖前景图片
        ForeCanvas.Children.Clear();
        for (int y=0; y<_gameLevel._colGrid; y++)
        {
            for (int x=0; x<_gameLevel._rowGrid; x++)
            {
                _foreImage[y, x] = new Image();
                _foreImage[y, x].Source = ImageHelper.CutImage(_bmpForeground, new Int32Rect(0, 0,
                       _cellSize.Width, _cellSize.Height));
    
                _foreImage[y, x].SetValue(Canvas.LeftProperty, x * (double)_cellSize.Width);
                _foreImage[y, x].SetValue(Canvas.TopProperty, y * (double)_cellSize.Height);
                ForeCanvas.Children.Add(_foreImage[y, x]);
            }
        }
    
        // 动画行数
        iCount = _gameLevel._colGrid - 1;
    
        _timerWinAnim.Start();
    }

    这是计时动画事件方法:

    private void _timerWinAnim_Tick(object sender, EventArgs e)
    {
        if (iCount >= 0)
        {
            Storyboard sb1 = new Storyboard();
            DoubleAnimation daOpacity = null;
    
            for (int x = 0; x < _gameLevel._rowGrid; x++)
            {
                if (_backData[iCount, x] == (int)BackState.MINE)
                {
                    SetCellFore(x, iCount, ForeState.FLAG);
                    continue;
                }
    
                daOpacity = new DoubleAnimation();
                daOpacity.From = 1d;
                daOpacity.To = 0d;
                daOpacity.Duration = new Duration(TimeSpan.FromMilliseconds(600));
    
                Storyboard.SetTarget(daOpacity, _foreImage[iCount, x]);
                Storyboard.SetTargetProperty(daOpacity, new PropertyPath("(Image.Opacity)"));
    
                sb1.Children.Add(daOpacity);
            }
    
            sb1.Begin();
            iCount--;
        }
        else
        {
            _timerWinAnim.Stop();
    
             if (MessageBox.Show("你胜利了。要重新开始吗?", "恭喜", MessageBoxButton.OKCancel, MessageBoxImage.Information)
                        == MessageBoxResult.OK)
            {
                MenuGameStart_Click(null, null);
            }
        }
    }

    十六、踩雷后的处理
    为了不让主程序复杂化,我们另外创建一个Bomb的新类

    public class Bomb
    {
        Image bombImg = new Image();
        BitmapSource bmpBomb = null;
        const int FRAME_COUNT = 6;
        BitmapSource[] bmpSourceBomb = new BitmapSource[FRAME_COUNT];
        int currFrame = 0;
        DispatcherTimer timerBomb;
        Canvas _canvas = new Canvas();
        int dx, dy;
    
        public Bomb(Canvas canvas, int dx, int dy, BitmapSource bmpImgBomb)
        {
            _canvas = canvas;
            this.dx = dx;
            this.dy = dy;
            bmpBomb = bmpImgBomb;
    
            for (int i=0; i<FRAME_COUNT; i++)
            {
                bmpSourceBomb[i] = ImageHelper.CutImage(bmpBomb, new System.Windows.Int32Rect(i * 35, 0, 35, 35));
            }
    
            timerBomb = new DispatcherTimer();
            timerBomb.Interval = TimeSpan.FromMilliseconds(200);
            timerBomb.Tick += TimerBomb_Tick;
        }
    
        private void TimerBomb_Tick(object sender, EventArgs e)
        {
            bombImg.Source = bmpSourceBomb[currFrame];
            currFrame++;
            if (currFrame == 5)
            {
                currFrame = 0;
    
                if (_canvas.Children.Contains(bombImg))
                {
                    _canvas.Children.Remove(bombImg);
                }
    
                timerBomb.Stop();
            }
        }
    
        public void DrawBomb()
        {
            if (!_canvas.Children.Contains(bombImg))
                _canvas.Children.Add(bombImg);
    
            Canvas.SetLeft(bombImg, dy * 35);
            Canvas.SetTop(bombImg, dx * 35);
    
            timerBomb.Start();
        }
    
        public System.Drawing.Point GetPosition()
        {
            return new System.Drawing.Point(dx, dy);
        }
    }

    创建实例时,一并将相关参数传递过去。然后调用DrawBomb在启动内部计时器,该计时器顺序更新前景图片源,从而实现爆炸效果。

    主程序添加两个字段:

    private Bomb[] bombs;
    private int bombCount = 0;

    在InitGameData方法中进行初始化:

    bombs = new Bomb[_gameLevel._mineNum];

    踩雷动作方法:

    private void FriedMines(int y, int x)
    {
        if (_backData[y, x] == (int)BackState.MINE)
        {
            _backImage[y, x].Source = ImageHelper.CutImage(_bmpBomb, new Int32Rect(1 * _cellSize.Width, 0, 
                _cellSize.Width, _cellSize.Height));
        }
    
        int bombCount = 0;
        for (int j=0; j<_gameLevel._colGrid; j++)
        {
            for (int i=0; i<_gameLevel._rowGrid; i++)
            {
                if (_backData[j,i] == (int)BackState.MINE && _foreData[j,i] != (int)ForeState.NONE
                    && ForeCanvas.Children.Contains(_foreImage[j,i]))
                {
                    bombs[bombCount++] = new Bomb(ForeCanvas, j, i, _bmpBomb);
                }
            }
        }
    
        _timerBomb.Start();
    }

     现在遍历游戏区,在有地雷的位置创建Bomb实例,然后启动_timerBomb计时事件。

    private void _timerBomb_Tick(object sender, EventArgs e)
    {
        bombs[bombCount].DrawBomb();
        ForeCanvas.Children.Remove(_foreImage[bombs[bombCount].GetPosition().X, bombs[bombCount].GetPosition().Y]);
        bombCount++;
    
        if (bombCount == (bombs.Length - 1))
        {
            bombCount = 0;
            _timerBomb.Stop();
        }
    }

    该计时器按顺序调用Bomb类的DrawBomb方法,实现爆炸效果。

    在ForeCanavas的左键事件中调用该方法,处理踩雷事件。

    if (_backData[dy, dx] == (int)BackState.MINE)
    {
        if (ForeCanvas.Children.Contains(_foreImage[dy, dx]))
        {
            ForeCanvas.Children.Remove(_foreImage[dy, dx]);
        }
    
        FriedMines(dy, dx);
    
        _gameState = GameState.NONE;
        _stopWatchGame.Stop();
    }

    先到这里了,其他次要功能就不再这里啰嗦了。

    另外:发现个小问题,如果困难难度踩着地雷的话,按原来设定的时间间隙,99个地雷爆炸需要比较长时间。这样调整下:

    在InitGameData方法中的switch里,分别添加一行:

    case  Level.SIMPLE:
        ...
        _timerBomb.Interval = new TimeSpan(0, 0, 0, 0, 220);
        ...
    
    case Level.NORMAL:
        ...
       _timerBomb.Interval = new TimeSpan(0, 0, 0, 0, 120);
        ...
    
    case Level.HARD:
       ...
       _timerBomb.Interval = new TimeSpan(0, 0, 0, 0, 50);
       ...
  • 相关阅读:
    常用SQL语句
    H5内嵌原生app
    github 从一个仓库换到另一个仓库
    vue使用install函数把组件做成插件方便全局调用
    git 支持tree命令
    vue---vue2.x自定义plugin,给vue添加全局方法,原型上增加全局方法
    vue 生命周期函数
    登录拦截设置白名单-坑
    vue
    vue实现滚动条滚到相应高度触发动画的操作
  • 原文地址:https://www.cnblogs.com/moonblogcore/p/10945224.html
Copyright © 2011-2022 走看看