理解贪吃蛇游戏
配置项目
添加必要的类
游戏循环
处理用户输入
游戏逻辑
游戏渲染
本教程或许有点长,但希望你能耐心看完。
源代码下载:贪吃蛇源代码。
想必大家都玩过贪吃蛇这款游戏,因此游戏规则就不用我多做解释了。贪吃蛇是一款简单容易理解的游戏,但麻雀虽小五脏俱全,这款游戏已经包含了一款游戏的必要元素。游戏的功能可划分为:
游戏逻辑:碰撞检测、积分、蛇的位移及成长等。
游戏渲染:绘制蛇、绘制蛇的食物、绘制积分。
我将使用C#及Visual Studio 2012来开发这个游戏项目。在这个游戏中,将使用GDI+进行渲染,真正的游戏项目中是不会选用GDI+进行渲染的,一般都是使用DirectX进行渲染,因为GDI+渲染实在太慢了,不过在这个小游戏的练习项目中,GDI+绝对是一个好选择。
创建一个Windows窗体项目,命名为Snake。
在这个项目中,我们需要2个控件:
PictureBox:用来渲染游戏画面。
Timer:每隔数毫秒更新游戏。
现在就将这2个控件拖到窗口里吧,不过还需要对他们的属性做一些调整。
窗体属性:
窗体属性 | 值 |
Size | 300,320 |
StartPosition | CenterScreen |
Text | 贪吃蛇简单版 |
PictureBox属性:
PictureBox属性 | 值 |
(Name) | pbCanvas |
Location | 12,12 |
Size | 260,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
new
List
|
蛇的身体实际上数个SnakePart类串在一起组成的,用List
我们需要定义每个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
));
//绘制得分
}
}
|
好了,一个简单的贪吃蛇游戏就完成了,大家有什么不懂的可以去本站论坛提问。