C#——大作业之贪吃蛇

首先,要制作贪吃蛇游戏,就需要先了解它的游戏规则还有特点:

通过键盘上下左右控制蛇的方向,寻找吃的东西,每吃一口就能得到一定的积分,而且蛇的身子会越吃越长,身子越长玩的难度就越大,而且蛇的速度越来越快,不能碰墙,不能咬到自己的身体,更不能咬自己的尾巴。

需求分析:游戏的主界面,游戏开始的界面,显示得分的界面,蛇和食物的设计。

游戏界面需要合理使用各种控件布局,蛇移动的区域应该是两种颜色交替排布的方格组成,这样才能清晰的看见蛇的移动,也是为了增加游戏体验。

蛇的设计:根据面向对象的设计思想,可以以一个方格为单位,然后多个方格就可以组成一条蛇了,为了区分蛇头和蛇身,还需要加一个bool类型的字段判断这方格是不是蛇头。

源文件及说明
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="WPF 贪吃蛇:0"
        SizeToContent="WidthAndHeight"
        Loaded="Window_Loaded"
        ContentRendered="Window_ContentRendered" KeyUp="Window_KeyUp" WindowStyle="None" ResizeMode="NoResize" MouseLeftButtonDown="Window_MouseLeftButtonDown"
        >
    <DockPanel Background="Black">
        <Grid DockPanel.Dock="Top" Name="pnlTitleBar">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Grid.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="FontFamily" Value="Consolas"/>
                    <Setter Property="Foreground" Value="White"/>
                    <Setter Property="FontSize" Value="24"/>
                    <Setter Property="FontWeight" Value="Bold"/>
                </Style>
                <Style TargetType="Button">
                    <Setter Property="Background" Value="Transparent"/>
                    <Setter Property="Foreground" Value="White"/>
                    <Setter Property="FontWeight" Value="Bold"/>
                    <Setter Property="FontSize" Value="20"/>
                    <Setter Property="BorderThickness" Value="0"/>
                    <Setter Property="Padding" Value="10,0"/>
                </Style>
            </Grid.Resources>
            <WrapPanel Margin="10,0,0,0">
                <TextBlock>Score:</TextBlock>
                <TextBlock x:Name="tbStatusScore">0</TextBlock>
            </WrapPanel>
            <WrapPanel Grid.Column="1">
                <TextBlock>Speed:</TextBlock>
                <TextBlock x:Name="tbStatusSpeed">0</TextBlock>
            </WrapPanel>
            <Button Focusable="False" Visibility="Hidden" x:Name="btPause"  Grid.Column="2" Content="II" Background="Transparent" Click="BtPause_Click"/>
            <Button  Grid.Column="3" Content="X" Background="Transparent" Click="Button_Click"/>
        </Grid>
        <Border BorderThickness="5" BorderBrush="Black">
            <Canvas x:Name="GameArea" ClipToBounds="True" Width="400" Height="400">
                <Border Visibility="Collapsed" Panel.ZIndex="1" x:Name="bdrWelcomeMessage" Width="300" Height="300" BorderThickness="2" BorderBrush="Silver" Canvas.Left="50" Canvas.Top="50">
                    <StackPanel Background="AliceBlue">
                        <TextBlock Text="WPF贪吃蛇" FontSize="50" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,20,0,0"/>
                        <TextBlock FontWeight="Bold" TextWrapping="Wrap" FontSize="16" Margin="20" Text="用方向键来控制绿色蛇去吃红色的苹果。请注意,不要撞到墙壁,也不要撞到自己的身体,一旦撞到,则游戏结束!"/>
                        <TextBlock Text="按空格键开始游戏" FontSize="24" Foreground="Maroon" HorizontalAlignment="Center" FontWeight="Bold"/>
                    </StackPanel>
                </Border>
                <Border Visibility="Visible" Panel.ZIndex="1" x:Name="bdrEndOfGame" Width="300" Height="300" BorderThickness="2" BorderBrush="Silver" Canvas.Left="50" Canvas.Top="50">
                    <StackPanel Background="AliceBlue">
                        <TextBlock Text="OH NO!" FontSize="40" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,20,0,0"/>
                        <TextBlock FontWeight="Bold" TextWrapping="Wrap" FontSize="26" Margin="20" Text="...你的蛇,死了!" HorizontalAlignment="Center"/>
                        <TextBlock x:Name="tbFinalScore" HorizontalAlignment="Center" Margin="0,0,0,20" FontSize="48">0</TextBlock>
                        <TextBlock Text="按空格键开始游戏" FontSize="24" Foreground="Maroon" HorizontalAlignment="Center" FontWeight="Bold"/>
                    </StackPanel>
                </Border>
            </Canvas>
        </Border>
    </DockPanel>
