File indexing completed on 2024-04-21 04:02:34
0001 /* 0002 This file is part of the KDE project "KLines" 0003 0004 SPDX-FileCopyrightText: 2006 Dmitry Suzdalev <dimsuz@gmail.com> 0005 0006 SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "animator.h" 0010 #include "scene.h" 0011 #include "ballitem.h" 0012 #include "renderer.h" 0013 0014 #include <math.h> // for pow, sqrt 0015 0016 // Needed by A* pathfinding algorithm 0017 struct PathNode 0018 { 0019 FieldPos pos; 0020 PathNode *parent; 0021 int G; 0022 float H; 0023 float F; 0024 PathNode( FieldPos fpos, PathNode* p = nullptr, int g=0, int h=0 ) 0025 : pos(fpos), parent(p), G(g), H(h), F(g+h) { } 0026 }; 0027 0028 // helper function - used in findPath() 0029 // returns: 0030 // -1 - if position not found 0031 // index of node in list if position is found 0032 static inline int indexOfNodeWithPos( FieldPos pos, const QList<PathNode*>& list ) 0033 { 0034 for(int i=0;i<list.count(); ++i) 0035 if( list.at(i)->pos == pos ) 0036 return i; 0037 0038 return -1; 0039 } 0040 0041 KLinesAnimator::KLinesAnimator( KLinesScene* scene ) 0042 : m_scene(scene), m_movingBall(nullptr) 0043 { 0044 // timing & framing setup is done in corresponding animate* functions 0045 0046 connect(&m_moveTimeLine, &QTimeLine::frameChanged, this, &KLinesAnimator::moveAnimationFrame); 0047 connect(&m_moveTimeLine, &QTimeLine::finished, this, &KLinesAnimator::moveFinished); 0048 0049 m_removeTimeLine. setEasingCurve(QEasingCurve::Linear); 0050 connect(&m_removeTimeLine, &QTimeLine::frameChanged, this, &KLinesAnimator::removeAnimationFrame); 0051 connect(&m_removeTimeLine, &QTimeLine::finished, this, &KLinesAnimator::removeFinished); 0052 0053 m_bornTimeLine. setEasingCurve(QEasingCurve::Linear); 0054 connect(&m_bornTimeLine, &QTimeLine::frameChanged, this, &KLinesAnimator::bornAnimationFrame); 0055 connect(&m_bornTimeLine, &QTimeLine::finished, this, &KLinesAnimator::slotBornFinished); 0056 } 0057 0058 bool KLinesAnimator::isAnimating() const 0059 { 0060 return (m_bornTimeLine.state() == QTimeLine::Running 0061 || m_moveTimeLine.state() == QTimeLine::Running 0062 || m_removeTimeLine.state() == QTimeLine::Running); 0063 } 0064 0065 bool KLinesAnimator::animateMove( FieldPos from, FieldPos to ) 0066 { 0067 findPath(from, to); 0068 0069 if(m_foundPath.isEmpty()) 0070 return false; 0071 0072 m_movingBall = m_scene->ballAt(from); 0073 m_movingBall->stopAnimation(); 0074 0075 int numPoints = m_foundPath.count(); 0076 // there will be numPoints-1 intervals of 0077 // movement (interval=cell). We want each of them to take animDuration(MoveAnim) ms 0078 m_moveTimeLine.setDuration((numPoints-1)*KLinesRenderer::animDuration(KLinesRenderer::MoveAnim)); 0079 // each interval will take cellSize frames 0080 m_moveTimeLine.setFrameRange(0, (numPoints-1)*KLinesRenderer::cellSize()); 0081 m_moveTimeLine.setCurrentTime(0); 0082 m_moveTimeLine.start(); 0083 return true; 0084 } 0085 0086 void KLinesAnimator::animateRemove( const QList<BallItem*>& list ) 0087 { 0088 m_moveTimeLine.stop(); 0089 m_removeTimeLine.stop(); 0090 0091 if(list.isEmpty()) 0092 { 0093 Q_EMIT removeFinished(); 0094 return; 0095 } 0096 0097 m_removedBalls = list; 0098 0099 // called here (not in constructor), to stay in sync in case theme's reloaded 0100 m_removeTimeLine.setDuration(KLinesRenderer::animDuration(KLinesRenderer::DieAnim)); 0101 // we setup here one 'empty' frame at the end, because without it 0102 // m_scene will delete 'burned' items in removeAnimFinished() slot so quickly 0103 // that last frame won't get shown in the scene 0104 m_removeTimeLine.setFrameRange(0, KLinesRenderer::frameCount(KLinesRenderer::DieAnim)); 0105 0106 m_removeTimeLine.start(); 0107 } 0108 0109 void KLinesAnimator::animateBorn( const QList<BallItem*>& list ) 0110 { 0111 m_bornBalls = list; 0112 for (BallItem* ball : std::as_const(m_bornBalls)) { 0113 ball->setRenderSize(KLinesRenderer::cellExtent()); 0114 } 0115 0116 // called here (not in constructor), to stay in sync in case theme's reloaded 0117 m_bornTimeLine.setDuration(KLinesRenderer::animDuration(KLinesRenderer::BornAnim)); 0118 m_bornTimeLine.setFrameRange(0, KLinesRenderer::frameCount(KLinesRenderer::BornAnim)-1); 0119 0120 m_bornTimeLine.setCurrentTime( 0 ); 0121 m_bornTimeLine.start(); 0122 } 0123 0124 void KLinesAnimator::moveAnimationFrame(int frame) 0125 { 0126 int cellSize = m_moveTimeLine.endFrame() / (m_foundPath.count()-1); 0127 int intervalNum = frame/cellSize; 0128 0129 if(intervalNum == m_foundPath.count()-1) 0130 { 0131 m_movingBall->setPos(m_scene->fieldToPix(m_foundPath.last())); 0132 return; 0133 } 0134 // determine direction of movement on this interval 0135 int kx=0, ky=0; 0136 0137 FieldPos from = m_foundPath.at(intervalNum); 0138 FieldPos to = m_foundPath.at(intervalNum+1); 0139 0140 if( to.x - from.x > 0 ) 0141 kx = 1; 0142 else if( to.x - from.x < 0 ) 0143 kx = -1; 0144 else 0145 kx = 0; 0146 0147 if( to.y - from.y > 0 ) 0148 ky = 1; 0149 else if( to.y - from.y < 0 ) 0150 ky = -1; 0151 else 0152 ky = 0; 0153 0154 int frameWithinInterval = frame%cellSize; 0155 QPointF pos = m_scene->fieldToPix(from); 0156 m_movingBall->setPos( pos.x()+kx*frameWithinInterval, 0157 pos.y()+ky*frameWithinInterval ); 0158 } 0159 0160 void KLinesAnimator::removeAnimationFrame(int frame) 0161 { 0162 if(frame == KLinesRenderer::frameCount(KLinesRenderer::DieAnim)) 0163 return; 0164 for (BallItem* ball : std::as_const(m_removedBalls)) { 0165 ball->setSpriteKey(KLinesRenderer::animationFrameId( KLinesRenderer::DieAnim, 0166 ball->color(), frame) ); 0167 } 0168 } 0169 0170 void KLinesAnimator::bornAnimationFrame(int frame) 0171 { 0172 for (BallItem* ball : std::as_const(m_bornBalls)) { 0173 ball->setSpriteKey( KLinesRenderer::animationFrameId( KLinesRenderer::BornAnim, 0174 ball->color(), frame) ); 0175 } 0176 } 0177 0178 void KLinesAnimator::findPath( FieldPos from, FieldPos to ) 0179 { 0180 // Implementation of A* pathfinding algorithm 0181 // Thanks to Patrick Lester for excellent tutorial on gamedev.net. 0182 // See https://www.gamedev.net/articles/programming/artificial-intelligence/a-pathfinding-for-beginners-r2003/ 0183 0184 QList<PathNode*> openList; 0185 QList<PathNode*> closedList; 0186 0187 m_foundPath.clear(); 0188 0189 openList.append( new PathNode(from) ); 0190 0191 PathNode *curNode=nullptr; 0192 bool pathFound = false; 0193 // see exit conditions at the end of while loop below 0194 while(true) 0195 { 0196 // find the square with lowest F(=G+H) on the open list 0197 PathNode *minF = openList.at(0); 0198 for(int i=1; i<openList.count(); ++i) 0199 if( openList.at(i)->F < minF->F ) 0200 minF = openList.at(i); 0201 0202 // move it to closed list 0203 closedList.append(minF); 0204 openList.removeAll(minF); 0205 0206 curNode = minF; 0207 0208 // for each of adjacent 4 squares (upper,lower, on the left and on the right)... 0209 QList<FieldPos> adjacentSquares; 0210 int x = curNode->pos.x; 0211 int y = curNode->pos.y; 0212 if( x != 0 ) adjacentSquares.append( FieldPos(x-1,y) ); 0213 if( y != 0 ) adjacentSquares.append( FieldPos(x,y-1) ); 0214 if( x != FIELD_SIZE-1 ) adjacentSquares.append( FieldPos(x+1,y) ); 0215 if( y != FIELD_SIZE-1 ) adjacentSquares.append( FieldPos(x,y+1) ); 0216 0217 for (const FieldPos &pos : std::as_const(adjacentSquares)) { 0218 if( m_scene->ballAt(pos) != nullptr ) // skip non-walkable cells 0219 continue; 0220 0221 // skip if closed list contains this square 0222 if(indexOfNodeWithPos(pos, closedList) != -1) 0223 continue; 0224 0225 // search for node with position 'pos' in openList 0226 int idx = indexOfNodeWithPos(pos, openList); 0227 if(idx == -1) // not found 0228 { 0229 PathNode *node = new PathNode( pos ); 0230 node->parent = curNode; 0231 node->G = curNode->G + 10; 0232 // h is manhattanLength from node to target square 0233 node->H = sqrt( pow( (to.x - pos.x)*10, 2. ) + pow( (to.y - pos.y)*10, 2. ) ); 0234 node->F = node->G+node->H; 0235 openList.append( node ); 0236 } 0237 else 0238 { 0239 PathNode *node = openList.at(idx); 0240 // check if this path to square is better 0241 if( curNode->G + 10 < node->G ) 0242 { 0243 // yup, it's better, reparent and recalculate G,F 0244 node->parent = curNode; 0245 node->G = curNode->G + 10; 0246 node->F = node->G + node->H; 0247 } 0248 } 0249 } // for 0250 0251 // exit conditions: 0252 // a) if closeList contains "to" 0253 // b) we can't find "to" in closedList and openlist is empty 0254 // => no path exists 0255 int idx = indexOfNodeWithPos(to, closedList); 0256 if(idx != -1) 0257 { 0258 pathFound = true; 0259 // let's save last node in curNode variable 0260 curNode = closedList.at(idx); 0261 break; // while 0262 } 0263 else if(openList.isEmpty()) 0264 { 0265 pathFound = false; 0266 break; 0267 } 0268 } 0269 0270 if(pathFound) 0271 { 0272 // restoring path starting from last node: 0273 PathNode* node = curNode; 0274 while(node) 0275 { 0276 m_foundPath.prepend( node->pos ); 0277 node = node->parent; 0278 } 0279 } 0280 0281 // cleanup 0282 qDeleteAll( openList ); 0283 qDeleteAll( closedList ); 0284 } 0285 0286 void KLinesAnimator::startGameOverAnimation() 0287 { 0288 blockSignals(true); 0289 QList<BallItem*> balls; 0290 const QList<QGraphicsItem*> items = m_scene->items(); 0291 BallItem *ball=nullptr; 0292 for (QGraphicsItem* item : items) { 0293 ball = qgraphicsitem_cast<BallItem*>(item); 0294 if(ball) 0295 balls.append(ball); 0296 } 0297 animateRemove(balls); 0298 } 0299 0300 void KLinesAnimator::stopGameOverAnimation() 0301 { 0302 blockSignals(false); 0303 } 0304 0305 void KLinesAnimator::slotBornFinished() 0306 { 0307 for (BallItem* ball : std::as_const(m_bornBalls)) { 0308 ball->setColor(ball->color(), true); 0309 } 0310 Q_EMIT bornFinished(); 0311 } 0312 0313 #include "moc_animator.cpp"