1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
|
..
---------------------------------------------------------------------------
Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
All rights reserved.
This work, unless otherwise expressly stated, is licensed under a
Creative Commons Attribution-ShareAlike 2.5.
The full license document is available from
http://creativecommons.org/licenses/by-sa/2.5/legalcode .
---------------------------------------------------------------------------
Boss enemy
==========
The boss enemy appears at the end of a level after a certain number of targets has been passed.
Unlike in the first part of the level, the player observes the fight not from the back of the hamburger*, but from the top. That also means the *hamburger* (and the enemy) can only be moved on the x-axis. At the beginning of the fight, we have to set the `y` value to `0`, which we already did when we defined the states.
Camera movement
---------------
The camera will start moving when the enemy has arrived at its final position, in front of the player. To accomplish this, a `SequentialAnimation` is started which moves the camera's `center` to the enemy's position. After that, the camera pans around the enemy and at the end moves the `eye` to the top of the scene and adjusts the camera's `center` to the middle of the fighting scene.
After the animation is finished, the start of the fight is triggered by setting a new state for the root item:
.. code-block:: js
//game.qml
...
Viewport {
...
SequentialAnimation {
id: rotateAroundBoss
running: false
PropertyAnimation{
target: cam
properties: "center"
to: enemy.position
duration: 400
}
PropertyAnimation{
target: cam
properties: "eye"
duration: 2000
to: Qt.vector3d(30,5,50);
}
PropertyAnimation{
target: cam
properties: "eye"
duration: 2000
to: Qt.vector3d(-30,5,50);
}
PropertyAnimation{
target: cam
properties: "eye"
duration: 1000
to: Qt.vector3d(0,5,0);
}
ParallelAnimation {
PropertyAnimation{
target: cam
properties: "eye"
duration: 2000
to: Qt.vector3d(0, 140, -1);
}
PropertyAnimation{
target: cam
properties: "center"
running: false
duration: 1000;
to: Qt.vector3d(0,0,20);
}
}
onRunningChanged: {
if (running==false) {
root.state="BossFight"
}
}
}
}
We can also add the animation to the states:
.. code-block:: js
//game.qml
...
State{
name: "BossRotation"
PropertyChanges {target: rotateAroundBoss; running: true }
}
...
Movement
--------
For the boss enemy, we create `Enemy.qml`.
It uses the `Fruits.3ds` model, which has to first be pretransformed in order for it to fit into our scene:
.. code-block:: js
//Enemy.qml
import QtQuick 2.0
import Qt3D 1.0
//Creates an enemy
Item3D {
id: enemy
//Size of the object for the collision detection
property real radius: 1.5
mesh: Mesh { source: "Fruits/Fruits.3ds"; options: "ForceSmooth";}
pretransform : [
Rotation3D {
angle: -180
axis: Qt.vector3d(0, 1, 0)
},
Scale3D {
scale: 0.01
}
]
}
The enemy will be created after ten targets have been passed so we have to extend the `targetTimer` code and add the boss enemy component:
.. code-block:: js
//Gamelogic.qml
...
property variant bossEnemyComponent: Qt.createComponent("Enemy.qml")
...
Timer {
id: targetTimer
...
onTriggered: {
var component;
//After a certain amount of targets were created the boss enemy appears
if (targetCount>10) {
targetTimer.stop()
enemy = bossEnemyComponent.createObject(level)
}
//Targets are constantly created and fly towards the player
else {
targetCount++
var object = targetComponent.createObject(level,
{"position.x": (Math.random()-0.5) * 8,
"position.y": (Math.random()-0.5) * 6,
"scale": 3-0.2*targetCount,
"collisionTarget": player})
object.collisionDetected.connect(targetCollision)
}
}
}
...
Furthermore, we are adding a property called enemy* to `main.qml` to be able to easily access the object:
.. code-block:: js
//game.qml
...
Viewport {
...
property variant enemy
...
}
When the enemy is created, it will approach the player, stop at a distance of 40, and afterwards, set a new state for the root element which will trigger the camera movement.
.. code-block:: js
//Enemy.qml
...
//Animation which moves the the enemy towards the player
NumberAnimation on z{
running: true
duration: 10000
from: 200
to: 40
onRunningChanged: { if (running == false) root.state="BossRotation" }
}
...
The enemy will simply move from left to right and fire in constant intervals. Both the `SequentialAnimation` and the `Timer` will only run if the root item is in the `BossFight` state.
.. code-block:: js
//Enemy.qml
...
Item3D {
...
//The enemy movement
SequentialAnimation {
id: bossMovement
running: root.state=="BossFight"
loops: Animation.Infinite
PropertyAnimation{
target: enemy
properties: "x"
duration: 5000
to: -16
easing.type: Easing.InOutSine
}
PropertyAnimation{
target: enemy
properties: "x"
duration: 5000
easing.type: Easing.InOutSine
to: 16
}
}
Timer {
id: shootTimer
interval: 1000
repeat: true
running: root.state=="BossFight"
onTriggered: {
shootLaser()
}
}
}
Because we use a `SequentialAnimation` here, more complex movements could be implemented (for example the enemy flying in circles or at altering speed).
Weaponfire
----------
We use a very popular technique called Billboarding* for bullets that are fired from and at the enemy. It adjusts an item's orientation so that it always faces the camera. *Billboarding* is very often used for particle effects, (distant) vegetation or just to cut down polygons on far away 3D Objects. Usually a billboard consists of a rectangle that is always facing the camera, but any arbitrary 3D Object could be used for that.
In Qt3D there are two methods available that create billboard items. One of them is the `BillboardItem3D` which uses a very fast way for creating billboards that face the camera plane. This element however has some restrictions, whereas scaling and rotating of an item is not possible. Because of that we take the `LookAtTransform` for creating a billboard that faces the camera.
.. code-block:: js
//Bullet.qml
Quad{
//defines the shadereffect, that should be used for the item
effect: lasereffect
transform: [
Rotation3D{
angle: 90
axis: Qt.vector3d(1, 0, 0)
},
LookAt{ subject: camPos}
]
//wrapper around the camera position
Item3D { id: camPos
position: cam.eye
}
}
We are using a `Quad` for our particle effect, that has to be rotated first, because it is lying in the x,z plane. Afterwards the `LookAt` transformation is applied, which takes an `Item3D` as `subject`. That is why we have to embed the camera's position into a `Item3D` before assigning it to the `LookAt` transform. The subject is the item, that should be looked at.
For now, we will just create a simple `Effect` for each bullet, i.e. a semitransparent texture is mapped on top of the quad.
.. code-block:: js
Effect {
id: lasereffect
blending: true
material: Material {
textureUrl: "bullet.png"
}
}
We will reuse the collision detection, which we already built in the previous section, for the bullets. The difference between the bullet and the onion rings is that a bullet has a direction and a velocity that can both depend on the entity that shoots the bullet or power ups that the player has collected. We therefore have to implement a new animation that handles the movement of the bullet. Again, it is very important to only animate the position of the `BasicGameItem` and not the `Quad`. Otherwise collision detection will not work.
.. code-block:: js
//Bullet.qml
import QtQuick 2.0
import Qt3D 1.0
import Qt3D.Shapes 1.0
//This item represents a laser particle
BasicGameItem{
id: bullet
property variant dir: Qt.vector3d(0,0,1)
property real speed: 100;
Quad{
//defines the shadereffect, that should be used for the item
effect: lasereffect
transform: [
Rotation3D{
angle: 45
axis: Qt.vector3d(1, 0, 0)
},
LookAt{ subject: Item3D { position: cam.eye} }
]
Effect {
id: lasereffect
blending: true
material: Material {
textureUrl: "laser2.png"
emittedLight: Qt.rgba(1,0.8,0.8,1)
}
}
}
//The movement of the bullet
PropertyAnimation on position {
to: Qt.vector3d(x+speed*dir.x, y+speed*dir.y, z+speed*dir.z);
duration: 10000
onRunningChanged: {
//When the bulletanimation is finished and no target has been hit
if (running==false) {
bullet.destroy();
}
}
}
}
We have now got bullets with a working collision detection that move in a direction that can be specified. The feature that is still missing is the firing mechanism for the bullets. We need to make it possible for the player to shoot a bullet when pressing the space key, also the enemy should be able to shoot back. Furthermore, the player and the enemy need a property which holds the hit-points that are left and a function connected to the `collisionDetected` signal of the bullets so that the hit-points can be subtracted.
We first implement the latter for the enemy:
.. code-block:: js
//Enemy.qml
property int hitpoints: 10
property real maxHitPoints: 10
....
function hit() {
hitpoints--
if (hitpoints <= 0) {
explode();
}
}
function explode () {
enemy.destroy()
root.state="EnterHighscore"
}
....
It is nearly the same for the player except that we do not delete the player after it explodes:
.. code-block:: js
//Player.qml
property int hitpoints: 10
....
function hit() {
hitpoints--
if (hitpoints <= 0) {
explode();
}
}
function explode () {
root.state="EnterHighscore"
}
....
The firing mechanism for the enemy is very simple. We just create a new `Bullet` object with the player as the target. Then we connect the `collisionDetected` signal to the player's `hit` function:
.. code-block:: js
//Enemy.qml
...
//Shoots a bullet
function shootLaser() {
var component = Qt.createComponent("Bullet.qml")
var object = component.createObject(level, {"position": enemy.position,
"radius": 0.2, "dir": Qt.vector3d(0,0,-1),
"collisionTarget": player});
object.collisionDetected.connect(player.hit)
object.collisionDetected.connect(object.destroy)
}
...
We implement the firing of the player's bullet in `Gamelogic.qml`, where the `fireLaser` function is executed after the space key has been pressed. Every time a bullet is fired, a certain amount of energy is subtracted from the player, which we refill in the `gameTimer`:
.. code-block:: js
//Gamelogic.qml
...
id: gameTimer
onTriggered: {
if(player.energy<player.maxenergy)
player.energy++;
...
function fireLaser() {
if (player.energy>=40) {
print(player.y)
player.energy -=40
var component = Qt.createComponent("Bullet.qml")
var laserObject = component.createObject(level,
{"position": player.position,
"collisionTarget": enemy})
laserObject.collisionDetected.connect(enemy.hit)
}
}
...
The fight against the enemy should work fine now. One thing you have probably noticed is that the area in which the hamburger* can be moved is fairly small. This is because of the new perspective. That is why we have to expand the `x_bound` value during the fight against the boss enemy.
.. code-block:: js
//game.qml
...
property real x_bound: state == "BossFight" ? 16: 4.5;
...
You should now be able to fight against the boss enemy.
.. image:: img/fight.png
:scale: 80%
:align: center
.. rubric:: What's Next?
Next we will talk about shaders and see how to create particle effects with it.
|