</Window>

1.最上方设计的状态面板,有分数和蛇的移动速度,利用Grid可以更好地调整各个板块所占地比例,然后通过<Grid.Resourse>分别对TextBlock和Button控件定义样式。
2.“暂停“按钮默认一开始不显示,即Visibility=”Hidden“,分数和速度是需要实时改变的,可以通过x:name, x:Name 用来在XAML中表示一个指定对象的名称,然后就可以在逻辑代码里来改变分数和速度的显示。例如:tbStatusScore.Text = currentScore.ToString();
3.Button控件里,属性click来绑定源程序里的两个函数,分别实现暂停和关闭窗口功能,而暂停和关闭按钮直接用“Ⅱ”和“X”来展示了,其实也可以用path绘图功能,能简单表示就不设计得那么麻烦了。
4.还需要画出地图和蛇,我选择的是Canvas控件,因为它可以完全自由地对控件的位置进行安排

<Canvas x:Name="GameArea" ClipToBounds="True" Width="400" Height="400">

这是Canvas控件的定义,同样的,用x:name方便对游戏区域进行操作,这里的Width和Height定义了游戏区域大小,而且还需要用它和方格大小来计算出行和列的格子数量。

是这样计算的:

private void Window_ContentRendered(object sender, EventArgs e)//窗口一启动即 执行
        {
            DrawGameArea();

            maxX = (int)(GameArea.ActualWidth / SnakeSquareSize);
            maxY = (int)(GameArea.ActualHeight / SnakeSquareSize);

            bdrEndOfGame.Visibility = Visibility.Collapsed;//结束界面隐藏
            bdrWelcomeMessage.Visibility = Visibility.Visible;//展示游戏开始界面
        }

其中画游戏地图DrawGameArea函数如下:

        private void DrawGameArea()
        {
            int nextX = 0;//下一个方格相对顶部的距离
            int nextY = 0;//下一个方格相对左边界的距离
            Boolean netxtIsOdd = false;//用来判断下一个方格决定画哪种颜色
            while (true)
            {
                Rectangle rectangle = new Rectangle//用Rectangle来当格子
                {
                    Width = SnakeSquareSize,
                    Height = SnakeSquareSize,//宽高初始化是20,const int SnakeSquareSize = 20;
                    Fill = netxtIsOdd ? Brushes.White : Brushes.LightGray//白色和亮灰色
                };
                GameArea.Children.Add(rectangle);//先加进Canvas
                Canvas.SetTop(rectangle, nextY);
                Canvas.SetLeft(rectangle, nextX);//再调整位置
                netxtIsOdd = !netxtIsOdd;
                nextX += SnakeSquareSize;
                if (nextX >= GameArea.ActualWidth)//这里画的顺序是:由上到下,由左到右
                {
                    nextX = 0;
                    nextY += SnakeSquareSize;
                    netxtIsOdd = !netxtIsOdd;
                }
                if (nextY >= GameArea.ActualHeight)
                {
                    break;
                }
            }
        }

现在游戏区域有了,接下来就是画“蛇“,这里我另外建了一个类定义蛇的身体:

class SnakePart//蛇的身体,但不是整条蛇,即一个小方格
    {
        public UIElement UiElement { set; get; }//UIElement 是 WPF 核心级实现的基类
        public Point Position { set; get; }
        public Boolean IsHead { set; get; }//表示这个方格是不是蛇头
    }

