Создание игр с QCanvas

Creating games with QCanvas
Автор: Gunnstein Lye
Перевод: Andi Peredri

Qt-класс QCanvas обеспечивает работу с двухмерной графикой, поддержку спрайтов и обнаружение столкновений. В этой статье показано, как его использовать на примере небольшой игры.

Начальные сведения о QCanvas

Класс QCanvas ( canvas - полотно, холст, картина ) хорошо подходит для разработки двухмерных игр, так как он оптимизирован по скорости и поддерживает некоторые весьма полезные возможности, например, спрайты ( анимированные изображения ), обнаружение столкновений ( необходимо в большинстве двухмерных игр ), встроенные классы различных фигур и анимацию. Для его использования Вам необходимо добавить в Ваше приложение объект QCanvas. Для просмотра используется объект QCanvasView ( QWidget ). Затем Вы можете добавить объекты QCanvasItems. Мы собираемся продемонстрировать это в небольшой игре.

Игра

Демонстрируемая игра очень проста, но необычайно забавна: в ней представлены двухмерное пространство и шар. Шар можно захватить и отпустить с помощью мыши, и он станет колебаться вперед-назад до тех пор, пока воздушное сопротивление, трение и гравитация его не остановят, или пока Вы не толкнете его снова. Чтобы сделать это более занятным, в качестве пространства используется Вселенная, а в качестве шара... правильно, Земля. ( Некоторые люди могут возразить, что в космосе нет воздушного сопротивления, потому что там нет воздуха, и что направленная вниз сила тяжести лишена смысла. Они же возразят Вам, что счастливое пение птиц в прекрасный весенний день является агрессивной охраной территории. Технически это правильно, но в чем тогда интерес? )

Классы

Мы используем 3 класса: класс Ball, который является потомком QCanvasSprite ( который наследуется от QCanvasItem ). Это отскакивающий земной шар, он содержит функции, управляющие его движением. Класс BounceCanvasView, который является потомком QCanvasView и содержит функции, обрабатывающие события мыши. И, наконец, класс BounceWidget ( QWidget ) является окном приложения, он содержит Canvas-вид и кнопку "Quit".

BounceWidget

Конструктор BounceWidget создает объект QCanvas и настраивает BounceCanvasView для его просмотра. Также мы устанавливаем интервал для обновления изображения равным 10 мс, создаем планету и устанавливаем ее начальное положение и скорость. Кроме этого, мы инициализируем изображения планеты и фона. Для их смены просто замените "stars.png" и "earth.png" чем-нибудь другим. ( Примечание: изображение земли, конечно же квадратное, а не круглое. Для достижения иллюзии круга Вы должны использовать формат, который поддерживает прозрачность, например, PNG или GIF. )

Конструктор BounceWidget

BounceWidget::BounceWidget ( QWidget *parent, const char *name )
    : QWidget ( parent, name )
{
    // Создаем и настраиваем QCanvas и QCanvasView
    Canvas = new QCanvas ( this, "Canvas" );
    Canvas->setBackgroundColor ( Qt::black );
    Canvas->setBackgroundPixmap ( QPixmap ( "stars.png" ) );
    Canvas->setAdvancePeriod ( 10 );
    Canvas->resize ( 500, 500 );
    CanvasView = new BounceCanvasView ( Canvas, this, "CanvasView" );

    // Создаем кнопку завершения работы приложения
    QuitButton = new QPushButton ( "&Quit", this, "QuitButton" );
    connect ( QuitButton, SIGNAL ( clicked() ), qApp, SLOT( quit() ) );

    // Создаем менеджер компоновки
    QVBoxLayout *vlay = new QVBoxLayout ( this );
    vlay->setResizeMode ( QLayout::Fixed );
    vlay->addWidget ( CanvasView );
    vlay->addWidget ( QuitButton );

    // Добавляем планету
    Ball *sphere = new Ball ( new QCanvasPixmapArray ( "earth.png" ), Canvas );
    sphere->move ( 100, 150 );
    sphere->setXVelocity ( 2 );
    sphere->setYVelocity ( 4 );
    sphere->show();
}

BounceCanvasView

Класс BounceCanvasView содержит таймер QTimer, который запускается в момент захвата шара и, пока мы его не отпустим, посылает сигнал с интервалом в 10 мс. Этот сигнал связывается со слотом timeout(), который сохраняет текущее положение указателя мыши и помещает его в очередь десяти последних положений. После отпускания шара мы используем эту очередь, чтобы вычислить среднюю скорость движения мыши за последние 100 мс. Полученное значение скорости используется для броска шара. Благодаря этому хорошо моделируется бросок, мы можем заставить шар двигаться быстрее, медленнее или вовсе его остановить.

BounceCanvasView::contentsMouseReleaseEvent

Это обработчик событий мыши. ( Ссылка на файл с полным исходным кодом программы в конце этой статьи. )

