How to add the LevelEditor to an existing game

Introduction

The LevelEditor is one of the most valuable and time-saving components in V-Play. It can be used during development to create and modify levels for your game, which you can then bundle in your final publishing build. Additionally, you can also integrate the in-game level editor to your published game and let your gamers create new levels. The gamers can then share their levels with the whole game community and thus drive new installs to your game and continuously provide new content so your game stays interesting and on top of the charts.

The integration of the LevelEditor to your games is no big deal. We will take a look at this process in this tutorial.

This is an advanced tutorial. We assume you are already finished some of the beginner tutorials and/or already made at least a simple game on your own.

Our basis will be the Stack The Box Demo game. The tutorial about how to create this game can be found here: Entity Based Game Design

The game is about having as many boxes as you can on your screen at the same time. Every few seconds a new box will drop from the top. If your pile of boxes hits the ceiling, the round is over. You can drag the boxes around with your mouse/finger to stack them as low as possible.

If you are curious about what StackTheBox will look like after this tutorial, check out the short video we made: YouTube video

Getting the source code

Open Qt Creator and go to File -> New File or Project.... Then select the Physics - StackTheBox game template from the list of V-Play templates and create the project.

Stack The Box source code review

Let's examine the present source code of Stack The Box real quick. I will describe the most important parts for you to understand the game mechanics.

  • EntityManager:

    We are creating and removing entities (Boxes) at runtime, that's why the EntityManager is definitely needed here.

  • PhysicsWorld:

    This is a physics driven game, with boxes falling around affected by gravity just like in reality. We achieve this behavior with the PhysicsWorld component. Because boxes are crashing together at quite high speed, we need a high value at PhysicsWorld::updatesPerSecondForPhysics, else the boxes may slip too far into each other before the collision actually is detected. As always, set this as low as possible so it still looks good, not to waste performance.

  • MouseJoint and MouseArea:

    The MouseJoint is used to tie a box to the mouse to drag it around. The Component element causes the same, as if the MouseJoint was defined in a separate file, and its children (so the MouseJoint) are not created when the Scene is loaded. Instead, we create a new joint every time the user touches a box and remove it when the box is released.

  • Box and its safety zone:

    The reason a safety zone was added, is because we create boxes with a random rotation and therefore they may use more space than only their width (width*Sqrt2). To prevent creating boxes with their edges inside the wall, we create them at a safe distance.

    The code of entities/Box.qml is pretty straightforward and should be nothing new for you. Only thing to mention here is the Particle component; if you want to learn more about using and creating particles, check out the Particle documentation and the Particle Editor Demo.

  • Walls:

    Only the ceiling is red and restarts the game (clears all boxes) on collision.

  • Timer:

    Create boxes after random intervals and with a random rotation.

What's about to come

First we will add the ItemEditor, which is used to modify properties of components at runtime. For example we will be able to modify the weight or the bounciness of boxes. This gives us the possibility to balance the game while actually playing it, awesome!

Then we will add the LevelEditor to create, save and load custom levels.

Add the ItemEditor

Like I just mentioned, we want to change some properties of the boxes at runtime, to change the game balancing.

To accomplish this we have to add the ItemEditor to our Scene in your main.qml. I'd say it fits best after the property declarations like this:

 //...

 Scene {
   id: scene

   // gets increased when a new box is created, and reset to 0 when a new game is started
   // start with 1, because initially 1 Box is created
   property int createdBoxes: 1

   // this is the required minimum distance from the left and from the right (the scene.width)
   // when the box is rotated at 90 degree, its distance from the center is box1.width*Sqrt2, because width and height are the same
   // the hint about this issue was kindly provided by Martin Eigel
   property real safetyDistance: -1

   ItemEditor {
     id: itemEditor
     opacity: 0.7
     z:1
     // start visible
   }

   //...

 }
 //...

If you run the project you can already see the empty ItemEditor on the left hand side. Why is it empty? Because he can't read our mind about what properties we want to change. We need to tell him. And that is as easy as it could be.

Go to Box.qml, and add following to the BoxCollider

 //...
 BoxCollider {
   id: boxCollider

   // the size effects the physics settings (the bigger the heavier)
   // this is set automatically in any collider - the default size is the one of parent!
   //        width: parent.width
   //        height: parent.height
   // the collider should have its origin at the x/y of the entity (so the center is in the TopLeft)
   x: -width/2
   y: -height/2


   friction: 1.6
   restitution: 0 // restitution is bounciness - a wooden box doesn't bounce
   density: 0.1 // this makes the box more heavy (0.1 kg / pixel)

   categories: Box.Category1

   fixture.onBeginContact: {
     // when colliding with another entity, play the sound and start particleEffect
     collisionSound.play();
     collisionParticleEffect.start();
   }

   EditableComponent {
     editableType: "Balancing"
     defaultGroup: "Box"
     properties: {
       "friction": {"min": 0, "max": 10, "stepsize": 0.1 },
       "restitution": {"min": 0, "max": 1,"stepsize": 0.1 },
       "density": {"min": 0, "max": 1000 }
     }
   }
 }
 //...

