文章目录
- 一个好玩的🐍……
- 一、介绍
- 1)背景
- 2)环境
- 3)演示
- 游戏过程
- 文件格式
- 4)基本逻辑概述
- i 游戏主体
- ii 刷新
- iii 控制蛇的移动
- iiii 保存最高记录
- iiiii 吃食物
- iiiiii 记录用户的输入(很重要)
- 二、步骤
- 1)ControlFrm窗体
- 2)GameFrm窗体:游戏窗体
- i 变量
- ii 窗体加载事件
- iii 玩家敲击键盘的事件
- iiii timer1 tick事件:
- iiiii 相关方法
- 蛇移动的方法
- 擦除蛇身
- 生成食物
- 判断是否吃到食物
- 从磁盘文件中获取历史最高分
- 播放音乐
- 死之前判断是否打破了记录
- iiiiii timer2事件
- 三、最后
- 1)整个工程文件
- 2)扩展
- 3)最后
一、介绍
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、生成一些特殊的食物,比如吃了能加一条命?
等等吧,看你自由发挥。