void BounceCanvasView::contentsMouseReleaseEvent ( QMouseEvent* )
{
    if ( Moving )         // Если мы удерживали шар
    {
        Timer->stop();   // Останавливаем таймер

        if ( MousePosQueue.count() ) // Если мы сохраняли положения мыши...
        {
            // ...то вычисляем среднюю скорость
            int count = MousePosQueue.count();
            QPoint velocity ( 0, 0 );
            QPoint pos;
            QPoint prevPos = *MousePosQueue.head();

            while ( MousePosQueue.count () )
            {
                pos = *MousePosQueue.dequeue();
                velocity += pos - prevPos;
                prevPos = pos;
            }

            // Устанавливаем новую скорость шара
            Moving->setXVelocity ( velocity.x() / count );
            Moving->setYVelocity ( velocity.y() / count );
        }
        else  // Если сохраненных положений мыши нет ( быстрый щелчок )
        {
            // устанавливаем нулевую скорость ( мы останавливаем шар )
            Moving->setXVelocity ( 0 );
            Moving->setYVelocity ( 0 );
        }

        Moving->setZ ( 1 );
        canvas()->setAdvancePeriod ( 10 );  // перезапускаем анимацию
        Moving = 0;  // обнуляем указатель, мы больше не удерживаем шар
    }
}

Ball

Этот класс содержит физические параметры шара ( массу и сопротивление ), функцию hit(), которая проверяет, попали ли мы в прозрачную область шара, и функцию advance(int), которая работает в два этапа. На первом этапе мы устанавливаем скорость движения шара, учитывая сопротивление и гравитацию. При столкновении шара со стеной мы меняем скорость на противоположную под смежным углом к стене и прекращаем его дальнейшее движение в этом направлении, чтобы он не двигался сквозь стену. На втором этапе мы перемещаем шар на один шаг. Вам не следует беспокоиться о вызове advance(int) с верными параметрами, это обеспечивает QCanvas.

Ball::advance

void Ball::advance ( int phase )
{
    if ( phase == 0 ) // вызывается перед началом перемещения
    {
        // учитываем сопротивление, чтобы замедлить движение шара
        setXVelocity ( xVelocity() * Air_resistance );
        setYVelocity ( yVelocity() * Air_resistance );

        // учитываем гравитацию
        setYVelocity( yVelocity() + Mass );

        // проверка столкновения с правым краем
        if ( !canvas()->onCanvas( x() + width()/2 + xVelocity(), 1 ))
        {
            MoveX = false;
            move ( canvas()->width() - width()/2, y() );
            setXVelocity ( xVelocity() * -1 * Bump_resistance );
        }

        // проверка столкновения с левым краем
        else if ( !canvas()->onCanvas ( x() - width()/2 + xVelocity(), 1 ))
        {
            MoveX = false;
            move ( 0 + width()/2, y() );
            setXVelocity ( xVelocity() * -1 * Bump_resistance );
        }

        // проверка столкновения с нижним краем
        if ( !canvas()->onCanvas( 1, y() + height()/2 + yVelocity() ))
        {
            MoveY = false;
            move ( x(), canvas()->height() - height()/2 );
            setYVelocity ( yVelocity() * -1 * Bump_resistance );
        }
	
        // проверка столкновения с верхним краем
        else if ( !canvas()->onCanvas ( 1, y() - height()/2 + yVelocity() ))
        {
            MoveY = false;
            move ( x(), 0 + height()/2 );
            setYVelocity ( yVelocity() * -1 * Bump_resistance );
        }
    }
    else    // перемещение
    {
        if ( MoveX && MoveY )
            move( x() + xVelocity(), y() + yVelocity() );
        else if ( MoveX )
            move( x() + xVelocity(), y() );
        else if ( MoveY )
            move( x(), y() + yVelocity() );

        MoveX = MoveY = true;
    }
}

Main

Файл main.cpp прост, как в большинстве приложений Qt. Мы создаем объекты QApplication, BounceWidget и устанавливаем заголовок окна.

int main ( int argc, char **argv )
{
    QApplication a( argc, argv );

    BounceWidget bw ( 0, "BounceWidget" );
    a.setMainWidget ( &bw );
    bw.setCaption ( "Astrophysics for beginners!" );
    bw.show();

    return a.exec();
}

Что теперь?

Ниже перечисленные идеи предоставлены для реализации читателям:
1. Добавьте солнце и остальные планеты, обеспечьте их взаимное столкновение. Сделайте кнопки для их добавления и удаления.
2. Добавьте возможность движением мыши с нажатой левой кнопкой отбрасывать шар также, как Вы отбрасываете теннисный шарик. Для этого Вы должны оставлять таймер постоянно действующим, чтобы знать скорость перемещения мыши в момент столкновения с шаром. Для того, чтобы обеспечить корректное перемещение шара при ударе по нему под углом, Вам придется добавить некоторые дополнительные вычисления.
3. Добавьте влияние движения шара на его вращение и/или его вращение вокруг своей оси. Для этого Вам придется создать дополнительные изображения.
4. Добавьте возможность отключения сопротивления, это приведет к бесконечному движению шара.
5. Сделайте так, чтобы при сильных столкновениях шара о стены, встряхивалось окно.

Инструкции по сборке:
tar -zxf bounce.tar.gz
cd bounce
qmake bounce.pro -o Makefile
make
./bounce

Исходный код программы: bounce.tar.gz