接下来我打算使用qml进行一个简单的rpg游戏编写,以此来练习qml
1要做一个rpg游戏首先要知道一个网格系统的概念,游戏中使用网格系统来达到图块资源的重复利用,更重要的是把地图分为一个个的小方块能够更好的处理我们的地图,对于程序后续编写的便利性很重要。
我们这里采用tiled来进行地图的绘制,程序中使用手动解析的方式进行图块信息的解析,使用qml进行画面渲染和每一帧时间间隙的控制
首先我们选择tiled绘制一个基本的土块地图
绘制地图我们要知道应该知道想要的地图大小是多少,考虑到rpg游戏要略微大于整体的屏幕并且尽可能的容纳更多东西,这里使用32.32的长宽进行构建地图
然后我们可以设置好地图之后,保存为json文档就可以开始编码工作了
1.业务方面新建一个MapLoading类和MapRenderer渲染类
这个是进行地图加载的核心逻辑先检测url是否存在,如果存在就转化为doc,然后根据json内容转化为object,这个类主要是为了接受信号进行处理,继承 QObject
bool MapLoading::loadMap(const QString &url) {
m_currentMapPath = url; // 设置当前地图路径
QFile file(url);
if (!file.open(QIODevice::ReadOnly)) {
emit loadError(tr("Failed to open map file"));
return false;
}
QByteArray data = file.readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isNull()) {
emit loadError(tr("Invalid JSON format"));
return false;
}
QJsonObject root = doc.object();
// 解析地图基本信息
m_mapData.width = root["width"].toInt();
m_mapData.height = root["height"].toInt();
m_mapData.tileWidth = root["tilewidth"].toInt();
m_mapData.tileHeight = root["tileheight"].toInt();
// 清空现有数据
m_mapData.layers.clear();
m_mapData.tilesets.clear();
// 解析tilesets
QJsonArray tilesetsArray = root["tilesets"].toArray();
for (const QJsonValue &tilesetValue : tilesetsArray) {
TileSet tileset;
if (parseTileset(tilesetValue.toObject(), tileset)) {
m_mapData.tilesets.append(tileset);
}
}
// 解析layers
QJsonArray layersArray = root["layers"].toArray();
for (const QJsonValue &layerValue : layersArray) {
TileLayer layer;
if (parseLayer(layerValue.toObject(), layer)) {
m_mapData.layers.append(layer);
}
}
emit mapLoaded();
return true;
}
MapRenderer作为一个渲染类负责对画面进行渲染,继承了QQuickPaintedItem,主要是因为需要重写绘制函数,普通的控件或者qml控件很难实现
void MapRenderer::paint(QPainter *painter) {
if (!m_mapData) return;
painter->setRenderHint(QPainter::Antialiasing);
// 绘制每个可见图层
for (const TileLayer &layer : m_mapData->layers) {
if (!layer.visible) continue;
for (int y = 0; y < layer.height; y++) {
for (int x = 0; x < layer.width; x++) {
int tileId = layer.data[y * layer.width + x];
if (tileId == 0) continue; // 0表示空图块
// 查找对应的tileset
TileSet* targetTileset = nullptr;
for (TileSet &tileset : m_mapData->tilesets) {
if (tileId >= tileset.firstGid &&
(tileset.tileCount == 0 || tileId < tileset.firstGid + tileset.tileCount)) {
targetTileset = &tileset;
break;
}
}
if (targetTileset && !targetTileset->imagePath.isEmpty()) {
// 加载图块集图像
static QHash<QString, QImage> imageCache;
if (!imageCache.contains(targetTileset->imagePath)) {
QImage img(targetTileset->imagePath);
if (img.isNull()) {
qWarning() << "Failed to load image:" << targetTileset->imagePath;
continue;
}
imageCache[targetTileset->imagePath] = img;
}
const QImage &tilesetImage = imageCache[targetTileset->imagePath];
// 计算图块在tileset中的位置
int localId = tileId - targetTileset->firstGid;
int tileX = (localId % targetTileset->columns) * targetTileset->tileWidth;
int tileY = (localId / targetTileset->columns) * targetTileset->tileHeight;
QRect sourceRect(tileX, tileY, targetTileset->tileWidth, targetTileset->tileHeight);
QRectF targetRect(x * m_mapData->tileWidth,
y * m_mapData->tileHeight,
m_mapData->tileWidth,
m_mapData->tileHeight);
painter->drawImage(targetRect, tilesetImage, sourceRect);
}
}
}
}
}
void MapRenderer::setMapData(MapData* data) {
if (m_mapData != data) {
m_mapData = data;
if (m_mapData) {
setWidth(m_mapData->width * m_mapData->tileWidth);
setHeight(m_mapData->height * m_mapData->tileHeight);
}
update();
emit mapDataChanged();
}
}
界面方面使用自定义qml
// MapComponent.qml
import QtQuick 2.15
import RpgGame
Item {
id: mapContainer
property string mapUrl: ""
Component.onCompleted: {
if (mapUrl) {
mapLoader.loadMap(mapUrl);
}
}
MapLoading {
id: mapLoader
onMapLoaded: {
mapRenderer.mapData = mapLoader.getMapData();
}
onLoadError: {
console.error("Map loading error:", error);
}
}
MapRenderer {
id: mapRenderer
anchors.fill: parent
}
}
整体逻辑为: 当我们调用mapLoader.loadMap(mapUrl)会进行map的json文件解析,解析结束触发槽函数获取地图数据,然后触发界面更新的逻辑
运行结果如下
