#include "mazescene.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_PHONON #include "mediaplayer/mediaplayer.h" #endif View::View() { resize(1024, 768); setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers))); setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); } void View::resizeEvent(QResizeEvent *) { resetMatrix(); qreal factor = width() / 4.0; scale(factor, factor); } MazeScene::MazeScene(const char *map, int width, int height) : m_cameraPos(1.5, 1.5) , m_cameraAngle(0.1) , m_walkingVelocity(0) , m_strafingVelocity(0) , m_turningVelocity(0) , m_simulationTime(0) , m_walkTime(0) , m_width(width) , m_height(height) { m_doorAnimation = new QTimeLine(1000, this); m_doorAnimation->setUpdateInterval(20); connect(m_doorAnimation, SIGNAL(valueChanged(qreal)), this, SLOT(moveDoors(qreal))); QMap types; types[' '] = -2; types['-'] = -1; types['#'] = 0; types['&'] = 1; types['@'] = 2; types['%'] = 3; types['$'] = 4; types['?'] = 5; types['!'] = 6; types['='] = 7; int type; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { type = types[map[y*width+x]]; if (type >= 0) continue; type = types[map[(y-1)*width+x]]; if (type >= -1) addWall(QPointF(x, y), QPointF(x+1, y), type); type = types[map[(y+1)*width+x]]; if (type >= -1) addWall(QPointF(x+1, y+1), QPointF(x, y+1), type); type = types[map[y*width+x-1]]; if (type >= -1) addWall(QPointF(x, y+1), QPointF(x, y), type); type = types[map[y*width+x+1]]; if (type >= -1) addWall(QPointF(x+1, y), QPointF(x+1, y+1), type); } } QTimer *timer = new QTimer(this); timer->setInterval(20); timer->start(); connect(timer, SIGNAL(timeout()), this, SLOT(move())); m_time.start(); updateTransforms(); } void MazeScene::addWall(const QPointF &a, const QPointF &b, int type) { WallItem *item = new WallItem(this, a, b, type); item->setVisible(false); addItem(item); m_walls << item; if (type == -1) m_doors << item; setSceneRect(-1, -1, 2, 2); if (item->childItem()) { QObject *widget = item->childItem()->widget()->children().value(0); QPushButton *button = qobject_cast(widget); if (button) m_buttons << button; } } static inline QTransform rotatingTransform(qreal angle) { QTransform transform; transform.rotate(angle); return transform; } void MazeScene::drawBackground(QPainter *painter, const QRectF &rect) { QLinearGradient g(QPointF(0, rect.top()), QPointF(0, rect.bottom())); g.setColorAt(0, QColor(0, 0, 0, 50)); g.setColorAt(0.4, QColor(0, 0, 0, 100)); g.setColorAt(0.6, QColor(0, 0, 0, 100)); g.setColorAt(1, QColor(0, 0, 0, 50)); painter->fillRect(QRectF(rect.topLeft(), QPointF(rect.right(), rect.center().y())), QColor(100, 120, 200)); painter->fillRect(QRectF(QPointF(rect.left(), rect.center().y()), rect.bottomRight()), QColor(127, 190, 100)); QTransform rotation = rotatingTransform(m_cameraAngle); rotation.translate(-m_cameraPos.x(), -m_cameraPos.y()); static QImage floor = QImage("floor.png").convertToFormat(QImage::Format_RGB32); QBrush floorBrush(floor); static QImage ceiling = QImage("ceiling.png").convertToFormat(QImage::Format_RGB32); QBrush ceilingBrush(ceiling); QTransform brushScale; brushScale.scale(0.5 / floor.width(), 0.5 / floor.height()); floorBrush.setTransform(brushScale); ceilingBrush.setTransform(brushScale); QTransform project; const qreal fov = 0.5; const qreal wallHeight = 0.5 + 0.04 * qSin(0.01 * m_walkTime) + 0.1; const qreal ceilingHeight = -0.5 + 0.04 * qSin(0.01 * m_walkTime) + 0.1; const QRectF r(1, 1, m_width-2, m_height-2); painter->save(); project = QTransform(rotation.m11(), 0, fov * rotation.m12(), rotation.m21(), 0, fov * rotation.m22(), rotation.m31(), wallHeight, fov * rotation.m32()); painter->setTransform(project, true); painter->fillRect(r, floorBrush); painter->restore(); painter->save(); project = QTransform(rotation.m11(), 0, fov * rotation.m12(), rotation.m21(), 0, fov * rotation.m22(), rotation.m31(), ceilingHeight, fov * rotation.m32()); painter->setTransform(project, true); painter->fillRect(r, ceilingBrush); painter->restore(); painter->fillRect(rect, g); } ProjectedItem::ProjectedItem(const QRectF &bounds, bool shadow) : m_bounds(bounds) , m_shadowItem(0) { if (shadow) { m_shadowItem = new QGraphicsRectItem(bounds, this); m_shadowItem->setPen(Qt::NoPen); m_shadowItem->setZValue(10); } m_targetRect = m_bounds; } void ProjectedItem::setPosition(const QPointF &a, const QPointF &b) { m_a = a; m_b = b; } WallItem::WallItem(MazeScene *scene, const QPointF &a, const QPointF &b, int type) : ProjectedItem(QRectF(-0.5, -0.5, 1.0, 1.0)) , m_type(type) { setPosition(a, b); static QImage brown = QImage("brown.png").convertToFormat(QImage::Format_RGB32); static QImage book = QImage("book.png").convertToFormat(QImage::Format_RGB32); static QImage door = QImage("door.png").convertToFormat(QImage::Format_RGB32); switch (type) { case -1: setImage(door); break; case 1: setImage(book); break; case 2: break; default: setImage(brown); break; } static const char *urls[] = { "http://www.google.com", "http://programming.reddit.com", "http://www.trolltech.com", "http://www.planetkde.org", "http://labs.trolltech.com/blogs/" }; qreal scale = 0.8; QPalette palette; palette.setColor(QPalette::Window, QColor(Qt::transparent)); m_childItem = 0; QWidget *childWidget = 0; if (type == 3 && a.y() == b.y()) { QWidget *widget = new QWidget; QPushButton *button = new QPushButton("Open Sesame", widget); QObject::connect(button, SIGNAL(clicked()), scene, SLOT(toggleDoors())); widget->setLayout(new QVBoxLayout); widget->layout()->addWidget(button); widget->setPalette(palette); childWidget = widget; scale = 0.3; } else if (type == 4) { View *view = new View; view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); view->resize(480, 320); // not soo big view->setViewport(new QWidget); // no OpenGL here childWidget = view; } else if (type == 5) { Entity *entity = new Entity(QPointF(6.5, 2.5)); scene->addEntity(entity); childWidget = new ScriptWidget(scene, entity); } else if (type == 7) { #ifdef USE_PHONON Q_INIT_RESOURCE(mediaplayer); childWidget = new MediaPlayer(QString()); #endif } else if (type == 0 || type == 2) { static int index; if (index == 0) { QWidget *widget = new QWidget; QCheckBox *checkBox = new QCheckBox("Use OpenGL", widget); checkBox->setChecked(true); QObject::connect(checkBox, SIGNAL(toggled(bool)), scene, SLOT(toggleRenderer()), Qt::QueuedConnection); widget->setLayout(new QVBoxLayout); widget->layout()->addWidget(checkBox); widget->setPalette(palette); childWidget = widget; scale = 0.2; } else if (!(index % 7)) { static int webIndex = 0; const char *url = urls[webIndex++ % (sizeof(urls)/sizeof(char*))]; QWebView *view = new QWebView; view->setUrl(QUrl(url)); childWidget = view; } ++index; } if (!childWidget) return; m_childItem = new QGraphicsProxyWidget(this); m_childItem->setWidget(childWidget); m_childItem->setCacheMode(QGraphicsItem::ItemCoordinateCache); QRectF rect = m_childItem->boundingRect(); QPointF center = rect.center(); scale = qMin(scale / rect.width(), scale / rect.height()); m_childItem->translate(0, -0.05); m_childItem->scale(scale, scale); m_childItem->translate(-center.x(), -center.y()); } void ProjectedItem::setDepths(qreal za, qreal zb) { if (!m_shadowItem) return; const qreal falloff = 40; const int maxAlpha = 180; int va = int(falloff * zb); int vb = int(falloff * za); if (va == vb || va >= maxAlpha && vb >= maxAlpha) { m_shadowItem->setBrush(QColor(0, 0, 0, qMin(maxAlpha, va))); } else { qreal xa = 0; qreal xb = 1; if (va >= maxAlpha) { xa = 1 - (maxAlpha - vb) / (va - vb); va = maxAlpha; } if (vb >= maxAlpha) { xb = (maxAlpha - va) / (vb - va); vb = maxAlpha; } const QRectF rect = boundingRect(); QLinearGradient g(rect.topLeft(), rect.topRight()); g.setColorAt(xa, QColor(0, 0, 0, va)); g.setColorAt(xb, QColor(0, 0, 0, vb)); m_shadowItem->setBrush(g); } } QRectF ProjectedItem::boundingRect() const { return m_bounds; } void ProjectedItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (!m_image.isNull()) { QRectF target = m_targetRect.translated(0.5, 0.5); QRectF source = QRectF(0, 0, m_image.width() * (1 - target.x()), m_image.height()); painter->drawImage(m_targetRect, m_image, source); } } void ProjectedItem::setAnimationTime(qreal time) { QRectF rect = boundingRect(); m_targetRect = QRectF(QPointF(rect.left() + rect.width() * time, rect.top()), rect.bottomRight()); if (m_shadowItem) m_shadowItem->setRect(m_targetRect); update(); } void ProjectedItem::setImage(const QImage &image) { m_image = image; update(); } void ProjectedItem::updateTransform(const QPointF &cameraPos, qreal cameraAngle, qreal time) { QTransform rotation = rotatingTransform(cameraAngle); rotation.translate(-cameraPos.x(), -cameraPos.y()); QPointF ca = rotation.map(m_a); QPointF cb = rotation.map(m_b); if (ca.y() <= 0 && cb.y() <= 0) { setVisible(false); return; } const qreal mx = ca.x() - cb.x(); const qreal tx = 0.5 * (ca.x() + cb.x()); const qreal mz = ca.y() - cb.y(); const qreal tz = 0.5 * (ca.y() + cb.y()); const qreal fov = 0.5; const QTransform project(mx, 0, mz * fov, 0, 1, 0, tx, 0.04 * qSin(10 * time) + 0.1, tz * fov); const qreal za = QLineF(QPointF(), ca).length(); const qreal zb = QLineF(QPointF(), cb).length(); const qreal zm = QLineF(QPointF(), (ca + cb) / 2).length(); setVisible(true); setZValue(-zm); setTransform(project); setDepths(za, zb); } void MazeScene::keyPressEvent(QKeyEvent *event) { if (handleKey(event->key(), true)) { event->accept(); return; } QGraphicsScene::keyPressEvent(event); } void MazeScene::keyReleaseEvent(QKeyEvent *event) { if (handleKey(event->key(), false)) { event->accept(); return; } QGraphicsScene::keyReleaseEvent(event); } bool MazeScene::handleKey(int key, bool pressed) { if (focusItem()) return false; switch (key) { case Qt::Key_Left: case Qt::Key_Right: m_turningVelocity = (pressed ? (key == Qt::Key_Left ? -1 : 1) * 0.5 : 0.0); return true; case Qt::Key_Up: case Qt::Key_Down: m_walkingVelocity = (pressed ? (key == Qt::Key_Down ? -1 : 1) * 0.01 : 0.0); return true; case Qt::Key_Z: case Qt::Key_X: m_strafingVelocity = (pressed ? (key == Qt::Key_Z ? -1 : 1) * 0.01 : 0.0); return true; } return false; } static inline QRectF rectFromPoint(const QPointF &point, qreal size) { return QRectF(point, point).adjusted(-size/2, -size/2, size/2, size/2); } bool MazeScene::blocked(const QPointF &pos, Entity *me) const { const QRectF rect = rectFromPoint(pos, me ? 0.7 : 0.25); foreach (WallItem *item, m_walls) { if (item->type() == 6 || item->type() == -1 && m_doorAnimation->state() != QTimeLine::Running && m_doorAnimation->direction() == QTimeLine::Backward) continue; const QPointF a = item->a(); const QPointF b = item->b(); QRectF wallRect = QRectF(a, b).adjusted(-0.01, -0.01, 0.01, 0.01); if (wallRect.intersects(rect)) return true; } foreach (Entity *entity, m_entities) { if (entity == me) continue; QRectF entityRect = rectFromPoint(entity->pos(), 0.8); if (entityRect.intersects(rect)) return true; } if (me) { QRectF cameraRect = rectFromPoint(m_cameraPos, 0.4); if (cameraRect.intersects(rect)) return true; } return false; } bool MazeScene::tryMove(QPointF &pos, const QPointF &delta, Entity *entity) const { const QPointF old = pos; if (delta.x() != 0 && !blocked(pos + QPointF(delta.x(), 0), entity)) pos.setX(pos.x() + delta.x()); if (delta.y() != 0 && !blocked(pos + QPointF(0, delta.y()), entity)) pos.setY(pos.y() + delta.y()); return pos != old; } void MazeScene::updateTransforms() { foreach (WallItem *item, m_walls) { item->updateTransform(m_cameraPos, m_cameraAngle, m_walkTime * 0.001); if (item->isVisible()) { // embed recursive scene if (QGraphicsProxyWidget *child = item->childItem()) { View *view = qobject_cast(child->widget()); if (view && !view->scene()) { const char *map = "#$###" "# #" "# @ #" "# #" "#####"; MazeScene *embeddedScene = new MazeScene(map, 5, 5); view->setScene(embeddedScene); } } } } foreach (Entity *entity, m_entities) entity->updateTransform(m_cameraPos, m_cameraAngle, m_walkTime * 0.001); setFocusItem(0); // setVisible(true) might give focus to one of the items update(); } void MazeScene::move() { QSet movedEntities; long elapsed = m_time.elapsed(); bool walked = false; while (m_simulationTime <= elapsed) { m_cameraAngle += m_turningVelocity; bool walking = false; if (m_walkingVelocity != 0) { QPointF walkingDelta = QLineF::fromPolar(m_walkingVelocity, m_cameraAngle - 90).p2(); if (tryMove(m_cameraPos, walkingDelta)) walking = true; } if (m_strafingVelocity != 0) { QPointF walkingDelta = QLineF::fromPolar(m_strafingVelocity, m_cameraAngle).p2(); if (tryMove(m_cameraPos, walkingDelta)) walking = true; } walked = walked || walking; if (walking) m_walkTime += 5; m_simulationTime += 5; foreach (Entity *entity, m_entities) { if (entity->move(this)) movedEntities.insert(entity); } } if (walked || m_turningVelocity != 0) { updateTransforms(); } else { foreach (Entity *entity, movedEntities) entity->updateTransform(m_cameraPos, m_cameraAngle, m_walkTime * 0.001); } } void MazeScene::toggleDoors() { setFocusItem(0); if (m_doorAnimation->state() == QTimeLine::Running) return; foreach (QPushButton *button, m_buttons) { if (m_doorAnimation->direction() == QTimeLine::Forward) button->setText("Close Sesame!"); else button->setText("Open Sesame!"); } m_doorAnimation->toggleDirection(); m_doorAnimation->start(); } void MazeScene::moveDoors(qreal value) { foreach (WallItem *item, m_doors) item->setAnimationTime(1 - value); } void MazeScene::toggleRenderer() { if (views().size() == 0) return; QGraphicsView *view = views().at(0); if (view->viewport()->inherits("QGLWidget")) view->setViewport(new QWidget); else view->setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers))); } const QImage toAlpha(const QImage &image) { if (image.isNull()) return image; QRgb alpha = image.pixel(0, 0); QImage result = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); QRgb *data = reinterpret_cast(result.bits()); int size = image.width() * image.height(); for (int i = 0; i < size; ++i) if (data[i] == alpha) data[i] = 0; return result; } Entity::Entity(const QPointF &pos) : ProjectedItem(QRectF(-0.3, -0.4, 0.6, 0.9), false) , m_pos(pos) , m_angle(180) , m_walking(false) , m_walked(false) , m_turnVelocity(0) , m_useTurnTarget(false) , m_animationIndex(0) , m_angleIndex(0) { startTimer(300); } void Entity::walk() { m_walking = true; } void Entity::stop() { m_walking = false; m_useTurnTarget = false; m_turnVelocity = 0; } void Entity::turnTowards(qreal x, qreal y) { m_turnTarget = QPointF(x, y); m_useTurnTarget = true; } void Entity::turnLeft() { m_useTurnTarget = false; m_turnVelocity = 0.5; } void Entity::turnRight() { m_useTurnTarget = false; m_turnVelocity = -0.5; } static QVector loadSoldierImages() { QVector images; for (int i = 1; i <= 40; ++i) { QImage image(QString("soldier/O%0.png").arg(i, 2, 10, QLatin1Char('0'))); images << toAlpha(image.convertToFormat(QImage::Format_RGB32)); } return images; } static inline int mod(int x, int y) { return ((x % y) + y) % y; } void Entity::updateTransform(const QPointF &cameraPos, qreal cameraRotation, qreal time) { qreal angleToCamera = QLineF(m_pos, cameraPos).angle(); int cameraAngleIndex = mod(qRound(angleToCamera + 22.5), 360) / 45; m_angleIndex = mod(qRound(cameraAngleIndex * 45 - m_angle + 22.5), 360) / 45; QPointF delta = QLineF::fromPolar(1, 270.1 + 45 * cameraAngleIndex).p2(); setPosition(m_pos - delta, m_pos + delta); updateImage(); ProjectedItem::updateTransform(cameraPos, cameraRotation, time); } bool Entity::move(MazeScene *scene) { bool moved = false; if (m_useTurnTarget) { qreal angleToTarget = QLineF::fromPolar(1, m_angle) .angleTo(QLineF(m_pos, m_turnTarget)); if (angleToTarget != 0) { if (angleToTarget >= 180) angleToTarget -= 360; if (angleToTarget < 0) m_angle -= qMin(-angleToTarget, 0.5); else m_angle += qMin(angleToTarget, 0.5); moved = true; } } else if (m_turnVelocity != 0) { m_angle += m_turnVelocity; moved = true; } m_walked = false; if (m_walking) { QPointF walkingDelta = QLineF::fromPolar(0.006, m_angle).p2(); if (scene->tryMove(m_pos, walkingDelta, this)) { moved = true; m_walked = true; } } return moved; } void Entity::timerEvent(QTimerEvent *) { ++m_animationIndex; updateImage(); } void Entity::updateImage() { static QVector images = loadSoldierImages(); if (m_walked) setImage(images.at(8 + 8 * (m_animationIndex % 4) + m_angleIndex)); else setImage(images.at(m_angleIndex)); } void MazeScene::addEntity(Entity *entity) { addItem(entity); m_entities << entity; } static QScriptValue qsRand(QScriptContext *, QScriptEngine *engine) { QScriptValue value(engine, qrand() / (RAND_MAX + 1.0)); return value; } void ScriptWidget::setPreset(int preset) { const char *presets[] = { "// available functions:\n" "// entity.turnLeft()\n" "// entity.turnRight()\n" "// entity.turnTowards(x, y)\n" "// entity.walk()\n" "// entity.stop()\n" "// rand()\n" "// script.display()\n" "\n" "// available variables:\n" "// my_x\n" "// my_y\n" "// player_x\n" "// player_y\n" "// time\n" "\n" "entity.stop();\n", "entity.walk();\n" "if ((time % 20000) < 10000) {\n" " entity.turnTowards(10, 2.5);\n" " if (my_x >= 5.5)\n" " entity.stop();\n" "} else {\n" " entity.turnTowards(-10, 2.5);\n" " if (my_x <= 2.5)\n" " entity.stop();\n" "}\n", "dx = player_x - my_x;\n" "dy = player_y - my_y;\n" "if (dx * dx + dy * dy < 5) {\n" " entity.stop();\n" "} else {\n" " entity.walk();\n" " entity.turnTowards(player_x, player_y);\n" "}\n" }; m_sourceEdit->setPlainText(QLatin1String(presets[preset])); } ScriptWidget::ScriptWidget(MazeScene *scene, Entity *entity) : m_scene(scene) , m_entity(entity) { new QVBoxLayout(this); m_statusView = new QLineEdit; m_statusView->setReadOnly(true); layout()->addWidget(m_statusView); m_sourceEdit = new QPlainTextEdit; layout()->addWidget(m_sourceEdit); QPushButton *compileButton = new QPushButton(QLatin1String("Compile")); layout()->addWidget(compileButton); QComboBox *combo = new QComboBox; layout()->addWidget(combo); combo->addItem(QLatin1String("Default")); combo->addItem(QLatin1String("Patrol")); combo->addItem(QLatin1String("Follow")); setPreset(0); connect(combo, SIGNAL(currentIndexChanged(int)), this, SLOT(setPreset(int))); connect(compileButton, SIGNAL(clicked()), this, SLOT(updateSource())); m_engine = new QScriptEngine(this); QScriptValue entityObject = m_engine->newQObject(m_entity); m_engine->globalObject().setProperty("entity", entityObject); QScriptValue widgetObject = m_engine->newQObject(this); m_engine->globalObject().setProperty("script", widgetObject); m_engine->globalObject().setProperty("rand", m_engine->newFunction(qsRand)); m_engine->setProcessEventsInterval(5); resize(300, 400); updateSource(); startTimer(50); m_time.start(); } void ScriptWidget::timerEvent(QTimerEvent *event) { QPointF player = m_scene->cameraPosition(); QPointF entity = m_entity->pos(); QScriptValue px(m_engine, player.x()); QScriptValue py(m_engine, player.y()); QScriptValue ex(m_engine, entity.x()); QScriptValue ey(m_engine, entity.y()); QScriptValue time(m_engine, m_time.elapsed()); m_engine->globalObject().setProperty("player_x", px); m_engine->globalObject().setProperty("player_y", py); m_engine->globalObject().setProperty("my_x", ex); m_engine->globalObject().setProperty("my_y", ey); m_engine->globalObject().setProperty("time", time); m_engine->evaluate(m_source); if (m_engine->hasUncaughtException()) { QString text = m_engine->uncaughtException().toString(); m_statusView->setText(text); } } void ScriptWidget::display(QScriptValue value) { m_statusView->setText(value.toString()); } void ScriptWidget::updateSource() { bool wasEvaluating = m_engine->isEvaluating(); if (wasEvaluating) m_engine->abortEvaluation(); m_time.restart(); m_source = m_sourceEdit->toPlainText(); if (wasEvaluating) m_statusView->setText(QLatin1String("Aborted long running evaluation!")); else if (m_engine->canEvaluate(m_source)) m_statusView->setText(QLatin1String("Evaluation succeeded")); else m_statusView->setText(QLatin1String("Evaluation failed")); }