C#贪吃蛇教程

原文地址:C#贪吃蛇教程

概况

  1. 理解贪吃蛇游戏

  2. 配置项目

  3. 添加必要的类

  4. 游戏循环

  5. 处理用户输入

  6. 游戏逻辑

  7. 游戏渲染

本教程或许有点长,但希望你能耐心看完。

源代码下载:贪吃蛇源代码。

理解贪吃蛇游戏

想必大家都玩过贪吃蛇这款游戏,因此游戏规则就不用我多做解释了。贪吃蛇是一款简单容易理解的游戏,但麻雀虽小五脏俱全,这款游戏已经包含了一款游戏的必要元素。游戏的功能可划分为:

  • 游戏逻辑:碰撞检测、积分、蛇的位移及成长等。

  • 游戏渲染:绘制蛇、绘制蛇的食物、绘制积分。

配置项目

我将使用C#及Visual Studio 2012来开发这个游戏项目。在这个游戏中,将使用GDI+进行渲染,真正的游戏项目中是不会选用GDI+进行渲染的,一般都是使用DirectX进行渲染,因为GDI+渲染实在太慢了,不过在这个小游戏的练习项目中,GDI+绝对是一个好选择。

创建项目

创建一个Windows窗体项目,命名为Snake。

拖入控件

在这个项目中,我们需要2个控件:

  • PictureBox:用来渲染游戏画面。

  • Timer:每隔数毫秒更新游戏。

现在就将这2个控件拖到窗口里吧,不过还需要对他们的属性做一些调整。

窗体属性:

窗体属性
Size300,320
StartPositionCenterScreen
Text贪吃蛇简单版

PictureBox属性:


PictureBox属性
(Name)pbCanvas
Location12,12
Size260,260

Timer属性:

(Name):gameTimer

修改属性后,窗体差不多是这个样子:


添加必要的类

在进入贪吃蛇项目的核心内容前,我们需要添加一个必要的类:SnakePart.cs。

在贪吃蛇游戏中,蛇的身体是一节一节组成起来的,而SnakePart类就表示蛇身体的一节。另外,SnakePart类还在游戏中表示蛇的食物。该类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace  Snake
{
     class  SnakePart
     {
         public  int  X {  get set ; }
         public  int  Y {  get set ; }
                 
         public  SnakePart()
         {
             X =  0 ;
             Y =  0 ;
         }
     }
}

游戏循环

大部分游戏都会使用游戏循环的结构,因此这个游戏也不例外。在C#Windows窗体项目中,利用计时器可以方便的实现游戏的循结构。计时器会每隔指定毫秒后,就会触发事件,而该事件将会调用我们的Update()方法。

首先让我们添加Update()方法,该方法用来执行游戏中的逻辑代码。

1
2
3
4
private  void  Update(object sender, EventArgs e)
{
       //执行游戏逻辑代码
  }

接下来,我们要在窗体初始化时,来初始化我们的gameTimer计时器。代码如下:

1
2
3
4
5
6
7
8
public  Form1()
{
        InitializeComponent();
        gameTimer.Interval =  1000  4 //指定间隔时间
        gameTimer.Tick += Update;  //指定事件触发的方法
        gameTimer.Start(); //启动计时器
        StartGame(); //暂时不用考虑该方法
}

上面2段代码已经实现了游戏循环,计时器每隔数毫秒就会触发一次Update方法,而该方法就会执行我们的游戏逻辑代码。

不过我们还有一项工作没做,就是在Update方法最后添加如下语句:

1
pbCanvas.Invalidate();

该语句会使程序重新绘制游戏画面,以便将游戏的改变反应给用户。

游戏逻辑

接下来是重头戏,游戏的逻辑代码,这个游戏的逻辑大致可以分为下面4个部分,让我们一个一个来实现。

  • 游戏中的移动的4个方向。

  • 产生食物

  • 移动蛇

  • 碰撞检测及检测游戏是否结束

逻辑基础

在开始,实行上面4个功能前,有些基础不得不说。

