想写这篇博客很久了,因为觉得能够把自己的程序有条理的写出来,是一种挑战,也是一种学习过程。
很久之前写了一个贪吃蛇游戏,其中很多思想是受到博文《控制台小游戏》的影响,原文博主已经删除了该系列的文章,但网上仍有部分转载内容,感兴趣的可以上网搜索。
废话不多说,先看下效果图:
界面主要元素包括:
单元格(Cell):游戏界面被划分为小的单元格
砖块(Brick):墙的基本单元,继承自单元格Cell
墙(Wall):作为障碍物及界面布局元素
蛇(Snake):单元格Cell的集合
食物(Food):继承自单元格Cell
下面我将一步步带大家构建这个游戏:
1、构建基本的显示单元:Cell
在游戏中,贪吃蛇、食物、墙都是由一个个单元格组成,在这里,单元格有坐标x,坐标y,宽度width,高度height这四个属性。
新建一个窗体,添加以下代码:
private void FormMain_Paint(object sender, PaintEventArgs e)
{
//绘制网格
Bitmap bitmap = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
Graphics g = Graphics.FromImage(bitmap);
for (int i = 0; i < 30; i++)
{
g.DrawLine(new Pen(Color.LightGray), new Point(20 * i, 0), new Point(20 * i, 600));
g.DrawLine(new Pen(Color.LightGray), new Point(0, 20 * i), new Point(600, 20 * i));
}
Graphics fg = e.Graphics;
fg.DrawImage(bitmap, this.ClientRectangle);
//贪吃蛇
Draw(0, 0, 20, 20, fg);
Draw(1, 0, 20, 20, fg);
Draw(2, 0, 20, 20, fg);
//食物
Draw(4, 4, 20, 20, fg);
}
private void Draw(int x, int y, int width, int height, Graphics g)
{
Pen pen = new Pen(Color.Black, 2);
g.DrawRectangle(pen, x * width + 2, y * height + 2, width - 3, height - 3);
g.DrawRectangle(pen, x * width + 6, y * height + 6, width - 11, height - 11);
}
显示效果如下:
从图中可以看出,基本的元素贪吃蛇和食物已经有了,为方便调用,封装Cell.cs,并添加相应的方法和字段:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
namespace SnakeGame
{
class Cell
{
/// <summary>
/// 单元格左上角x坐标
/// </summary>
protected int x;
public int X
{
get { return x; }
set { x = value; }
}
/// <summary>
/// 单元格左上角y坐标
/// </summary>
protected int y;
public int Y
{
get { return y; }
set { y = value; }
}
/// <summary>
/// 单元格宽度
/// </summary>
protected int width;
public int Width
{
get { return width; }
set { width = value; }
}
/// <summary>
/// 单元格高度
/// </summary>
protected int height;
public int Height
{
get { return height; }
set { height = value; }
}
public Cell()
{
}
public Cell(int x, int y, int width, int height)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
/// <summary>
/// 画单元格
/// </summary>
/// <param name="g"></param>
public virtual void Draw(Graphics g, Color c)
{
Pen pen = new Pen(c, 2);
g.DrawRectangle(pen, x * width + 2, y * height + 2, width - 3, height - 3);
g.DrawRectangle(pen, x * width + 6, y * height + 6, width - 11, height - 11);
//SolidBrush brush = new SolidBrush(Color.Black);
//g.FillRectangle(brush, x + 5, y + 5, width - 10, height - 10);
}
/// <summary>
/// 清除单元格
/// </summary>
/// <param name="g"></param>
/// <param name="c"></param>
public void Clear(Graphics g, Color c)
{
SolidBrush brush = new SolidBrush(c);
g.FillRectangle(brush, x * width + 1, y * height + 1, width - 1, height - 1);
}
}
}
此时,主程序中的代码可以修改为:
private void FormMain_Paint(object sender, PaintEventArgs e)
{
//绘制网格
Bitmap bitmap = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
Graphics g = Graphics.FromImage(bitmap);
for (int i = 0; i < 30; i++)
{
g.DrawLine(new Pen(Color.LightGray), new Point(20 * i, 0), new Point(20 * i, 600));
g.DrawLine(new Pen(Color.LightGray), new Point(0, 20 * i), new Point(600, 20 * i));
}
Graphics fg = e.Graphics;
fg.DrawImage(bitmap, this.ClientRectangle);
//贪吃蛇
Cell snake1 = new Cell(0, 0, 20, 20);
Cell snake2 = new Cell(1, 0, 20, 20);
Cell snake3 = new Cell(2, 0, 20, 20);
snake1.Draw(fg, Color.Black);
snake2.Draw(fg, Color.Black);
snake3.Draw(fg, Color.Black);
//食物
Cell food = new Cell(4, 4, 20, 20);
food.Draw(fg, Color.Green);
}
运行后可以得到与上图中相同的效果。
2、绘制更多界面元素:Brick、Wall、Food
重写Cell中的Draw方法,绘制更多样式的图形:
Brick:
public override void Draw(Graphics g, Color c)
{
SolidBrush brush = new SolidBrush(Color.LightGray);
g.FillRectangle(brush, x * width, y * height, width, height);
Pen pen = new Pen(c, 1);
g.DrawRectangle(pen, x * width + 1, y * height + 1, width - 2, height - 2);
g.DrawLine(pen, x * width + 1, y * width + 7, x * width + width - 2, y * width + 7);
g.DrawLine(pen, x * width + 1, y * width + 13, x * width + width - 2, y * width + 13);
g.DrawLine(pen, x * width + 10, y * width + 1, x * width + 10, y * width + 7);
g.DrawLine(pen, x * width + 10, y * width + 13, x * width + 10, y * width + 19);
g.DrawLine(pen, x * width + 5, y * width + 7, x * width + 5, y * width + 13);
g.DrawLine(pen, x * width + 15, y * width + 7, x * width + 15, y * width + 13);
}
Food:
public override void Draw(Graphics g, Color c)
{
Pen pen = new Pen(c, 1);
SolidBrush brush = new SolidBrush(c);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillEllipse(brush, x * width + 3, y * height + 5, width - 6, height - 8);
g.DrawLine(pen, x * width + 13, y * height + 2, x * width + 10, y * height + 7);
g.DrawLine(pen, x * width + 11, y * height + 2, x * width + 15, y * height + 3);
}
界面中,Snake为Cell的集合,Wall为Brick的集合。
在主窗体中添加相关的测试代码,可得到下图中的效果:
使用Brick元素绘制边界,效果如下:
如果有勇于实践的朋友,测试上图效果的时候会发现,墙的效果是逐渐加载的,后面在游戏过程中也会出现闪烁的现象,解决的方法是:首先将所有单元格绘制到bitmap,再将bitmap绘制到界面,相当于双缓冲,代码如下:
private void Form1_Paint(object sender, PaintEventArgs e)
{
//首先将所有单元格绘制到bitmap,再将bitmap绘制到界面
//相当于双缓冲,解决界面闪烁的问题
Bitmap bitmap = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
Graphics g = Graphics.FromImage(bitmap);
for (int i = 0; i < 30; i++)
{
g.DrawLine(new Pen(Color.LightGray), new Point(20 * i, 0), new Point(20 * i, 600));
g.DrawLine(new Pen(Color.LightGray), new Point(0, 20 * i), new Point(600, 20 * i));
}
wall.Draw(g, Color.Black);
snake.Draw(g, Color.Black);
food.Draw(g, Color.Green);
Graphics fg = e.Graphics;
fg.DrawImage(bitmap, this.ClientRectangle);
}
3、游戏的主角:Snake
前面已经提到,Snake实际就是Cell的集合,将这个集合实时刷新显示在界面上,就构成了贪吃蛇这个主角。
Snake的游戏逻辑无外乎上、下、左、右四个方向的运动,以及添加新的头部(Head)元素,并移除尾部(Tail)元素
Snake.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
namespace SnakeGame
{
class Snake
{
public List<Cell> snakeLst = new List<Cell>();
private int cellwidth;
private int cellheight;
public Point headpoint = new Point(3, 1);
public Snake(int width, int height)
{
this.cellwidth = width;
this.cellheight = height;
headpoint = new Point(3, 1);
snakeLst.Add(new Cell(headpoint.X - 2, headpoint.Y, width, height));
snakeLst.Add(new Cell(headpoint.X - 1, headpoint.Y, width, height));
snakeLst.Add(new Cell(headpoint.X, headpoint.Y, width, height));
}
public void Draw(Graphics g, Color c)
{
foreach (Cell snake in snakeLst)
{
snake.Draw(g, c);
}
}
public void Clear(Graphics g, Color c)
{
foreach (Cell snake in snakeLst)
{
snake.Clear(g, c);
}
snakeLst.Clear();
}
public void RemoveTail(Graphics g, Color c)
{
snakeLst[0].Clear(g, c);
snakeLst.RemoveAt(0);
}
public void AddHead(Graphics g, Color c)
{
Cell newsnake = new Cell(headpoint.X, headpoint.Y, cellwidth, cellheight);
snakeLst.Add(newsnake);
newsnake.Draw(g, c);
}
public void MoveRight()
{
headpoint.X++;
}
public void MoveDown()
{
headpoint.Y++;
}
public void MoveLeft()
{
headpoint.X--;
}
public void MoveUp()
{
headpoint.Y--;
}
}
}
4、游戏逻辑
游戏在运行过程中,通过按键改变贪吃蛇的下一个运动方向,相关代码如下: private enum Direction { Left, Right, Up, Down };
private Direction snakeDirection = Direction.Right;
private void FormMain_KeyDown(object sender, KeyEventArgs e)
{
if (!isGamePalying) return;
//如果方向键已经按下,则等待timer tick事件响应
if (isDirChanged == true)
{
return;
}
if (e.KeyCode == Keys.Up && snakeDirection != Direction.Down)
{
snakeDirection = Direction.Up;
isDirChanged = true;
}
if (e.KeyCode == Keys.Down && snakeDirection != Direction.Up)
{
snakeDirection = Direction.Down;
isDirChanged = true;
}
if (e.KeyCode == Keys.Left && snakeDirection != Direction.Right)
{
snakeDirection = Direction.Left;
isDirChanged = true;
}
if (e.KeyCode == Keys.Right && snakeDirection != Direction.Left)
{
snakeDirection = Direction.Right;
isDirChanged = true;
}
}
游戏的运行通过一个timer时钟进行控制,每Tick一次,Snake移动一次,运行逻辑:
对应的代码如下:
private void timer1_Tick(object sender, EventArgs e)
{
if (!isGamePalying) return;
if (!isGameOver)
{
isDirChanged = false;
//根据方向判断坐标值的变化
if (snakeDirection == Direction.Right)
{
snake.MoveRight();
}
else if (snakeDirection == Direction.Down)
{
snake.MoveDown();
}
else if (snakeDirection == Direction.Left)
{
snake.MoveLeft();
}
else if (snakeDirection == Direction.Up)
{
snake.MoveUp();
}
//判断是否撞墙
if (snake.headpoint.X >= 19 || snake.headpoint.X <= 0 ||
snake.headpoint.Y >= 19 || snake.headpoint.Y <= 0)
{
isGameOver = true;
this.lblGameOver.Visible = true;
return;
}
//判断是否撞到自己
if (food.isFull(snake.headpoint.X, snake.headpoint.Y, snake))
{
isGameOver = true;
this.lblGameOver.Visible = true;
return;
}
//判断是否吃到食物
if (snake.headpoint.X == food.X && snake.headpoint.Y == food.Y)
{
food.Clear(this.pictureBox1.CreateGraphics(), this.BackColor);
food = food.NewFood(snake);
food.Draw(this.pictureBox1.CreateGraphics(), Color.Green);
}
else
{
//没有吃到食物,移除Tail
snake.RemoveTail(this.pictureBox1.CreateGraphics(), this.BackColor);
}
//绘制新的head
snake.AddHead(this.pictureBox1.CreateGraphics(), Color.Black);
}
}
至此,游戏已经可以运行了,接下来可以添加游戏的分数(Score)及等级(Level)系统,添加游戏的开始、暂停等逻辑,在此我就不做更多的介绍了,具体代码可以参见我提供的源代码。
5、游戏源代码及改进
游戏源码:贪吃蛇游戏
游戏的改进:
1)从上文中可以看出,所有单元格均通过GDI+进行绘制,效果相对简约,在这里,可以把相关代码替换为绘制图片,图片资源可以从网络下载,或提取相关游戏的资源。
2)贪吃蛇的模型做的比较简单,可以复杂化,比如头部和尾部绘制不同的图形,并根据运动方向不同来改变它们的样式。
3)还可以加入更多的功能:排行榜、奖励、音效、动画等,大家可以发挥自己的想象。