C#小游戏——贪吃蛇~详细过程+全部代码

一个好玩的🐍……

一、介绍

1)背景

一到期末课就基本没了。闲来无事,整理一下之前写的C#的一个小游戏——贪吃蛇。今年疫情在家写的,蛮有趣的吧!

2)环境

系统:Windows 10

环境:.Net Framework 4.7

平台:VS 2019

3)演示

游戏过程

在这里插入图片描述

文件格式

在这里插入图片描述

4)基本逻辑概述

i 游戏主体

声明Label类型的二维数组,表示每一个格子,用不同的颜色来区分蛇和地图(例如,表示蛇身的label的BackColor设置为蓝色,食物设置为红色,其他的设置为绿色);

ii 刷新

通过timer刷新。每一次刷新,蛇会朝着其蛇头的方向移动一格。所以通过对timer的interval属性控制蛇移动的快慢。

iii 控制蛇的移动

在timer中,同时判断有无键盘(w,s,a,d)的输入,进行改变蛇的移动方向(也是通过修改label的颜色实现)。

iiii 保存最高记录

程序自动创建二进制文件MaxScore.dat,每次游戏结束时,读取数据,判断大小,并保存。

该文件路径是程序的bin目录Debug下。

iiiii 吃食物

random生成食物的位置,将该位置的label颜色设置为红色,就可以吃了。在timer中判断是否吃到了食物,进行增长的操作。

iiiiii 记录用户的输入(很重要)

有时用户有可能连续输入多个值(如上上上左左),这些值输入的间隔可能会小于timer的interval值,造成”上一个动作还没有完成,直接进行下一个动作了,从而导致误判死亡!”

我的解决办法是:用string字符串记录用户的输入,每次输入都加到该字符串的末尾。而每次的timer从该字符串的串首取值,这样一来,玩家可以的连续输入,不影响timer的刷新。

二、步骤

1)ControlFrm窗体

项目的初始运行的窗体,就三个按钮,分别打开不同的窗体:

在这里插入图片描述

代码逻辑如下:

namespace 贪吃蛇
{
    public partial class ControlFrm : Form
    {
        public ControlFrm()
        {
            InitializeComponent();
        }
        //“关于”按钮click
        private void button3_Click(object sender, EventArgs e)
        {
            AboutMe hf = new AboutMe();
            hf.ShowDialog();
        }
        //“玩法介绍”按钮click
        private void button2_Click(object sender, EventArgs e)
        {
            HowToPlay htp = new HowToPlay();
            htp.ShowDialog();
        }
        //“开始游戏”按钮click
        private void button1_Click(object sender, EventArgs e)
        {
            GameFrm gf = new GameFrm();
            gf.ShowDialog();
        }
    }
}

2)GameFrm窗体:游戏窗体

在这里插入图片描述

i 变量

//绘制地图
int mapX = 40, mapY = 20;//设定游戏地图边界
Label[,] mapArray;//定义地图格子的二维数组
List<int> snakeX = new List<int> { 0, 1, 2 }//snakeX[snakeX.Count-1]是蛇的头部。即初始时头部为2
List<int> snakeY = new List<int> { 1, 1, 1 };//利用集合存放蛇身
int foodX, foodY;//食物的坐标点
string kCode = "D";//控制蛇移动的方向,初始值向右
//level等级snakeX.Count>=10
int[] level = { 300, 250, 200, 150, 100 };//值是timer的时间,等级越高时间越短
int k = 0;//等级
//枚举,游戏中的背景音乐,吃食物以及死亡音乐
public enum Sound
{
    background, eat, gameover
};

//得分
int Score = 0;
//历史最高分
int MaxScore;
//存储最高分记录的文件路径
string path = "MaxScore.dat";

//字符串队列,键盘每次的输入则入队
string ClikRecord = "";

#endregion

ii 窗体加载事件

private void Form1_Load(object sender, EventArgs e)
{
    //从磁盘文件中获取历史最高分
    GetMaxScore();
    //1、创建地图
    GreateMap();
    //2、创建蛇身
    GreateSnake();
    //3、创建食物
    GreateFood();
    //4、蛇身移动(死亡、吃食物)
    timer1.Start();//计时器开启
    //5、播放音乐
    PlayerMusic(Sound.background);
    //初始化窗体下部的状态栏
    toolStripStatusLabel3.Text = "         当前得分:" + Score.ToString();
    toolStripStatusLabel4.Text = "         历史最高得分:" + MaxScore.ToString();
    toolStripStatusLabel1.Text = DateTime.Now.ToString();//日期时间
    toolStripStatusLabel2.Text = "                当前等级:" + (k + 1).ToString();
} 

