How to make a game like Pong with Felgo - The Level and the Ball
Now you will start to fill your Scene with objects. After your setup of the Scene - the game world - driven by a physical simulation, you have to add, remove, collect and arrange items, for instance, a ball. In a simple case, you add and arrange your game items in a level. Adding a level works similar to your VPongScene.qml. When the Level.qml was created, remove the Rectangle of the file and add the level item code from below.
import QtQuick 2.0 import Felgo 4.0 Item { // When using the game for several displays it should be anchored correctly anchors.fill: gameWindowAnchorItem }
Similar to the Scene, you have created a level template which needs to be added to the game Scene as Level item. The Level size is the same as the Scene size because you want to use the whole game world in your level and not only a part of it.
... PhysicsWorld { id: physicsWorld z: 1 updatesPerSecondForPhysics: 60 velocityIterations: 5 positionIterations: 5 } Level { id: level // The logical size needs to be set to provide it for all child objects width: parent.width height: parent.height } ...
Now you are able to add some objects to you level. For better maintenance and several other reasons you have to add an EntityManager in your GameWindow. More information about the EntityManager and the whole Entity concept can be read in Entity Based Game Design. The position where EntityManager is added does not matter, it is only important that you add it as a GameWindow child so it is accessible from all other scenes.
... fpsTextItem.color: "white" displayFpsEnabled: true EntityManager { id: entityManager entityContainer: scene.entityContainer } // The game scene which includes everything presented on screen VPongScene { id: scene } ...
The EntityManager needs to know which component is responsible for entity creation. Therefore, you provided the binding of the entityContainer in the EntityManager item
entityContainer: scene.entityContainer
, but currently the Scene does not provide an entityContainer property. You already created an Level item which is responsible for your entity creation in future. Therefore, you can add a simple alias at the start of your Scene.qml to signal that the Level is used as entity container. Additionally, an alias for the level should be added to get access to the level item from every scene element.
... width: 480 height: 320 property alias level: level property alias entityContainer: level PhysicsWorld { ...
Now the project is ready to receive the first entity. As you will create different entities, such as, Ball, Wall, Goal and Paddle it would be great idea to collect them in one directory in your project. Create a new folder in your VPong/qml directory, called entities, and add a new Ball.qml file in this new entities folder. Remove the Rectangle and replace it with following Code:
import QtQuick 2.0 import Felgo 4.0 EntityBase { id: entity entityType: "ball" CircleCollider { id: circleCollider radius: sprite.width/2 // Entity does not have width and height, therefore we center the CircleCollider in the entity. anchors.centerIn: parent // set friction between 0 and 1 fixture.friction: 0 // restitution is bounciness fixture.restitution: 1.0 // The ball is a fast and small object therefore, we set the bullet flag to true to get better physic results // during collision detection. bullet: true body.fixedRotation: true } // Debug Rectangle which will be replaced later on with a real Sprite. Rectangle { id: sprite width: 16 height: 16 anchors.centerIn: parent } }
The ball is an EntityBase with entity type "ball". This helps to identify the entity when using the EntityManager. In this Entity you can place a debug Rectangle to see the ball afterwards. The Rectangle will be replaced later on by a real Sprite. For now it is a Rectangle with 16*16 pixels. The entity itself does not have a size because the size will be defined from the sprite later on. The center of the Rectangle is the center of the entity. The CircleCollider is the physical representation of the ball which is also centered in the entity. This element is used by the physics engine to calculate the collisions and to simulate the entity. The radius of the CircleCollider is set to the sprites width because the image size will define the size of the object later on. The properties, such as friction, restitution, density are set to values which are needed to get correct behavior of a pong ball. Now you can add the ball entity to your level. Just center it in the level. The ball entity will not be known by the system so you have to add an import. Furthermore you can add an alias because the ball will be used in other objects, for instance in the Paddle entity to calculate the position for the AI.
... import "entities" Item { // When using the game for several displays it should be anchored correctly anchors.fill: gameWindowAnchorItem property alias ball: ball // The ball started from the middle Ball { id: ball x: gameWindowAnchorItem.width/2 y: parent.height/2 } ...
You should also add an alias in your FelgoScene.qml to grant access in all scene elements.
... width: 480 height: 320 property alias level: level property alias entityContainer: level property alias ball: level.ball ...
Now you can see your Ball Rectangle in the middle of the GameWindow when you run VPong. If you want to debug the physics you will have to use the qml renderer. It can be de-/activated in main.cpp by changing the flag
vplayApplication.setQmlRendererEnabled(false);
Once activated, two windows will be displayed.
Now it is time to add a background image. First you should create an img folder in your qml directory. Then you can create a nice background image 1140px*720px as suggested in How to create mobile games for different screen sizes and resolutions in this img folder.
After you have added a MultiResolutionImage in the Level.qml before the Ball you can see your new image in-game.
... property alias ball: ball // Use a MultiResolutionImage for performance improvements involving blending function and pixelFormat, especially important for Android! // the background image should have dimension of 1140x720 for the hd-version MultiResolutionImage { id:levelBackground source: "../assets/img/background.png" // use this if the image should be centered, which is the most common case // if the image should be aligned at the bottom, probably the whole scene should be aligned at the bottom, and the image should be moved up by the delta between the imageSize and the scene.y! anchors.centerIn: parent visible: true } // The ball started from the middle Ball { id: ball x: gameWindowAnchorItem.width/2 y: parent.height/2 } ...
A Rectangle as ball is great, but a real pong ball would be much better.
You can replace the Rectangle in Ball.qml with a GameSprite item which loads a black ball with the size of 16px*16px placed in your img folder.
import QtQuick 2.0 import Felgo 4.0 EntityBase { id: entity entityType: "ball" CircleCollider { id: circleCollider radius: sprite.width/2 // Entity does not have width and height, therefore we center the CircleCollider in the entity. anchors.centerIn: parent // set friction between 0 and 1 fixture.friction: 0 // restitution is bounciness fixture.restitution: 1.0 fixture.density: 0.001 // The ball is a fast and small object therefore, we set the bullet flag to true to get better physic results // during collision detection. bullet: true body.fixedRotation: true } Image { id: sprite source: "../../assets/img/ball.png" // this is the size compared to the scene size (480x320) - it will automatically be scaled when the window size is bigger than the scene // the image will automatically be scaled to this size. // The size of the loaded image is automatically for the Image size. // this is required to have the center in the middle of the entity and not the top left point as origin of the image anchors.centerIn: parent } }
The ball is now in the middle of the screen and not visible. Therefore you should add a function to your Ball.qml which shoots the ball from the middle of the screen towards the edge on start-up. First of all, the position and the initial velocity is reset to the initial values. Afterwards a random angle between -45 and 45 degree is created and then a random side is selected. Following the direction is multiplied with the speed in pixel per second which gets applied to the ball as linear impulse. When the application is started, the Component.onComplete() function will call reStart() which restarts your ball and the physic system moves the ball.
... // default speed of the ball in pixel per second property int speed: 400 ... Image { id: sprite source: "../../assets/img/ball.png" anchors.centerIn: parent } function applyForwardImpulse(accSpeed) { var localForward = circleCollider.body.toWorldVector(Qt.point(accSpeed,0)); circleCollider.body.applyLinearImpulse(localForward, circleCollider.body.getWorldCenter()); } // Set the ball to the center of the scene and start it. function reStart(posx,posy) { reset(posx,posy) var angle = getRandomInt(-45, 45) // if close to zero, adjust again if (Math.abs(angle) < 5 || Math.abs(angle-180) < 5) angle = getRandomInt(10,20) // pick a side with a random call if(Math.random() > 0.5) angle += 180 // add a toRad() function to the angle, to change the grades to radian if (typeof(Number.prototype.toRad) === "undefined") { Number.prototype.toRad = function() { return this * Math.PI / 180; } } // do the trig to get the x and y components var x = Math.cos(angle.toRad())*speed var y = Math.sin(angle.toRad())*speed // apply physics impulse to start the ball circleCollider.applyLinearImpulse(Qt.point(x, y), circleCollider.pos) } // sets the ball to the middle position and stops it. function reset(xpos,ypos) { // center the ball in the middle of the screen x = xpos y = ypos speed = 400 entity.rotation = 0.0; // set linear velocity of the entity to 0 circleCollider.linearVelocity = Qt.point(0, 0) } // returns the linear velocity of the ball, can be used for simple AI calculations function linearVelocity() { return circleCollider.linearVelocity } // Random range function returns a random value between min and max function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } // Starts the ball temporarily until a button is created Component.onCompleted: { reStart(scene.width/2, scene.height/2) } ...
Finally you have created a moving ball, but it would be great to have some walls around the game field, right?