还记得前面定义的SnakePart类吗?现在我们就要使用他了,我们将使用List集合来表示蛇的身体。

1
List<SnakePart> snake =  new  List<SnakePart>();

蛇的身体实际上数个SnakePart类串在一起组成的,用List<SnakePart>来表示蛇的身体再好不过。

我们需要定义每个SnakePart的长度与宽度,以便在渲染时使用他们。

1
2
const  int  width =  16 ;
const  int  heigth =  16 ;

定义贪吃蛇活动场所的行数与列数。如下:

1
2
const  int  row =  16 ;
const  int  col =  16 ;

现在有必要说明下行数与列数的作用。在本游戏中SnakePart中X,Y表示的都是其所在的行数与列数,而不是其在窗体上的坐标。因此在渲染SnakePart时需要计算其在窗体上的目标,这个坐标应该是(X*width,Y*heigth)。

我们还需要定义如下的字段:

1
2
3
4
5
bool GameOver =  false ; //游戏是否结束
bool Reset =  false ; //是否重新开始游戏
int  score =  0 ; //得分
Direction current = Direction.Right; //当前的行动方向
SnakePart food =  new  SnakePart(); //贪吃蛇的食物

添加StartGame()方法,该方法将会初始化游戏。

1
2
3
4
5
6
7
8
9
10
11
12
private  void  StartGame()
{
      GameOver =  false ;
      Reset =  false ;
      score =  0;
      snake.Clear(); //清空蛇的身体
      SnakePart head =  new  SnakePart(); //定义蛇的头部
      head.X = row /  2  1 ;
      head.Y = row /  2  1 ;
      snake.Add(head); //添加头部到身体
      current = Direction.Right; //默认初始方向:→
      NextFood(); //产生下一个食物
}

定义游戏方向

本游戏中使用枚举来定义了游戏的4方向,代码如下:

1
2
3
4
5
6
7
enum Direction
{
      Down,
      Up,
      Left,
      Right
}

产生食物