要变成蛇,所以需要由许多SnakePart组成,即List snakeParts = new List();
蛇也有运动方向,定义枚举:

public enum SnakeDirection
    {
        Left,
        Right,
        Up,
        Down
    }

还定义了蛇身和蛇头颜色,来进行区分

//蛇身颜色
SolidColorBrush snakeBodyBrush = Brushes.Green;
//蛇头颜色
SolidColorBrush snakeHeadBrush = Brushes.Blue;
//蛇身
List<SnakePart> snakeParts = new List<SnakePart>();
//爬行方向
SnakeDirection snakeDirection = SnakeDirection.Right;

画蛇代码:

private void DrawSnake()
        {
            foreach (var snakePart in snakeParts)//遍历list,画出蛇的每一部分
            {
                if (snakePart.UiElement == null)
                {
                    snakePart.UiElement = new Rectangle//还是用Rectangle控件
                    {
                        Width = SnakeSquareSize,
                        Height = SnakeSquareSize,
                        Fill = snakePart.IsHead ? snakeHeadBrush : snakeBodyBrush//根据是不是头决定颜色
                    };
                    GameArea.Children.Add(snakePart.UiElement);//把一个格子先加进去
                    Canvas.SetTop(snakePart.UiElement, snakePart.Position.Y);//再调整位置
                    Canvas.SetLeft(snakePart.UiElement, snakePart.Position.X);
                }
            }
        }

蛇的食物:

UIElement snakeFood = null;
//食物颜色为红色,食物的位置利用随机数来决定:
SolidColorBrush foodBrush = Brushes.Red;
//随机数
Random random = new Random();
private Point GetNextFoodPosition()//随机出现一个食物
        {
            int foodX = random.Next(0, maxX) * SnakeSquareSize;
            int foodY = random.Next(0, maxY) * SnakeSquareSize;//食物的坐标
            foreach (var snakePart in snakeParts)
            {
                if ((snakePart.Position.X == foodX) && (snakePart.Position.Y == foodY))//不能定位到蛇身上
                {
                    return GetNextFoodPosition();//递归,如果下一个食物的位置在蛇的身上,重新返回一个食物位置
                }
            }
            return new Point(foodX, foodY);
        }
        private void DrawSnakeFood()
        {
            Point foodPosition = GetNextFoodPosition();//调用上面的函数得到食物位置
            snakeFood = new Ellipse//用圆形
            {
                Width = SnakeSquareSize,
                Height = SnakeSquareSize,
                Fill = foodBrush//红色的食物//食物颜色SolidColorBrush foodBrush = Brushes.Red;
            };
            GameArea.Children.Add(snakeFood);//先加进去
            Canvas.SetLeft(snakeFood, foodPosition.X);
            Canvas.SetTop(snakeFood, foodPosition.Y);//再调整位置
        }

蛇的移动速度和语言播报:

//游戏是否在运行,游戏结束或者暂停后,即为false 
Boolean isGameRuning = true;
//语音合成
private SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer();
//定时器
DispatcherTimer gameTickTimer = new DispatcherTimer();
//蛇具有一定的初始长度
const int SnakeStartLength = 3;
//蛇初始速度
const int SnakeStartSpeed = 500;
//蛇的最大速速
const int SnakeSpeedThreshold = 100;

其实蛇每次都只移动一个空格的,那他是怎么可以跑的快呢?
原因就是:gameTickTimer.Interval,它决定了计时器事件执行的间隔,计时器事件如下:

private void GameTickTimer_Tick(object sender, EventArgs e) //计时器事件
        {
            if (isGameRuning == false) return;
            MoveSnake();//游戏正在运行。就一直移动蛇
        }

接下来看“蛇”是如何移动的:

