十四、响应鼠标点击事件
(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 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(); }
我们一开始在初始化阶段根据地雷数创建了bombs数组,现在遍历游戏区,在有地雷的位置创建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(); }
先到这里了,其他次要功能就不再这里啰嗦了。