1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtQuick module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qquickspriteengine_p.h" |
41 | #include "qquicksprite_p.h" |
42 | #include <qqmlinfo.h> |
43 | #include <qqml.h> |
44 | #include <QDebug> |
45 | #include <QPainter> |
46 | #include <QRandomGenerator> |
47 | #include <QSet> |
48 | #include <QtGui/qopengl.h> |
49 | #include <QOpenGLFunctions> |
50 | |
51 | QT_BEGIN_NAMESPACE |
52 | |
53 | /* |
54 | \internal Stochastic/Sprite engine implementation docs |
55 | |
56 | Nomenclature: 'thing' refers to an instance of a running sprite or state. It could be renamed. |
57 | States and Transitions are referred to in the state machine sense here, NOT in the QML sense. |
58 | |
59 | The Stochastic State engine takes states with stochastic state transitions defined and transitions them. |
60 | When a state is started, it's added to a list of pending updates sorted by their time they want to update. |
61 | An external driver calls the update function with an elapsed time, which becomes the new time offset. |
62 | The pending update stack is popped until all entries are past the current time, which simulates all intervening time. |
63 | |
64 | The Sprite Engine subclass has two major differences. Firstly all states are sprites (and there's a new vector with them |
65 | cast to sprite). Secondly, it chops up images and states to fit a texture friendly format. |
66 | Before the Sprite Engine starts running, its user requests a texture assembled from all the sprite images. This |
67 | texture is made by pasting the sprites into one image, with one sprite animation per row (in the future it is planned to have |
68 | arbitrary X/Y start ends, but they will still be assembled and recorded here and still have to be contiguous lines). |
69 | This cut-up allows the users to calcuate frame positions with a texture percentage width and elapsed time. |
70 | It also means that large sprites cover multiple lines to fit inside the texture memory limit (which is a square). |
71 | |
72 | Large sprites covering multiple lines breaks this simple interface for the users, so each line is treated as a pseudostate |
73 | and it's mostly hidden from the spriteengine users (except that they'll get advanced signals where the state is the same |
74 | but the visual parameters changed). These are not real states because that would get very complex with bindings. Instead, |
75 | when sprite attributes are requested from a sprite that has multiple pseudostates, it returns the values for the psuedostate |
76 | it is in. State advancement is intercepted and hollow for pseudostates, except the last one. The last one transitions as the |
77 | state normally does. |
78 | */ |
79 | |
80 | static const int NINF = -1000000;//magic number for random start time - should be more negative than a single realistic animation duration |
81 | //#define SPRITE_IMAGE_DEBUG |
82 | #ifdef SPRITE_IMAGE_DEBUG |
83 | #include <QFile> |
84 | #include <QDir> |
85 | #endif |
86 | /* TODO: |
87 | make sharable? |
88 | solve the state data initialization/transfer issue so as to not need to make friends |
89 | */ |
90 | |
91 | QQuickStochasticEngine::QQuickStochasticEngine(QObject *parent) : |
92 | QObject(parent), m_timeOffset(0), m_addAdvance(false) |
93 | { |
94 | //Default size 1 |
95 | setCount(1); |
96 | } |
97 | |
98 | QQuickStochasticEngine::QQuickStochasticEngine(const QList<QQuickStochasticState *> &states, QObject *parent) : |
99 | QObject(parent), m_states(states), m_timeOffset(0), m_addAdvance(false) |
100 | { |
101 | //Default size 1 |
102 | setCount(1); |
103 | } |
104 | |
105 | QQuickStochasticEngine::~QQuickStochasticEngine() |
106 | { |
107 | } |
108 | |
109 | QQuickSpriteEngine::QQuickSpriteEngine(QObject *parent) |
110 | : QQuickStochasticEngine(parent), m_startedImageAssembly(false), m_loaded(false) |
111 | { |
112 | } |
113 | |
114 | QQuickSpriteEngine::QQuickSpriteEngine(const QList<QQuickSprite *> &sprites, QObject *parent) |
115 | : QQuickSpriteEngine(parent) |
116 | { |
117 | for (QQuickSprite* sprite : sprites) |
118 | m_states << (QQuickStochasticState*)sprite; |
119 | } |
120 | |
121 | QQuickSpriteEngine::~QQuickSpriteEngine() |
122 | { |
123 | } |
124 | |
125 | |
126 | int QQuickSpriteEngine::maxFrames() const |
127 | { |
128 | return m_maxFrames; |
129 | } |
130 | |
131 | /* States too large to fit in one row are split into multiple rows |
132 | This is more efficient for the implementation, but should remain an implementation detail (invisible from QML) |
133 | Therefore the below functions abstract sprite from the viewpoint of classes that pass the details onto shaders |
134 | But States maintain their listed index for internal structures |
135 | TODO: All these calculations should be pre-calculated and cached during initialization for a significant performance boost |
136 | TODO: Above idea needs to have the varying duration offset added to it |
137 | */ |
138 | //TODO: Should these be adding advanceTime as well? But only if advanceTime was added to your startTime... |
139 | /* |
140 | To get these working with duration=-1, m_startTimes will be messed with should duration=-1 |
141 | m_startTimes will be set in advance/restart to 0->(m_framesPerRow-1) and can be used directly as extra. |
142 | This makes it 'frame' instead, but is more memory efficient than two arrays and less hideous than a vector of unions. |
143 | */ |
144 | int QQuickSpriteEngine::pseudospriteProgress(int sprite, int state, int* rowDuration) const |
145 | { |
146 | int myRowDuration = m_duration[sprite] * m_sprites[state]->m_framesPerRow / m_sprites[state]->m_frames; |
147 | if (rowDuration) |
148 | *rowDuration = myRowDuration; |
149 | |
150 | if (m_sprites[state]->reverse()) //shift start-time back by the amount of time the first frame is smaller than rowDuration |
151 | return (m_timeOffset - (m_startTimes[sprite] - (myRowDuration - (m_duration[sprite] % myRowDuration))) ) |
152 | / myRowDuration; |
153 | else |
154 | return (m_timeOffset - m_startTimes[sprite]) / myRowDuration; |
155 | } |
156 | |
157 | int QQuickSpriteEngine::spriteState(int sprite) const |
158 | { |
159 | if (!m_loaded) |
160 | return 0; |
161 | int state = m_things[sprite]; |
162 | if (!m_sprites[state]->m_generatedCount) |
163 | return state; |
164 | |
165 | int ; |
166 | if (m_sprites[state]->frameSync()) |
167 | extra = m_startTimes[sprite]; |
168 | else if (!m_duration[sprite]) |
169 | return state; |
170 | else |
171 | extra = pseudospriteProgress(sprite, state); |
172 | if (m_sprites[state]->reverse()) |
173 | extra = (m_sprites[state]->m_generatedCount - 1) - extra; |
174 | |
175 | return state + extra; |
176 | } |
177 | |
178 | int QQuickSpriteEngine::spriteStart(int sprite) const |
179 | { |
180 | if (!m_duration[sprite] || !m_loaded) |
181 | return m_timeOffset; |
182 | int state = m_things[sprite]; |
183 | if (!m_sprites[state]->m_generatedCount) |
184 | return m_startTimes[sprite]; |
185 | int rowDuration; |
186 | int = pseudospriteProgress(sprite, state, rowDuration: &rowDuration); |
187 | if (m_sprites[state]->reverse()) |
188 | return m_startTimes[sprite] + (extra ? (extra - 1)*rowDuration + (m_duration[sprite] % rowDuration) : 0); |
189 | return m_startTimes[sprite] + extra*rowDuration; |
190 | } |
191 | |
192 | int QQuickSpriteEngine::spriteFrames(int sprite) const |
193 | { |
194 | if (!m_loaded) |
195 | return 1; |
196 | int state = m_things[sprite]; |
197 | if (!m_sprites[state]->m_generatedCount) |
198 | return m_sprites[state]->frames(); |
199 | |
200 | int ; |
201 | if (m_sprites[state]->frameSync()) |
202 | extra = m_startTimes[sprite]; |
203 | else if (!m_duration[sprite]) |
204 | return m_sprites[state]->frames(); |
205 | else |
206 | extra = pseudospriteProgress(sprite, state); |
207 | if (m_sprites[state]->reverse()) |
208 | extra = (m_sprites[state]->m_generatedCount - 1) - extra; |
209 | |
210 | |
211 | if (extra == m_sprites[state]->m_generatedCount - 1) {//last state |
212 | const int framesRemaining = m_sprites[state]->frames() % m_sprites[state]->m_framesPerRow; |
213 | if (framesRemaining > 0) |
214 | return framesRemaining; |
215 | } |
216 | return m_sprites[state]->m_framesPerRow; |
217 | } |
218 | |
219 | int QQuickSpriteEngine::spriteDuration(int sprite) const //Full duration, not per frame |
220 | { |
221 | if (!m_duration[sprite] || !m_loaded) |
222 | return m_duration[sprite]; |
223 | int state = m_things[sprite]; |
224 | if (!m_sprites[state]->m_generatedCount) |
225 | return m_duration[sprite]; |
226 | int rowDuration; |
227 | int = pseudospriteProgress(sprite, state, rowDuration: &rowDuration); |
228 | if (m_sprites[state]->reverse()) |
229 | extra = (m_sprites[state]->m_generatedCount - 1) - extra; |
230 | |
231 | if (extra == m_sprites[state]->m_generatedCount - 1) {//last state |
232 | const int durationRemaining = m_duration[sprite] % rowDuration; |
233 | if (durationRemaining > 0) |
234 | return durationRemaining; |
235 | } |
236 | return rowDuration; |
237 | } |
238 | |
239 | int QQuickSpriteEngine::spriteY(int sprite) const |
240 | { |
241 | if (!m_loaded) |
242 | return 0; |
243 | int state = m_things[sprite]; |
244 | if (!m_sprites[state]->m_generatedCount) |
245 | return m_sprites[state]->m_rowY; |
246 | |
247 | int ; |
248 | if (m_sprites[state]->frameSync()) |
249 | extra = m_startTimes[sprite]; |
250 | else if (!m_duration[sprite]) |
251 | return m_sprites[state]->m_rowY; |
252 | else |
253 | extra = pseudospriteProgress(sprite, state); |
254 | if (m_sprites[state]->reverse()) |
255 | extra = (m_sprites[state]->m_generatedCount - 1) - extra; |
256 | |
257 | |
258 | return m_sprites[state]->m_rowY + m_sprites[state]->m_frameHeight * extra; |
259 | } |
260 | |
261 | int QQuickSpriteEngine::spriteX(int sprite) const |
262 | { |
263 | if (!m_loaded) |
264 | return 0; |
265 | int state = m_things[sprite]; |
266 | if (!m_sprites[state]->m_generatedCount) |
267 | return m_sprites[state]->m_rowStartX; |
268 | |
269 | int ; |
270 | if (m_sprites[state]->frameSync()) |
271 | extra = m_startTimes[sprite]; |
272 | else if (!m_duration[sprite]) |
273 | return m_sprites[state]->m_rowStartX; |
274 | else |
275 | extra = pseudospriteProgress(sprite, state); |
276 | if (m_sprites[state]->reverse()) |
277 | extra = (m_sprites[state]->m_generatedCount - 1) - extra; |
278 | |
279 | if (extra) |
280 | return 0; |
281 | return m_sprites[state]->m_rowStartX; |
282 | } |
283 | |
284 | QQuickSprite* QQuickSpriteEngine::sprite(int sprite) const |
285 | { |
286 | return m_sprites[m_things[sprite]]; |
287 | } |
288 | |
289 | int QQuickSpriteEngine::spriteWidth(int sprite) const |
290 | { |
291 | int state = m_things[sprite]; |
292 | return m_sprites[state]->m_frameWidth; |
293 | } |
294 | |
295 | int QQuickSpriteEngine::spriteHeight(int sprite) const |
296 | { |
297 | int state = m_things[sprite]; |
298 | return m_sprites[state]->m_frameHeight; |
299 | } |
300 | |
301 | int QQuickSpriteEngine::spriteCount() const //TODO: Actually image state count, need to rename these things to make sense together |
302 | { |
303 | return m_imageStateCount; |
304 | } |
305 | |
306 | void QQuickStochasticEngine::setGoal(int state, int sprite, bool jump) |
307 | { |
308 | if (sprite >= m_things.count() || state >= m_states.count() |
309 | || sprite < 0 || state < 0) |
310 | return; |
311 | if (!jump){ |
312 | m_goals[sprite] = state; |
313 | return; |
314 | } |
315 | |
316 | if (m_things.at(i: sprite) == state) |
317 | return;//Already there |
318 | m_things[sprite] = state; |
319 | m_duration[sprite] = m_states.at(i: state)->variedDuration(); |
320 | m_goals[sprite] = -1; |
321 | restart(index: sprite); |
322 | emit stateChanged(idx: sprite); |
323 | emit m_states.at(i: state)->entered(); |
324 | return; |
325 | } |
326 | |
327 | QQuickPixmap::Status QQuickSpriteEngine::status() const //Composed status of all Sprites |
328 | { |
329 | if (!m_startedImageAssembly) |
330 | return QQuickPixmap::Null; |
331 | int null, loading, ready; |
332 | null = loading = ready = 0; |
333 | for (QQuickSprite* s : m_sprites) { |
334 | switch (s->m_pix.status()) { |
335 | // ### Maybe add an error message here, because this null shouldn't be reached but when it does, the image fails without an error message. |
336 | case QQuickPixmap::Null : null++; break; |
337 | case QQuickPixmap::Loading : loading++; break; |
338 | case QQuickPixmap::Error : return QQuickPixmap::Error; |
339 | case QQuickPixmap::Ready : ready++; break; |
340 | } |
341 | } |
342 | if (null) |
343 | return QQuickPixmap::Null; |
344 | if (loading) |
345 | return QQuickPixmap::Loading; |
346 | if (ready) |
347 | return QQuickPixmap::Ready; |
348 | return QQuickPixmap::Null; |
349 | } |
350 | |
351 | void QQuickSpriteEngine::startAssemblingImage() |
352 | { |
353 | if (m_startedImageAssembly) |
354 | return; |
355 | m_loaded = false; |
356 | m_errorsPrinted = false; |
357 | |
358 | //This could also trigger the start of the image loading in Sprites, however that currently happens in Sprite::setSource |
359 | |
360 | QList<QQuickStochasticState*> removals; |
361 | |
362 | for (QQuickStochasticState* s : qAsConst(t&: m_states)) { |
363 | QQuickSprite* sprite = qobject_cast<QQuickSprite*>(object: s); |
364 | if (sprite) { |
365 | m_sprites << sprite; |
366 | } else { |
367 | removals << s; |
368 | qDebug() << "Error: Non-sprite in QQuickSpriteEngine" ; |
369 | } |
370 | } |
371 | for (QQuickStochasticState* s : qAsConst(t&: removals)) |
372 | m_states.removeAll(t: s); |
373 | m_startedImageAssembly = true; |
374 | } |
375 | |
376 | QImage QQuickSpriteEngine::assembledImage(int maxSize) |
377 | { |
378 | QQuickPixmap::Status stat = status(); |
379 | if (!m_errorsPrinted && stat == QQuickPixmap::Error) { |
380 | for (QQuickSprite* s : qAsConst(t&: m_sprites)) |
381 | if (s->m_pix.isError()) |
382 | qmlWarning(me: s) << s->m_pix.error(); |
383 | m_errorsPrinted = true; |
384 | } |
385 | |
386 | if (stat != QQuickPixmap::Ready) |
387 | return QImage(); |
388 | |
389 | int h = 0; |
390 | int w = 0; |
391 | m_maxFrames = 0; |
392 | m_imageStateCount = 0; |
393 | qreal pixelRatio = 1.0; |
394 | |
395 | for (QQuickSprite* state : qAsConst(t&: m_sprites)) { |
396 | if (state->frames() > m_maxFrames) |
397 | m_maxFrames = state->frames(); |
398 | |
399 | QImage img = state->m_pix.image(); |
400 | |
401 | { |
402 | const QSize frameSize(state->m_frameWidth, state->m_frameHeight); |
403 | if (!(img.size() - frameSize).isValid()) { |
404 | qmlWarning(me: state).nospace() << "SpriteEngine: Invalid frame size " << frameSize << "." |
405 | " It's bigger than image size " << img.size() << "." ; |
406 | return QImage(); |
407 | } |
408 | } |
409 | |
410 | //Check that the frame sizes are the same within one sprite |
411 | if (!state->m_frameWidth) |
412 | state->m_frameWidth = img.width() / state->frames(); |
413 | |
414 | if (!state->m_frameHeight) |
415 | state->m_frameHeight = img.height(); |
416 | |
417 | pixelRatio = qMax(a: pixelRatio, b: state->devicePixelRatio()); |
418 | |
419 | if (state->frames() * state->frameWidth() > maxSize){ |
420 | struct helper{ |
421 | static int divRoundUp(int a, int b){return (a+b-1)/b;} |
422 | }; |
423 | int rowsNeeded = helper::divRoundUp(a: state->frames(), b: (maxSize / state->frameWidth())); |
424 | if (h + rowsNeeded * state->frameHeight() > maxSize){ |
425 | if (rowsNeeded * state->frameHeight() > maxSize) |
426 | qmlWarning(me: state) << "SpriteEngine: Animation too large to fit in one texture:" << state->source().toLocalFile(); |
427 | else |
428 | qmlWarning(me: state) << "SpriteEngine: Animations too large to fit in one texture, pushed over the edge by:" << state->source().toLocalFile(); |
429 | qmlWarning(me: state) << "SpriteEngine: Your texture max size today is " << maxSize; |
430 | return QImage(); |
431 | } |
432 | state->m_generatedCount = rowsNeeded; |
433 | h += state->frameHeight() * rowsNeeded; |
434 | w = qMax(a: w, b: ((int)(maxSize / state->frameWidth())) * state->frameWidth()); |
435 | m_imageStateCount += rowsNeeded; |
436 | }else{ |
437 | h += state->frameHeight(); |
438 | w = qMax(a: w, b: state->frameWidth() * state->frames()); |
439 | m_imageStateCount++; |
440 | } |
441 | } |
442 | |
443 | if (h > maxSize){ |
444 | qWarning() << "SpriteEngine: Too many animations to fit in one texture..." ; |
445 | qWarning() << "SpriteEngine: Your texture max size today is " << maxSize; |
446 | return QImage(); |
447 | } |
448 | |
449 | //maxFrames is max number in a line of the texture |
450 | QImage image(w * pixelRatio, h * pixelRatio, QImage::Format_ARGB32_Premultiplied); |
451 | image.setDevicePixelRatio(pixelRatio); |
452 | image.fill(pixel: 0); |
453 | QPainter p(&image); |
454 | int y = 0; |
455 | for (QQuickSprite* state : qAsConst(t&: m_sprites)) { |
456 | QImage img(state->m_pix.image()); |
457 | const int frameWidth = state->m_frameWidth; |
458 | const int frameHeight = state->m_frameHeight; |
459 | const int imgHeight = img.height() / img.devicePixelRatioF(); |
460 | const int imgWidth = img.width() / img.devicePixelRatioF(); |
461 | if (imgHeight == frameHeight && imgWidth < maxSize){ //Simple case |
462 | p.drawImage(targetRect: QRect(0, y, state->m_frames * frameWidth, frameHeight), |
463 | image: img, |
464 | sourceRect: QRect(state->m_frameX * img.devicePixelRatioF(), 0, state->m_frames * frameWidth * img.devicePixelRatioF(), frameHeight * img.devicePixelRatioF())); |
465 | state->m_rowStartX = 0; |
466 | state->m_rowY = y; |
467 | y += frameHeight; |
468 | } else { //Chopping up image case |
469 | state->m_framesPerRow = w/frameWidth; |
470 | state->m_rowY = y; |
471 | int x = 0; |
472 | int curX = state->m_frameX; |
473 | int curY = state->m_frameY; |
474 | int framesLeft = state->frames(); |
475 | while (framesLeft > 0){ |
476 | if (w - x + curX <= imgWidth){//finish a row in image (dest) |
477 | int copied = w - x; |
478 | framesLeft -= copied/frameWidth; |
479 | p.drawImage(targetRect: QRect(x, y, copied, frameHeight), |
480 | image: img, |
481 | sourceRect: QRect(curX * img.devicePixelRatioF(), curY * img.devicePixelRatioF(), copied * img.devicePixelRatioF(), frameHeight * img.devicePixelRatioF())); |
482 | y += frameHeight; |
483 | curX += copied; |
484 | x = 0; |
485 | if (curX == imgWidth){ |
486 | curX = 0; |
487 | curY += frameHeight; |
488 | } |
489 | }else{//finish a row in img (src) |
490 | int copied = imgWidth - curX; |
491 | framesLeft -= copied/frameWidth; |
492 | p.drawImage(targetRect: QRect(x, y, copied, frameHeight), |
493 | image: img, |
494 | sourceRect: QRect(curX * img.devicePixelRatioF(), curY * img.devicePixelRatioF(), copied * img.devicePixelRatioF(), frameHeight * img.devicePixelRatioF())); |
495 | curY += frameHeight; |
496 | x += copied; |
497 | curX = 0; |
498 | } |
499 | } |
500 | if (x) |
501 | y += frameHeight; |
502 | } |
503 | } |
504 | |
505 | #ifdef SPRITE_IMAGE_DEBUG |
506 | QString fPath = QDir::tempPath() + "/SpriteImage.%1.png" ; |
507 | int acc = 0; |
508 | while (QFile::exists(fPath.arg(acc))) acc++; |
509 | image.save(fPath.arg(acc), "PNG" ); |
510 | qDebug() << "Assembled image output to: " << fPath.arg(acc); |
511 | #endif |
512 | |
513 | m_loaded = true; |
514 | m_startedImageAssembly = false; |
515 | return image; |
516 | } |
517 | |
518 | //TODO: Add a reset() function, for completeness in the case of a SpriteEngine being restarted from 0 |
519 | void QQuickStochasticEngine::setCount(int c) |
520 | { |
521 | m_things.resize(asize: c); |
522 | m_goals.resize(asize: c); |
523 | m_duration.resize(asize: c); |
524 | m_startTimes.resize(asize: c); |
525 | } |
526 | |
527 | void QQuickStochasticEngine::start(int index, int state) |
528 | { |
529 | if (index >= m_things.count()) |
530 | return; |
531 | m_things[index] = state; |
532 | m_duration[index] = m_states.at(i: state)->variedDuration(); |
533 | if (m_states.at(i: state)->randomStart()) |
534 | m_startTimes[index] = NINF; |
535 | else |
536 | m_startTimes[index] = 0; |
537 | m_goals[index] = -1; |
538 | m_addAdvance = false; |
539 | restart(index); |
540 | m_addAdvance = true; |
541 | } |
542 | |
543 | void QQuickStochasticEngine::stop(int index) |
544 | { |
545 | if (index >= m_things.count()) |
546 | return; |
547 | //Will never change until start is called again with a new state (or manually advanced) - this is not a 'pause' |
548 | for (int i=0; i<m_stateUpdates.count(); i++) |
549 | m_stateUpdates[i].second.removeAll(t: index); |
550 | } |
551 | |
552 | void QQuickStochasticEngine::restart(int index) |
553 | { |
554 | bool randomStart = (m_startTimes.at(i: index) == NINF); |
555 | m_startTimes[index] = m_timeOffset; |
556 | if (m_addAdvance) |
557 | m_startTimes[index] += m_advanceTimer.elapsed(); |
558 | if (randomStart) |
559 | m_startTimes[index] -= QRandomGenerator::global()->bounded(highest: m_duration.at(i: index)); |
560 | int time = m_duration.at(i: index) + m_startTimes.at(i: index); |
561 | for (int i=0; i<m_stateUpdates.count(); i++) |
562 | m_stateUpdates[i].second.removeAll(t: index); |
563 | if (m_duration.at(i: index) >= 0) |
564 | addToUpdateList(t: time, idx: index); |
565 | } |
566 | |
567 | void QQuickSpriteEngine::restart(int index) //Reimplemented to recognize and handle pseudostates |
568 | { |
569 | bool randomStart = (m_startTimes.at(i: index) == NINF); |
570 | if (m_loaded && m_sprites.at(i: m_things.at(i: index))->frameSync()) {//Manually advanced |
571 | m_startTimes[index] = 0; |
572 | if (randomStart && m_sprites.at(i: m_things.at(i: index))->m_generatedCount) |
573 | m_startTimes[index] += QRandomGenerator::global()->bounded(highest: m_sprites.at(i: m_things.at(i: index))->m_generatedCount); |
574 | } else { |
575 | m_startTimes[index] = m_timeOffset; |
576 | if (m_addAdvance) |
577 | m_startTimes[index] += m_advanceTimer.elapsed(); |
578 | if (randomStart) |
579 | m_startTimes[index] -= QRandomGenerator::global()->bounded(highest: m_duration.at(i: index)); |
580 | int time = spriteDuration(sprite: index) + m_startTimes.at(i: index); |
581 | if (randomStart) { |
582 | int curTime = m_timeOffset + (m_addAdvance ? m_advanceTimer.elapsed() : 0); |
583 | while (time < curTime) //Fast forward through psuedostates as needed |
584 | time += spriteDuration(sprite: index); |
585 | } |
586 | |
587 | for (int i=0; i<m_stateUpdates.count(); i++) |
588 | m_stateUpdates[i].second.removeAll(t: index); |
589 | addToUpdateList(t: time, idx: index); |
590 | } |
591 | } |
592 | |
593 | void QQuickStochasticEngine::advance(int idx) |
594 | { |
595 | if (idx >= m_things.count()) |
596 | return;//TODO: Proper fix(because this has happened and I just ignored it) |
597 | int nextIdx = nextState(curState: m_things.at(i: idx), idx); |
598 | m_things[idx] = nextIdx; |
599 | m_duration[idx] = m_states.at(i: nextIdx)->variedDuration(); |
600 | restart(index: idx); |
601 | emit m_states.at(i: nextIdx)->entered(); |
602 | emit stateChanged(idx); |
603 | } |
604 | |
605 | void QQuickSpriteEngine::advance(int idx) //Reimplemented to recognize and handle pseudostates |
606 | { |
607 | if (!m_loaded) { |
608 | qWarning() << QLatin1String("QQuickSpriteEngine: Trying to advance sprites before sprites finish loading. Ignoring directive" ); |
609 | return; |
610 | } |
611 | |
612 | if (idx >= m_things.count()) |
613 | return;//TODO: Proper fix(because this has happened and I just ignored it) |
614 | if (m_duration.at(i: idx) == 0) { |
615 | if (m_sprites.at(i: m_things.at(i: idx))->frameSync()) { |
616 | //Manually called, advance inner substate count |
617 | m_startTimes[idx]++; |
618 | if (m_startTimes.at(i: idx) < m_sprites.at(i: m_things.at(i: idx))->m_generatedCount) { |
619 | //only a pseudostate ended |
620 | emit stateChanged(idx); |
621 | return; |
622 | } |
623 | } |
624 | //just go past the pseudostate logic |
625 | } else if (m_startTimes.at(i: idx) + m_duration.at(i: idx) |
626 | > int(m_timeOffset + (m_addAdvance ? m_advanceTimer.elapsed() : 0))) { |
627 | //only a pseduostate ended |
628 | emit stateChanged(idx); |
629 | addToUpdateList(t: spriteStart(sprite: idx) + spriteDuration(sprite: idx) + (m_addAdvance ? m_advanceTimer.elapsed() : 0), idx); |
630 | return; |
631 | } |
632 | int nextIdx = nextState(curState: m_things.at(i: idx), idx); |
633 | m_things[idx] = nextIdx; |
634 | m_duration[idx] = m_states.at(i: nextIdx)->variedDuration(); |
635 | restart(index: idx); |
636 | emit m_states.at(i: nextIdx)->entered(); |
637 | emit stateChanged(idx); |
638 | } |
639 | |
640 | int QQuickStochasticEngine::nextState(int curState, int curThing) |
641 | { |
642 | int nextIdx = -1; |
643 | int goalPath = goalSeek(curState, idx: curThing); |
644 | if (goalPath == -1){//Random |
645 | qreal r = QRandomGenerator::global()->generateDouble(); |
646 | qreal total = 0.0; |
647 | for (QVariantMap::const_iterator iter=m_states.at(i: curState)->m_to.constBegin(); |
648 | iter!=m_states.at(i: curState)->m_to.constEnd(); ++iter) |
649 | total += (*iter).toReal(); |
650 | r*=total; |
651 | for (QVariantMap::const_iterator iter= m_states.at(i: curState)->m_to.constBegin(); |
652 | iter!=m_states.at(i: curState)->m_to.constEnd(); ++iter){ |
653 | if (r < (*iter).toReal()){ |
654 | bool superBreak = false; |
655 | for (int i=0; i<m_states.count(); i++){ |
656 | if (m_states.at(i)->name() == iter.key()){ |
657 | nextIdx = i; |
658 | superBreak = true; |
659 | break; |
660 | } |
661 | } |
662 | if (superBreak) |
663 | break; |
664 | } |
665 | r -= (*iter).toReal(); |
666 | } |
667 | }else{//Random out of shortest paths to goal |
668 | nextIdx = goalPath; |
669 | } |
670 | if (nextIdx == -1)//No 'to' states means stay here |
671 | nextIdx = curState; |
672 | return nextIdx; |
673 | } |
674 | |
675 | uint QQuickStochasticEngine::updateSprites(uint time)//### would returning a list of changed idxs be faster than signals? |
676 | { |
677 | //Sprite State Update; |
678 | m_timeOffset = time; |
679 | m_addAdvance = false; |
680 | int i = 0; |
681 | for (; i < m_stateUpdates.count() && time >= m_stateUpdates.at(i).first; ++i) { |
682 | const auto copy = m_stateUpdates.at(i).second; |
683 | for (int idx : copy) |
684 | advance(idx); |
685 | } |
686 | |
687 | m_stateUpdates.remove(i: 0, n: i); |
688 | m_advanceTimer.start(); |
689 | m_addAdvance = true; |
690 | if (m_stateUpdates.isEmpty()) |
691 | return uint(-1); |
692 | return m_stateUpdates.constFirst().first; |
693 | } |
694 | |
695 | int QQuickStochasticEngine::goalSeek(int curIdx, int spriteIdx, int dist) |
696 | { |
697 | QString goalName; |
698 | if (m_goals.at(i: spriteIdx) != -1) |
699 | goalName = m_states.at(i: m_goals.at(i: spriteIdx))->name(); |
700 | else |
701 | goalName = m_globalGoal; |
702 | if (goalName.isEmpty()) |
703 | return -1; |
704 | //TODO: caching instead of excessively redoing iterative deepening (which was chosen arbitrarily anyways) |
705 | // Paraphrased - implement in an *efficient* manner |
706 | for (int i=0; i<m_states.count(); i++) |
707 | if (m_states.at(i: curIdx)->name() == goalName) |
708 | return curIdx; |
709 | if (dist < 0) |
710 | dist = m_states.count(); |
711 | QQuickStochasticState* curState = m_states.at(i: curIdx); |
712 | for (QVariantMap::const_iterator iter = curState->m_to.constBegin(); |
713 | iter!=curState->m_to.constEnd(); ++iter){ |
714 | if (iter.key() == goalName) |
715 | for (int i=0; i<m_states.count(); i++) |
716 | if (m_states.at(i)->name() == goalName) |
717 | return i; |
718 | } |
719 | QSet<int> options; |
720 | for (int i=1; i<dist; i++){ |
721 | for (QVariantMap::const_iterator iter = curState->m_to.constBegin(); |
722 | iter!=curState->m_to.constEnd(); ++iter){ |
723 | int option = -1; |
724 | for (int j=0; j<m_states.count(); j++)//One place that could be a lot more efficient... |
725 | if (m_states.at(i: j)->name() == iter.key()) |
726 | if (goalSeek(curIdx: j, spriteIdx, dist: i) != -1) |
727 | option = j; |
728 | if (option != -1) |
729 | options << option; |
730 | } |
731 | if (!options.isEmpty()){ |
732 | if (options.count()==1) |
733 | return *(options.begin()); |
734 | int option = -1; |
735 | qreal r = QRandomGenerator::global()->generateDouble(); |
736 | qreal total = 0; |
737 | for (QSet<int>::const_iterator iter=options.constBegin(); |
738 | iter!=options.constEnd(); ++iter) |
739 | total += curState->m_to.value(akey: m_states.at(i: (*iter))->name()).toReal(); |
740 | r *= total; |
741 | for (QVariantMap::const_iterator iter = curState->m_to.constBegin(); |
742 | iter!=curState->m_to.constEnd(); ++iter){ |
743 | bool superContinue = true; |
744 | for (int j=0; j<m_states.count(); j++) |
745 | if (m_states.at(i: j)->name() == iter.key()) |
746 | if (options.contains(value: j)) |
747 | superContinue = false; |
748 | if (superContinue) |
749 | continue; |
750 | if (r < (*iter).toReal()){ |
751 | bool superBreak = false; |
752 | for (int j=0; j<m_states.count(); j++){ |
753 | if (m_states.at(i: j)->name() == iter.key()){ |
754 | option = j; |
755 | superBreak = true; |
756 | break; |
757 | } |
758 | } |
759 | if (superBreak) |
760 | break; |
761 | } |
762 | r-=(*iter).toReal(); |
763 | } |
764 | return option; |
765 | } |
766 | } |
767 | return -1; |
768 | } |
769 | |
770 | void QQuickStochasticEngine::addToUpdateList(uint t, int idx) |
771 | { |
772 | for (int i=0; i<m_stateUpdates.count(); i++){ |
773 | if (m_stateUpdates.at(i).first == t){ |
774 | m_stateUpdates[i].second << idx; |
775 | return; |
776 | } else if (m_stateUpdates.at(i).first > t) { |
777 | QVector<int> tmpList; |
778 | tmpList << idx; |
779 | m_stateUpdates.insert(i, t: qMakePair(x: t, y: tmpList)); |
780 | return; |
781 | } |
782 | } |
783 | QVector<int> tmpList; |
784 | tmpList << idx; |
785 | m_stateUpdates << qMakePair(x: t, y: tmpList); |
786 | } |
787 | |
788 | QT_END_NAMESPACE |
789 | |
790 | #include "moc_qquickspriteengine_p.cpp" |
791 | |