照例先演示一下:
一个简单的Qt贪吃蛇,所有的图片都是我自己画的(得意)。
大致的运行逻辑和之前那个200行写一个C++小黑窗贪吃蛇差不多,因此在写这个项目的时候,大多情况是在想怎么通过Qt给展现出来。
背景图片:
由于背景图片是自己画的,因此大小是刚刚好符合我设定的主窗口大小的,直接是把背景图放大两倍,然后放在坐标为0,0的位置上,下面这段代码写在绘画事件的函数里,窗口初始化的时候系统会自动调用绘画事件,因此我们不用手动去调用就可以实现背景图片的渲染。
QPainter* paint = new QPainter(this);
//画背景,可以修改为可选背景,感兴趣可以自己去搞一下
QPixmap background;
background.load(":/image/background.png");
background=background.scaled(background.width()*2, background.height()*2);
paint->drawPixmap(0,0,background);
那么除了背景图片以外,还有要画的就是蛇和食物了(这里的食物我画了我最喜欢的水果——芭乐,应该看得出来叭),所以在主窗口里需要有对应的蛇和食物的类,并且这些类里需要有它们所在位置的坐标,然后再绘画事件里根据它们的坐标来绘制。
画食物比较简单,直接在食物的坐标上绘制就行。
有界面的贪吃蛇和小黑窗不一样,我们需要展示出蛇的移动方向,也就是说不同的移动方向,蛇头 应该看起来是不一样的,所以在绘画蛇头的时候我们需要判断蛇的移动方向,以此来决定蛇头应该用哪个图片。(直接旋转图片有些麻烦,所以我直接画了四个方向的蛇头图片)。
蛇身子应该也是要根据移动的情况而绘制不同的图片的,但是逻辑太复杂了,不仅要考虑蛇的每一节身子是上下方向的还是左右方向的,甚至有些身子是弯曲的转向的,所以我直接投降了,把蛇的身子画成圆乎乎的,这样就分辨不出什么方向什么的了,统一用同一个图片就好啦。
蛇尾和蛇头类型,也是要根据具体情况来绘制不同的图片,不过和蛇头不一样的是,蛇尾的图片不取决与蛇的移动方向,因为蛇尾具有滞后性,所以判断的依据是蛇的最后一节身子。
if (isBegin) { //当游戏开始时才画蛇和食物
//画食物
QPixmap bale;
bale.load(":/image/bale.png");
paint->drawPixmap(food[0],food[1],bale);
//画蛇头
QPixmap snakeHead;
//根据蛇移动的方位来改变蛇头的图片
if(greedysnake.direction=='r')snakeHead.load(":/image/head(right).png");
else if(greedysnake.direction=='l')snakeHead.load(":/image/head(left).png");
else if(greedysnake.direction=='u')snakeHead.load(":/image/head(up).png");
else snakeHead.load(":/image/head(down).png");
paint->drawPixmap(greedysnake.head[0], greedysnake.head[1], snakeHead);
//画蛇身
for (auto& body : greedysnake.body) {
QPixmap snakeBody;
snakeBody.load(":/image/body.png");
paint->drawPixmap(body[0], body[1], snakeBody);
}
//画蛇尾
QPixmap snakeTail;
vector<int>lastbody = *(greedysnake.body.end() - 1);
//根据蛇的最后一节身子来改变蛇尾的图片
if (greedysnake.tail[0] == lastbody[0]) {
if (greedysnake.tail[1] > lastbody[1]) snakeTail.load(":/image/tail(up).png");
else snakeTail.load(":/image/tail(down).png");
}
else if (greedysnake.tail[1] == lastbody[1]) {
if (greedysnake.tail[0] > lastbody[0]) snakeTail.load(":/image/tail(left).png");
else snakeTail.load(":/image/tail(right).png");
}
paint->drawPixmap(greedysnake.tail[0],greedysnake.tail[1],snakeTail);
}
生成食物:
生成食物的时间点在游戏刚开始的时候,以及蛇吃掉食物以后。
生成食物其实就是更新食物的坐标,我们使用随机数去生成,并且我这边设置的是食物大小是50*50像素的,并且蛇的每一节身子都是50*50像素的,所以我们在生成坐标的时候,需要保证生成出来的坐标是50的倍数(包括蛇自身的坐标)。
并且生成出来的坐标不能在蛇身上。
void MainUI::createFood(){
//生成食物
std::uniform_int_distribution<int>uw(0,1300/50-1);
std::uniform_int_distribution<int>uh(1,800/50-1);
bool flag = true;
while (1) {
flag = true;
int x = uw(e)*50; int y = uh(e)*50;
//不能生成在蛇头的位置
if (x == greedysnake.head[0] && y == greedysnake.head[1]) continue;
//不能生成在蛇尾的位置
if (x == greedysnake.tail[0] && y == greedysnake.tail[1]) continue;
//不能生成在蛇身的位置
for (const auto &body : greedysnake.body) {
if (body[0] == x && body[1] == y) {
flag = false;
break;
}
}
if (flag) {
food[0] = x, food[1] = y;
qDebug() << x << ' ' << y << endl;
break;
}
}
}
让蛇动起来:
Qt有定时器,这是对比小黑窗来说比较方便的一个点,我们可以通过定时器,每个一段时间就更新蛇的坐标,然后再更新绘图,以此来达到让蛇移动的效果。
每次移动蛇,我们就把蛇头的坐标按照移动的方向来做出改变。而蛇的身子要达到移动的效果则是把最后一节身子的坐标从身子里删去,然后在存放身子的容器的开头加上移动前蛇头的坐标,这样蛇的身子也就可以达到移动的效果了。
而蛇的尾部只需要更新成移动前蛇的最后一节身子的坐标即可。
在移动的时候我们还应该有个判断,如果吃到了芭乐,即新蛇头的坐标等于芭乐的坐标,那么身子加长,并且重新生成新的芭乐。加长身子是将移动前头的坐标加入到蛇身子的容器的开头,而芭乐的坐标成为新的蛇头的坐标,这样就达到了增长的效果。
除了判断食物,我们还应该判断如果吃到了自己的身体,我们就应该弹出一个消息提示框,可以选择是否重新开始一局游戏。
如果蛇移动移出了边界,那么可以判断是失败了,而我这里的处理是可以穿越到界面的另一侧继续游戏,这样降低了难度,增加了可玩性。
void MainUI::timerEvent(QTimerEvent* e){ //定时器
if (isBegin) { //如果开始游戏
//蛇的移动
greedysnake.body.insert(greedysnake.body.begin(), greedysnake.head);
greedysnake.tail = *(greedysnake.body.end() - 1);
greedysnake.body.pop_back();
if (greedysnake.direction == 'r') greedysnake.head[0] += greedysnake.speed;
else if (greedysnake.direction == 'l') greedysnake.head[0] -= greedysnake.speed;
else if (greedysnake.direction == 'u') greedysnake.head[1] -= greedysnake.speed;
else greedysnake.head[1] += greedysnake.speed;
for (auto& body : greedysnake.body) {
//如果碰到身体
if (body[0] == greedysnake.head[0] && body[1] == greedysnake.head[1]) {
isBegin = false;
killTimer(TimerID);
QMessageBox::StandardButton ans = QMessageBox::question(this, "game over", QString::fromLocal8Bit("你吃到了自己,游戏结束,是否在来一把"));
if (ans == QMessageBox::No) {
exit(0);
}
else {
//手动初始化一下蛇
greedysnake.head = { 650,350 };
greedysnake.body = {{ 600, 350 }};
greedysnake.tail = { 550,350 };
beginGame();
}
return;
}
}
if (greedysnake.head[0] == food[0] && greedysnake.head[1] == food[1]) {
//如果吃到食物,身体变长并且重新生成食物.
greedysnake.body.insert(greedysnake.body.begin(), greedysnake.head);
if (greedysnake.direction == 'r') greedysnake.head[0] += greedysnake.speed;
else if (greedysnake.direction == 'l') greedysnake.head[0] -= greedysnake.speed;
else if (greedysnake.direction == 'u') greedysnake.head[1] -= greedysnake.speed;
else greedysnake.head[1] += greedysnake.speed;
//增加难度,加快速度,不要也可以
if (greedysnake.body.size() > 5) {
killTimer(TimerID);
TimerID = startTimer(200); //通过改变调用定时器的频率来达到改变蛇的速度
}
else if (greedysnake.body.size() > 10) {
killTimer(TimerID);
TimerID = startTimer(180);
}
else if (greedysnake.body.size() > 15) {
killTimer(TimerID);
TimerID = startTimer(150);
}
else if (greedysnake.body.size() > 20) {
killTimer(TimerID);
TimerID = startTimer(120);
}
else if (greedysnake.body.size() > 25) {
killTimer(TimerID);
TimerID = startTimer(100);
}
createFood();
}
//全屏移动,即可以从下面穿过,到达上面
if (greedysnake.head[0] < 0) greedysnake.head[0] = 1250;
if (greedysnake.head[0] > 1300)greedysnake.head[0] = 0;
if (greedysnake.head[1] >= 800) greedysnake.head[1] = 50;
if (greedysnake.head[1] <= 0) greedysnake.head[1] = 800;
update(); //更新绘图
}
}
按键事件:
主要的核心功能就在上面了,我这里最后补充一个按键操控蛇的点。
我们可以通过重写窗口的按键事件来获取到玩家按下的按钮。
通过e->key()来获取一个整型数据,每个按钮都对应一个数值,我一个个把按键试出来了,然后写了个逻辑,如果在一开始的时候,我们除了可以通过鼠标点击来开始游戏,也可以通过按下回车键来开始,另外也可以按下空格来开始游戏,但是实际上我没有写关于空格的逻辑,但是它就是可以通过空格来触发那个按钮。
另外在游戏中可以按下空格来暂停游戏。
值得一提的是在改变蛇的移动方向的时候,需要注意不能直接180°转弯。
void MainUI::keyPressEvent(QKeyEvent* e){
int nowKey = e->key();
if (nowKey == 16777220) { //回车键
if(begin!=nullptr) begin->click();
}
if (isBegin) {
//修改运动方向,注意不能直接180°转弯
if ((nowKey == 87 || nowKey == 16777235) && greedysnake.direction != 'd') greedysnake.direction = 'u';
else if ((nowKey == 83 || nowKey == 16777237) && greedysnake.direction != 'u') greedysnake.direction = 'd';
else if ((nowKey == 65 || nowKey == 16777234) && greedysnake.direction != 'r') greedysnake.direction = 'l';
else if ((nowKey == 68 || nowKey == 16777236) && greedysnake.direction != 'l')greedysnake.direction = 'r';
else if (nowKey == 32) isBegin=false;
}else {
if (nowKey == 32) isBegin = true;
}
}
小结:
我的评价是不管是学习什么技术,最好的练手项目就是写一个贪吃蛇,之前学C,java,python,C++的时候都写了贪吃蛇,而现在学了qt也写了个贪吃蛇,这对于巩固基础有着非常好的效果。
想要源码的小伙伴可以直接在我CSDN的主页里找到对应的资源免费下载,我已经上传到SCDN了,也可以关注我的微信公众号:折途想要敲代码,回复关键词“qt贪吃蛇”领取完整的源码。