iii 玩家敲击键盘的事件

private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    string newKey = e.KeyCode.ToString();
    //注意,当蛇向右时不能直接向左。同理,其他情况也类似
    List<string> list = new List<string>() { "A", "W", "D", "S", "P" ,"L"};
    if(list.Contains(newKey)==false)//若按下的键不是这六个键的话
    {
        return;
    }
    if(kCode=="A"&&newKey=="D"||
       kCode == "D" && newKey == "A" ||
       kCode == "W" && newKey == "S" ||
       kCode == "S" && newKey == "W" )
    {
        return;
    }
    if (newKey == "P")
    {
        timer1.Stop();
        return;
    }
    if(newKey=="L")
    {
        timer1.Start();
        return;
    }
    //修复BUG:
    //这里一个BUG的情况:在游戏时,假如蛇的方向为D,快速敲击键盘W、D时,
    //程序会来不及进入timer事件,这时蛇的状态不能经历W,直接到D,造成后面的程序
    //会判断蛇咬到了自己。
    //办法:加入string类型的ClikRecord当作键盘输入队列,在timer事件中取其第一个字符。
    //同时仍要加入kCode=newKey,若不加的话,则前面的kcode可能会等于现在的newkey,即使他们没有挨着
    kCode = newKey;
    ClikRecord = ClikRecord + newKey;
} 
#endregion

此处的“修复BUG”就是我开始的时候说的:有时用户有可能连续输入多个值(如上上上左左),这些值输入的间隔可能会小于timer的interval值,造成”上一个动作还没有完成,直接进行下一个动作了,从而导致误判死亡!”

iiii timer1 tick事件:

这个就是对蛇进行操作刷新的事件

private void timer1_Tick(object sender, EventArgs e)
{
    //窗体下部的状态栏保持更新
    toolStripStatusLabel3.Text = "                当前得分:" + Score.ToString();
    toolStripStatusLabel2.Text = "                 当前等级:" + (k + 1).ToString();
    
    if(Score%10==0 && Score/10<=2)
    {
        k = Score / 10; //当得分在20以内时,设置等级
        timer1.Interval = level[k]; //根据不同的等级设置不同的interval值,下同
    }
    if(Score%10==0 && Score/10==4)
    {
        k++;//等级加1
        timer1.Interval = level[k];
    }
    if (Score % 10 == 0 && Score / 10 == 6)
    {
        k++;
        timer1.Interval = level[k];
    }
    if (ClikRecord.Length>0)
    {
        kCode = ClikRecord.Substring(0, 1);//取第一个字符的字符串
        ClikRecord = ClikRecord.Substring(1, ClikRecord.Length - 1);
    }
    SnakeMove();//蛇移动方法
}

iiiii 相关方法

蛇移动的方法
private void SnakeMove()
{
    //1、擦除
    ClearSnake();
    //2、修改坐标
    if (kCode != "P"&&kCode!="L")
    {
        for (int i = 0; i < snakeX.Count - 1; i++)//snakeX.Count-1忽略头部
        {
            snakeX[i] = snakeX[i + 1];
            snakeY[i] = snakeY[i + 1];
        }
        switch (kCode)
        {
            case "A":
                snakeX[snakeX.Count - 1]--;//头部坐标减1
                break;
            case "W":
                snakeY[snakeX.Count - 1]--;
                break;
            case "D":
                snakeX[snakeX.Count - 1]++;
                break;
            case "S":
                snakeY[snakeX.Count - 1]++;
                break;
        }
    }
    //3、吃食物
    if(EatFood())//若吃到了食物
    {
        Score++;//得分加1
        PlayerMusic(Sound.eat);//播放吃食物的声音
        snakeX.Add(0);
        snakeY.Add(0);//增加一个元素,值先不管,就按0吧
        //蛇身还原
        for(int i=snakeY.Count-1;i>0;i--)
        {
            snakeX[i] = snakeX[i - 1];
            snakeY[i] = snakeY[i - 1];
        }
        GreateFood();//重新生成一个食物
    }

    //4、判断是否咬到自己的身体(只需判断头部与身体是否重叠即可)
    for (int i=0;i<snakeX.Count-1;i++)
    {
        if(snakeX[snakeX.Count-1]==snakeX[i]&&
            snakeY[snakeX.Count-1]==snakeY[i])
        {
            timer1.Stop();
            PlayerMusic(Sound.gameover);//播放死亡的音乐
            MessageBox.Show("你把自己咬死了!!!");
            if(Score>MaxScore)//如果打破了记录
            {
                BreadARecord();//将新纪录写入文件
            }
            this.Close();
            return;
        }
    }
    //5、判断蛇是否超出边界
    if(snakeX[snakeX.Count-1]<0||
        snakeY[snakeX.Count-1]<0||
        snakeX[snakeX.Count-1]>mapX-1||
        snakeY[snakeX.Count-1]>mapY-1)
    {
        timer1.Stop();
        PlayerMusic(Sound.gameover);//播放死亡的音乐
        MessageBox.Show("你撞墙上了!!!");
        if (Score > MaxScore)//如果打破了记录
        {
            BreadARecord();//将新纪录写入文件
        }
        this.Close();
        return;
    }
    //6、重新显示
    GreateSnake();
}
擦除蛇身
private void ClearSnake()
{
    for (int i = 0; i < snakeX.Count; i++)
    {
        mapArray[snakeY[i], snakeX[i]].BackColor = Color.Green;//label数组mapArry,根据蛇身的下标,改变label的颜色当作蛇身
    }
}
生成食物
private void GreateFood()
{
    bool flag;//标记是否重叠
    do
    {
        flag = false;
        Random r = new Random();
        foodX = r.Next(mapX);//0-39
        foodY = r.Next(mapY);//0-19
        for(int i=0;i<snakeX.Count;i++)
        {
            if(snakeX[i]==foodX&&snakeY[i]==foodY)//如果食物与蛇身重叠
            {
                flag = true;
                break;
            }
        }
    } while (flag);//若flag为true,则表示重叠,则重新循环
    
    mapArray[foodY, foodX].BackColor = Color.Red;
}

