周末没事,手写小游戏继续~
预览
步骤
1 定义数据结构
逻辑与界面分离,游戏场景是个二维数组区域,贪吃蛇是若干个连续的坐标点集合,用动态链表维护,果实是一个随机坐标点。
const int BLOCK_SIZE=25; //单个方块单元的边长
const int MARGIN=5; //场景边距
const int AREA_ROW=15; //场景行数
const int AREA_COL=15; //场景列数
QPoint foodPoint; //果实出现坐标
QList<QPoint> snake; //贪吃蛇结构
const int TIME_INTERVAL=500; //定时器间隔时间
enum Direction
{
UP,
DOWN,
LEFT,
RIGHT
};
2 添加界面刷新和键盘监听
首先需要定时器来控制贪吃蛇的移动以及界面的刷新,绑定定时器的信号槽
gameTimer=new QTimer(this);
connect(gameTimer,SIGNAL(timeout()),this,SLOT(SnakeUpdate()));
窗口重绘
void Widget::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
//绘制游戏场景
painter.setBrush(Qt::yellow);
painter.setPen(Qt::blue);
painter.drawRect(MARGIN,MARGIN,AREA_COL*BLOCK_SIZE,AREA_ROW*BLOCK_SIZE);
//绘制贪吃蛇
painter.setBrush(Qt::red);
painter.setPen(Qt::green);
for(int i=0;i<snake.size();i++)
painter.drawRect(MARGIN+snake[i].x()*BLOCK_SIZE,MARGIN+snake[i].y()*BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE);
//绘制果实
painter.setBrush(Qt::green);
painter.drawEllipse(MARGIN+foodPoint.x()*BLOCK_SIZE,MARGIN+foodPoint.y()*BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE);
//绘制游戏分数
painter.setPen(Qt::black);
painter.setFont(QFont("Arial",14));
painter.drawText(MARGIN*3+AREA_COL*BLOCK_SIZE,MARGIN+2*BLOCK_SIZE,"score: "+QString::number(score));
}
键盘监听
void Widget::keyPressEvent(QKeyEvent *event)
{
//贪吃蛇的方向是个状态机,注意各个方向之间切换有限制
switch(event->key())
{
case Qt::Key_Up:
if(dir!=DOWN)
dir=UP;
break;
case Qt::Key_Down:
if(dir!=UP)
dir=DOWN;
break;
case Qt::Key_Left:
if(dir!=RIGHT)
dir=LEFT;
break;
case Qt::Key_Right:
if(dir!=LEFT)
dir=RIGHT;
break;
case Qt::Key_P:
PauseResumeGame();
break;
default:
break;
}
}
贪吃蛇的移动是个状态机,总是朝着当前的方向前进,移动的逻辑是,不断地消除尾部节点,并添加一个新的头部结点,如果碰到了果实,则不消除尾部结点
每次吃到了果实后会重新随机生成一个果实,并且保证果实不会与贪吃蛇身体重叠。
void Widget::SnakeUpdate()
{
//贪吃蛇移动的策略是每次删除尾部,然后添加新的头部,维护一个动态链表
switch(dir)
{
case UP:
snake.push_front(QPoint(snake.front().x(),snake.front().y()-1));
break;
case DOWN:
snake.push_front(QPoint(snake.front().x(),snake.front().y()+1));
break;
case LEFT:
snake.push_front(QPoint(snake.front().x()-1,snake.front().y()));
break;
case RIGHT:
snake.push_front(QPoint(snake.front().x()+1,snake.front().y()));
break;
default:
break;
}
//如果吃到了果实,则尾部不删除,否则删除尾部更新头部
if(snake.contains(foodPoint))
{
score+=1; //得分
GenerateFood(); //重新生成果实
}
else
snake.pop_back();
//游戏是否结束
if(IsGameOver())
{
GameOver();
return; //赶在重绘之前跳出函数,防止贪吃蛇真的出界
}
update(); //重绘,比repaint函数效果好
}
void Widget::GenerateFood()
{
//随机产生位置
foodPoint.setX(rand()%AREA_COL);
foodPoint.setY(rand()%AREA_ROW);
//如果与贪吃蛇位置冲突,重新生成
if(snake.contains(foodPoint))
GenerateFood();
}
bool Widget::IsGameOver()
{
int x=snake.front().x();
int y=snake.front().y();
//出边界
if(x<0||x>AREA_COL-1||y<0||y>AREA_ROW-1)
return true;
//撞了自己
for(int i=3;i<snake.size();i++)
if(snake[i]==snake.front())
return true;
return false;
}
游戏初始化
void Widget::InitGame()
{
//初始化贪吃蛇,初始长度5,注意头部在前,尾部在后
for(int j=4;j>=0;j--)
snake.push_back(QPoint(j,0));
dir=RIGHT;//初始时往右走
//初始化果实
srand(time(0));
GenerateFood();
//初始化游戏分数
score=0;
//初始化暂停变量
isPause=false;
//初始化计时器
gameTimer=new QTimer(this);
connect(gameTimer,SIGNAL(timeout()),this,SLOT(SnakeUpdate()));
gameTimer->start(TIME_INTERVAL);
}
游戏暂停和恢复
void Widget::PauseResumeGame()
{
//暂停和恢复定时器
if(!isPause)
{
isPause=!isPause;
gameTimer->stop();
}
else
{
isPause=!isPause;
gameTimer->start(TIME_INTERVAL);
}
}
游戏结束
void Widget::GameOver()
{
gameTimer->stop();
QMessageBox::information(this,"failed","game over!");
}