The LevelEditor is one of the most valuable and time-saving components in Felgo. 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
Open Qt Creator and go to File
-> New File or Project...
. Then select the Physics - StackTheBox
game template from the list of Felgo templates and create the project.
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.
We are creating and removing entities (Boxes) at runtime, that's why the EntityManager is definitely needed here.
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.
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.
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.
Only the ceiling is red and restarts the game (clears all boxes) on collision.
Create boxes after random intervals and with a random rotation.
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.
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.
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.
Create a new qml file named Obstacle.qml in the entities
folder of your project, containing this code
import QtQuick 2.0 import Felgo 3.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!
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" toCreateEntityTypeUrl: "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:
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".
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.
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 Felgo LevelEditor to your game, see Felgo Crash Course Lesson 7 - How to boost level creation and balancing of your game with Felgo Level Editor. All collected resources how to use LevelEditor with Felgo 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 Felgo Games Examples and Demos to gain more information about game creation with Felgo and to see the source code of existing apps in the app stores.
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 Felgo: Stack With Friends Demo contains the full source code of a game with LevelEditor, Felgo 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.