private void MoveSnake()
        {
            while (snakeParts.Count >= snakeLength)//这里的循环是针对一开始蛇还没完全出来的时候,就不需要移除蛇尾巴
            {//蛇尾巴为是list的第一个元素
                GameArea.Children.Remove(snakeParts[0].UiElement);//在Canvas里移除掉尾巴
                snakeParts.RemoveAt(0);//
            }
            foreach (var snakePart in snakeParts)
            {
                (snakePart.UiElement as Rectangle).Fill = snakeBodyBrush;//换上身体的颜色
                snakePart.IsHead = false;//蛇头变成身体
            }
            SnakePart snakeHead = snakeParts[snakeParts.Count - 1];//蛇头是list最后一个元素
            double nextX = snakeHead.Position.X;
            double nextY = snakeHead.Position.Y;//当前蛇头坐标
            switch (snakeDirection)//蛇的运动方向
            {//计算下一个蛇头的坐标
                case SnakeDirection.Left:
                    nextX -= SnakeSquareSize;
                    break;
                case SnakeDirection.Right:
                    nextX += SnakeSquareSize;
                    break;
                case SnakeDirection.Up:
                    nextY -= SnakeSquareSize;
                    break;
                case SnakeDirection.Down:
                    nextY += SnakeSquareSize;
                    break;
                default:
                    break;
            }
            snakeParts.Add(new SnakePart //添个新蛇头
            {
                Position = new Point(nextX, nextY),
                IsHead = true
            }
            );
            DrawSnake();//重新画出来
            DoCollisionCheck();//每次移动完都做判断,是否撞墙,是否撞到身体,是否吃到食物
        }

所以蛇移动的本质其实是删掉蛇尾,蛇头变蛇身,然后再加一个蛇头,最后重新画出来,就形成蛇移动的画面了。其中的while循环,针对蛇没有完全出来和吃食物的情况,吃到食物或者蛇没有完全出来都不需要把尾巴去掉。

蛇每次移动都要判断蛇有没有死掉,或者吃到食物,蛇撞到身体和墙壁都结束游戏:

private void DoCollisionCheck()
        {
            SnakePart snakeHead = snakeParts[snakeParts.Count - 1];
            if ((snakeHead.Position.X == Canvas.GetLeft(snakeFood)) &&//蛇吃食物
                (snakeHead.Position.Y == Canvas.GetTop(snakeFood))//判断,蛇头的位置和食物的位置重合
                )
            {
                EatSnakeFood();
                return;
            }
            if ((snakeHead.Position.Y < 0) ||
                (snakeHead.Position.X < 0) ||
                (snakeHead.Position.Y >= GameArea.ActualHeight) ||
                (snakeHead.Position.X >= GameArea.ActualWidth)
                )//蛇有没有越界
            {
                EndGame();
                return;
            }
            foreach (var snakeBodyPart in snakeParts.Take(snakeParts.Count - 1))
            {   //蛇有没有撞到自己的身体
                if ((snakeHead.Position.X == snakeBodyPart.Position.X) &&
                    (snakeHead.Position.Y == snakeBodyPart.Position.Y)
                    )
                {
                    EndGame();
                    return;
                }
            }
        }

如果蛇吃到食物:

private void EatSnakeFood()
        {
            snakeLength++;//蛇长增加一个
            currentScore++;//分数增加一个
            //增加游戏难度,即降低执行计时器事件的间隔
            int timerInterval = Math.Max(SnakeSpeedThreshold, (int)(gameTickTimer.Interval.TotalMilliseconds) - (currentScore * 2));
            gameTickTimer.Interval = TimeSpan.FromMilliseconds(timerInterval);//每次吃食物都会刷新速度,利用max让速度不会超过最大速
            GameArea.Children.Remove(snakeFood);//去掉当前食物
            DrawSnakeFood();//重新画一个食物
            UpdateGameStatus();//更新游戏界面的分数和速度
        }

如果结束游戏:

private void EndGame()
        {
            tbFinalScore.Text = currentScore.ToString();//最终分数传给界面
            bdrEndOfGame.Visibility = Visibility.Visible;//游戏结束界面展示,同时也展示分数
            PromptBuilder promptBuilder = new PromptBuilder();
            promptBuilder.AppendText("你的得分:");
            promptBuilder.AppendTextWithHint(currentScore.ToString(), SayAs.NumberCardinal);
            promptBuilder.AppendText("分");//编辑播报内容
            speechSynthesizer.SpeakAsync(promptBuilder);//语言播报分数
            gameTickTimer.IsEnabled = false;//计时器暂停
        }