That's it; you defined the properties you want to be editable together with their minimum value, maximum value and also the step size.

Run the project and start balancing. I highly recommend maxing the restitution (bounciness)! ;-)

To show you how we can switch between the components in the ItemEditor, we add just a few editable properties to our GameWindow. You will find even more in the full source code of the StackTheBoxWithEditor Demo.

Add the following code to your GameWindow, just above the Scene, in main.qml

 //...
 GameWindow {
   id: gameWindow

   //...

   // balancing settings for the MouseJoint
   property real maxForce: 30000
   property real dampingRatio: 1
   property real frequencyHz: 2

   EditableComponent {
     // The default target: parent does not work when the EditableComponent is located directly in the GameWindow because the parent is not the actual GameWindow. Therefore, use the concrete GameWindow id assigned to the target.
     target: gameWindow
     editableType: "Balancing"
     defaultGroup: "MouseJoint"
     properties: {
       "maxForce": {"min": 0, "max": 100000},
       "dampingRatio": {"min": 0, "max": 1,"stepsize": 0.1 },
       "frequencyHz": {"min": 0, "max": 100, "stepsize": 0.1 }
     }
   }
   //...
 }

Now that we have defined the properties, and are already able to change them with our ItemEditor, we need to actually use them. The properties I chose here are interesting for the MouseJoint that we use to drag the boxes.

Search for the MouseJoint in main.qml, and change it to the following

 Component {
   id: mouseJoint
   MouseJoint {
     // make this high enough so the box with its density is moved quickly
     maxForce: gameWindow.maxForce
     // The damping ratio. 0 = no damping, 1 = critical damping
     dampingRatio: gameWindow.dampingRatio
     // The response speed
     frequencyHz: gameWindow.frequencyHz
   }
 }

Now run the Project and try out some more balancing.

Once you're done with balancing, you want to enjoy the full gaming experience, so there's no more reason to show the ItemEditor. Simple solution: Create a button to hide/show it.

Search for the Column in your main.qml and add this SimpleButton:

 Column {
   anchors.right: parent.right

   spacing: 5

   //... other buttons

   SimpleButton {
     text: "ItemEditor"
     onClicked: itemEditor.visible = !itemEditor.visible
     anchors.right: parent.right
   }
 }

Okay that's pretty cool, but we should move on to why we are really here.

Add the LevelEditor

Since adding the ItemEditor was so easy, why should adding the LevelEditor be so much harder? Exactly our thought.

First of all, we need to think about what a level is made of. What do we want to edit with the LevelEditor? We decided it would be nice to add obstacles, blocking the boxes from falling and forcing the player to stack the boxes around them.

What also belongs to the level are the current balancing settings of the ItemEditor EditableComponent elements. The cool thing is, the LevelEditor automatically takes care of saving and loading the property values together with the level, so no additional effort required from your side.

Defining the obstacles

Create a new qml file named Obstacle.qml in the entities folder of your project, containing this code

 import QtQuick 2.0
 import VPlay 2.0
  // for accessing the Body.Static type

 EntityBaseDraggable {
   id: obstacle
   entityType: "obstacle"

   width: 32
   height: 32

   colliderComponent: collider

   selectionMouseArea.anchors.fill: rectangle

   // the gridSize can also be smaller than the width, then the levels do not look as blocky and it is not a real grid
   gridSize: 16
   colliderSize: width

   // if the obstacle was pressed and held, remove it
   onEntityPressAndHold: removeEntity()

   Rectangle {
     id: rectangle
     color: "grey"
     x: -width/2
     y: -height/2
     width: parent.width
     height: parent.height
   }
   BoxCollider {
     id: collider
     x: -width/2
     y: -height/2
     // this is set automatically from BoxCollider
     // width: parent.width
     // height: parent.height

     // use "" to access the Body.Static type
     bodyType: Body.Static // the body shouldn't move

   }
 }

The level will be designed by creating obstacles and dragging them to the position you want them to be. Therefore we use the EntityBaseDraggable component which comes along with other handy features, like defining the action when you press and hold the component (we destroy it in that case).

Time to add the LevelEditor and start creating our first level!

Creating a level

We will have to do some changes to main.qml. Carefully compare your existing code to the following and adapt it like this:

 GameWindow {
   id: gameWindow

   EntityManager {
     id: entityManager
     entityContainer: scene
     // required for LevelEditor, so the entities can be created by entityType
     dynamicCreationEntityList: [ Qt.resolvedUrl("entities/Obstacle.qml") ]
   }

   //...

   Scene {
     id: scene

     //... properties

     // state: "levelEditing" enables dragging and clicking of obstacles
     // change the state in the next line to start in levelEditing mode:
     state: "playing" //state: "levelEditing"

     onStateChanged: {
       if(state === "levelEditing") {
         stopGame()
       }
     }

     function stopGame() {
       // remove all entities of type "box", but not the walls
       entityManager.removeEntitiesByFilter(["box"]);
       // reset the createdBoxes amount
       scene.createdBoxes = 0;
     }

     LevelEditor {
       id: levelEditor
       toRemoveEntityTypes: [ "obstacle" ]
       toStoreEntityTypes: [ "obstacle" ]
     }

     // ...

     Column {
       anchors.right: parent.right

       spacing: 5

       //... other buttons

       SimpleButton {
         text: scene.state === "playing" ? "Level Mode" : "Game Mode"
         onClicked: {
           if(text === "Level Mode")
             scene.state = "levelEditing"
           else
             scene.state = "playing"
         }

         anchors.right: parent.right
       }

       BuildEntityButton {
         visible: scene.state === "levelEditing"
         toCreateEntityType: "entities/Obstacle.qml"

         width: 50
         height: 50
         anchors.right: parent.right

         // the obstacle is just a grey entity, we can customize the look of the button here
         Rectangle {
           color: "grey"
           anchors.fill: parent
         }
       }
     }

     Timer {
       id: timer
       interval: generateRandomInterval()
       running: scene.state === "playing"// start running from the beginning, when the scene is loaded
       //...
     }
   }
 }

