練手WPF(三)——掃雷小遊戲的簡易實現(下)

十4、響應鼠標點擊事件
    (1)設置對應座標位置爲相應的前景狀態canvas

/// <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)鼠標點擊空白區域時,自動打開附近連片的空白區域。使用瞭如下遞歸方法。this

/// <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"。後臺代碼以下:spa

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);等待後補
    }

    // 是否勝利判斷
}

實現了主要狀態判斷和動做,踩雷和勝利判斷的狀況由於要使用動畫效果,因此這裏先留空,待後再作。

code

(4)添加鼠標右鍵功能
編輯xaml文件,在ForeCanvas元素內添加MouseRightButtonDown="ForeCanvas_MouseRightButtonDown",後臺代碼:orm

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();

        // 待加勝利檢測
    }
}

每單擊一次右鍵,將相應的單元格的圖片從原始-紅旗-問號-原始,循環遞增,並從新計算顯示的地雷數。

blog

十5、添加判斷是否勝利
看註釋的判斷。遞歸

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);
        }
    }
}

 

十6、踩雷後的處理
爲了避免讓主程序複雜化,咱們另外建立一個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);
   ...
相關文章
相關標籤/搜索