QMLで2Dタイルマップを作成します。 パート1





私を訪問した最初の考えは、「実際、これは何ですか?」

まあ、それは何もないようです:

•テクスチャの配列を作成し、

•カードのサイズを示し、

•配列をループして、オブジェクトを作成します。

それはまさに私が最初からやったことです...



小さな余談


タイルとは何なのかを詳しく説明したくありません。記事ではそのことについて説明していません。 読者は、ゲーム内のアイソメ図とは何か、タイルとは何か、それらはどのようなものか、どのように描画されるかについて既にある程度の考えを持っていると想定されています。 基本的な等尺性タイルは2対1の比率で作成されます。つまり、タイルの幅が2単位の場合、高さは1単位でなければなりません。

私のプロジェクトでは、寸法が1から1の擬似3Dタイルを使用することに注意してください。これらは次のようになります。







ただし、この「キューブ」の半分のみが使用されます(赤で強調表示)。 これまでのところ、クリップされたボトムの使用方法は思いつきませんでしたが、将来的には、マップの山、くぼみ、または平凡な崖に使用される可能性が高いでしょう。 次に、おそらくz-indexを使用する必要がありますが、それは別の話です



PS 記事の最後にプロジェクトソースがあります



最初のステップ



これは、コードが私のパスの最初にどのように見えるかです:

property int mapcols: 4 // -   x () property int maprows: mapcols * 3 // -   y () //  3   :   //    -    function createMap() { //         -     // (    !), //       var tilecount = mapcols * maprows //     for(var tileid = 0; tileid < tilecount; tileid++) { //         var col = tileid % mapcols var row = Math.floor(tileid / mapcols) //    //   ,      //        ,    var iseven = !(row&1) //    var tx = iseven ? col * tilesizew : col * tilesizew + tilesizew/2 var ty = iseven ? row * tilesizeh : row * tilesizeh - tilesizeh/2 ty -= Math.floor(row/2) * tilesizeh //  ,      var component = Qt.createComponent("Tile.qml"); var tile = component.createObject(mapitem, { "x": tx, "y": ty, "z": tileid, "col": col, "row": row, "id": tileid }); } }
      
      







以上です。 最小限の労力で、このような素晴らしいマップを作成できました。





Tile.qmlのコンテンツはペイントしません。将来的にはこのコンポーネントはまったく必要ないからです。 そして、そうすることは全く価値がないからです!

説明してみましょう:寸法4x12( mapcols * maprows )の地図を描くことにより、48個のオブジェクトが作成されました。 しかし、そのような競技場は明らかに小さすぎます。 たとえば、幅20タイルの大きなフィールドを描画すると、その高さは60タイルになり、これは1200のビジュアルオブジェクトになります ! 非常に多くのオブジェクトを格納するためにどれだけのメモリが使用されるかを想像することは難しくありません。 一言で言えば-たくさん。



反射



マップを作成するための新しい方法について長く考える必要はありませんでした。 まず、新しい方法で達成すべきマップの主なパラメーターを特定しました。

1.カードは移動可能でなければなりません(プレイヤーはカードを任意の方向にスクロールできます)。

2.ウィンドウの外側にあるオブジェクトは描画しないでください。

3.メソッドはできるだけシンプルに実装する必要があります%)



最初のウィッシュリストは、 Flickable要素を使用して非常に簡単に実装できます。 どうして? スクロール、イベントのキャッチに煩わされる必要はありません...一般的に、まったく気にする必要はありません。これは3番目のポイントを完全に満たします:-)要素の名前はmap_area -map_areaになります。



Flickableにマップを移動する機能を付与するには、ピクセル単位のマップのフルサイズに等しいサイズで、フリックに要素を作成する必要があります。 このためには、通常のItemが適しています-この要素は視覚的ではないため、そのサイズは消費されるメモリの量に影響しません。 彼はキーネームマップ -マップを負います。



テクスチャを描画するには、追加の要素を使用する必要があります。これは、 マップ要素内に配置する必要があります。 同時に、そのサイズはmap_areaのサイズに対応する必要があり、この要素が常に「見える」ようにするには、マップスクロールの反対側に移動する必要があります。 つまり ユーザーがマップを左に移動した場合、この要素は右に移動して再描画する必要があります。