Let me explain what we just accomplished, step by step:

  • The first minor change happened at the EntityManager. We added the EntityManager::dynamicCreationEntityList property, defining the path to the entity which we want to create in level editing mode. If the path is defined here, the LevelEditor can simply use the EntityBase::entityType for creating and destroying entities.
  • Then we added a state. By default, EntityBaseDraggable components are draggable if the state of the surrounding scene is "levelEditing". You can change this by setting the EntityBaseDraggable::inLevelEditingMode property yourself.

    In our case we will distinguish between 2 states, "playing" and "levelEditing". Whenever we are changing the state to "levelEditing" we want the game to stop. And our Timer for box creation (at the end of the code) should only run if the state is "playing".

  • The LevelEditor component needs to know which entities it has to take care of. In our case it's just the obstacles.
  • And finally we added 2 buttons. One to enable/disable level editing by changing the state of the scene. And the other one is the BuildEntityButton. This one is used to create a new instance of the defined entityType every time it's touched.

Check out what we accomplished in just a few minutes. We can switch between level editing and playing seamlessly. We can create levels and balance the game on the fly without restarting.

The only thing missing now is saving and loading our levels.

Saving and loading levels

We will have to add 2 things to accomplish this. Buttons to save the current level or start a new one and a LevelSelectionList displaying all the existing levels.

We start with the LevelSelectionList. Add this to your main.qml, anywhere in the Scene, e.g. under the 2 editors:

 LevelSelectionList {
   id: levelSelectionList
   width: 150
   z: 3
   // at the beginning it is invisible, only gets visible after a click on the Levels button
   visible: false
   anchors.centerIn: parent
   levelMetaDataArray: levelEditor.authorGeneratedLevels

   onLevelSelected: {
     levelEditor.loadSingleLevel(levelData)
     // make invisible afterwards
     levelSelectionList.visible = false
   }
 }

Now we add buttons to start a new level, save the current one and to show the LevelSelectionList to load an existing level.

Search the Column component in your main.qml and add the following buttons, just above the BuildEntityButton:

 Column {

   //...

   SimpleButton {
     text: "New Level"
     onClicked: levelEditor.createNewLevel()
     anchors.right: parent.right
     visible: scene.state === "levelEditing"
   }

   SimpleButton {
     text: "Save Level"
     onClicked: nativeUtils.displayTextInput("Enter levelName", "", levelEditor.currentLevelName)
     anchors.right: parent.right
     visible: scene.state === "levelEditing"

     Connections {
       target: nativeUtils
       onTextInputFinished: {
         if(accepted) {
           levelEditor.saveCurrentLevel( {levelMetaData: {levelName: enteredText}} )
         }
       }
     }
   }

   SimpleButton {
     text: "Show All Levels"
     anchors.right: parent.right
     visible: scene.state === "levelEditing"
     onClicked: {
       levelEditor.loadAllLevelsFromStorageLocation(levelEditor.authorGeneratedLevelsLocation)
       levelSelectionList.visible = true
     }
   }

   BuildEntityButton {
     //...
   }
 }

If you run the project, you can try out the fully functional small level editor for our game. Wasn't that simple? :)

You can read about how to export your locally designed levels and bundle them with the application, following this link: Export levels to bundle them with the application.

For another tutorial how to add the V-Play LevelEditor to your game, see V-Play Crash Course Lesson 7 - How to boost level creation and balancing of your game with V-Play Level Editor. All collected resources how to use LevelEditor with V-Play can be found in the Further Level Editor Resources.

That's it for now, enjoy the gaming experience! If you have any questions regarding this tutorial, don't hesitate to visit the support forums.

Visit V-Play Engine Examples and Demos to gain more information about game creation with V-Play and to see the source code of existing apps in the app stores.

Further StackTheBox Demo Perspectives

The next step could be to add some more, different shaped obstacles, with one BuildEntityButton for each. Those buttons could then be placed inside a Flickable.

This game is extended in a game published by V-Play: Stack With Friends Demo contains the full source code of a game with LevelEditor, V-Play Game Network and LevelStore. It also adds the option to spawn a box earlier, adds another obstacle and allows players to share user-generated levels with the game community.

Videos

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