Forums

OverviewV-Play 2 Support (Qt 5) › Camera movement and zoom

Viewing 7 posts - 1 through 7 (of 7 total)
  • Author
    Posts
  • #10384

    Phil

    Hi

    In my game I have the camera centred on the player. I’ve written a simplified demo below. The camera movement is similar to the Chicken Outbreak game, but my approach is different because there are several objects in my game with different velocities, so just moving the background image wouldn’t work (please correct me if I’m wrong). My demo works fine, but now I want to add a zoom to the camera using a toggle button. The button changes the size of the Scene, but when I do this the positioning of the camera goes wrong. Do you know why this happens?

    Thanks,

    Phil

     

    import QtQuick 2.0
    import VPlay 2.0
    
    GameWindow{
        id: window
        width: 600
        height: 450
    
        Scene {
            id: scene
            width: 600
            height: 450
    
            property bool zoomOn: false
    
            PhysicsWorld {
                id: physicsWorld
                gravity: Qt.point(0,0)
            }
    
            EntityManager {
               id: entityManager
               entityContainer: scene
            }
    
            // a timer that updates the 'camera' position and the position of the button on screen
            Timer {
                id: cameraTimer
                running: false
                repeat: true
                interval: 5
                onTriggered: {
                    scene.x = scene.width/2 - collider.body.getWorldCenter().x
                    scene.y = scene.height/2 - collider.body.getWorldCenter().y
                    zoomButton.x = collider.body.getWorldCenter().x - scene.width / 2 + 10
                    zoomButton.y = collider.body.getWorldCenter().y - scene.height / 2 + 10
                }
            }
    
            // background
            Grid {
                x: scene.width/2
                y: scene.height/2
                width: 2 * window.width
                height: 2 * window.height
                columns: 8
                rows: columns * window.height / window.width    // make each grid a square
                Repeater {
                    model: parent.columns * parent.rows
                    Rectangle{
                        property int rectangleNumber: index
                        width: parent.width / parent.columns
                        height: width
                        color: "transparent"
                        border.color: "black"
                        border.width: 3
                        Text {
                            anchors.centerIn: parent
                            text: parent.rectangleNumber + 1
                            font.pointSize: parent.height / 3
                        }
                    }
                }
            }
    
            // button to toggle zoom in
            Rectangle {
                id: zoomButton
                x: 10
                y: 10
                width: 100
                height: 40
                radius: 3
                color: "black"
                opacity: 0.2
                Text {
                    anchors.centerIn: parent
                    font.pixelSize: 30
                    text: "zoom"
                }
                MouseArea {
                    anchors.fill: parent
                    hoverEnabled: true
                    onEntered: parent.opacity = 0.3
                    onExited: parent.opacity = 0.2
                    onClicked: {
                        if(!scene.zoomOn){
                            scene.width = 500
                            scene.height = 500*3/4
                            scene.zoomOn = !scene.zoomOn
                        }
                        else{
                            scene.width = 600
                            scene.height = 450
                            scene.zoomOn = !scene.zoomOn
                        }
                    }
                }
            }
    
            // the player
            EntityBase {
                id: entityBase
                x: scene.width / 2
                y: scene.height / 2
    
                Rectangle {
                    id: player
                    x: -width / 2
                    y: -height / 2
                    width: 80
                    height: width
                    radius: width / 2
                    color: "blue"
    
                    // start player moving when clicked, and start the timer to update the camera's position
                    MouseArea {
                        anchors.fill: parent
                        onClicked: {
                            cameraTimer.start()
                            collider.linearVelocity = Qt.point(100, 75)
                            colliderObstacle.linearVelocity = Qt.point(-100,75)
                        }
                    }
                    Text {
                        anchors.centerIn: parent
                        font.pixelSize: 15
                        text: "click me"
                        color: "white"
                    }
                }
                CircleCollider {
                    id: collider
                    radius: player.width / 2
                    anchors.centerIn: player
                    bodyType: Body.Dynamic
                    linearVelocity: Qt.point(0, 0)
                    categories: Box.Category1
                    collidesWith: Box.Category2
                    fixture.onBeginContact: player.opacity = 0.5
                }
            }
    
            // an obstacle
            EntityBase {
                id: entityBase2
                x: scene.width / 2 + 2*window.width
                y: scene.height / 2
    
                Rectangle {
                    id: obstacle
                    x: -width / 2
                    y: -height / 2
                    width: 80
                    height: width
                    color: "red"
                    Text {
                        anchors.centerIn: parent
                        font.pixelSize: 15
                        text: "obstacle"
                        color: "white"
                    }
                }
                BoxCollider {
                    id: colliderObstacle
                    width: player.width
                    height: width
                    anchors.centerIn: obstacle
                    bodyType: Body.Dynamic
                    sensor: true
                    linearVelocity: Qt.point(0, 0)
                    categories: Box.Category2
                    collidesWith: Box.Category1
                }
            }
    
    
        }
    }
    

     

    #10402

    Alex
    V-Play Team

    Hi,

    I prepared a working example for you to try, using a bit different approach:

    import QtQuick 2.0
    import VPlay 2.0
    
    GameWindow{
      id: window
    
      // this will be our game scene
      Scene {
        id: scene
    
        property bool zoomOn: false
    
        EntityManager {
          id: entityManager
          entityContainer: container // the entityContainer is now our container that will be moving
        }
    
        // instead of changing the scene position we rather introduce a container that holds the entities, and adepts its position accordingly to the player position to keep the player centered in the screen. Also we are sing a property binding to update the position every frame instead of a timer, this is more reliable to keep in in sync.
        Item {
          id: container
          transformOrigin: Item.Center
    
          // property binding to update the position and keep the player centered in the screen  
          x: scene.width/2 - entityBase.x
          y: scene.height/2 - entityBase.y
    
          // put the physicsworld inside the entitycontainer to have correct debug draw
          PhysicsWorld {
            id: physicsWorld
            gravity: Qt.point(0,0)
          }
    
          // background
          Grid {
            id: grid
            x: scene.width/2
            y: scene.height/2
            width: 2 * window.width
            height: 2 * window.height
            columns: 8
            rows: columns * window.height / window.width    // make each grid a square
            Repeater {
              model: parent.columns * parent.rows
              Rectangle{
                property int rectangleNumber: index
                width: parent.width / parent.columns
                height: width
                color: "transparent"
                border.color: "black"
                border.width: 3
                Text {
                  anchors.centerIn: parent
                  text: parent.rectangleNumber + 1
                  font.pointSize: parent.height / 3
                }
              }
            }
          }
    
          // the player
          EntityBase {
            id: entityBase
            x: scene.width / 2
            y: scene.height / 2
    
            Rectangle {
              id: player
              x: -width / 2
              y: -height / 2
              width: 80
              height: width
              radius: width / 2
              color: "blue"
    
              // start player moving when clicked, and start the timer to update the camera's position
              MouseArea {
                anchors.fill: parent
                onClicked: {
                  //cameraTimer.start()
                  collider.linearVelocity = Qt.point(100, 75)
                  colliderObstacle.linearVelocity = Qt.point(-100,75)
                }
              }
              Text {
                anchors.centerIn: parent
                font.pixelSize: 15
                text: "click me"
                color: "white"
              }
            }
            CircleCollider {
              id: collider
              radius: player.width / 2
              anchors.centerIn: player
              bodyType: Body.Dynamic
              linearVelocity: Qt.point(0, 0)
              categories: Box.Category1
              collidesWith: Box.Category2
              fixture.onBeginContact: player.opacity = 0.5
            }
          }
    
          // an obstacle
          EntityBase {
            id: entityBase2
            x: scene.width / 2 + 2*window.width
            y: scene.height / 2
    
            Rectangle {
              id: obstacle
              x: -width / 2
              y: -height / 2
              width: 80
              height: width
              color: "red"
              Text {
                anchors.centerIn: parent
                font.pixelSize: 15
                text: "obstacle"
                color: "white"
              }
            }
            BoxCollider {
              id: colliderObstacle
              width: player.width
              height: width
              anchors.centerIn: obstacle
              bodyType: Body.Dynamic
              sensor: true
              linearVelocity: Qt.point(0, 0)
              categories: Box.Category2
              collidesWith: Box.Category1
            }
          }
        }
      }
    
      // to avoid having to recalculate the size and position of the HUD elements, we just introduce a 2nd scene that we put on top of the game scene, to hold the hud elements, this one will stay untouched when we zoom into the game sceen
      Scene {
        id: hudScene
        // button to toggle zoom in
        Rectangle {
          id: zoomButton
          x: 10
          y: 10
          width: 100
          height: 40
          radius: 3
          color: "black"
          opacity: 0.2
          Text {
            anchors.centerIn: parent
            font.pixelSize: 30
            text: "zoom"
          }
          MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onEntered: parent.opacity = 0.3
            onExited: parent.opacity = 0.2
            onClicked: {
              // for zooming, we just scale up the game scene now, the hud scene stays the same
              // your previous solution messed around with many sizes and positions, this one simply scales up the game scene, all its internal sizes, positions and velocities stay untouched
              if(!scene.zoomOn){
                scene.scale = 1.2
              }
              else{
                scene.scale = 1
              }
              scene.zoomOn = !scene.zoomOn
            }
          }
        }
      }
    }
    

    I hope you understand everything, I tried to add many comments.

    Cheers,
    Alex

    #10441

    Phil

    Yes, that’s perfect! Many thanks, Phil

    #10443

    Phil

    Ok, so now I want to toggle the position of the camera between the two objects using a button. I’ve posted my attempt below but it doesn’t work as I expected. What code should I use instead? Thanks, Phil

     

    import QtQuick 2.0
    import VPlay 2.0
    
    GameWindow{
        id: window
    
        // this will be our game scene
        Scene {
    
            //...
    
            property bool playerFocus: true
    
            //...
    
        }
    
        Scene {
            id: hudScene
    
            //...
    
            // button to toggle camera focus
            Rectangle {
                id: cameraButton
                x: 10
                y: 60
                width: 100
                height: 40
                radius: 3
                color: "black"
                opacity: 0.2
                Text {
                    anchors.centerIn: parent
                    font.pixelSize: 30
                    text: "camera"
                }
                MouseArea {
                    anchors.fill: parent
                    hoverEnabled: true
                    onEntered: parent.opacity = 0.3
                    onExited: parent.opacity = 0.2
                    onClicked: {
                        if(scene.playerFocus){
                            scene.x = scene.width/2 - entityBase2.x
                            scene.y = scene.height/2 - entityBase2.y
                        }
                        else{
                            scene.x = scene.width/2 - entityBase.x
                            scene.y = scene.height/2 - entityBase.y
                        }
                        scene.playerFocus = !scene.playerFocus
                    }
                }
            }
        }
    }      

     

    #10449

    Alex
    V-Play Team

    Hi,

    I highly recommend not touching the scene too much, it’s automatically positioned to fit in the screen, rather work with the container of your game area.

    In the example I posted, this is the part of the code that keeps the player (entityBase) centered in the screen, so to keep another object (entityBase2) centered, it would be the easiest thing just to change this part of the code.

    Item {
      id: container
      transformOrigin: Item.Center
    
      // property binding to update the position and keep the player centered in the screen  
      x: scene.width/2 - entityBase.x
      y: scene.height/2 - entityBase.y
    
      // ...
    }
    

    One solution would be to replace this property binding (a property binding automatically updates and recalculates as soon as one of the involved properties change) with a new one, like this (I just replaced the MouseArea of my own example that I posted):

    MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onEntered: parent.opacity = 0.3
            onExited: parent.opacity = 0.2
            onClicked: {
              // I just commented the zoom part and use the same button as before to switch centered object
              if(!scene.zoomOn){
                //scene.scale = 1.2
                // using entityBase2 here to center that object
                container.x = Qt.binding(function() { return scene.width/2 - entityBase2.x })
                container.y = Qt.binding(function() { return scene.height/2 - entityBase2.y })
              }
              else{
                //scene.scale = 1
                // revert back to the inital binding to center entityBase
                container.x = Qt.binding(function() { return scene.width/2 - entityBase.x })
                container.y = Qt.binding(function() { return scene.height/2 - entityBase.y })
              }
              scene.zoomOn = !scene.zoomOn
            }
          }
    

    A more elegant way would be to introduce a new property which holds the object that should get centered, and benefit from the power of property bindings to handle the rest, here is an example for that:

    import QtQuick 2.0
    import VPlay 2.0
    
    GameWindow{
      id: window
    
      // this will be our game scene
      Scene {
        id: scene
    
        property bool zoomOn: false
        property variant focusedObject: entityBase
    
        EntityManager {
          id: entityManager
          entityContainer: container // the entityContainer is now our container that will be moving
        }
    
        // instead of changing the scene position we rather introduce a container that holds the entities, and adepts its position accordingly to the player position to keep the player centered in the screen. Also we are sing a property binding to update the position every frame instead of a timer, this is more reliable to keep in in sync.
        Item {
          id: container
          transformOrigin: Item.Center
    
          // property binding to update the position and keep the player centered in the screen
          x: scene.width/2 - scene.focusedObject.x
          y: scene.height/2 - scene.focusedObject.y
    
          // put the physicsworld inside the entitycontainer to have correct debug draw
          PhysicsWorld {
            id: physicsWorld
            gravity: Qt.point(0,0)
          }
    
          // background
          Grid {
            id: grid
            x: scene.width/2
            y: scene.height/2
            width: 2 * window.width
            height: 2 * window.height
            columns: 8
            rows: columns * window.height / window.width    // make each grid a square
            Repeater {
              model: parent.columns * parent.rows
              Rectangle{
                property int rectangleNumber: index
                width: parent.width / parent.columns
                height: width
                color: "transparent"
                border.color: "black"
                border.width: 3
                Text {
                  anchors.centerIn: parent
                  text: parent.rectangleNumber + 1
                  font.pointSize: parent.height / 3
                }
              }
            }
          }
    
          // the player
          EntityBase {
            id: entityBase
            x: scene.width / 2
            y: scene.height / 2
    
            Rectangle {
              id: player
              x: -width / 2
              y: -height / 2
              width: 80
              height: width
              radius: width / 2
              color: "blue"
    
              // start player moving when clicked, and start the timer to update the camera's position
              MouseArea {
                anchors.fill: parent
                onClicked: {
                  //cameraTimer.start()
                  collider.linearVelocity = Qt.point(100, 75)
                  colliderObstacle.linearVelocity = Qt.point(-100,75)
                }
              }
              Text {
                anchors.centerIn: parent
                font.pixelSize: 15
                text: "click me"
                color: "white"
              }
            }
            CircleCollider {
              id: collider
              radius: player.width / 2
              anchors.centerIn: player
              bodyType: Body.Dynamic
              linearVelocity: Qt.point(0, 0)
              categories: Box.Category1
              collidesWith: Box.Category2
              fixture.onBeginContact: player.opacity = 0.5
            }
          }
    
          // an obstacle
          EntityBase {
            id: entityBase2
            x: scene.width / 2 + 2*window.width
            y: scene.height / 2
    
            Rectangle {
              id: obstacle
              x: -width / 2
              y: -height / 2
              width: 80
              height: width
              color: "red"
              Text {
                anchors.centerIn: parent
                font.pixelSize: 15
                text: "obstacle"
                color: "white"
              }
            }
            BoxCollider {
              id: colliderObstacle
              width: player.width
              height: width
              anchors.centerIn: obstacle
              bodyType: Body.Dynamic
              sensor: true
              linearVelocity: Qt.point(0, 0)
              categories: Box.Category2
              collidesWith: Box.Category1
            }
          }
        }
      }
    
      // to avoid having to recalculate the size and position of the HUD elements, we just introduce a 2nd scene that we put on top of the game scene, to hold the hud elements, this one will stay untouched when we zoom into the game sceen
      Scene {
        id: hudScene
        // button to toggle zoom in
        Rectangle {
          id: zoomButton
          x: 10
          y: 10
          width: 100
          height: 40
          radius: 3
          color: "black"
          opacity: 0.2
          Text {
            anchors.centerIn: parent
            font.pixelSize: 30
            text: "zoom"
          }
          MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onEntered: parent.opacity = 0.3
            onExited: parent.opacity = 0.2
            onClicked: {
              if(!scene.zoomOn){
                scene.scale = 1.2
              }
              else{
                scene.scale = 1
              }
              scene.zoomOn = !scene.zoomOn
            }
          }
        }
    
        // button to toggle centered object
        Rectangle {
          id: focusButton
          x: 120
          y: 10
          width: 100
          height: 40
          radius: 3
          color: "black"
          opacity: 0.2
          Text {
            anchors.centerIn: parent
            font.pixelSize: 30
            text: "focus"
          }
          MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onEntered: parent.opacity = 0.3
            onExited: parent.opacity = 0.2
            onClicked: {
              if(scene.focusedObject === entityBase){
                scene.focusedObject = entityBase2
              }
              else{
                scene.focusedObject = entityBase
              }
            }
          }
        }
      }
    }
    

    Cheers,
    Alex

    #10486

    Phil

    Thanks. Yes, using property bindings is an elegant solution! The final thing I wanted was to make the camera switch smooth, using a number animation. I tried the code below, but this interferes with the normal camera movement. Thanks, Phil

     

                Behavior on x { NumberAnimation { duration: 200; easing.type: Easing.Linear } }
                Behavior on y { NumberAnimation { duration: 200; easing.type: Easing.Linear } }

     

     

    #10487

    Alex
    V-Play Team

    Hi,

    well that one’s a little tricky, I came up with one quick solution, might not be the best but it kind of works, I’m sure it can be improved further.

    I introduced a new Camera component that handles the focusing and zooming. There is still a minor “problem”, if the focused object is moving while the smooth focusing is running, I added comments for that to the code.

    Main.qml

    import QtQuick 2.0
    import VPlay 2.0
    
    GameWindow{
      id: window
    
      // this will be our game scene
      Scene {
        id: scene
    
        EntityManager {
          id: entityManager
          entityContainer: container // the entityContainer is now our container that will be moving
        }
    
        // instead of changing the scene position we rather introduce a container that holds the entities, and adepts its position accordingly to the player position to keep the player centered in the screen. Also we are sing a property binding to update the position every frame instead of a timer, this is more reliable to keep in in sync.
        Item {
          id: container
          transformOrigin: Item.Center
    
          Camera {
            id: camera
            scene: scene
            objectContainer: container
            focusedObject: entityBase
          }
    
          // put the physicsworld inside the entitycontainer to have correct debug draw
          PhysicsWorld {
            id: physicsWorld
            gravity: Qt.point(0,0)
          }
    
          // background
          Grid {
            id: grid
            x: scene.width/2
            y: scene.height/2
            width: 2 * window.width
            height: 2 * window.height
            columns: 8
            rows: columns * window.height / window.width    // make each grid a square
            Repeater {
              model: parent.columns * parent.rows
              Rectangle{
                property int rectangleNumber: index
                width: parent.width / parent.columns
                height: width
                color: "transparent"
                border.color: "black"
                border.width: 3
                Text {
                  anchors.centerIn: parent
                  text: parent.rectangleNumber + 1
                  font.pointSize: parent.height / 3
                }
              }
            }
          }
    
          // the player
          EntityBase {
            id: entityBase
            x: scene.width / 2
            y: scene.height / 2
    
            Rectangle {
              id: player
              x: -width / 2
              y: -height / 2
              width: 80
              height: width
              radius: width / 2
              color: "blue"
    
              // start player moving when clicked, and start the timer to update the camera's position
              MouseArea {
                anchors.fill: parent
                onClicked: {
                  //cameraTimer.start()
                  collider.linearVelocity = Qt.point(100, 75)
                  colliderObstacle.linearVelocity = Qt.point(-100,75)
                }
              }
              Text {
                anchors.centerIn: parent
                font.pixelSize: 15
                text: "click me"
                color: "white"
              }
            }
            CircleCollider {
              id: collider
              radius: player.width / 2
              anchors.centerIn: player
              bodyType: Body.Dynamic
              linearVelocity: Qt.point(0, 0)
              categories: Box.Category1
              collidesWith: Box.Category2
              fixture.onBeginContact: player.opacity = 0.5
            }
          }
    
          // an obstacle
          EntityBase {
            id: entityBase2
            x: scene.width / 2 + 2*window.width
            y: scene.height / 2
    
            Rectangle {
              id: obstacle
              x: -width / 2
              y: -height / 2
              width: 80
              height: width
              color: "red"
              Text {
                anchors.centerIn: parent
                font.pixelSize: 15
                text: "obstacle"
                color: "white"
              }
            }
            BoxCollider {
              id: colliderObstacle
              width: player.width
              height: width
              anchors.centerIn: obstacle
              bodyType: Body.Dynamic
              sensor: true
              linearVelocity: Qt.point(0, 0)
              categories: Box.Category2
              collidesWith: Box.Category1
            }
          }
        }
      }
    
      // to avoid having to recalculate the size and position of the HUD elements, we just introduce a 2nd scene that we put on top of the game scene, to hold the hud elements, this one will stay untouched when we zoom into the game sceen
      Scene {
        id: hudScene
        // button to toggle zoom in
        Rectangle {
          id: zoomButton
          x: 10
          y: 10
          width: 100
          height: 40
          radius: 3
          color: "black"
          opacity: 0.2
          Text {
            anchors.centerIn: parent
            font.pixelSize: 30
            text: "zoom"
          }
          MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onEntered: parent.opacity = 0.3
            onExited: parent.opacity = 0.2
            onClicked: {
              if(camera.zoom == 1){
                camera.zoom = 1.2
              }
              else{
                camera.zoom = 1
              }
            }
          }
        }
    
        // button to toggle centered object
        Rectangle {
          id: focusButton
          x: 120
          y: 10
          width: 100
          height: 40
          radius: 3
          color: "black"
          opacity: 0.2
          Text {
            anchors.centerIn: parent
            font.pixelSize: 30
            text: "focus"
          }
          MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onEntered: parent.opacity = 0.3
            onExited: parent.opacity = 0.2
            onClicked: {
              if(camera.focusedObject === entityBase){
                camera.focusedObject = entityBase2
              }
              else{
                camera.focusedObject = entityBase
              }
            }
          }
        }
      }
    }
    

    Camera.qml

    import QtQuick 2.0
    import VPlay 2.0
    
    Item {
      id: camera
      property Scene scene
      property Item objectContainer
      property Item focusedObject
    
      property int focusEasing: Easing.InOutQuad
      property int zoomEasing: Easing.InOutQuad
      property int focusDuration: 200
      property int zoomDuration: 200
    
      property real zoom: 1
      Behavior on zoom{NumberAnimation{duration: zoomDuration; easing.type: zoomEasing}}
    
      Component.onCompleted: {
        objectContainer.x = Qt.binding(function() { return scene.width/2 - focusedObject.x })
        objectContainer.y = Qt.binding(function() { return scene.height/2 - focusedObject.y })
      }
    
      onFocusedObjectChanged: {
        // the problem is that we are taking the position of the object to focus and set it as target for the animation, then the animationr runs for e.g. 200ms, but in the meantime also the object moved by some pixels, so the animation ends up at the position where the object was 200ms ago, and then jumps the the actual position. So to improve this you would need to calculate the camera movement during the focus change yourself every frame (with a Timer) instead of using a simple animation.
        objectContainer.x = objectContainer.x
        objectContainer.y = objectContainer.y
        smoothX.from = objectContainer.x
        smoothX.to = scene.width/2 - focusedObject.x
        smoothY.from = objectContainer.y
        smoothY.to = scene.height/2 - focusedObject.y
        smoothFocus.start()
      }
    
      onZoomChanged: {
        scene.scale = zoom
      }
    
      ParallelAnimation {
        id: smoothFocus
        NumberAnimation {
          id: smoothX
          target: objectContainer
          property: "x"
          duration: camera.focusDuration
          easing.type: camera.focusEasing
        }
        NumberAnimation {
          id: smoothY
          target: objectContainer
          property: "y"
          duration: camera.focusDuration
          easing.type: camera.focusEasing
        }
        onStopped: {
          objectContainer.x = Qt.binding(function() { return scene.width/2 - focusedObject.x })
          objectContainer.y = Qt.binding(function() { return scene.height/2 - focusedObject.y })
        }
      }
    }
    

    Cheers,
    Alex

    • This reply was modified 3 years, 2 months ago by  Alex.
Viewing 7 posts - 1 through 7 (of 7 total)

RSS feed for this thread

You must be logged in to reply to this topic.

Voted #1 for:

  • Easiest to learn
  • Most time saving
  • Best support

Develop Cross-Platform Apps and Games 50% Faster!

  • Voted the best supported, most time-saving and easiest to learn cross-platform development tool
  • Based on the Qt framework, with native performance and appearance on all platforms including iOS and Android
  • Offers a variety of plugins to monetize, analyze and engage users
FREE!
create apps
create games
cross platform
native performance
3rd party services
game network
multiplayer
level editor
easiest to learn
biggest time saving
best support