Implement actual game engine and rendering opengl and vulkan

This commit is contained in:
feedc0de committed 2026-11-14 15:41:14 +01:00
1 parent 543196d99e
commit defb83c9c3
18 files changed
+1243 -981

No files matched your search

+2
View File
@@ -54,6 +54,7 @@ HEADERS += \
src/editor/widgets/zoomablescrollarea.h \
src/engine/gameengine.h \
src/engine/glgamewindow.h \
src/engine/gamescene.h \
src/engine/vulkangamerenderer.h \
src/engine/vulkangamewindow.h \
src/futurecpp.h \
@@ -132,6 +133,7 @@ SOURCES += \
src/editor/widgets/zoomablescrollarea.cpp \
src/engine/gameengine.cpp \
src/engine/glgamewindow.cpp \
src/engine/gamescene.cpp \
src/engine/vulkangamerenderer.cpp \
src/engine/vulkangamewindow.cpp \
src/main.cpp \
+69 -41
View File
@@ -1,68 +1,96 @@
#include "gameengine.h"
#include <QVulkanInstance>
#include <algorithm>
#include <QEventLoop>
#include <QTimer>
#include "closeeventfilter.h"
#include "projectcontainer.h"
GameEngine::GameEngine(const ProjectContainer &project, QObject *parent) :
QObject{parent},
m_project{project},
m_glGameWindow{m_project, m_rotation},
m_vulkanGameWindow{m_project, m_rotation},
m_timerId(startTimer(1000/60, Qt::PreciseTimer))
m_scene{project},
m_glGameWindow{m_project, m_scene},
m_vulkanGameWindow{m_project, m_scene}
{
m_vulkanInstance.setLayers(QByteArrayList { "VK_LAYER_LUNARG_standard_validation" });
if (!m_vulkanInstance.create())
qFatal("Failed to create Vulkan instance: %d", m_vulkanInstance.errorCode());
m_vulkanGameWindow.setVulkanInstance(&m_vulkanInstance);
m_vulkanAvailable = m_vulkanInstance.create();
if (m_vulkanAvailable)
m_vulkanGameWindow.setVulkanInstance(&m_vulkanInstance);
else
qWarning("Could not create Vulkan instance: %d", m_vulkanInstance.errorCode());
m_glGameWindow.setModality(Qt::WindowModal);
m_vulkanGameWindow.setModality(Qt::WindowModal);
m_frameTimer.setTimerType(Qt::PreciseTimer);
m_frameTimer.setSingleShot(true);
connect(&m_frameTimer, &QTimer::timeout, this, [this]() {
constexpr qint64 frameNanoseconds = 1000000000 / 60;
m_scene.advanceTo(m_elapsed.elapsed());
m_scene.draw();
m_glGameWindow.requestUpdate();
if (m_vulkanAvailable)
m_vulkanGameWindow.requestUpdate();
const qint64 now = m_elapsed.nsecsElapsed();
m_nextFrameNanoseconds = std::max(m_nextFrameNanoseconds + frameNanoseconds,
(now / frameNanoseconds + 1) * frameNanoseconds);
const qint64 remaining = m_nextFrameNanoseconds - m_elapsed.nsecsElapsed();
m_frameTimer.start(int(std::max<qint64>(1, (remaining + 999999) / 1000000)));
});
}
void GameEngine::run()
{
m_glGameWindow.setTitle(tr("Game Window - OpenGL"));
m_vulkanGameWindow.setTitle(tr("Game Window - Vulkan"));
if (m_project.rooms.empty())
return;
m_scene.start([this](ObjectInstance &target, const Action &action, ObjectInstance *) {
if (target.applyAction(action))
return;
static constexpr std::array actionNames{
"MoveFixed", "MoveFree", "MoveTowards", "SpeedHorizontal",
"SpeedVertical", "SetGravity", "ExecuteCode"
};
const std::size_t type = action.index();
if (!m_warnedActionTypes[type])
{
m_warnedActionTypes[type] = true;
qWarning("Game action %s has no runtime implementation yet", actionNames[type]);
}
});
CloseEventFilter closeEventFilter;
m_glGameWindow.installEventFilter(&closeEventFilter);
m_vulkanGameWindow.installEventFilter(&closeEventFilter);
if (m_vulkanAvailable)
m_vulkanGameWindow.installEventFilter(&closeEventFilter);
QEventLoop eventLoop;
bool shuttingDown = false;
connect(&closeEventFilter, &CloseEventFilter::closeEventReceived,
&eventLoop, &QEventLoop::quit);
&eventLoop, [this, &eventLoop, &shuttingDown]() {
if (shuttingDown)
return;
shuttingDown = true;
m_frameTimer.stop();
// Finish both QWindow close paths before their Vulkan instance is destroyed.
if (m_vulkanAvailable)
m_vulkanGameWindow.close();
m_glGameWindow.close();
QTimer::singleShot(0, &eventLoop, &QEventLoop::quit);
}, Qt::QueuedConnection);
m_glGameWindow.setTitle(m_glGameWindow.title() + tr(" - OpenGL"));
m_glGameWindow.show();
m_vulkanGameWindow.show();
QTimer::singleShot(100, &m_glGameWindow, [this](){
auto geometry = m_glGameWindow.geometry();
qDebug() << geometry;
geometry.moveLeft(geometry.left() - (geometry.width() / 2));
m_glGameWindow.setGeometry(geometry);
});
QTimer::singleShot(100, &m_vulkanGameWindow, [this](){
auto geometry = m_vulkanGameWindow.geometry();
qDebug() << geometry;
geometry.moveLeft(geometry.left() + (geometry.width() / 2));
m_vulkanGameWindow.setGeometry(geometry);
});
if (m_vulkanAvailable)
{
m_vulkanGameWindow.setTitle(m_vulkanGameWindow.title() + tr(" - Vulkan"));
m_vulkanGameWindow.show();
}
m_elapsed.start();
m_nextFrameNanoseconds = 1000000000 / 60;
m_frameTimer.start(17);
eventLoop.exec();
}
void GameEngine::timerEvent(QTimerEvent *event)
{
if (event->timerId() == m_timerId)
{
m_rotation += 1.f;
}
else
QObject::timerEvent(event);
}
+13 -11
View File
@@ -1,7 +1,13 @@
#pragma once
#include <QObject>
#include <array>
#include <QObject>
#include <QElapsedTimer>
#include <QTimer>
#include <QVulkanInstance>
#include "engine/gamescene.h"
#include "engine/glgamewindow.h"
#include "engine/vulkangamewindow.h"
@@ -13,21 +19,17 @@ class GameEngine : public QObject
public:
explicit GameEngine(const ProjectContainer &project, QObject *parent = nullptr);
void run();
protected:
void timerEvent(QTimerEvent *event) override;
private:
const ProjectContainer &m_project;
GameScene m_scene;
QVulkanInstance m_vulkanInstance;
float m_rotation{};
GlGameWindow m_glGameWindow;
VulkanGameWindow m_vulkanGameWindow;
const int m_timerId;
bool m_vulkanAvailable{};
QTimer m_frameTimer;
QElapsedTimer m_elapsed;
qint64 m_nextFrameNanoseconds{};
std::array<bool, std::variant_size_v<Action>> m_warnedActionTypes{};
};
+359
View File
@@ -0,0 +1,359 @@
#include "gamescene.h"
#include <algorithm>
#include <cmath>
#include <numbers>
#include <type_traits>
#include <utility>
#include <QHash>
#include <QRandomGenerator>
namespace {
std::size_t frameIndex(double imageIndex, std::size_t count)
{
const auto size = static_cast<qint64>(count);
qint64 index = static_cast<qint64>(std::floor(imageIndex)) % size;
if (index < 0)
index += size;
return static_cast<std::size_t>(index);
}
QPointF motion(double degrees, double speed)
{
const double radians = degrees * std::numbers::pi / 180.0;
return {speed * std::cos(radians), -speed * std::sin(radians)};
}
} // namespace
void ObjectInstance::executeActions(const ActionsContainer &actions, GameScene &scene,
const ActionExecutor &executor, ObjectInstance *other)
{
if (!executor)
return;
for (const Action &action : actions)
{
const AppliesTo &appliesTo = std::visit([](const auto &value) -> const AppliesTo & {
return value.appliesTo;
}, action);
std::visit([&](const auto &target) {
using Target = std::decay_t<decltype(target)>;
if constexpr (std::is_same_v<Target, AppliesToSelf>)
executor(*this, action, other);
else if constexpr (std::is_same_v<Target, AppliesToOther>)
{
if (other)
executor(*other, action, this);
}
else
{
for (ObjectInstance &instance : scene.instances)
{
if (instance.definition && instance.definition->name == target.objectName)
executor(instance, action, this);
}
}
}, appliesTo);
}
}
void ObjectInstance::executeEvent(Object::EventType event, GameScene &scene,
const ActionExecutor &executor, ObjectInstance *other)
{
if (!definition)
return;
const auto found = definition->events.find(event);
if (found != definition->events.end())
executeActions(found->second, scene, executor, other);
}
void ObjectInstance::executeCollisionEvent(const QString &objectName, GameScene &scene,
const ActionExecutor &executor, ObjectInstance &other)
{
if (!definition)
return;
const auto found = definition->collisionEvents.find(objectName);
if (found != definition->collisionEvents.end())
executeActions(found->second, scene, executor, &other);
}
void ObjectInstance::step()
{
const QPointF gravityMotion = motion(gravityDirection, gravity);
hspeed += gravityMotion.x();
vspeed += gravityMotion.y();
position += QPointF{hspeed, vspeed};
imageIndex += imageSpeed;
}
bool ObjectInstance::setAlarm(std::size_t index, int steps)
{
if (index >= alarms.size())
return false;
alarms[index] = steps > 0 ? steps : -1;
return true;
}
bool ObjectInstance::applyAction(const Action &action)
{
return std::visit([this](const auto &value) -> bool {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, SpeedHorizontalAction>)
hspeed = value.relative ? hspeed + value.speed : value.speed;
else if constexpr (std::is_same_v<T, SpeedVerticalAction>)
vspeed = value.relative ? vspeed + value.speed : value.speed;
else if constexpr (std::is_same_v<T, SetGravityAction>)
{
gravityDirection = value.relative ? gravityDirection + value.direction : value.direction;
gravity = value.relative ? gravity + value.gravity : value.gravity;
}
else if constexpr (std::is_same_v<T, MoveFreeAction>)
{
const QPointF velocity = motion(value.direction, value.speed);
hspeed = value.relative ? hspeed + velocity.x() : velocity.x();
vspeed = value.relative ? vspeed + velocity.y() : velocity.y();
}
else if constexpr (std::is_same_v<T, MoveTowardsAction>)
{
const QPointF offset{double(value.x), double(value.y)};
const QPointF goal = value.relative ? position + offset : offset;
const QPointF delta = goal - position;
const double distance = std::hypot(delta.x(), delta.y());
if (distance > 0)
{
hspeed = value.speed * delta.x() / distance;
vspeed = value.speed * delta.y() / distance;
}
else
hspeed = vspeed = 0;
}
else if constexpr (std::is_same_v<T, MoveFixedAction>)
{
std::array<int, 9> choices{};
int count = 0;
for (int i = 0; i < int(choices.size()); ++i)
if (value.directions[std::size_t(i)])
choices[std::size_t(count++)] = i;
if (count == 0)
return true;
const int direction = choices[QRandomGenerator::global()->bounded(count)];
constexpr std::array<double, 9> angles{135, 90, 45, 180, 0, 0, 225, 270, 315};
const QPointF velocity = direction == 4 ? QPointF{} : motion(angles[std::size_t(direction)], value.speed);
hspeed = value.relative ? hspeed + velocity.x() : velocity.x();
vspeed = value.relative ? vspeed + velocity.y() : velocity.y();
}
else
return false; // ExecuteCode needs a GML interpreter.
return true;
}, action);
}
GameScene::GameScene(const ProjectContainer &project)
{
if (project.rooms.empty())
return;
const Room &room = project.rooms.front();
roomSize = QSize{std::max(1, room.width), std::max(1, room.height)};
caption = room.caption.isEmpty() ? room.name : room.caption;
roomSpeed = std::max(1, room.speed);
QHash<QString, std::size_t> spriteIndices;
for (const ::Sprite &source : project.sprites)
{
Sprite sprite;
sprite.origin = QPoint{source.origin.x, source.origin.y};
for (const QPixmap &pixmap : source.pixmaps)
{
if (pixmap.isNull())
continue;
sprite.frames.push_back(textures.size());
textures.push_back(pixmap.toImage());
sprite.collisionBounds.push_back(QRect{QPoint{}, textures.back().size()});
}
spriteIndices.insert(source.name, sprites.size());
sprites.push_back(std::move(sprite));
}
QHash<QString, const Object *> objects;
for (const Object &object : project.objects)
objects.insert(object.name, &object);
for (const Room::Object &placed : room.objects)
{
const Object *object = objects.value(placed.objectName, nullptr);
if (!object)
continue;
const auto sprite = spriteIndices.constFind(object->spriteName);
Instance instance;
instance.definition = object;
if (sprite != spriteIndices.cend() && !sprites.at(*sprite).frames.empty())
instance.spriteIndex = *sprite;
const auto mask = spriteIndices.constFind(object->maskSpriteName);
if (!object->maskSpriteName.isEmpty() && mask != spriteIndices.cend() &&
!sprites.at(*mask).frames.empty())
instance.maskSpriteIndex = *mask;
instance.position = QPointF{placed.pos};
instance.depth = object->depth;
instance.visible = object->visible;
instances.push_back(instance);
}
}
std::size_t GameScene::textureFor(const Instance &instance) const
{
const Sprite &sprite = sprites.at(*instance.spriteIndex);
return sprite.frames.at(frameIndex(instance.imageIndex, sprite.frames.size()));
}
QRectF GameScene::collisionRect(const Instance &instance) const
{
const auto index = instance.maskSpriteIndex ? instance.maskSpriteIndex : instance.spriteIndex;
if (!index)
return {};
const Sprite &sprite = sprites.at(*index);
const QRect &bounds = sprite.collisionBounds.at(frameIndex(instance.imageIndex, sprite.frames.size()));
if (bounds.isEmpty())
return {};
return {instance.position.x() - sprite.origin.x() + bounds.x(),
instance.position.y() - sprite.origin.y() + bounds.y(),
double(bounds.width()), double(bounds.height())};
}
std::vector<const GameScene::Instance *> GameScene::drawOrder() const
{
std::vector<const Instance *> order;
order.reserve(instances.size());
for (const Instance &instance : instances)
order.push_back(&instance);
// Larger depths are behind smaller ones; equal depths keep room placement order.
std::stable_sort(order.begin(), order.end(), [](const Instance *left, const Instance *right) {
return left->depth > right->depth;
});
return order;
}
void GameScene::start(const ActionExecutor &executor)
{
m_executor = executor;
m_stepsDone = 0;
for (Instance &instance : instances)
if (!instance.pendingDestroy)
instance.executeEvent(Object::EventType::Create, *this, m_executor);
removeDestroyed();
}
void GameScene::destroyInstance(Instance &instance)
{
if (instance.pendingDestroy)
return;
instance.pendingDestroy = true;
instance.executeEvent(Object::EventType::Destroy, *this, m_executor);
}
void GameScene::removeDestroyed()
{
std::erase_if(instances, [](const Instance &instance) { return instance.pendingDestroy; });
}
void GameScene::advanceTo(qint64 elapsedMilliseconds)
{
const qint64 targetStep = elapsedMilliseconds * roomSpeed / 1000;
while (m_stepsDone < targetStep)
{
for (Instance &instance : instances)
if (!instance.pendingDestroy)
instance.executeEvent(Object::EventType::BeginStep, *this, m_executor);
for (Instance &instance : instances)
{
if (instance.pendingDestroy)
continue;
for (std::size_t index = 0; index < instance.alarms.size(); ++index)
{
int &remaining = instance.alarms[index];
if (remaining <= 0 || --remaining != 0)
continue;
remaining = -1;
const auto event = static_cast<Object::EventType>(int(Object::EventType::Alarm0) + int(index));
instance.executeEvent(event, *this, m_executor);
if (instance.pendingDestroy)
break;
}
}
for (Instance &instance : instances)
if (!instance.pendingDestroy)
instance.executeEvent(Object::EventType::Step, *this, m_executor);
std::vector<QPointF> previousPositions;
previousPositions.reserve(instances.size());
for (const Instance &instance : instances)
previousPositions.push_back(instance.position);
for (Instance &instance : instances)
if (!instance.pendingDestroy)
instance.step();
// Detect every pair at its moved position before collision actions can
// change either instance. A sprite-size rectangle is our first mask.
std::vector<std::pair<std::size_t, std::size_t>> collisions;
for (std::size_t i = 0; i < instances.size(); ++i)
{
if (instances[i].pendingDestroy)
continue;
const QRectF first = collisionRect(instances[i]);
if (first.isEmpty())
continue;
for (std::size_t j = i + 1; j < instances.size(); ++j)
{
if (instances[j].pendingDestroy)
continue;
const QRectF second = collisionRect(instances[j]);
if (!second.isEmpty() && first.intersects(second))
collisions.emplace_back(i, j);
}
}
for (const auto &[i, j] : collisions)
{
Instance &first = instances[i];
Instance &second = instances[j];
if (first.pendingDestroy || second.pendingDestroy)
continue;
const QString &firstName = first.definition->name;
const QString &secondName = second.definition->name;
const auto firstEvent = first.definition->collisionEvents.find(secondName);
const auto secondEvent = second.definition->collisionEvents.find(firstName);
const bool firstHandles = firstEvent != first.definition->collisionEvents.end() &&
!firstEvent->second.empty();
const bool secondHandles = secondEvent != second.definition->collisionEvents.end() &&
!secondEvent->second.empty();
if (!firstHandles && !secondHandles)
continue;
if (first.definition->solid || second.definition->solid)
{
first.position = previousPositions[i];
second.position = previousPositions[j];
}
if (firstHandles)
first.executeCollisionEvent(secondName, *this, m_executor, second);
if (secondHandles && !second.pendingDestroy && !first.pendingDestroy)
second.executeCollisionEvent(firstName, *this, m_executor, first);
}
for (Instance &instance : instances)
if (!instance.pendingDestroy)
instance.executeEvent(Object::EventType::EndStep, *this, m_executor);
removeDestroyed();
++m_stepsDone;
}
}
void GameScene::draw()
{
for (Instance &instance : instances)
if (instance.visible && !instance.pendingDestroy)
instance.executeEvent(Object::EventType::Draw, *this, m_executor);
removeDestroyed();
}
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <array>
#include <cstddef>
#include <functional>
#include <optional>
#include <vector>
#include <QImage>
#include <QPointF>
#include <QRectF>
#include <QSize>
#include <QString>
#include "projectcontainer.h"
struct GameScene;
struct ObjectInstance;
// The interpreter supplies action behavior. `other` is the collision counterpart
// for self actions, or the event source when an action targets another instance.
using ActionExecutor = std::function<void(ObjectInstance &target, const Action &action,
ObjectInstance *other)>;
struct ObjectInstance
{
ObjectInstance() { alarms.fill(-1); }
const Object *definition{};
std::optional<std::size_t> spriteIndex;
std::optional<std::size_t> maskSpriteIndex;
QPointF position;
double hspeed{};
double vspeed{};
double gravity{};
double gravityDirection{270.0};
double imageIndex{};
double imageSpeed{1.0};
int depth{};
bool visible{true};
bool pendingDestroy{};
std::array<int, 12> alarms;
void step();
bool setAlarm(std::size_t index, int steps);
bool applyAction(const Action &action);
void executeActions(const ActionsContainer &actions, GameScene &scene,
const ActionExecutor &executor, ObjectInstance *other = nullptr);
void executeEvent(Object::EventType event, GameScene &scene,
const ActionExecutor &executor, ObjectInstance *other = nullptr);
void executeCollisionEvent(const QString &objectName, GameScene &scene,
const ActionExecutor &executor, ObjectInstance &other);
};
// Mutable runtime state shared by the OpenGL and Vulkan renderers.
struct GameScene
{
struct Sprite
{
std::vector<std::size_t> frames;
std::vector<QRect> collisionBounds;
QPoint origin;
};
using Instance = ObjectInstance;
explicit GameScene(const ProjectContainer &project);
std::size_t textureFor(const Instance &instance) const;
QRectF collisionRect(const Instance &instance) const;
std::vector<const Instance *> drawOrder() const;
void start(const ActionExecutor &executor);
void destroyInstance(Instance &instance);
void advanceTo(qint64 elapsedMilliseconds);
void draw();
QSize roomSize;
QString caption;
int roomSpeed{30};
std::vector<QImage> textures;
std::vector<Sprite> sprites;
std::vector<Instance> instances;
private:
qint64 m_stepsDone{};
ActionExecutor m_executor;
void removeDestroyed();
};
+178 -105
View File
@@ -1,160 +1,233 @@
#include "glgamewindow.h"
#include <QOpenGLContext>
#include <QOpenGLPaintDevice>
#include <QOpenGLShaderProgram>
#include <QPainter>
#include <QScreen>
#include <QKeyEvent>
#include <QDebug>
#include <array>
#include <cstddef>
GlGameWindow::GlGameWindow(const ProjectContainer &project, const float &rotation, QWindow *parent) :
#include <QKeyEvent>
#include <QMatrix4x4>
#include <QOpenGLContext>
#include <QOpenGLShaderProgram>
#include <QPlatformSurfaceEvent>
#include "gamescene.h"
#include "projectcontainer.h"
namespace {
struct Vertex
{
float x, y, z, u, v;
};
constexpr std::array<Vertex, 4> quad {{
{0.f, 0.f, 0.f, 0.f, 0.f},
{0.f, 1.f, 0.f, 0.f, 1.f},
{1.f, 0.f, 0.f, 1.f, 0.f},
{1.f, 1.f, 0.f, 1.f, 1.f}
}};
}
GlGameWindow::GlGameWindow(const ProjectContainer &project, const GameScene &scene, QWindow *parent) :
QWindow{parent},
m_project{project},
m_rotation{rotation}
m_scene{scene}
{
setSurfaceType(QWindow::OpenGLSurface);
setMinimumWidth(640);
setMaximumWidth(640);
setMinimumHeight(480);
setMaximumHeight(480);
resize(640, 480);
if (!m_project.rooms.empty())
{
resize(m_scene.roomSize);
if (!m_project.globalGameSettings.resizable)
{
setMinimumSize(m_scene.roomSize);
setMaximumSize(m_scene.roomSize);
}
setTitle(m_scene.caption);
}
}
GlGameWindow::~GlGameWindow() = default;
void GlGameWindow::initialize()
GlGameWindow::~GlGameWindow()
{
m_program = new QOpenGLShaderProgram{this};
if (!m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, QStringLiteral(":/qtgameengine/opengl_shaders/color.vert")))
qFatal("could not load vertex shader");
if (!m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, QStringLiteral(":/qtgameengine/opengl_shaders/color.frag")))
qFatal("could not load fragment shader");
m_program->link();
m_posAttr = m_program->attributeLocation("posAttr");
Q_ASSERT(m_posAttr != -1);
m_colAttr = m_program->attributeLocation("colAttr");
Q_ASSERT(m_colAttr != -1);
m_matrixUniform = m_program->uniformLocation("matrix");
Q_ASSERT(m_matrixUniform != -1);
releaseResources();
}
void GlGameWindow::render()
bool GlGameWindow::initialize()
{
const qreal retinaScale = devicePixelRatio();
glViewport(0, 0, width() * retinaScale, height() * retinaScale);
initializeOpenGLFunctions();
glClear(GL_COLOR_BUFFER_BIT);
m_program = new QOpenGLShaderProgram;
if (!m_program->addShaderFromSourceFile(QOpenGLShader::Vertex,
QStringLiteral(":/qtgameengine/opengl_shaders/texture.vert")) ||
!m_program->addShaderFromSourceFile(QOpenGLShader::Fragment,
QStringLiteral(":/qtgameengine/opengl_shaders/texture.frag")) ||
!m_program->link())
{
qWarning("Could not compile OpenGL sprite shaders: %s", qPrintable(m_program->log()));
return false;
}
if (!m_vao.create())
{
qWarning("Could not create OpenGL vertex array");
return false;
}
m_vao.bind();
glGenBuffers(1, &m_vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad.data(), GL_STATIC_DRAW);
m_program->bind();
QMatrix4x4 matrix;
matrix.perspective(60.0f, 4.0f / 3.0f, 0.1f, 100.0f);
matrix.translate(0, 0, -2);
matrix.rotate(m_rotation, 0, 1, 0);
m_program->setUniformValue(m_matrixUniform, matrix);
static const GLfloat vertices[] {
0.0f, 0.707f,
-0.5f, -0.5f,
0.5f, -0.5f
};
static const GLfloat colors[] {
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f
};
glVertexAttribPointer(m_posAttr, 2, GL_FLOAT, GL_FALSE, 0, vertices);
glVertexAttribPointer(m_colAttr, 3, GL_FLOAT, GL_FALSE, 0, colors);
glEnableVertexAttribArray(m_posAttr);
glEnableVertexAttribArray(m_colAttr);
glDrawArrays(GL_TRIANGLES, 0, 3);
glDisableVertexAttribArray(m_colAttr);
glDisableVertexAttribArray(m_posAttr);
const int position = m_program->attributeLocation("position");
const int texCoord = m_program->attributeLocation("texCoord");
if (position < 0 || texCoord < 0)
{
qWarning("OpenGL sprite shader has missing attributes");
return false;
}
glEnableVertexAttribArray(position);
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<const void *>(offsetof(Vertex, x)));
glEnableVertexAttribArray(texCoord);
glVertexAttribPointer(texCoord, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<const void *>(offsetof(Vertex, u)));
m_program->setUniformValue("tex", 0);
m_program->release();
m_vao.release();
m_textures.reserve(m_scene.textures.size());
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
for (const QImage &source : m_scene.textures)
{
const QImage image = source.convertToFormat(QImage::Format_RGBA8888);
GLuint texture{};
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(),
0, GL_RGBA, GL_UNSIGNED_BYTE, image.constBits());
m_textures.push_back(texture);
}
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
return true;
}
void GlGameWindow::renderLater()
void GlGameWindow::releaseResources()
{
requestUpdate();
if (!m_context)
return;
if (m_context->makeCurrent(this))
{
if (!m_textures.empty())
glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
if (m_vertexBuffer)
glDeleteBuffers(1, &m_vertexBuffer);
m_vao.destroy();
delete m_program;
m_program = nullptr;
m_context->doneCurrent();
}
else
delete m_program;
m_textures.clear();
m_vertexBuffer = 0;
m_program = nullptr;
delete m_context;
m_context = nullptr;
}
void GlGameWindow::renderNow()
{
if (!isExposed())
if (!isExposed() || m_project.rooms.empty())
return;
bool needsInitialize = false;
if (!m_context)
{
m_context = new QOpenGLContext{this};
m_context = new QOpenGLContext;
m_context->setFormat(requestedFormat());
m_context->create();
needsInitialize = true;
if (!m_context->create())
{
qWarning("Could not create OpenGL context for the game window");
delete m_context;
m_context = nullptr;
return;
}
if (!m_context->makeCurrent(this) || !initialize())
{
releaseResources();
return;
}
}
else if (!m_context->makeCurrent(this))
return;
m_context->makeCurrent(this);
const qreal scale = devicePixelRatio();
glViewport(0, 0, qRound(width() * scale), qRound(height() * scale));
glClearColor(1.f, 1.f, 1.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
if (needsInitialize)
QMatrix4x4 projection;
projection.ortho(0.f, float(m_scene.roomSize.width()),
float(m_scene.roomSize.height()), 0.f, -1.f, 1.f);
m_program->bind();
m_vao.bind();
glActiveTexture(GL_TEXTURE0);
for (const GameScene::Instance *instance : m_scene.drawOrder())
{
initializeOpenGLFunctions();
initialize();
if (!instance->visible || !instance->spriteIndex)
continue;
const std::size_t textureIndex = m_scene.textureFor(*instance);
const QImage &image = m_scene.textures.at(textureIndex);
const QPoint &origin = m_scene.sprites.at(*instance->spriteIndex).origin;
QMatrix4x4 matrix = projection;
matrix.translate(float(instance->position.x() - origin.x()),
float(instance->position.y() - origin.y()));
matrix.scale(float(image.width()), float(image.height()));
m_program->setUniformValue("matrix", matrix);
glBindTexture(GL_TEXTURE_2D, m_textures.at(textureIndex));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
render();
m_vao.release();
m_program->release();
glBindTexture(GL_TEXTURE_2D, 0);
m_context->swapBuffers(this);
renderLater();
}
bool GlGameWindow::event(QEvent *event)
{
switch (event->type())
if (event->type() == QEvent::PlatformSurface &&
static_cast<QPlatformSurfaceEvent *>(event)->surfaceEventType() ==
QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed)
releaseResources();
if (event->type() == QEvent::UpdateRequest)
{
case QEvent::UpdateRequest:
renderNow();
return true;
default:
return QWindow::event(event);
}
return QWindow::event(event);
}
void GlGameWindow::keyPressEvent(QKeyEvent *event)
{
QWindow::keyPressEvent(event);
switch (event->key())
{
case Qt::Key_Escape:
if (event->key() == Qt::Key_Escape && m_project.globalGameSettings.letEscEndGame)
close();
break;
}
}
void GlGameWindow::keyReleaseEvent(QKeyEvent *event)
{
QWindow::keyReleaseEvent(event);
else
QWindow::keyPressEvent(event);
}
void GlGameWindow::exposeEvent(QExposeEvent *event)
{
Q_UNUSED(event);
QWindow::exposeEvent(event);
if (isExposed())
renderNow();
}
+14 -19
View File
@@ -1,11 +1,14 @@
#pragma once
#include <QWindow>
#include <vector>
#include <QOpenGLFunctions>
#include <QOpenGLVertexArrayObject>
#include <QWindow>
class QOpenGLContext;
class QOpenGLPaintDevice;
class QOpenGLShaderProgram;
struct GameScene;
struct ProjectContainer;
class GlGameWindow : public QWindow, protected QOpenGLFunctions
@@ -13,33 +16,25 @@ class GlGameWindow : public QWindow, protected QOpenGLFunctions
Q_OBJECT
public:
explicit GlGameWindow(const ProjectContainer &project, const float &m_rotation, QWindow *parent = nullptr);
~GlGameWindow();
explicit GlGameWindow(const ProjectContainer &project, const GameScene &scene, QWindow *parent = nullptr);
~GlGameWindow() override;
void initialize();
void render();
public slots:
void renderLater();
void renderNow();
protected:
bool event(QEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void keyReleaseEvent(QKeyEvent *event) override;
void exposeEvent(QExposeEvent *event) override;
private:
bool initialize();
void releaseResources();
const ProjectContainer &m_project;
const GameScene &m_scene;
QOpenGLContext *m_context{};
QOpenGLPaintDevice *m_device{};
GLint m_posAttr = 0;
GLint m_colAttr = 0;
GLint m_matrixUniform = 0;
QOpenGLShaderProgram *m_program{};
const float &m_rotation;
QOpenGLVertexArrayObject m_vao;
GLuint m_vertexBuffer{};
std::vector<GLuint> m_textures;
};
+11
View File
@@ -0,0 +1,11 @@
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 vTexCoord;
uniform sampler2D tex;
void main()
{
gl_FragColor = texture2D(tex, vTexCoord);
}
+10
View File
@@ -0,0 +1,10 @@
attribute vec3 position;
attribute vec2 texCoord;
varying vec2 vTexCoord;
uniform mat4 matrix;
void main()
{
vTexCoord = texCoord;
gl_Position = matrix * vec4(position, 1.0);
}
+2
View File
@@ -2,6 +2,8 @@
<qresource prefix="/qtgameengine">
<file>opengl_shaders/color.frag</file>
<file>opengl_shaders/color.vert</file>
<file>opengl_shaders/texture.frag</file>
<file>opengl_shaders/texture.vert</file>
<file>vulkan_shader_modules/color_frag.spv</file>
<file>vulkan_shader_modules/color_vert.spv</file>
<file>vulkan_shader_modules/texture_frag.spv</file>
@@ -1,10 +1,9 @@
#version 440
layout(location = 0) in vec2 v_texcoord;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D tex;
layout(binding = 0) uniform sampler2D tex;
void main()
{
@@ -1,18 +1,16 @@
#version 440
layout(location = 0) in vec4 position;
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 texcoord;
layout(location = 0) out vec2 v_texcoord;
layout(std140, binding = 0) uniform buf {
mat4 mvp;
} ubuf;
out gl_PerVertex { vec4 gl_Position; };
layout(push_constant) uniform Push {
mat4 matrix;
} push;
void main()
{
v_texcoord = texcoord;
gl_Position = ubuf.mvp * position;
gl_Position = push.matrix * vec4(position, 1.0);
}
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large. Load diff
+32 -34
View File
@@ -1,59 +1,57 @@
#pragma once
#include <QVulkanWindow>
#include <QImage>
#include <vector>
struct ProjectContainer;
#include <QImage>
#include <QVulkanWindow>
struct GameScene;
class VulkanGameRenderer : public QVulkanWindowRenderer
{
public:
VulkanGameRenderer(const ProjectContainer &project, const float &rotation, QVulkanWindow *w, bool msaa = false);
VulkanGameRenderer(const GameScene &scene, QVulkanWindow *window);
void initResources() override;
void initSwapChainResources() override;
void releaseSwapChainResources() override;
void releaseResources() override;
void startNextFrame() override;
protected:
private:
struct Texture
{
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkImage stagingImage = VK_NULL_HANDLE;
VkDeviceMemory stagingMemory = VK_NULL_HANDLE;
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
QSize size;
bool layoutPending{};
bool stagingPending{};
};
VkShaderModule createShader(const QString &name);
bool createTextureFromFile(const QString &name);
bool createTexture(const QImage &image);
bool createTextureImage(const QSize &size, VkImage *image, VkDeviceMemory *mem,
VkImageTiling tiling, VkImageUsageFlags usage, uint32_t memIndex);
bool writeLinearImage(const QImage &img, VkImage image, VkDeviceMemory memory);
void ensureTexture();
bool createTexture(const QImage &image, Texture &texture);
bool createTextureImage(const QSize &size, VkImageTiling tiling,
VkImageUsageFlags usage, VkImage *image, VkDeviceMemory *memory);
bool writeLinearImage(const QImage &image, VkImage target, VkDeviceMemory memory);
void ensureTextures();
const GameScene &m_scene;
QVulkanWindow *m_window;
QVulkanDeviceFunctions *m_devFuncs;
VkDeviceMemory m_bufMem = VK_NULL_HANDLE;
VkBuffer m_buf = VK_NULL_HANDLE;
VkDescriptorBufferInfo m_uniformBufInfo[QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT];
QVulkanDeviceFunctions *m_devFuncs{};
VkBuffer m_vertexBuffer = VK_NULL_HANDLE;
VkDeviceMemory m_vertexMemory = VK_NULL_HANDLE;
VkDescriptorPool m_descPool = VK_NULL_HANDLE;
VkDescriptorSetLayout m_descSetLayout = VK_NULL_HANDLE;
VkDescriptorSet m_descSet[QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT];
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE;
VkPipeline m_pipeline = VK_NULL_HANDLE;
VkSampler m_sampler = VK_NULL_HANDLE;
VkImage m_texImage = VK_NULL_HANDLE;
VkDeviceMemory m_texMem = VK_NULL_HANDLE;
bool m_texLayoutPending = false;
VkImageView m_texView = VK_NULL_HANDLE;
VkImage m_texStaging = VK_NULL_HANDLE;
VkDeviceMemory m_texStagingMem = VK_NULL_HANDLE;
bool m_texStagingPending = false;
QSize m_texSize;
VkFormat m_texFormat;
QMatrix4x4 m_proj;
const ProjectContainer &m_project;
const float &m_rotation;
VkFormat m_texFormat = VK_FORMAT_R8G8B8A8_UNORM;
std::vector<Texture> m_textures;
QMatrix4x4 m_projection;
};
+17 -13
View File
@@ -2,35 +2,39 @@
#include <QKeyEvent>
#include "projectcontainer.h"
#include "gamescene.h"
#include "vulkangamerenderer.h"
VulkanGameWindow::VulkanGameWindow(const ProjectContainer &project, const float &rotation, QWindow *parent) :
VulkanGameWindow::VulkanGameWindow(const ProjectContainer &project, const GameScene &scene, QWindow *parent) :
QVulkanWindow{parent},
m_project{project},
m_rotation{rotation}
m_scene{scene}
{
setMinimumWidth(640);
setMaximumWidth(640);
setMinimumHeight(480);
setMaximumHeight(480);
resize(640, 480);
setPreferredColorFormats({VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM});
if (!m_project.rooms.empty())
{
resize(m_scene.roomSize);
if (!m_project.globalGameSettings.resizable)
{
setMinimumSize(m_scene.roomSize);
setMaximumSize(m_scene.roomSize);
}
setTitle(m_scene.caption);
}
}
QVulkanWindowRenderer *VulkanGameWindow::createRenderer()
{
return new VulkanGameRenderer{m_project, m_rotation, this, true}; // try MSAA, when available
return new VulkanGameRenderer{m_scene, this};
}
void VulkanGameWindow::keyPressEvent(QKeyEvent *event)
{
QVulkanWindow::keyPressEvent(event);
switch (event->key())
{
case Qt::Key_Escape:
if (event->key() == Qt::Key_Escape && m_project.globalGameSettings.letEscEndGame)
close();
break;
}
}
void VulkanGameWindow::keyReleaseEvent(QKeyEvent *event)
+3 -2
View File
@@ -4,11 +4,12 @@
#include <QVulkanWindowRenderer>
struct ProjectContainer;
struct GameScene;
class VulkanGameWindow : public QVulkanWindow
{
public:
explicit VulkanGameWindow(const ProjectContainer &project, const float &rotation, QWindow *parent = nullptr);
explicit VulkanGameWindow(const ProjectContainer &project, const GameScene &scene, QWindow *parent = nullptr);
QVulkanWindowRenderer *createRenderer() override;
@@ -18,5 +19,5 @@ protected:
private:
const ProjectContainer &m_project;
const float &m_rotation;
const GameScene &m_scene;
};