我们将使用NextFood()方法来产生食物。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private  void  NextFood()
{
     Random random =  new  Random();
     food =  new  SnakePart();
     do
     {
         food.X = random.Next(row);
         food.Y = random.Next(col);
         if  (!IsInSnake(food.X,food.Y)) //避免产生的食物在蛇身体上
             break ;
     while  ( true );
                     
}
         
private  bool IsInSnake( int  x,  int  y) //判断食物是否在蛇的身体上
{
     for  ( int  i =  0 ; i < snake.Count -  1 ; i++)
     {
         if  (x == snake[i].X && y == snake[i].Y)
             return  true ;
     }
     return  false ;
}

移动蛇

要移动蛇,首先就需要检测用户的输入,我们需要为窗体添加KeyDown事件,以便监听用户的输入。

选择窗体属性中的事件,在事件列表中找到KeyDown事件并为该事件添加代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private  void  Form1_KeyDown(object sender, KeyEventArgs e)
        {
            if  (GameOver) //判断游戏是否结束
            {
                if  (e.KeyCode == Keys.Enter)
                {
                    Reset =  true ;
                }
            }
            else
            {
                switch  (e.KeyCode)
                {
                    case  Keys.Up:
                        if (current!=Direction.Down) //防止蛇直接反方向运动
                        current = Direction.Up;
                        break ;
                    case  Keys.Down:
                        if  (current != Direction.Up)
                        current = Direction.Down;
                        break ;
                    case  Keys.Left:
                        if  (current != Direction.Right)
                        current = Direction.Left;
                        break ;
                    case  Keys.Right:
                        if  (current != Direction.Left)
                        current = Direction.Right;
                        break ;
                }
            }
        }

碰撞检测及检测游戏是否结束

现在我们要实现本游戏的核心方法Update方法,该方法包含了碰撞检测的代码已经判断游戏是否结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private  bool IsHitSelf() //判断是否撞击到自身
{
     SnakePart head = snake.First();
     for  ( int  i =  1 ; i < snake.Count -  1 ; i++)
     {
         if  (head.X == snake[i].X && head.Y == snake[i].Y)
             return  true ;
     }
     return  false ;
   }
     
private  void  Update(object sender, EventArgs e)
{
     if  (GameOver) //判断游戏是否结束
     {
         if  (Reset)
         {
             StartGame();
         }
     }
     else
     {
         SnakePart head = snake.First();
                 
         if  (head.X <  0  || head.X >= col || head.Y <  0  || head.Y >= col||IsHitSelf()) //是否超出边境或撞击到自身
         {
             GameOver =  true ;
             goto outside; //如果是,结束游戏并跳过剩余代码
         }
         if  (head.X == food.X && head.Y == food.Y) //判断蛇是否吃的食物
         {
             SnakePart part =  new  SnakePart();
             part.X = snake[snake.Count -  1 ].X;
             part.Y = snake[snake.Count -  1 ].Y;
             snake.Add(part); //为蛇添加一节身体
             NextFood(); //产生下一个食物
             score++; //得分加1
         }
         for  ( int  i = snake.Count -  1 ; i >  0 ; i--) //整体移动蛇,蛇的后一节会跟随前一节行动,
         {                                         //因此只需将前一节的坐标赋给后一节即可
             snake[i].X = snake[i -  1 ].X;
             snake[i].Y = snake[i -  1 ].Y;
         }
      
         switch  (current) //依据当前的方向,移动蛇的头部
         {
             case  Direction.Down:
                 head.Y++;
                 break ;
             case  Direction.Up:
                 head.Y--;
                 break ;
             case  Direction.Left:
                 head.X--;
                 break ;
             case  Direction.Right:
                 head.X++;
                 break ;
         }
                      
     }
     outside:
     pbCanvas.Invalidate(); //重新绘制画面
}

渲染游戏

在完成游戏的逻辑后,现在我们需要渲染游戏的画面,为PictureBox添加Paint事件,并添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private  void  pbCanvas_Paint(object sender, PaintEventArgs e)
{
     Graphics g = e.Graphics;
     g.DrawRectangle(Pens.Black,  0 0 , width*col, heigth*row);
     if  (GameOver) //判断游戏是否结束
     {
         Font font =  this .Font;
         string gameover_msg =  "Gameover" ;
         string score_msg =  "得分: "  + score.ToString();
         string newgame_msg =  "按回车键重新开始" ;
         int  center_width = pbCanvas.Width /  2 ;
         SizeF msg_size = g.MeasureString(gameover_msg, font);
         PointF msg_point =  new  PointF(center_width - msg_size.Width /  2 16 );
         g.DrawString(gameover_msg, font, Brushes.Black, msg_point);
         msg_size = g.MeasureString(score_msg, font);
         msg_point =  new  PointF(center_width - msg_size.Width /  2 32 );
         g.DrawString(score_msg, font, Brushes.Black, msg_point);
         msg_size = g.MeasureString(newgame_msg, font);
         msg_point =  new  PointF(center_width - msg_size.Width /  2 48 );
         g.DrawString(newgame_msg, font, Brushes.Black, msg_point);
     }
     else
     {
         g.FillRectangle(Brushes.Orange,  new  Rectangle(food.X * width, food.Y * heigth, width, heigth)); //绘制食物
         for  ( int  i =  0 ; i < snake.Count; i++) //绘制蛇的身体
         {
             Brush snake_color = i ==  0  ? Brushes.SeaGreen : Brushes.SkyBlue; //如果是头部就绘制成绿色
             g.FillRectangle(snake_color,  new  Rectangle(snake[i].X * width, snake[i].Y * heigth, width, heigth));
         }
         g.DrawString( "得分: "  + score.ToString(),  this .Font, Brushes.Black,  new  PointF( 4 4 )); //绘制得分
     }
}

总结

好了,一个简单的贪吃蛇游戏就完成了,大家有什么不懂的可以去本站论坛提问。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值