The idea behind Crazy Carousel is a mini-game with the goal to collect as many coins as possible while avoiding to get hit by paper planes. These planes are thrown at you from the top of the screen. You can collect the coins or avoid the paper planes by jumping from one carousel ride to another by swiping to the left or the right.
This tutorial will cover the setup of multiple game scenes, how to support different screen sizes and aspect ratios, different forms of moving the entities and the basic mechanics for the game like handling user input or collision detection.
If you are not yet familiar with the basic setup of projects, QML, how to create your own entities, the usage of components like PhysicsWorld and Timer or the dynamic creation of entities with the EntityManager please consider taking a look at the following tutorials to help you get started:
We also provide the full source code of Crazy Carousel made with Felgo in the Felgo SDK. See here where to find the demo in your SDK directory.
For an overview of the game features, see Crazy Carousel Game.
All the resources for the game will be placed within the assets directory of the project. You can download them here.
To get you started we first need to create a new empty project in QtCreator. Please don't forget to set the interface orientation to portrait mode when configuring the project properties. When you're done, place the
downloaded game resources in the assets folder of your project. You should be able to see them at Other files\assets
in the project tree. Please do not place the downloaded resources in a subdirectory, which might
be created when you unpack the archive. Now we're ready to go!
We will now start to create the basic Scene structure of our game. Scenes in Felgo are a great way to implement a game that should allow switching between different game screens like the menu screen, the actual game and a game-over screen. Our game will have three scenes:
Before implementing the scenes it is common to start with a base component for all scenes. Just create a new folder common
within the qml
directory of the project and add a file
SceneBase.qml
:
qml/common/SceneBase.qml
:
import Felgo 4.0 import QtQuick 2.0 Scene { id: sceneBase // by default, set the opacity to 0 - this will be changed from the main.qml with PropertyChanges opacity: 0 // we set the visible property to false if opacity is 0 because the renderer skips invisible items, this is an performance improvement visible: opacity > 0 // if the scene is invisible, we disable it. In Qt 5, components are also enabled if they are invisible. This means any MouseArea in the Scene would still be active even we hide the Scene, since we do not want this to happen, we disable the Scene (and therefore also its children) if it is hidden enabled: visible }
By using this component as the base for all scenes, they will be invisible by default. We can show a specific scene by setting its opacity to 1. In addition, by binding the property visible
to opacity >
0
the renderer will automatically skip invisible scenes. Each time the opacity is changed the value for the property visible will be calculated automatically due to property binding.
We can now implement our three scenes. They will be placed in a new subdirectory qml/scenes
that you should create now. Let's start with the title scene by implementing the file TitleScene.qml
in
your qml/scenes
folder:
qml/scenes/TitleScene.qml
:
import Felgo 4.0 import QtQuick 2.0 import "../common" SceneBase { id:titleScene // the "logical size" - the scene content is auto-scaled to match the GameWindow size width: 320 height: 480 // property alias to allow switching the background later property alias backgroundImage: background // background image is based on game window (bigger as logical scene) MultiResolutionImage { id: background anchors.centerIn: titleScene.gameWindowAnchorItem source: "../../assets/CarouselTitle.jpg" } }
We first have to import ../common
to make use of our previously defined SceneBase. We set the size of the scene to 320x480px, which is the screen resolution of small devices like the iPhone 3GS. Every content we
place within the scene will be automatically scaled up proportionally on devices with a higher resolution. As we want the game to be playable on all devices, we should use a logical scene size of 320x480px for all our scenes,
which represents a fixed aspect ratio of 3:2 for the main game area. The background image of the scene is actually bigger than the basic 320x480px and uses a different aspect ratio. On devices with bigger screens, more of the
background image is shown to avoid dark borders - even though the main game area has always a 3:2 ratio.
Note: The content scaling as described above refers to the default scaling mode of Felgo. You may also use other scaling modes for your own games, check the tutorial about scaling modes and support of multiple screen sizes.
The recommended background image size for a 320x480px game scene is 360x570px to cover the screen for every possible aspect ratio. As you might know, it is common to use higher resolution images on more modern devices with
higher pixel densities. Luckily, the MultiResolutionImage we just added already takes care of the automatic image swapping. The used background image
assets/CarouselTitle.jpg
actually refers to the image with the lowest quality. The subfolders assets/+hd
and assets/+hd2
contain images double and quadruple the original size. It is common
practice to create images in the best quality (+hd2) and scale them down for the lower resolution versions.
Now let us move on to creating the remaining scenes! The game-over scene is very similar to the title scene, we just need to set a different background image - so we can use the title scene as the base this time:
qml/scenes/GameOverScene.qml
:
import Felgo 4.0 import QtQuick 2.0 TitleScene { id: gameOverScene // switch background image backgroundImage.source: "../../assets/CarouselGameOver.jpg" }
We can easily overwrite the source of the previous background image by using the property alias that we added to the title scene before. That's all we need to do for now. Up next is the game scene, which will be the most complex scene in our game.
qml/scenes/GameScene.qml
import Felgo 4.0 import QtQuick 2.0 import "../common" SceneBase { id:gameScene // the "logical size" - the scene content is auto-scaled to match the GameWindow size width: 320 height: 480 // background image is based on game window (bigger as logical scene) MultiResolutionImage { anchors.centerIn: gameScene.gameWindowAnchorItem source: "../../assets/CarouselBackground.jpg" } // background music plays automatically BackgroundMusic { id: backgroundMusic source: "../../assets/sound/Carousel.mp3" } property int points: 0 // initialize a new game function startNewGame() { gameScene.points = 0 } }
The scene already contains a few additional elements. We added the background music, a property for counting points and a function to reset the game properties back to their default values when a new game is started. Before
we can check out what we achieved so far, we need to add the scenes to our Main.qml file to display them. Also we added a state machine to display the correct scene depending on the value of the state property of our GameWindow. Note that we are switching the scenes only by changing their opacity, they don't actually get unloaded. Just replace the default code of the Main.qml
file with the following
implementation:
qml/Main.qml
:
import Felgo 4.0 import QtQuick 2.0 import "scenes" GameWindow { id: gameWindow // start size of the window // usually set to resolution of main target device, this resolution is for iPhone 4/4S screenWidth: 640 screenHeight: 960 // the entity manager allows dynamic creation of entities within the game scene (e.g. bullets and coins) EntityManager { id: entityManager entityContainer: gameScene } // title scene TitleScene { id: titleScene // the title scene is our start scene, so if back is pressed there (on android) we ask the user if he wants to quit the application onBackButtonPressed: { NativeUtils.displayMessageBox("Really quit the game?", "", 2) } // listen to the return value of the MessageBox Connections { target: NativeUtils onMessageBoxFinished: { // only quit, if the activeScene is titleScene - the messageBox might also get opened from other scenes in your code if(accepted && gameWindow.activeScene === titleScene) Qt.quit() } } } // game scene to play GameScene { id: gameScene // the game scene is our main scene of the game, if back is pressed there we return to the title scene onBackButtonPressed: { gameWindow.state = "title" } } // game over scene GameOverScene { id: gameOverScene // the game over scene is very similar to the title scene, so if back is pressed there (on android) we ask the user if he wants to quit the application onBackButtonPressed: { NativeUtils.displayMessageBox("Really quit the game?", "", 2) } // listen to the return value of the MessageBox Connections { target: NativeUtils onMessageBoxFinished: { // only quit, if the activeScene is gameOverScene - the messageBox might also get opened from other scenes in your code if(accepted && gameWindow.activeScene === gameOverScene) Qt.quit() } } } // the default state is title -> now our default scene is the titleScene state: "title" // state machine, takes care of reversing the PropertyChanges when switching the state, e.g. changing the opacity back to 0 states: [ State { name: "title" PropertyChanges {target: titleScene; opacity: 1} PropertyChanges {target: gameWindow; activeScene: titleScene} }, State { name: "game" PropertyChanges {target: gameScene; opacity: 1} PropertyChanges {target: gameWindow; activeScene: gameScene} }, State { name: "gameover" PropertyChanges {target: gameOverScene; opacity: 1} PropertyChanges {target: gameWindow; activeScene: gameOverScene} } ] }
When you start the game at this point, you will already see the title scene since we set the initial value of the state property to “title”. We also hear the background music of the invisible game scene, because by default the BackgroundMusic component starts playing automatically. This is ok for us as the music can be played all the time. If we don't want it to play during some scenes, we would have to start and stop it manually when switching scenes.
Another scene can now be loaded just by changing the state property of the GameWindow. Let's add a play button and start a new game when clicking it by using a MouseArea. We also play a click sound using the GameSoundEffect component and implement a simple button effect by adding a Timer to alternate the colors of the button between black and yellow.
qml/scenes/TitleScene.qml
:
import Felgo 4.0 import QtQuick 2.0 import "../common" SceneBase { // ... // play button to start game Text { id: text text: "play!" color: "black" x: 60 y: 380 z: 1 font.pixelSize: 20 font.family: "Arial" // start new game when start is clicked MouseArea { width: parent.width height: parent.height onClicked: { clickSound.play() gameScene.startNewGame() gameWindow.state = "game" } } } // alternate text color of play button over time Timer { id: colorTimer interval: 1000 // 1 sec repeat: true running: true onTriggered: { if(text.color == "#000000") text.color = "yellow" else text.color = "black" } } // sound effect when clicking the play button GameSoundEffect { id: clickSound source: "../../assets/sound/Click.wav" } }
We are now able to easily switch scenes. To learn more about scenes you can check out the tutorial on how to create a game with multiple scenes and levels.
Before we create any new entities, please take a look at the following picture to get the general idea of how the game screen is going to be composed in terms of entities:
The player is going to jump between rides (horse, boat, bike, car) to avoid dynamically created bullets and coins. The bullets and coins start at the height of the enemies and either hit the player or the invisible floor.
Each enemy is described using an Enemy entity and all of them are then grouped within a single Enemies entity. So let's add the enemies to our game scene. They will be moving up and down like on a real carousel! First we have
to create a new directory qml/entities
for our own entities and then add a new file Enemy.qml
. Here's the generic code for the enemies:
qml/entities/Enemy.qml
:
import QtQuick 2.0 import Felgo 4.0 EntityBase { id: enemy entityType: "enemy" property alias image: image property double speed: 10 MultiResolutionImage { id: image width: parent.width height: parent.height } // provides up/down movement MovementAnimation { id: upDownMovement target: enemy // set animation target to enemy object property: "y" // we animate the y position velocity: -enemy.speed // start with up movement running: true // animation starts automatically // limits the y property (defines possible movement area) // we set a random area for each enemy between minPropertyValue: -(Math.random() * 10 + 5) // min y is random between -5 and -15 maxPropertyValue: Math.random() * 10 + 5 // max y is random between +5 to +15 // change direction after min/max is reached (e.g. move up after down movement is finished) onLimitReached: { if(upDownMovement.velocity > 0) upDownMovement.velocity = -enemy.speed; else upDownMovement.velocity = enemy.speed; } } }
Each enemy is displayed as an image. By using the MultiResolutionImage component we get an automatic image swapping mechanism for the correct image quality. The MovementAnimation component allows us to animate the y-position of the enemy constantly by a fixed velocity and is set to start the animation automatically. When reaching a limit of the y-position the movement velocity is reversed, thus creating an alternating up- and down-movement. As we want to set different images for each enemy entity, we use a property alias for the image object to allow setting it from the outside.
We then create the Enemies entity, that contains four Enemy objects.
qml/entities/Enemies.qml
:
import QtQuick 2.0 import Felgo 4.0 EntityBase { id: enemies entityType: "enemies" Enemy { id: enemy1 width: 95.5; height: 112 image.source: "../../assets/enemy1.png" } Enemy { id: enemy2 x: 95.5 width: 69.5; height: 112 image.source: "../../assets/enemy2.png" } Enemy { id: enemy3 x: 165 width: 49.5; height: 112 image.source: "../../assets/enemy3.png" } Enemy { id: enemy4 x: 214.5 width: 105.5; height: 112 image.source: "../../assets/enemy4.png" } }
The Enemies entity now contains all four enemies with their different images and their correct positions from left to right. All that's left is to add the enemies to our game scene.
qml/scenes/GameScene.qml
:
import Felgo 4.0 import QtQuick 2.0 import "../common" import "../entities" SceneBase { id:gameScene // ... // enemies are grouped as a single enemies entity Enemies { id: enemies; y: 61 } }
After we use the import command to load our entities we can simply place all of them in our scene at once. Press play and watch our enemies moving up and down on the carousel! Looks pretty cool!
Next up is our player entity, but we will keep it simple for the beginning and add the complex movement animations later.
qml/entities/Player.qml
:
import QtQuick 2.0 import Felgo 4.0 // player character entity EntityBase { id: player entityType: "player" width: 65 height: 96 MultiResolutionImage { id: image width: parent.width height: parent.height x: -width / 2 y: -height / 2 source: "../../assets/player.png" } // for collision detection BoxCollider { id: boxCollider width: 25 height: 50 anchors.centerIn: image collisionTestingOnlyMode: true } }
In addition to the image for the player, we also add a BoxCollider to be able to check collisions with coins or bullets later. Please note that we set collisionTestingOnlyMode:
true
in order to prevent the player from any movement based on gravity and physics calculations, as it is easier for us to move the player explicitly from one ride to another by defining the movement using a fixed
MovementAnimation.
We will use two separate MovementAnimations for the player, one for the jump (y-position) and one for switching between lines (x-position).
qml/entities/Player.qml
:
EntityBase { id: player entityType: "player" width: 65 height: 96 // while switching rides/jumping isMoving is set to true property bool isMoving: false // when user wants to jump again while the player is moving, the intended move is memorized to be executed after the player stands again property string nextMove: "" // ... // provides horizontal movement (switch ride) MovementAnimation { id: switchMovement target: player property: "x" } // provides vertical movement (jump) MovementAnimation { id: jumpMovement target: player property: "y" // limits the y property between 0 and the default player position (where the player stands) minPropertyValue: 0 maxPropertyValue: gameScene.playerY // when jump is finished, set default image (standing) and initiate memorized additional jump onLimitReached: { isMoving = false image.source = "../../assets/player.png" if(nextMove !== "") { if(nextMove === "right") moveRight() else moveLeft() } } } // triggers player movement to the left function moveLeft() { // only if player is not on the first ride if(player.x - gameScene.laneWidth < 0) return if(!isMoving) { var distance = gameScene.laneWidth // configure movement switchMovement.velocity = -distance * 4.5 switchMovement.minPropertyValue = player.x - distance switchMovement.maxPropertyValue = gameScene.width // do animation isMoving = true nextMove="" switchMovement.start() jump() } else { nextMove = "left" // memorize attempted move } } // triggers player movement to the right function moveRight() { // only if player is not on the last ride if(player.x + gameScene.laneWidth > gameScene.width) return if(!isMoving) { var distance = gameScene.laneWidth // configure movement switchMovement.velocity = distance * 4.5 switchMovement.minPropertyValue = 0 switchMovement.maxPropertyValue = player.x + distance // do animation isMoving = true nextMove = "" switchMovement.start() jump() } else { nextMove = "right" // memorize attempted move } } // start jump function jump() { // configure jump and start movement jumpMovement.velocity = -500 jumpMovement.acceleration = 3000 jumpMovement.start() jumpSound.stop() jumpSound.play() image.source = "../../assets/player_jump.png" } // stop all movement function stopMovement() { jumpMovement.stop() switchMovement.stop() isMoving = false nextMove = "" } // jump sound GameSoundEffect { id: jumpSound source: "../../assets/sound/Jump.wav" } }
That's already a lot of code, but I'm sure you will get it after we take a closer look. Our Player entity now provides two important functions: The moveRight()
-function will be called when the user wants to jump
to the right, the moveLeft()
-function handles jumping to the left. Each of these two functions won't work if the player is already at the end of the screen (the last or first ride). Before initiating any movement
the property isMoving is checked, because we don't want the player to jump again while already in the air. But for the players convenience, we will memorize the intended move to execute it after we landed again.
The switchMovement
allows animating the x-position of the player and is configured to automatically stop once the desired position is reached. This is done by setting the limits of the movement. For moving to
the left or moving to the right different limits are set for the movement. We must also set the velocity property to define the direction and speed of the movement. The functions then start the switchMovement
and
additionally call the jump()
-function to make the player jump.
When jumping, we also play the jump sound using the GameSoundEffect component and simply swap the player image to a jump image. For the jumpMovement
animation the event
handler onLimitReached
is used to perform actions after the player landed again. This includes changing the player image back from jumping to standing and triggering a second jump if we memorized one during the
last jump.
Alright, then let's add the player to our scene:
qml/scenes/GameScene.qml
:
import Felgo 4.0 import QtQuick 2.0 import "../common" import "../entities" SceneBase { id:gameScene // setup scene layout property int lanes: 4 // number of lanes (rides) property double laneWidth: gameScene.width / gameScene.lanes // pixelWidth of a lane property int startLane: 0 // start lane of player property double playerY: 400 // y position of player in scene // ... // initialize a new game function startNewGame() { gameScene.points = 0 // set new random starting lane player.stopMovement() gameScene.startLane = Math.random() * gameScene.lanes + 1 player.x = (gameScene.startLane * gameScene.laneWidth) - (gameScene.laneWidth / 2) player.y = gameScene.playerY } // player character Player { id: player z: 10 x: (gameScene.startLane * gameScene.laneWidth) - (gameScene.laneWidth / 2) y: gameScene.playerY } // enemies are grouped as a single enemies entity Enemies { id: enemies; y: 61 } }
We define a few properties to set up our game layout, like the number of lanes (number of available rides), the width in pixels of each lane and the default y-Position of the player. These parameters are used to calculate the distance for the players switch-movement or to determine the landing point (limit) of the jump-movement. We also calculate a random starting point for the player each time a new game is started. If you start the game, you can already see the player at a random position, but it is a bit boring if we can't jump to other rides, so let's check the user input.
qml/scenes/GameScene.qml
SceneBase { id:gameScene // ... // handle user input (check for swipe to left/right) MouseArea { anchors.fill: gameScene.gameWindowAnchorItem // check full game window property bool touch: false // true when user touches screen property int firstX: 0 // x position of swipe start point // recognize start of swipe on press onPressed: { if(touch == false) firstX = mouseX touch = true } // recognize end of swipe on release onReleased: { if(touch == true) checkSwipe(15) touch = false } // also recognize swipe if mouse moved a long distance onPositionChanged: { if(touch) checkSwipe(50) } // move player based on swipe (left or right) function checkSwipe(minDistance) { var distance = mouseX - firstX if(Math.abs(distance) > minDistance) { if(distance > 0) player.moveRight() else player.moveLeft() touch = false } } } }
We check for swipes whenever the user touches the screen (presses the mouse). When moving 50 pixels without releasing the mouse we already recognize a swipe - even if the player is still touching the screen. In addition, when the mouse is released we check for a swipe using a minimum swipe distance of 15 pixels. Based on the swipe direction the according movement functions of the player entity are called. Hit play and enjoy our main character jumping around!
So let's add bullets and coins that move from the enemies toward the player area. The structure of both entities is nearly the same, each consisting of an image, a sound effect, and a collider for collision detection.
qml/entities/Bullet.qml
:
import QtQuick 2.0 import Felgo 4.0 EntityBase { id: bullet entityType: "bullet" width: 32 height: 37 MultiResolutionImage { id: image width: parent.width height: parent.height x: -width / 2 y: -height / 2 source: "../../assets/bullet.png" } // for collision detection BoxCollider { id: boxCollider width: 15 height: 15 anchors.centerIn: image collisionTestingOnlyMode: true fixture.onBeginContact: { // for access of the collided entity and the entityType and entityId: var collidedEntity = other.getBody().target var collidedEntityType = collidedEntity.entityType // check if bullet was avoided if(collidedEntityType === "floor") { bulletSound.play() parent.visible = false } else { gameScene.logic.gameOver() entityManager.removeEntityById(parent.entityId) } } } // bullet sound GameSoundEffect { id: bulletSound source: "../../assets/sound/Bullet.wav" onPlayingChanged: { if(bulletSound.playing === false) { entityManager.removeEntityById(parent.entityId) } } } }
qml/entities/Coin.qml
:
import QtQuick 2.0 import Felgo 4.0 EntityBase { id: coin entityType: "coin" width: 32 height: 33 MultiResolutionImage { id: image width: parent.width height: parent.height x: -width / 2 y: -height / 2 source: "../../assets/coin.png" } // for collision detection BoxCollider { id: boxCollider width: 20 height: 20 anchors.centerIn: image collisionTestingOnlyMode: true fixture.onBeginContact: { // for access of the collided entity and the entityType and entityId: var collidedEntity = other.getBody().target var collidedEntityType = collidedEntity.entityType // only increase points if coin was collected if(collidedEntityType === "player") { boxCollider.active = false parent.visible = false coinSound.play() gameScene.logic.increasePoints() } else { entityManager.removeEntityById(parent.entityId) } } } // coin sound GameSoundEffect { id: coinSound source: "../../assets/sound/Coin.wav" onPlayingChanged: { if(coinSound.playing === false) { entityManager.removeEntityById(parent.entityId) } } } }
When a collision is detected, we remove the entity from the game and call an appropriate handler function if necessary. We will provide these handler functions within a designated entity for our game logic. By calling
gameScene.logic.increasePoints()
or gameScene.logic.gameOver()
we can react to the player collecting a coin or getting hit by a paper plane. Note that in case a sound effect is played, the entity is
removed after the sound has finished playing. This is handled by implementing the onPlayingChanged
handler of the GameSoundEffect component.
The movement of the bullets and coins in our game is a bit tricky, as we want them to move from the enemies to special target spots within the player area. They should move along the lanes of the carousel. We also need them to get bigger when moving to their target points to make it look like they are coming closer. We can achieve this by adding the following lines to both of our entities.
qml/entities/Bullet.qml
:
EntityBase { // ... // properties for handling the movement towards a goal property double goalX: 0 property double startX: 0 property double startY: 0 property double speed: 3000 scale: 0.2 // initial scale // parallel animation of x-position, y-position and scaling ParallelAnimation { running: true // all animations are started NumberAnimation { target: bullet property: "x" from: startX to: goalX duration: speed easing.type: Easing.InCubic } NumberAnimation { target: bullet property: "y" from: startY to: gameScene.gameWindowAnchorItem.height duration: speed easing.type: Easing.InCubic } NumberAnimation { target: bullet property: "scale" to: 1.0 duration: speed easing.type: Easing.InCubic } } }
qml/entities/Coin.qml
:
EntityBase { // ... // properties for handling the movement towards a goal property double goalX: 0 property double startX: 0 property double startY: 0 property double speed: 3000 scale: 0.2 // initial scale // parallel animation of x-position, y-position and scaling ParallelAnimation { running: true // all animations are started NumberAnimation { target: coin property: "x" from: startX to: goalX duration: speed easing.type: Easing.InCubic } NumberAnimation { target: coin property: "y" from: startY to: gameScene.gameWindowAnchorItem.height duration: speed easing.type: Easing.InCubic } NumberAnimation { target: coin property: "scale" to: 0.9 duration: speed easing.type: Easing.InCubic } } }
We define some additional properties to be able to set the target point of the movement for each entity. Based on these properties the x-position, y-position and scale of the objects are continuously modified by a NumberAnimation, that allows changing a property from a start value to a goal within a set period of time. We also add easing.type: Easing.InCubic
to
let the animation begin slow and speed up over time. The ParallelAnimation is used for combining multiple small animations, that will all be executed at
the same time whenever the ParallelAnimation is started.
To be able to dynamically add the entities to the game, we already defined the EntityManager in our Main.qml beforehand. So now we need to implement timers to add them during the game. For handling the timers and their configuration we create a new Logic object.
qml/entities/Logic.qml
:
import QtQuick 2.0 import Felgo 4.0 QtObject { id: logic // properties for handling game speed and bullet/coin timeouts // can be used to speed up the game property double speedUp: 1.0 property int minBulletTimeout: 1000 property int minCoinTimeout: 2000 property int bulletTimeout: 4000 property int coinTimeout: 2000 // increasePoints is called by coins when they are collected function increasePoints(coin) { gameScene.points++ } // is called by bullets when game is over function gameOver() { gameWindow.state = "gameover" } // timer for dynamic bullet creation property Timer bulletTimer: Timer { id: bulletTimer interval : Math.random() * logic.bulletTimeout + logic.minBulletTimeout running: gameScene.visible // start running from the beginning, when the scene is loaded onTriggered: { // calculate bullet properties var randomLane = Math.floor(Math.random() * 4 + 1) var enemyWidth = gameScene.laneWidth * 0.74 var laneOffset = gameScene.width * 0.13 var xPosition = Math.round(laneOffset + (randomLane * enemyWidth) - (enemyWidth / 2)) var xGoal = randomLane * gameScene.laneWidth - (gameScene.laneWidth/2) var newEntityProperties = { x: xPosition, y: 95, // position at height of enemies, z: 12, goalX: xGoal, startX: xPosition, startY: 95, speed: 3000 } // add bullet to scene entityManager.createEntityFromUrlWithProperties( Qt.resolvedUrl("Bullet.qml"), newEntityProperties) // recalculate new interval between 2000 and 5000ms interval = Math.random() * logic.bulletTimeout + logic.minBulletTimeout // restart the timer bulletTimer.restart() } } // timer for dynamic coin creation property Timer coinTimer: Timer { id: coinTimer interval : Math.random() * logic.coinTimeout + logic.minCoinTimeout running: gameScene.visible // start running from the beginning, when the scene is loaded onTriggered: { // calculate coin properties var randomLane = Math.floor(Math.random() * 3 + 1) var enemyWidth = gameScene.laneWidth * 0.74 var laneOffset = gameScene.width * 0.13 var xPosition = Math.round(laneOffset + (randomLane * enemyWidth)) var xGoal = randomLane * gameScene.laneWidth // add coin to scene var newEntityProperties = { x: xPosition , y: 75, // position slightly above enemies z: 12, goalX: xGoal, startX: xPosition, startY: 75, speed: 3000 } // add coin to scene entityManager.createEntityFromUrlWithProperties( Qt.resolvedUrl("Coin.qml"), newEntityProperties) // calculate random interval for next coin interval = Math.random() * logic.coinTimeout + logic.minCoinTimeout // restart the timer coinTimer.restart() } } }
We base our logic object on QtObject instead of EntityBase so it won't be affected by operations like
entityManger.removeAllEntities()
, which is what we expect to happen because the logic has no visual representation in our game scene. The timers for coins and bullets are nearly the same. They just use different
intervals and set different properties for positioning the entities. In addition to the x, y and z values for bullets and coins, the special properties goalX, startX, startY and speed are set to define the movement of entities.
We also have the previously mentioned functions for handling player collisions with coins or bullets, that increase player points or switch from the game scene to the game-over scene.
After the entities are added to the scene we want them to be removed if they reach the bottom of the screen. To achieve that, we need to create a simple invisible Floor entity that will be placed at the bottom of our game window.
qml/entities/Floor.qml
:
import QtQuick 2.0 import Felgo 4.0 // non-visible floor for collision testing EntityBase { id: floor entityType: "floor" width: parent.width // width of the scene height: 5 // for collision testing BoxCollider { anchors.fill: parent bodyType: Body.Static // the body shouldn't move } }
Now let's add the logic entity to our GameScene, along with the Floor, the PhysicsWorld and a Text component to show the points.
qml/scenes/GameScene.qml
SceneBase { id:gameScene // ... // make important objects accessible for other entities property alias logic: logic // game logic Logic { id: logic } // initialize a new game function startNewGame() { gameScene.points = 0 // initial speed multiplier gameScene.logic.speedUp = 1.0 // reset bullet and coin timeouts gameScene.logic.bulletTimeout = 4000 gameScene.logic.coinTimeout = 2000 // set new starting lane player.stopMovement() gameScene.startLane = Math.random() * gameScene.lanes + 1 player.x = (gameScene.startLane * gameScene.laneWidth) - (gameScene.laneWidth / 2) player.y = gameScene.playerY } // basic physics PhysicsWorld { gravity.y: 0 // we use fixed animations instead of gravity z: 5 // draw the debugDraw on top of the entities // these are performance settings to avoid boxes colliding too far together // set them as low as possible so it still looks good updatesPerSecondForPhysics: 60 velocityIterations: 5 positionIterations: 5 // set this to true to see the debug draw of the physics system // this displays all bodies, joints and forces which is great for debugging debugDrawVisible: false } // display player score Text { text: gameScene.points color: "#e52222" y: 36 z: 1 font.pixelSize: 12 font.family: "Arial" font.weight: Font.Bold width: gameScene.width horizontalAlignment: Text.AlignHCenter } // invisible floor Floor { z: 100 anchors.bottom: gameScene.gameWindowAnchorItem.bottom } // ... }
Please don't forget to reset the logic configuration parameters within the startNewGame()
-function. We will be able to slow down or speed up the bullets and player movement with the speedUp
property
later in this tutorial. The floor is anchored at the bottom of the gameWindow and not the bottom of the scene, because we want the bullets to fly all the way to the bottom on every screen size.
One small thing that's still missing is to show the points in our game-over scene:
qml/scenes/GameOverScene.qml
:
SceneBase { id: gameOverScene // ... // display points Text { text: gameScene.points color: "white" x: 188 y: 300 z: 1 font.pixelSize: 34 font.family: "Arial" width: 108 horizontalAlignment: Text.AlignHCenter } }
Let's hit play and see how it works!
What is still missing? Right! The rides where the player has to sit or stand on! The modeling of the rides is a bit tricky and best described using an image, please take a look:
Each ride consists of two images, one for the front and one for the backside. The player is then positioned between the front and back by setting the appropriate z-position values for all the entities. If correctly positioned above one another it should look like the player is standing or sitting on the ride! So let's start with the front side of the rides.
qml/entities/RideFront.qml
:
import QtQuick 2.0 import Felgo 4.0 EntityBase { id: rideFront entityType: "rideFront" z: 9 property alias image: image property alias minY: upDownMovement.minPropertyValue property alias maxY: upDownMovement.maxPropertyValue property alias velocity: upDownMovement.velocity property double speed: 10 MultiResolutionImage { id: image width: parent.width height: parent.height } // provides up/down movement MovementAnimation { id: upDownMovement target: rideFront property: "y" velocity: -rideFront.speed // start with up movement running: true // change direction after min/max is reached onLimitReached: { if(rideFront.velocity > 0) rideFront.velocity = -rideFront.speed else rideFront.velocity = rideFront.speed } } }
The code is quite similar to the Enemy entity we created before. But there are some important differences:
The implementation of the RideBack entity then uses the matching FrontRide object as a reference for its values and calculations:
qml/entities/RideBack.qml
:
import QtQuick 2.0 import Felgo 4.0 EntityBase { id: rideBack entityType: "rideBack" z: 11 x: frontRide.x y: frontRide.y width: frontRide.width height: frontRide.height property QtObject frontRide: null property alias image: image MultiResolutionImage { id: image width: parent.width height: parent.height } // provides up/down movement MovementAnimation { id: upDownMovement target: rideBack property: "y" velocity: frontRide.velocity // start with up movement running: true // limits the y property (defines possible movement area) minPropertyValue: frontRide.minY maxPropertyValue: frontRide.maxY // change direction after min/max is reached onLimitReached: { upDownMovement.velocity = frontRide.velocity } } }
Let's add the rides to the game scene:
qml/scenes/GameScene.qml
:
SceneBase { id:gameScene // ... // make important objects accessible for other entities property alias logic: logic property alias ride1: horse property alias ride2: boat property alias ride3: bike property alias ride4: car // ... // player rides // ride1 - horse RideFront { id: horse x: -20; y: 397 minY: 397 - (Math.random() * 10 + 5); maxY: 397 + Math.random() * 10 + 5 width: 92.5; height: 128 image.source: "../../assets/ride1_f.png" } RideBack { frontRide: horse image.source: "../../assets/ride1_b.png" } // ride2 - boat RideFront { id: boat x: 73.5; y: 397 minY: 397 - (Math.random() * 10 + 5); maxY: 397 + Math.random() * 10 + 5 width: 94.5; height: 128 image.source: "../../assets/ride2_f.png" } RideBack { frontRide: boat image.source: "../../assets/ride2_b.png" } // ride3 - bike RideFront { id: bike x: 167; y: 397 minY: 397 - (Math.random() * 10 + 5); maxY: 397 + Math.random() * 10 + 5 width: 67.5; height: 128 image.source: "../../assets/ride3_f.png" } RideBack { frontRide: bike image.source: "../../assets/ride3_b.png" } // ride4 - car RideFront { id: car x: 234.5; y: 397 minY: 397 - (Math.random() * 10 + 5); maxY: 397 + Math.random()* 10 + 5 width: 105.5; height: 128 image.source: "../../assets/ride4_f.png" } RideBack { frontRide: car image.source: "../../assets/ride4_b.png" } // enemies are grouped as a single enemies entity Enemies { id: enemies; y: 61 } // ... }
For the RideBack entities we set a reference to the matching FrontRide and define the correct image. Note: this only works because the images for the front and the back of each ride have the same size and perfectly match each other at the same position.
We now want to move the player character along with the correct ride in the same way as the back of the ride moves with its respective front. To achieve this, we add another MovementAnimation for the up- and down-movement of the player, which will be active whenever the player is not in the process of jumping between rides.
qml/entities/Player.qml
:
import QtQuick 2.0 import Felgo 4.0 // player character entity EntityBase { id: player entityType: "player" width: 65 height: 96 property QtObject frontRide: null // current player ride for synchronizing the up/down-movement // ... // provides vertical movement (jump) MovementAnimation { // ... onLimitReached: { isMoving = false image.source = "../../assets/player.png" if(nextMove !== "") { if(nextMove === "right") moveRight() else moveLeft() } else { jumpMovement.stop() startUpDownMovement() } } } // triggers player movement to the left function moveLeft() { // ... // do animation isMoving = true nextMove="" upDownMovement.stop() switchMovement.start() jump() // ... } // triggers player movement to the right function moveRight() { // ... // do animation isMoving = true nextMove="" upDownMovement.stop() switchMovement.start() jump() // ... } // stop all movement function stopMovement() { jumpMovement.stop() switchMovement.stop() upDownMovement.stop() isMoving = false nextMove = "" } // ... // provides up/down movement along with rides MovementAnimation { id: upDownMovement target: player property: "y" // change direction after min/max is reached onLimitReached: { upDownMovement.velocity = frontRide.velocity } } // starts to move the player up and down along with ride he is currently standing on function startUpDownMovement() { // determine which ride the player is standing on var laneNr = 0 for(var i = 1; i <= gameScene.lanes; i++) { var minX = (i - 1) * gameScene.laneWidth var maxX = i * gameScene.laneWidth var playerOnRide = player.x < maxX && player.x > minX if(playerOnRide) { laneNr = i break } } // retrieve entity of the current ride switch(laneNr) { case 1: player.frontRide = gameScene.ride1; break case 2: player.frontRide = gameScene.ride2; break case 3: player.frontRide = gameScene.ride3; break case 4: player.frontRide = gameScene.ride4; break default: player.frontRide = null } // move player with active ride if(player.frontRide === null) player.y = gameScene.playerY else { // configure and start movement animation player.y = player.frontRide.y + 3 upDownMovement.stop() upDownMovement.velocity = frontRide.velocity upDownMovement.maxPropertyValue = frontRide.maxY + 3 upDownMovement.minPropertyValue = frontRide.minY + 3 upDownMovement.start() } } }
One problem that might arise from this implementation is, that due to the movement of the player along the ride, the players y-position might actually be lower than the minimum y-position limit of the jump animation. This might cause the players jumpMovement to fail. We can avoid this error by moving the player to the correct position right before the jump is executed:
qml/entities/Player.qml
:
import QtQuick 2.0 import Felgo 4.0 // player character entity EntityBase { // ... // start jump function jump() { // player may be lower than allowed (due to moving of rides) if(player.y > gameScene.playerY) player.y = gameScene.playerY // ... } // ... }
We can now move the player along with the rides, we just need to start the movement whenever the player starts on a random ride of a new game.
qml/scenes/GameScene.qml
:
SceneBase { id:gameScene // ... function startNewGame() { gameScene.points = 0 // initial speed multiplier gameScene.logic.speedUp = 1.0 // reset bullet and coin timeouts gameScene.logic.bulletTimeout = 4000 gameScene.logic.coinTimeout = 2000 // set new starting lane player.stopMovement() gameScene.startLane = Math.random() * gameScene.lanes + 1 player.x = (gameScene.startLane * gameScene.laneWidth) - (gameScene.laneWidth / 2) player.startUpDownMovement() } // ... }
That's all there is to it, press play and see it in action!
To add some spice to the game it would be nice to speed it up little by little and make it more difficult! We already have the possibility to lower the interval timers for the bullets and coins. In addition, we also want the bullets and coins to move faster. Let us start by modifying our logic object.
qml/entities/Logic.qml
:
EntityBase { id: logic entityType: "logic" // ... // timer for dynamic bullet creation property Timer bulletTimer: Timer { // ... var newEntityProperties = { x: xPosition, y: 95, // position at height of enemies, z: 12, goalX: xGoal, startX: xPosition, startY: 95, speed: 3000 / (1 + logic.speedUp / 8 * 1.5) // multiplier between 1 and 2.5 } // ... } // timer for dynamic coin creation property Timer coinTimer: Timer { // ... var newEntityProperties = { x: xPosition , y: 75, // position slightly above enemies z: 12, goalX: xGoal, startX: xPosition, startY: 75, speed: 3000 / (1 + logic.speedUp / 8 * 2) // multiplier between 1 and 3 } // ... } // timer for speeding up the game property Timer speedUpTimer: Timer { id: speedUpTimer interval: 15000 // speed up every 15 seconds running: gameScene.visible // timer is only active when gameScene is running onTriggered: { logic.speedUp += 0.7 // speed up game // stop bulletTimer, reduce timeout and restart timer bulletTimer.stop() logic.bulletTimeout -= 300 bulletTimer.interval = Math.random() * logic.bulletTimeout + logic.minBulletTimeout bulletTimer.restart() // stop coinTimer, reduce timeout and restart timer coinTimer.stop() logic.coinTimeout -= 100 coinTimer.interval = Math.random() * logic.coinTimeout + logic.minCoinTimeout coinTimer.restart() // stop after speed up limit is reached if(logic.speedUp < 8) speedUpTimer.restart() } } }
We now use the speedUp
property of our logic object to calculate the speed property of the bullets and coins to make them faster if the speedUp is higher. We also add a third Timer to increase the speed-up and lower the timeouts every 15 seconds until a maximum speed-up factor of 8.0 is reached.
Furthermore, it would be fun to speed up the movement of the rides and enemies to make the game as a whole look faster! We will begin with speeding up the enemies movement. To be able to increase the speed, we simply need to
include our speedUp
property in the movement speed calculation of the Enemy entity.
qml/entities/Enemy.qml
:
EntityBase { id: enemy entityType: "enemy" // ... property double speed: 10 * gameScene.logic.speedUp // ... }
In the same way, we speed up the player rides.
qml/entities/RideFront.qml
:
EntityBase { id: rideFront entityType: "rideFront" // ... property double speed: 10 * gameScene.logic.speedUp // ... }
This will also speed up the RideBack objects, because they use the movement speed and limits of their respective RideFront entities. With these changes the game will gradually speed up. But in contrast to all the other objects, the player movement still stays the same. It might be a good idea to also make the player move faster.
qml/entities/Player.qml
:
EntityBase { id: player entityType: "player" // ... // triggers player movement to the left function moveLeft() { // only if player is not on the first ride if(player.x - gameScene.laneWidth < 0) return if(!isMoving) { var distance = gameScene.laneWidth // use faster movement when game speed is higher var multiplier = 1 + (logic.speedUp / 8 * 3) // multiplier between 1 and 4 // configure movement switchMovement.velocity = -distance * 4.5 * multiplier switchMovement.minPropertyValue = player.x - distance switchMovement.maxPropertyValue = gameScene.width // do animation isMoving = true nextMove="" upDownMovement.stop() switchMovement.start() jump() } else { nextMove = "left" } } // triggers player movement to the right function moveRight() { // only if player is not on the last ride if(player.x + gameScene.laneWidth > gameScene.width) return if(!isMoving) { var distance = gameScene.laneWidth // use faster movement when game speed is higher var multiplier = 1 + (logic.speedUp / 8 * 3) // multiplier between 1 and 4 // configure movement switchMovement.velocity = distance * 4.5 * multiplier switchMovement.minPropertyValue = 0 switchMovement.maxPropertyValue = player.x + distance // do animation isMoving = true nextMove = "" upDownMovement.stop() switchMovement.start() jump() } else { nextMove = "right" } } // start jump function jump() { // player may be lower than allowed (due to moving of rides) if(player.y > gameScene.playerY) player.y = gameScene.playerY // use faster movement when game speed is higher var multiplier1 = 0.8 + (logic.speedUp / 8 * 1.0) // multiplier between 0.8 and 1.8 var multiplier2 = 1 + (logic.speedUp / 8 * 4) // multiplier between 1 and 5 // configure jump and start movement jumpMovement.velocity = -500 * multiplier1 jumpMovement.acceleration = 2000 * multiplier2 jumpMovement.start() jumpSound.stop() jumpSound.play() image.source = "../../assets/player_jump.png" } // ... }
All the velocities for the players movement animations are now influenced by a multiplier based on the general speed-up. With this final change this tutorial is complete, press play and enjoy your Crazy Carousel game!
If you are motivated to further improve the game, try adding a player highscore using the FelgoGameNetwork or improve the animations using the GameAnimatedSprite or GameSpriteSequence components!
Also 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.