QT学习路五
贪吃蛇游戏(二)
接下来是有关蛇的处理。
我们有两个必须面对的困难:
- 蛇具有复杂得多的形状。因为蛇的形状随着游戏者的控制而不同,因此,我们必须找出一个能够恰好包含蛇头和所有身体块的矩形。这也是 boundingRect() 函数所要解决的问题。
- 蛇会长大(比如吃了食物之后)。因此,我们需要在蛇对象中增加一个用于代表蛇身体长度的
growing
变量:当growing
为正数时,蛇的身体增加一格;当growing
为负数时,蛇的身体减少一格。 advance()
函数用于编码移动部分,这个函数会在一秒内调用 30 次(这是我们在GameController
的定时器中决定的)。
首先从boundingRect()
开始看起:
QRectF Snake::boundingRect() const
{
qreal minX = head.x();
qreal minY = head.y();
qreal maxX = head.x();
qreal maxY = head.y();
foreach (QPointF p, tail) {
maxX = p.x() > maxX ? p.x() : maxX;
maxY = p.y() > maxY ? p.y() : maxY;
minX = p.x() < minX ? p.x() : minX;
minY = p.y() < minY ? p.y() : minY;
}
QPointF tl = mapFromScene(QPointF(minX, minY));
QPointF br = mapFromScene(QPointF(maxX, maxY));
QRectF bound = QRectF(tl.x(), // x
tl.y(), // y
br.x() - tl.x() + SNAKE_SIZE, // width
br.y() - tl.y() + SNAKE_SIZE //height
);
return bound;
}
shape()
函数决定了蛇身体的形状,我们遍历蛇身体的每一个方块向路径中添加:
QPainterPath Snake::shape() const
{
QPainterPath path;
path.setFillRule(Qt::WindingFill);
path.addRect(QRectF(0, 0, SNAKE_SIZE, SNAKE_SIZE));
foreach (QPointF p, tail) {
QPointF itemp = mapFromScene(p);
path.addRect(QRectF(itemp.x(), itemp.y(), SNAKE_SIZE, SNAKE_SIZE));
}
return path;
}
在实现了shape()
函数的基础之上,paint()
函数就很简单了:
void Snake::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
painter->save();
painter->fillPath(shape(), Qt::yellow);
painter->restore();
}
现在,我们开始添加游戏控制的代码。首先我们从最简单的四个方向键开始:
void Snake::moveLeft()
{
head.rx() -= SNAKE_SIZE;
if (head.rx() < -100) {
head.rx() = 100;
}
}
void Snake::moveRight()
{
head.rx() += SNAKE_SIZE;
if (head.rx() > 100) {
head.rx() = -100;
}
}
void Snake::moveUp()
{
head.ry() -= SNAKE_SIZE;
if (head.ry() < -100) {
head.ry() = 100;
}
}
void Snake::moveDown()
{
head.ry() += SNAKE_SIZE;
if (head.ry() > 100) {
head.ry() = -100;
}
}
然后添加一个比较复杂的函数,借此,可以看出 Graphics View Framework 的强大之处:
void Snake::handleCollisions()
{
QList collisions = collidingItems();
// Check collisions with other objects on screen
foreach (QGraphicsItem *collidingItem, collisions) {
if (collidingItem->data(GD_Type) == GO_Food) {
// Let GameController handle the event by putting another apple
controller.snakeAteFood(this, (Food *)collidingItem);
growing += 1;
}
}
// Check snake eating itself
if (tail.contains(head)) {
controller.snakeAteItself(this);
}
}
下面是advance()
函数的代码:
void Snake::advance(int step)
{
if (!step) {
return;
}
if (tickCounter++ % speed != 0) {
return;
}
if (moveDirection == NoMove) {
return;
}
if (growing > 0) {
QPointF tailPoint = head;
tail << tailPoint;
growing -= 1;
} else {
tail.takeFirst();
tail << head;
}
switch (moveDirection) {
case MoveLeft:
moveLeft();
break;
case MoveRight:
moveRight();
break;
case MoveUp:
moveUp();
break;
case MoveDown:
moveDown();
break;
}
setPos(head);
handleCollisions();
}
给GameController
添加一个事件过滤器:
bool GameController::eventFilter(QObject *object, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
handleKeyPressed((QKeyEvent *)event);
return true;
} else {
return QObject::eventFilter(object, event);
}
}
下面看看这个handleKeyPressed()
函数:
void GameController::handleKeyPressed(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Left:
snake->setMoveDirection(Snake::MoveLeft);
break;
case Qt::Key_Right:
snake->setMoveDirection(Snake::MoveRight);
break;
case Qt::Key_Up:
snake->setMoveDirection(Snake::MoveUp);
break;
case Qt::Key_Down:
snake->setMoveDirection(Snake::MoveDown);
break;
}
}
接下来,要完成游戏逻辑:吃食物、生成新的食物以及咬到自己这三个逻辑:
void GameController::snakeAteFood(Snake *snake, Food *food)
{
scene.removeItem(food);
delete food;
addNewFood();
}
void GameController::addNewFood()
{
int x, y;
do {
x = (int) (qrand() % 100) / 10;
y = (int) (qrand() % 100) / 10;
x *= 10;
y *= 10;
} while (snake->shape().contains(snake->mapFromScene(QPointF(x + 5, y + 5))));
Food *food = new Food(x , y);
scene.addItem(food);
}
void GameController::snakeAteItself(Snake *snake)
{
QTimer::singleShot(0, this, SLOT(gameOver()));
}
void GameController::gameOver()
{
scene.clear();
snake = new Snake(*this);
scene.addItem(snake);
addNewFood();
}
至此,我们已经把这个简单的贪吃蛇游戏全部完成。最后我们来看一下运行结果: