PDA

View Full Version : Need Help Regarding Qt GraphicsItem Collision



RE
27th September 2012, 16:34
Hi. So I am a complete beginner with Qt. I am tasked to make a Plant vs. Zombie esque game from my professor and I am at complete loss about the collision detection.

I have been doing Google searches everywhere regarding the use of Qt Timeline, GraphicsItem, GraphicsScene for collision handling and I gave up.

A short description about my game: There are three sprites on the playing field: Towers, Cannonballs and Mobs. Towers are placed by player to guard the left side of the screen and mobs appear from the right side of the screen. Towers constantly fire cannonballs every few seconds.



cannonball::cannonball()
{
QGraphicsItemAnimation *animation = new QGraphicsItemAnimation();
QTimeLine * timer = new QTimeLine(4000);

animation->setItem(this);
animation->setTimeLine(timer);

timer->setLoopCount(0);

for (int i = this->pos().x(); i < 640; ++i)
{
animation->setPosAt(i / 640.0, QPointF(i, this->pos().y() ));
if (hits_enemy())
timer->setCurrentTime(0);
}

timer->start();
}



bool cannonball::hits_enemy()
{
for (int i=0; i<this->collidingItems().count(); i++)
if (qgraphicsitem_cast<mobs*>(this->collidingItems()[i]) ) return true;

return false;
}

Basically, those are some codes from the cannonball class.

I would like the cannonball upon collision to a mob sprite to disappear, damage the mob and fire again once the cooldown cleared.

I am pretty desperate right now as I have been looking for similar solutions for over 6 hours with no solid result. Please help! :(

d_stranz
27th September 2012, 19:58
You probably won't want to do this:



bool cannonball::hits_enemy()
{
for (int i=0; i<this->collidingItems().count(); i++)
if (qgraphicsitem_cast<mobs*>(this->collidingItems()[i]) ) return true;

return false;
}


because you'll be retrieving the "collidingItems" collection *twice* for every pass through the loop. Retrieve it *once* before the loop starts and assign it to some temporary variable, then use that variable to get the count, etc.

What do you want to happen when the cannonball hit detection returns true? The cannonball goes away? Then you simply need to remove the cannonball instance from the scene. QGraphicsScene::removeItem()



for (int i = this->pos().x(); i < 640; ++i)
{
animation->setPosAt(i / 640.0, QPointF(i, this->pos().y() ));
if (hits_enemy())
timer->setCurrentTime(0);
}


And why is this code in the cannonball constructor? The instance isn't even in the scene at this point, so how can it collide with anything?

You need to set up a timer in your main window (or in your QGraphicsView, if you have derived a custom one) that fires a couple of times per second so the animation looks smooth. In the slot that handles each time step timeout, you retrieve all the cannonball instances from the scene, and *then* do the hit detection on them.



// pseudocode:

for each cannonball
{
QList<QGraphicsItem *> colliders = scene.collidingItems( cannonball );
if ( colliders is not empty && colliders contains a mob )
add cannonball to list of items to be deleted
}

for each cannonball on the deletion list
{
scene.removeItem( cannonball )
delete cannonball;
}


You might also consider putting a timer inside each cannonball; at each timeout, it updates the cannonball's position by moving it further along its trajectory. If you derive your cannonballs from QGraphicsObject, then you can handle the x/y/zChanged() signals from each cannonball and do the collision detection there instead of in a slot driven by a master clock. Use QObject::sender() to determine which cannonball sent the signal, or use QSignalMapper to associate the cannonball instance.

RE
27th September 2012, 20:24
You probably won't want to do this because you'll be retrieving the "collidingItems" collection *twice* for every pass through the loop. Retrieve it *once* before the loop starts and assign it to some temporary variable, then use that variable to get the count, etc.

What do you want to happen when the cannonball hit detection returns true? The cannonball goes away? Then you simply need to remove the cannonball instance from the scene. QGraphicsScene::removeItem()



for (int i = this->pos().x(); i < 640; ++i)
{
animation->setPosAt(i / 640.0, QPointF(i, this->pos().y() ));
if (hits_enemy())
timer->setCurrentTime(0);
}


And why is this code in the cannonball constructor? The instance isn't even in the scene at this point, so how can it collide with anything?

You need to set up a timer in your main window (or in your QGraphicsView, if you have derived a custom one) that fires a couple of times per second so the animation looks smooth. In the slot that handles each time step timeout, you retrieve all the cannonball instances from the scene, and *then* do the hit detection on them.



// pseudocode:

for each cannonball
{
QList<QGraphicsItem *> colliders = scene.collidingItems( cannonball );
if ( colliders is not empty && colliders contains a mob )
add cannonball to list of items to be deleted
}

for each cannonball on the deletion list
{
scene.removeItem( cannonball )
delete cannonball;
}


You might also consider putting a timer inside each cannonball; at each timeout, it updates the cannonball's position by moving it further along its trajectory. If you derive your cannonballs from QGraphicsObject, then you can handle the x/y/zChanged() signals from each cannonball and do the collision detection there instead of in a slot driven by a master clock. Use QObject::sender() to determine which cannonball sent the signal, or use QSignalMapper to associate the cannonball instance.

Hi, thanks for the reply.

I apologize if my code seems to be rather crude. The professor was like "You guys make this. Ask Google how to do it, use any C++ Library. I expect results in 2 days", Hence I am doing self-learning with limited time.

The original intent was if the cannonball detection returns true, The cannonball will be simply loop itself because I set timer->setLoopCount(0); for the TimeLine (unlimited loop). That is done via if (hits_enemy()) timer->setCurrentTime(0); code, although I seem to misunderstood the code runtime (Currently, I ended up putting that setCurrentTime and the loop detection code inside painter function because it seem that only that part of the code is being run over and over again.)

I never knew that you should set the timer in the main widget instead on individual classes. I'll take note on that. Also, I derive my cannonballs from QGraphicsItem instead of QGraphicsObject so I haven't touched on that.

I have another question, when I create my QGraphicsScene outside the main widget / as a global variable, (so that it can be accessed by these sprites for remote deletion / etc.) the program crashes. Is there any workaround on it?

Thanks again.

EDIT: Here are the codes so far. Currently I am having trouble about deleting the enemy whenever they reach 0 HP upon collision with the cannons because I honestly don't have an idea about how to notify the main widget that a collision has happened and delete the object from the scenes there.

d_stranz
28th September 2012, 03:48
I honestly don't have an idea about how to notify the main widget that a collision has happened and delete the object from the scenes there.

If you derive your cannonball object from QGraphicsObject instead of QGraphicsItem, then it can have slots and signals. So, if you do your collision detection inside the cannonball code, then have it emit a signal "uhOhIAmAGoner( cannonball * unlucky )" that can be handled by a slot in the main window, view, or scene. Since the signal passes itself (this), you don't have to resort to tricks to find out which cannonball hit the mob.



class cannonball : public QGraphicsObject
{
Q_OBJECT;

// ...

signals:
void uhOhIAmAGoner( cannonball * unlucky );

protected slots:
void hitEnemy();

protected:
QTimer timer;
};

cannonball::cannonball( QGraphicsItem * parent ) :
: QGraphicsObject( parent )
, timer( this )
{
timer.setInterval( 100 );

connect( &timer, SIGNAL( timeout() ), this, SLOT( hitEnemy() ) );
timer.start();
}

void cannonball::hitEnemy()
{
if ( collidedWithAMob() ) // You can figure this part out
{
timer.stop();
emit uhOhIAmAGoner( this );
}
else
updatePosition();
}

class MainWindow: public QMainWindow
{
// ...

public slots:
void killThatCannonball( cannonball * cball );

protected:
void addACannonball();

private:
QGraphicsScene mobScene;
};


void MainWindow::addACannonball()
{
cannonball * cBall = new cannonball();
// establish initial trajectory, etc.

mobScene.addItem ( cBall );

connect( cBall, SIGNAL( uhOhIAmAGoner( cannonball * ) ), this, SLOT( killThatCannonball( cannonball * ) ) );
}

void MainWindow::killThatCannonball( cannonball * cBall )
{
mobScene.removeItem ( cBall );
delete cBall;
}


Of course, none of this code has been compiled and certainly not tested. That's why your professor gave it to you as a homework project...

wysota
28th September 2012, 11:12
Collisions should be detected from within the scene. Then notifying anyone you want is easy since the scene is derived from QObject. Depending on what your architecture looks like, you might want to do this from within QGraphisScene::advance().