判断是否吃到食物
private bool EatFood()
{
     if(snakeX[snakeX.Count-1]==foodX&&snakeY[snakeX.Count-1]==foodY)
     {
        return true;
     }
     return false;
}
从磁盘文件中获取历史最高分
private void GetMaxScore()
{
    if(File.Exists(path)==false)//若不存在此文件
    {
        FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write);
        BinaryWriter bw = new BinaryWriter(fs);
        bw.Write(0);
        MaxScore = 0;
        fs.Close();
        bw.Close();
    }
    else
    {
        FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
        BinaryReader br = new BinaryReader(fs);
        MaxScore = br.ReadInt32();
        fs.Close();
        br.Close();
    }  
}
播放音乐

在游戏开始、游戏结束、吃到食物和撞墙时发出相应的声音

SoundPlayer soundPlayer = new SoundPlayer();
private void PlayerMusic(Sound s)
{
    switch (s)
    {
        case Sound.background:
            soundPlayer.SoundLocation = "1.wav";
            break;
        case Sound.eat:
            soundPlayer.SoundLocation = "2.wav";
            break;
        case Sound.gameover:
            soundPlayer.SoundLocation = "4.wav";
            break;
    }
    soundPlayer.Play();//播放
}

死之前判断是否打破了记录
private void BreadARecord()
{
    //将新纪录写入文件
    FileStream fs = new FileStream(path, FileMode.Truncate, FileAccess.Write);
    BinaryWriter bw = new BinaryWriter(fs);
    bw.Write(Score);
    fs.Close();
    bw.Close();
    //告诉玩家这个好消息
    MessageBox.Show("恭喜你,创造了新纪录!!!");
}
#endregion

iiiiii timer2事件

这就一个作用:刷新在状态栏的时间

private void timer2_Tick(object sender, EventArgs e)
{
    toolStripStatusLabel1.Text = DateTime.Now.ToString();//日期时间
}

三、最后

1)整个工程文件

其实上面已经包含了所有代码。你只需要稍微动动手,就能写出来完整的程序了。

我建议你自己动手写写。或者你需要帮忙的话可以联系我!

如果你需要完成的文件,也可以私信我!

2)扩展

有什么扩展的思路吗?

1、弄成蛇头和蛇身不一样吧,比如这样的蛇?哈哈哈哈哈哈哈哈哈哈~

在这里插入图片描述

2、长按一个方向键能进行加速?
3、生成一些特殊的食物,比如吃了能加一条命

等等吧,看你自由发挥。

3)最后

有什么可以改进的地方请留言呐。
***************************************************************************************************************************
Don’t forget follow and like!!!
啥意思?我想想……别忘了点赞+关注~~~~------哈哈哈哈哈--------------

在这里插入图片描述

  • 100
    点赞
  • 325
    收藏
    觉得还不错? 一键收藏
  • 42
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值