贪吃蛇相于其它小游戏,算是简单的一个。没学GDI
或WPF
啥的,也不想学,恰好在C#编程课中学过WinForm
,所以就用WinForm
做了个简单的贪吃蛇。
完整代码在此:资源链接
游戏界面如下:
灰色边框为一圈灰色的Button
,设置Enable
属性为false
,避免鼠标对它有影响(颜色变化)。
中间的空白地图为24*24个PictureBox
,即pictureBox0~pictureBox575。
游戏地图可用一个图类Graph
表示,而单个位置又可通过一个点类Point
表示。有了Graph
类和Point
类,则可创建一个Snake
类,通过Graph
类和Point
类的支持,来模拟蛇的各种操作。
Point
类的建立可方便对单一网格或图中对应元素的操作,如可通过Point
对象p
来操作横坐标为p.X
,纵坐标为p.Y
的PictureBox
背景色,达到某些效果。也可通过p.X
和p.Y
获取对应图的元素信息。
//代码只用于说明思路,没有完整实现,具体实现详见资源链接
public class Point {
int x;
int y;
//判断点是否合法,只有获取valid值时才进行设置
bool valid;
//无参构造函数
public Point() {}
//有参构造函数
public Point(int posX,int posY) {}
//复制构造函数
public Point(Point p) {}
//判断两点坐标是否相同
public bool equal(Point p) {}
//设置点信息,通过坐标 (posX, posY)
public void setPoint(int posX,int posY) {}
//设置点信息,通过索引 index
public void setPoint(int index) {}
//x的get和set
public int X {}
//y的get和set
public int Y {}
//valid的get,不能set,因为它标志着点是否合法
public bool Valid {}
}
Graph
类记录游戏网格背后的数据,蛇的身体就是一些点组成,而这些点就反应在图中连续(上下左右)元素值不为零,此外,还记录了游戏中苹果的位置(通过与蛇身体的值不同的值,来标记苹果,如蛇身体标对应元素值为1,苹果为2,图中无任何内容的地方为0)。
public class Graph {
int[,] graph; //图对象
//每个元素可取值0、1、2,
//0: 此处为 空
//1: 此处有 蛇身体
//2: 此处有 苹果
//构造函数
public Graph() {}
//重置图信息
public void resetGraph() {}
//设置值,通过点和值 (p, value)
public void setValue(Point p,int value) {}
//设置值,通过坐标和值
public void setValue(int x, int y, int value) {}
//获取值,通过点
public int getValue(Point p) {}
//获取值,通过坐标
public int getValue(int x,int y) {}
}
Snake
类通过Graph
类和Point
类的协助,进行蛇的设定、模拟蛇的移动以及吃掉苹果等操作。蛇在移动的过程中,需要判断它正前方的点的信息,如果是苹果,则吃掉;如果是墙或自己的身体,则挂掉;如果是空,则更新蛇的身体,整体向前走一步。
class Snake {
Point[] snake; //snake数组,0下标为snake尾,大下标为snake头
int count; //snake身体长度
//构造函数
public Snake() {
snake = new Point[576]; //蛇身长最多为游戏界面的网格个数
count = 0;
}
//重置snake信息
public void resetSnake() {
count = 0; //只需重置蛇身体长度信息,不必删除各个点信息
}
//获取snake头
public Point getHead() {
Point p = new Point(snake[count - 1]);
return p;
}
//添加点到末尾,即吃了apple
public void append(Point p) {
if (!p.Valid) throw new Exception("点不合法");
snake[count++] = new Point(p.X, p.Y);
}
//移动,pos可取 0: 上 1: 下 2: 左 3: 右
//返回值 0: 挂了 1: 已移动 2: 吃了apple 3: 反方向移动
public int move(Graph graph,int pos) {
int x = snake[count - 1].X; //snake头横坐标
int y = snake[count - 1].Y; //snake头纵坐标
Point p; //记录蛇即将要走的点
switch (pos) { //根据蛇即将要走的方向,获取p点
case 0:
p = new Point(x - 1, y); break;
case 1:
p = new Point(x + 1, y); break;
case 2:
p = new Point(x, y - 1); break;
case 3:
p = new Point(x, y + 1); break;
default:
throw new Exception("方向信息错误");
}
//撞墙
if (!p.Valid) return 0;
//没撞墙,但反方向走
//必须先判断是否反方向走,再判断是否撞上了身体。因为反方向走时,下一个点为蛇身的
//第二个点,会被误判为撞上了身体
if (p.equal(snake[count - 2])) return 3;
//没撞墙,也没反方向走,但是撞上了身体(非snake第二个点)
if (graph.getValue(p.X,p.Y) ==1) return 0;
//有apple,吃掉
if (graph.getValue(p.X,p.Y) == 2) { //此处有apple
snake[count++] = new Point(p.X, p.Y); //将apple加入到snake头
graph.setValue(p, 1); //将apple添加到图中
return 2;
}
//可移动,更新snake信息及图信息
else {
//snake尾在图上消失
graph.setValue(snake[0], 0);
//身体往“前”移
for (int i = 0; i < count - 1; i++) {
snake[i].X = snake[i + 1].X;
snake[i].Y = snake[i + 1].Y;
}
//snake头更新
snake[count - 1].X = p.X;
snake[count - 1].Y = p.Y;
graph.setValue(p, 1); //将snake头添加到图中
return 1;
}
}
}
游戏主界面GameForm
类:
public partial class GameForm : Form {
Random random;
Graph graph; //图对象
Snake snake; //snake对象
int score; //游戏分数
int record; //游戏最高分
bool inGame; //游戏中
bool canPress; //每次计时间隔内第一次按键有效,避免玩家频繁操作
bool isGameOver; //游戏结束标记
int pos; //记录snake当前前进方向,0: 上 1: 下 2: 左 3: 右
Point apple; //apple点
public GameForm() {
//值初始化
}
//snake重生
public void snakeBorn() {
//设置默认的snake出生的三点
//将三点添加到snake身体中
//将三点添加到图中
}
//开始游戏
public void start() {
//重置图信息
//重置snake信息
//snake重生
//显示snake
//刷新apple
//重置分数
//更新记录标签
//更新分数标签
//开始计时
//进入游戏状态inGame=true
//可按键canPress = true;
//更新游戏结束标记isGameOver=false
//初始化方向
}
//获取新的apple并添加到图中
public void getNewApple() {}
//游戏结束处理
public void gameOver() {}
//重置PictureBox背景色为白色
public void resetPBBackColor() {}
//根据图信息刷新PictureBox的背景色,只刷新snake身体,即graph中元素值为1
public void showSnake() {
//重置PictureBox背景色
//显示蛇身体
//显示蛇头,设置颜色为DodgerBlue
}
//将apple显示在图中
public void showApple() {}
//根据坐标获取PictureBox
public PictureBox getPictureBox(int x, int y) {}
//计时器事件触发
private void timer1_Tick(object sender, EventArgs e) {
//假如游戏结束,进入结束处理
//游戏未结束,且玩家按键,则根据玩家按键来更新PictureBox背景色
//玩家没按键或按键失效,则进入相应操作
//显示分数
//刷新为可按键状态
}
//点击记录按钮
private void recordBtn_Click(object sender, EventArgs e) {
//游戏暂停
//显示游戏纪录窗体
//可能玩家已经重置记录,需要更新recordLabel
//点击关闭后,游戏继续
}
//点击开始按钮
private void startBtn_Click(object sender, EventArgs e) {
//每次点击开始,游戏暂停
//假如在游戏中,显示确认放弃当前游戏的窗口
}
//按键时的操作,键盘按键事件的辅助函数
public void keyPress(int n) {}
//键盘按键事件,因为上下左右键会被窗体提前捕获,因此用WASD来操控方向
private void GameForm_KeyDown(object sender, KeyEventArgs e) {
if (inGame && canPress) {
canPress = false; //计时器触发之前都按键无效,避免玩家频繁操作
switch (e.KeyCode) {
case Keys.W: keyPress(0); break;
case Keys.S: keyPress(1); break;
case Keys.A: keyPress(2); break;
case Keys.D: keyPress(3); break;
default: break;
}
}
}
}
需要注意的是,为了游戏的健壮性,需要判断各种玩家的可能操作,如正在游戏中,玩家点击了开始按钮,为避免玩家的误点击,可提示玩家是否放弃当前游戏。同时,需要停止计时器,以避免在此期间蛇因为计时器导致的自动向前移动而挂掉。点击记录标签页是一样。
在设定点和图类的时候,需要自身进行输入判断,如传入的点是否有效,值是否有效等,在输入错误时执行某些预定操作。仅依赖外界提供正确信息,往往导致出错后调试困难。
当游戏背景中网格很多时,通过for
循环找到某个控件会导致性能低下,也可以通过switch-case进行定位,不过代码量也会随之增长。