游戏结束会弹出结束界面,会提示按空格开始新游戏,空格可开始新的游戏,这是因为在Windows里加了KeyUp=“Window_KeyUp”,可以监听键盘事件,而且蛇方向的改变也是在这个Window_KeyUp函数里实现的:

private void Window_KeyUp(object sender, KeyEventArgs e)
        {
            if (isGameRuning == false) return;//游戏没有在运行,直接结束
            SnakeDirection originalSnakeDirection = snakeDirection;//得到蛇当前移动方向
            switch (e.Key)
            {
                case Key.Up:
                    if (snakeDirection != SnakeDirection.Down)//蛇在往下走就不能往上
                    {
                        snakeDirection = SnakeDirection.Up;
                    }
                    break;
                case Key.Down:
                    if (snakeDirection != SnakeDirection.Up)//蛇在往上走就不能往下
                    {
                        snakeDirection = SnakeDirection.Down;
                    }
                    break;
                case Key.Left:
                    if (snakeDirection != SnakeDirection.Right)//蛇在往右走就不能往左
                    {
                        snakeDirection = SnakeDirection.Left;
                    }
                    break;
                case Key.Right:
                    if (snakeDirection != SnakeDirection.Left)//蛇在往左走就不能往右
                    {
                        snakeDirection = SnakeDirection.Right;
                    }
                    break;
                case Key.Space://空格,开始新的游戏
                    StartNewGame();
                    break;
                default:
                    return;
            }

            if (snakeDirection != originalSnakeDirection)
            {
                MoveSnake();
            }
        }

按空格开始新的游戏:

private void StartNewGame()
        {
            foreach (var snakeBodyPart in snakeParts)
            {
                if (snakeBodyPart.UiElement != null)
                {
                    GameArea.Children.Remove(snakeBodyPart.UiElement);//把蛇从Canvas上移除
                }
            }
            snakeParts.Clear();//蛇的list也清空
            if (snakeFood != null)
            {
                GameArea.Children.Remove(snakeFood);//清除残存的食物
            }
            snakeFood = null;
            currentScore = 0;//分数清零
            snakeLength = SnakeStartLength;//长度初始化
            snakeDirection = SnakeDirection.Right;
            snakeParts.Add(new SnakePart { Position = new Point(SnakeSquareSize * 5, SnakeSquareSize * 5) });
            //蛇出现的位置
            gameTickTimer.Interval = TimeSpan.FromMilliseconds(SnakeStartSpeed);
            DrawSnake();//画蛇
            DrawSnakeFood();//画蛇的食物
            UpdateGameStatus();//更新界面分数和速度
            gameTickTimer.IsEnabled = true;
            gameTickTimer.Start();//开始游戏,开始计时
            btPause.Visibility = Visibility.Visible;//暂停按钮可见
            bdrWelcomeMessage.Visibility = Visibility.Collapsed;//欢迎页面隐藏
            bdrEndOfGame.Visibility = Visibility.Collapsed;//游戏结束界面隐藏
        }

其他一些功能(暂停,继续,关闭,托移):

private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            this.DragMove();//允许通过鼠标进行拖移窗口
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            this.Close();//关闭窗口
        }
        private void BtPause_Click(object sender, RoutedEventArgs e)
        {
            //游戏暂停
            if (isGameRuning == false)
            {
                isGameRuning = true;
                btPause.Content = "II";//即‘继续’变成‘暂停’
            }
            else
            {
                isGameRuning = false;
                btPause.Content = "▶";//改变按钮的内容,即’暂停‘变成‘继续’
            }
        }
总结:

通过这次大作业,提高了我的图形界面编程的技巧还有动手能力,学到了许多新东西,比如改变控件的属性和内容,增加删除控件的子元素,还有监听键盘的操作等等。

  • 7
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值