在WinForm中使用GDI+编写游戏--贪吃蛇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yank2009/article/details/51784110

想写这篇博客很久了,因为觉得能够把自己的程序有条理的写出来,是一种挑战,也是一种学习过程。

很久之前写了一个贪吃蛇游戏,其中很多思想是受到博文《控制台小游戏》的影响,原文博主已经删除了该系列的文章,但网上仍有部分转载内容,感兴趣的可以上网搜索。


废话不多说,先看下效果图:



界面主要元素包括:

单元格(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)还可以加入更多的功能:排行榜、奖励、音效、动画等,大家可以发挥自己的想象。

展开阅读全文

没有更多推荐了,返回首页