このアイデアを実装するには、 QQuickImageProvider備えImageバンドルが適している可能性がありますが、その機能はかなり不足しているため、ダークサイド-C ++に頼って独自のコンポーネントを作成する必要があります。 将来のアイテムはQQuickPaintedItemの子孫となり、 MapProviderという名前になります。



シンプルから...シンプルへ



私の見解では、それは次のように見えました。





コードでは、次のようになります。

 Window { id: root visible: true width: 600 height: 600 //   //  ,   ?     // ""  ,         property double tilesize: 128 property double tilesizew: tilesize property double tilesizeh: tilesizew / 2 //    X   Y (   .) property int mapcols: 20 property int maprows: mapcols * 3 Flickable { id: maparea width: root.width height: root.height contentWidth: map.width contentHeight: map.height Item { id: map width: mapcols * tilesizew height: maprows * tilesizeh Item /*MapProvider*/ { id: mapprovider } } } }
      
      





このコードは、今後の作業のスケルトンになります。 次のステップは、 MapProvider要素を作成することです 。 これを行うには、プロジェクトに新しいC ++クラスを作成します。

 class MapProvider : public QQuickPaintedItem { Q_OBJECT public: MapProvider(QQuickItem *parent = 0); void paint(QPainter *painter) { //      } };
      
      







この要素をすぐにQMLに登録します。そのためには、 main.cppを編集します。 その内容は次のようになります。



 #include <QGuiApplication> #include <QQmlApplicationEngine> #include "mapprovider.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); //   : qmlRegisterType<MapProvider>("game.engine", 1, 0, "MapProvider"); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec(); }
      
      







変更を保存した後、この要素をQMLで使用できます。



これを行うには、モジュールインポートをmain.qmlに追加します。

 import game.engine 1.0
      
      





そして、行を置き換えます

 Item /*MapProvider*/ {
      
      







 MapProvider {
      
      







メソッドがどのように機能するかを明確に示すために、フォーム上に2つの追加要素を作成しました。ウィンドウ内に特別な領域game_areaを指定し、そこにmap_area要素を移動しました 。 ゲーム領域のサイズをフォームのサイズよりも意図的に小さくし、この領域の境界を表示するために、通常のRectangleを作成しました。

 //    X   Y (   .) property int mapcols: 20 property int maprows: mapcols * 3 Item { id: gamearea width: root.width / 2 height: root.height / 2 x: width / 2 y: height / 2 clip: false Flickable { id: maparea width: root.width height: root.height contentWidth: map.width contentHeight: map.height Item { id: map width: mapcols * tilesizew height: maprows * tilesizeh MapProvider { id: mapprovider } } } } Rectangle { id: gameareaborder width: gamearea.width height: gamearea.height x: gamearea.x y: gamearea.y border.width: 1 border.color: "red" color: "transparent" } }
      
      







湿式計算-大量の水があるセクション



私たちはほぼ地図を描くことに近づいていますが、注意する価値のあるニュアンスがいくつかあります。 そして、考慮すべき最初の候補は、マップのエッジです。 私たちでは、彼らは「 歯が生える 」ことが判明しました。 これは前のプロジェクトで観察できましたが、新しいプロジェクトではそれを取り除く必要があります。 歯を左と上から隠すには、マップ( Item:map )を左に移動し、タイルの幅と高さの半分を上に移動します。

  Item { id: map width: mapcols * tilesizew height: maprows * tilesizeh x: -tilesizew / 2 y: -tilesizeh / 2
      
      











左右の歯を隠すには、 contentWidthおよびcontentHeightパラメーターを変更してスクロールを制限するだけです。 ここでは、カードを既に左に半分のサイズにシフトしているという事実を考慮する必要があります。つまり、コンテンツサイズをタイルサイズ全体で縮小する必要があります。

  Flickable { id: maparea contentWidth: map.width - tilesizew contentHeight: map.height - tilesizeh
      
      







スクロール中にMapProvider要素を移動する実装は次のようになります。

  MapProvider { id: mapprovider width: gamearea.width + tilesizew * 2 height: gamearea.height + tilesizeh * 2 x: maparea.contentX + (tilesizew/2 - maparea.contentX%tilesizew + map.x) y: maparea.contentY + (tilesizeh/2 - maparea.contentY%tilesizeh + map.y)
      
      





不気味な:)ここで何が起こっているのかを説明します。



実際、私たちの地図は、菱形のタイルが刻まれた長方形のブロックで構成されています。 このため、わずかなスクロールでマップの表示領域を再描画する必要はありません。表示領域の外側にある「 保護ゾーン 」(適切な名前が出ていない)を選択するだけで、マップ全体とともに描画され、マップ全体を再描画する必要があるのは、スクロールはこのゾーンのサイズを超えます。 このため、マップの必要な再描画の回数は数百回減少します(タイルのサイズによって異なります)。

このコードでは、この「保護ゾーン」は、 MapProviderの幅と高さに2倍のタイルサイズを追加して計算されます。 したがって、レンダリングされた領域を右と下にちょうど2タイル拡張します。 このエリアの半分を左上に広げるには、 map_areaのコンテンツのサイズとマップmapのサイズを調整する必要があります

  Flickable { id: maparea contentWidth: map.width - tilesizew * 1.5 contentHeight: map.height - tilesizeh / 2 /* ... */ Item { id: map width: mapcols * tilesizew + tilesizew height: maprows * tilesizeh / 2
      
      







MapProvider要素のXおよびYを計算する式は、スクロールが「保護ゾーン」を超えた場合にのみ、 ジャンプのような動きを提供します。 将来的には、これらのジャンプにマップ再描画イベントが添付されます。



体に近い



したがって、QML側の計算は終了しました。 次にMapProvider要素の「 body 」を正しくレンダリングするために必要な追加パラメーターのセットを決定する必要があります。

1. map_areaのコンテンツの実際の位置-マップが描画される列と行の数を計算するために必要になります(描画は左上から始まるため、左上のタイルのインデックスが見つかります)。 これらのパラメーターにcxおよびcyという名前を付けました。

2.タイルのサイズ-画像の描画に必要です。

3.マップサイズ-実際のタイルインデックスの計算に必要です。

4.実際には、テクスチャマップの説明。 リソースの名前を持つこの通常の1次元配列があります。

  MapProvider { id: mapprovider width: gamearea.width + tilesizew*2 height: gamearea.height + tilesizeh*2 x: maparea.contentX + (tilesizew/2 - maparea.contentX%tilesizew + map.x) y: maparea.contentY + (tilesizeh/2 - maparea.contentY%tilesizeh + map.y) cx: maparea.contentX cy: maparea.contentY tilesize: root.tilesize tilesizew: root.tilesizew tilesizeh: root.tilesizeh mapcols: root.mapcols maprows: root.maprows mapdata: [ "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004", "0004","0004","0004","0004","0004","0004","0004","0004" ] }
      
      





PS ここで、「0004」は、拡張子のない画像リソースの名前です。



もちろん、これらのパラメーターはすべてC ++側で宣言する必要があり、これはすべてQ_PROPERTYマクロを使用して行われます。

 class MapProvider : public QQuickPaintedItem { Q_OBJECT Q_PROPERTY(double tilesize READ tilesize WRITE setTilesize NOTIFY tilesizeChanged) Q_PROPERTY(double tilesizew READ tilesizew WRITE setTilesizew NOTIFY tilesizewChanged) Q_PROPERTY(double tilesizeh READ tilesizeh WRITE setTilesizeh NOTIFY tilesizehChanged) Q_PROPERTY(double mapcols READ mapcols WRITE setMapcols NOTIFY mapcolsChanged) Q_PROPERTY(double maprows READ maprows WRITE setMaprows NOTIFY maprowsChanged) Q_PROPERTY(double cx READ cx WRITE setCx NOTIFY cxChanged) Q_PROPERTY(double cy READ cy WRITE setCy NOTIFY cyChanged) Q_PROPERTY(QVariantList mapdata READ mapdata WRITE setMapdata NOTIFY mapDatachanged) public: /* ... */ }
      
      







QtCreator 'aのパワーにより、これらのパラメーターを数回クリックするだけで簡単に作成できます(知らない人のために:各行でコンテキストメニューを呼び出しますQ_PROPERTY->リファクタリング-> Q_PROPERTYメンバーの生成...



ファイナル



最後に、 paintメソッドの実装に到達しました 。 実際、画像キャッシュが追加されていることを除いて、前のプロジェクトのcreateMap()関数とそれほど違いはありません。

 void MapProvider::paint(QPainter *painter) { //     ,     int startcol = qFloor(m_cx / m_tilesizew); int startrow = qFloor(m_cy / m_tilesizeh); //     int tilecountw = qFloor(width() / m_tilesize); int tilecounth = qFloor(height() / m_tilesize) * 4; int tilecount = tilecountw * tilecounth; int col, row, globcol, globrow, globid = 0; double tx, ty = 0.0f; bool iseven; QPixmap tile; QString tileSourceID; for(int tileid = 0; tileid < tilecount; tileid++) { //         col = tileid % tilecountw; row = qFloor(tileid / tilecountw) ; //   ,     globcol = col + startcol; globrow = row + startrow * 2; globid = m_mapcols * globrow + globcol; //        //       if(globid >= m_mapdata.size()) { return; } //   ,      else if(globcol >= m_mapcols || globrow >= m_maprows) { continue; } //    iseven = !(row&1); //    tx = iseven ? col * m_tilesizew : col * m_tilesizew + m_tilesizew/2; ty = iseven ? row * m_tilesizeh : row * m_tilesizeh - m_tilesizeh/2; ty -= qFloor(row/2) * m_tilesizeh; //       tileSourceID = m_mapdata.at(globid).toString(); //    ,     if(tileCache.contains(tileSourceID)) { tile = tileCache.value(tileSourceID); } //          else { tile = QPixmap(QString(":/assets/texture/%1.png").arg(tileSourceID)) .scaled(QSize(m_tilesize, m_tilesize), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); tileCache.insert(tileSourceID, tile); } //   painter->drawPixmap(tx, ty, tile); //     painter->setFont(QFont("Helvetica", 8)); painter->setPen(QColor(255, 255, 255, 100)); painter->drawText(QRectF(tx, ty, m_tilesizew, m_tilesizeh), Qt::AlignCenter, QString("%1\n%2:%3").arg(globid).arg(globcol).arg(globrow)); } }
      
      







キャッシングは毎回画像を再描画しないために必要ですが、元の画像のサイズがタイルサイズよりもはるかに大きいために再描画されます(これは将来スケーリングを実装するために行われました)。 特にQt :: SmoothTransformationは画像を変更するときに使用されるため、再描画は多くのリソースを消費します。

ところで、理論的にスケーリングを実装できるようになりました。root.tilesizeパラメーターの増加係数を追加するだけです



tileCache変数MapProviderクラスで宣言されています

 private: QMap<QString, QPixmap> tileCache;
      
      







最後の仕上げは、接続のペアを作成してマップ再描画イベントを追加することです。

 MapProvider::MapProvider(QQuickItem *parent) : QQuickPaintedItem(parent) { connect(this, SIGNAL(xChanged()), this, SLOT(update())); connect(this, SIGNAL(yChanged()), this, SLOT(update())); }
      
      







発売日



さて、これですべてです。プロジェクトを実行して、次の図を見ることができます。



これは、最初のドラフトの写真と大差はありませんが、食いしん坊ではありません。



マップがどのように動いて描画されるかを確認するには、 root.mapcols変数の値を増やして、たとえば8に設定する必要があります(この値にroot.maprowsを掛けた値は、 mapprovider.mapdata変数の要素数に対応し、大きな値の場合は要素を追加する必要があります) )



カーテンの後ろに「保護ゾーン」を非表示にして、マップの有用な部分のみを表示するには、 gamearea.clipパラメーターをfalseからtrueに変更するだけです



次のパートでは、現在のプロジェクトに基づいてマップエディターを作成するプロセスについて説明します。 エディターは、マップを保存してロードできる必要があります。



現在のプロジェクトのソース(vk.com)




All Articles