dimitris kalamaras

mathematics, social network analysis, free software…

Qt (Μέρος IV): Φτιάξτε παιχνίδια

Στα τρία προηγούμενα μέρη αυτού του tutorial, μάθαμε τα βασικά κόλπα της Qt, και την αξιοποιήσαμε στη δημιουργία διαχειριστικών εργαλείων για βάσεις δεδομένων. Καλά όλα αυτά, αλλά πως χειριζόμαστε γραφικά με την Qt; Λοιπόν, δεν υπάρχει τίποτε πιο εύκολο από αυτό! Για να σας το αποδείξουμε, θα φτιάξουμε ένα μικρό παιχνίδι σκοποβολής, το MouseShooter.

Το παιχνίδι θα αποτελείται από ένα μοναδικό παράθυρο, πάνω στο οποίο θα τοποθετήσουμε δύο κουμπιά (για Εναρξη και Τερματισμό του παιχνιδιού) και από κάτω ένα καμβά, όπου θα εμφανίζονται τα γραφικά. Είναι πρωτόγονο, αλλά ο στόχος είναι να δούμε τις βασικές έννοιες, όχι να ξαναφτιάξουμε το …DOOM! Αυτή τη φορά, δεν θα χρησιμοποιήσουμε το QtDesigner για να σχεδιάσουμε κάποια φόρμα για το MouseShooter. Το παιχνίδι μας θα έχει ένα πολύ απλοικό interface, οπότε θα είναι εύκολο να του στήσουμε το περιβάλλον κατευθείαν από τον κώδικα – για να δείτε πως γίνεται κι αυτό…

Η ιδέα για το γραφικό θέμα του MouseShooter είναι απλή μέχρι αηδίας: τα ποντίκια έχουν πέσει πάνω στο τυρί, και εμείς τα πιάνουμε στα πράσα και τα …πυροβολούμε. Αυτά πανικοβάλλονται και αρχίζουν να τρέχουν αριστερά και δεξιά. Τα γραφικά, δηλαδή το spite του ποντικιού και το φόντο του τυριού, τα “δανειστήκαμε” από ένα απλοικό παράδειγμα της Qt. Εμείς όμως θα φτιάξουμε κάτι πολύ πιο σύνθετο…Ας ξεκινήσουμε λοιπόν!


Εικόνα 1: Το εκπληκτικό shooter μας σε όλο του το μεγαλείο! Τα ποντίκια δεν μένουν ακίνητα βέβαια…

Κλάσεις

Μπορεί να σκέφτεστε ήδη “πόσες κλάσεις θα έχει το παιχνίδι;” Αυτό είναι πολύ σημαντικό ερώτημα γιατί αυτή η απάντηση θα καθορίσει ποια θα είναι η δομή όλου του κώδικα μας. Ας το σκεφτούμε λογικά. Είπαμε, ότι θα φτιάξουμε: ένα μοναδικό παράθυρο (να η μία κλάση!), όπου θα τοποθετήσουμε δύο κουμπιά και έναν καμβά (ωπ, να και μια δεύτερη κλάση!), πάνω στον οποία θα εμφανίζονται να τρέχουν ποντίκια (να και η τρίτη κλάση!). Τα δύο κουμπιά είναι τετριμμένα widgets που μας προσφέρει ήδη η Qt, άρα δεν χρειάζεται να φτιάξουμε ξεχωριστή κλάση γι’ αυτά – απλά θα φτιάξουμε δύο αντικείμενα της QPushButton μέσα στο παράθυρο. Επομένως, καταλήγουμε στο συμπέρασμα ότι το πρόγραμμα θα αποτελείται από τρεις κλάσεις: μία για το κεντρικό παράθυρο, μία για τον καμβά και μία για τα ποντίκια μας. Με απλά λόγια, το κόλπο για να καταλαβαίνουμε πόσες κλάσεις χρειαζόμαστε είναι να εντοπίζουμε τις ξεχωριστές “οντότητες” ή “ενότητες” που θα υπάρχουν στο πρόγραμμα που θέλουμε να φτιάξουμε. Εμείς έχουμε τρεις “οντότητες”: παράθυρο, καμβάς και ποντίκι. Ωραία, λοιπόν, θα ονομάσουμε την πρώτη Window, την δεύτερη Canvas και την τρίτη Mouse. Θα χρειαστούμε προφανώς και την main, αλλά μετά από τρία tutorials θα έχετε καταλάβει ότι αυτή είναι το πιο απλό πράγμα.

Η κλάση Window θα μας δίνει το βασικό αντικείμενο της εφαρμογής, ένα απλό παράθυρο που θα προβάλλουμε μέσα από την main(). Αν θυμάστε, στα δύο προηγούμενα τεύχη χρησιμοποιήσαμε την κλάση QMainWindow για να παράγουμε το βασικό παράθυρο των εφαρμογών μας. Αυτό το κάναμε γιατί θέλαμε μενού, toolbars, κλπ. Δεν χρειαζόμαστε τίποτε από αυτά όμως πια, γι’ αυτό θα προτιμήσουμε να βασίσουμε την νέα κλάση Window πάνω στην QWidget της Qt. Η QWidget δίνει ένα κενό widget (ένα παράθυρο δηλαδή) και η Window θα κληρονομήσει όλες τις δυνατότητές της. Μέσα στην υποκλάση Window θα προσθέσουμε τα κουμπιά και τον καμβά (που θα δούμε παρακάτω). Να το header της (σώστε το σε ένα αρχείο με όνομα window.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef WINDOW_H 
#define WINDOW_H 
 
#include <QWidget> 
#include <QPushButton>
#include “canvas.h”; 
 
class Window : public QWidget { 
     Q_OBJECT 
public: 
  Window(); 
private: 
  QPushButton *startBt, *exitBt; 
  Canvas *myCanvas; 
}; 
 
 #endif

Δεν είναι κάτι το ιδιαίτερο, έτσι; Στις δύο πρώτες γραμμές ορίζουμε το νέο header (για να ξέρει ο compiler τι έχει αυτό το αρχείο!), ύστερα εισάγουμε την QWidget, την QPushButton, και την Canvas (που δεν έχουμε δει ακόμα). Από την 8η γραμμή και μετά δηλώνουμε την Window. Ουσιαστικά λέμε: “θέλουμε να φτιάξουμε μια νέα κλάση Window, που να κληρονομήσει τη QWidget, και να έχει μία δημόσια προσβάσιμη μέθοδο (την ομώνυμη constructor της) και τρία ιδιωτικά μέλη (δύο κουμπιά και έναν καμβά με όνομα myCanvas”.

Ουσιαστικά, η μόνη μέθοδος της Window που πρέπει να υλοποιήσουμε είναι η constructor. Να ο κώδικάς της (αποθηκεύσετε τον ως window.cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "window.h" 
 
Window::Window() { 
  startBt= new QPushButton("Start");	 
  exitBt=new QPushButton("Exit");	 
  myCanvas=new Canvas(); 
  QVBoxLayout *layout = new QVBoxLayout; 
  layout->addWidget(startBt); 
  layout->addWidget(exitBt); 
  layout->addWidget(myCanvas); 
  setLayout(layout); 
  connect (exitBt,SIGNAL(clicked()), qApp, SLOT(quit())); 
  connect (startBt,SIGNAL(clicked()), myCanvas, SLOT(runMiceRun())); 
  setWindowTitle("RatShooter"); 
}

Εδώ έχουμε μερικά καινούρια πράγματα. Στις πρώτες δύο γραμμές δημιουργούμε δύο νέα αντικείμενα QPushButton, τα startBt και exitBt, δηλαδή δύο κουμπιά με ετικέτες “Start” και “Exit”. Έπειτα δημιουργούμε και το αντικείμενο myCanvas της κλάσης Canvas (που ακόμα δεν έχουμε δει!).

Στη συνέχεια του κώδικα (7η γραμμή) έχουμε το QVBoxLayout. Όπως είπαμε σε αυτό το project θα φτιάξουμε το interface με κώδικα, αντί με το QtDesigner. Γι’ αυτό, δημιουργούμε το αντικείμενο layout της QVBoxLayout, το οποίo ουσιαστικά θα μας δώσει μια κατακόρυφη διαρρύθμιση των κουμπιών και του καμβά. Στις επόμενες τρεις γραμμές, προσθέτουμε τα δύο κουμπιά και το αντικείμενο του καμβά στο layout (ουσιαστικά τα ομαδοποιούμε, αν θυμάστε στο QtDesigner) και έπειτα ορίζουμε με την setLayout τη συγκεκριμένα διαρρύθμιση σε όλο το αντικείμενο Window. Στις τελευταίες γραμμές κάνουμε τις συνδέσεις μας με το σύστημα Signal/Slot του Qt:

  • συνδέουμε το σήμα clicked() του κουμπιού exitBt με τη συνάρτηση-slot quit() όλης της εφαρμογής. Δηλαδή, όταν ο χρήστης κάνει κλικ εκεί, η εφαρμογή θα κλείνει.
  • συνδέουμε το σήμα clicked() του κουμπιού startBt με τη συνάρτηση slot runMiceRun() της κλάσης Canvas. Δηλαδή, με κλικ στο κουμπί Start, το παιχνίδι θα ξεκινά και τα ποντίκια θα τρέχουν. Παρακάτω στην υλοποίηση της Canvas, θα δούμε το πως ακριβώς θα κινούνται.

Στην τελευταία γραμμή, πριν το άγκιστρο, καθορίζουμε τον τίτλο που θέλουμε να έχει το παράθυρο. Αυτό γίνεται με την setWindowTitle().

Χειρισμός γραφικών

Περνάμε τώρα στο ζουμί, δηλαδή το καμβά όπου θα εμφανίζονται τα γραφικά (φόντο, spites, κλπ). Πριν δείτε τον κώδικα, μια μικρή εισαγωγή. Τα γραφικά στην Qt δημιουργούνται από το Graphics View. Πρόκειται για ένα πλαίσιο εργασίας που ακολουθεί τη λογική του Model/View. Δηλαδή υπάρχει ένα εσωτερικό μοντέλο, όπου δημιουργούμε και χειριζόμαστε όλα τα γραφικά αντικείμενα (π.χ. τα spites των ποντικιών), και μια προβολή του μοντέλου σε ένα widget. Για να μας διευκολύνουν να το καταλάβουμε, οι developers της Qt έχουν ονομάζουν το μοντέλο “σκηνή” (scene) και η αντίστοιχη κλάση που είναι υπεύθυνη για τον χειρισμό όλων των γραφικών αντικειμένων λέγεται QGraphicsScene. Αντίστοιχα, η προβολή της σκηνής σε εμάς γίνεται με μια “θέα” (view) που μας παρέχει η κλάση QGraphicsView.

Αν δυσκολεύεστε να το καταλάβετε, σκεφτείτε το ως εξής: η QGraphicsScene μας δίνει μια πραγματική σκηνή (με 2 ή 3 διαστάσεις) μέσα στην οποία τοποθετούμε τα αντικείμενα που θέλουμε δίνοντάς τους συντεταγμένες (π.χ. x και y). Η QGraphicsView δημιουργεί ένα widget που λειτουργεί ως καμβάς πάνω στον οποίο σχεδιάζονται τα περιεχόμενα της σκηνής. Για να παίξουμε με γραφικά στην Qt, πρέπει πάντα να δημιουργούμε μια subclass της QGraphicsView και μέσα σε αυτήν να ορίζουμε μια σκηνή, στην οποία θα εισάγουμε όλα τα γραφικά αντικείμενα. Αυτό ακριβώς είναι η κλάση Canvas που ορίζουμε με το παρακάτω header (σώστε το ως canvas.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef CANVAS_H 
#define CANVAS_H 
 
#include <QtGui/QGraphicsView> 
 
class Canvas : public QGraphicsView { 
  Q_OBJECT 
public: 
  Canvas(); 
public slots: 
  void runMiceRun(); 
  void showScore(); 
}; 
 
#endif

Αν έχετε παρακολουθήσει προσεκτικά ότι είπαμε πριν, δεν θα πρέπει να έχετε πρόβλημα να καταλάβετε αυτό το API. Από την 6η γραμμή και μετά, ορίζουμε την κλάση Canvas να κληρονομεί την QGraphicsView (άρα και όλες τις ιδιότητές της) και με το keyword Q_OBJECT δηλώνουμε ότι η συγκεκριμένα κλάση θα έχει, ανάμεσα σε άλλα, το δικαίωμα να χρησιμοποιεί το μηχανισμό Signal/Slot της Qt. Έπειτα δηλώνουμε την constructror μέθοδο της κλάσης και στη συνέχεια δηλώνουμε ότι η κλάση μας θα έχει δύο δημόσιες συναρτήσεις-slots: την runMiceRun και την showScore(). Με την πρώτη θα ξεκινά το παιχνίδι, ενώ με τη δεύτερη θα εμφανίζουμε στο καμβά ένα μικρό κείμενο με το σκορ του παίκτη! Η runMiceRun, όπως είπαμε, θα καλείται από το κουμπί startBt της Window, ενώ η showScore() θα πυροδοτείται κάθε φορά που πετυχαίνουμε ένα ποντίκι (προκειμένου να ενημερώνει το σκορ!).

Οι υλοποιήσεις αυτών των μεθόδων δεν είναι δύσκολες. Να η αρχή του canvas.cpp με την constructor μέθοδο:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <QtGui/QGraphicsView> 
#include <QGraphicsScene> 
#include "canvas.h" 
#include <math.h> 
#include "mouse.h"
static const int MouseCount = 7; 
 
Canvas::Canvas() { 
  QGraphicsScene *scene = new QGraphicsScene(this); 
  scene->setItemIndexMethod(QGraphicsScene::NoIndex); 
  scene->setSceneRect(-200,-200, 400, 400); 
 
  setScene(scene); 
  setRenderHint(QPainter::Antialiasing); 
  setBackgroundBrush(QPixmap(":/images/cheese.jpg")); 
 
  for (int i = 0; i < MouseCount; ++i) { 
    Mouse *mouse = new Mouse; 
    mouse->setPos(-200+qrand() % 400, -200+qrand() % 400); 
    scene->addItem(mouse); 
    connect(mouse, SIGNAL(destroyed()), this, SLOT(showScore())); 
  } 
  setMinimumSize(600, 600); 
}

Στις πρώτες γραμμές, έχουμε τα αναγκαία includes για να εισάγουμε τα υπόλοιπα headers που θα χρειαστούμε. Μετά ορίζουμε μια ακέραια σταθερά MouseCount με τιμή 7 και στη συνέχεια ξεκινά ο ορισμός της Canvas(). Εκεί, αρχικά, δημιουργούμε το αντικείμενο scene της σκηνής και του ορίζουμε κάποιες ιδιότητες. Η σημαντικότερη είναι η setSceneRect, η οποία ορίζει την έκταση της σκηνής σε pixels. Να θυμάστε ότι το σημείο (0, 0) στη σκηνή είναι το κέντρο της. Άρα το -200, -200 είναι πάνω αριστερά και το 400, 400 είναι κάτω δεξιά. Στις επόμενες τρεις γραμμές, αναφερόμαστε στον καμβά και ορίζουμε ότι το αντικείμενο scene θα είναι η βασική σκηνή του ενώ κάνουμε και τα πρώτα μας μαγικά: ζητάμε antialising γραφικά και βάζουμε την εικόνα cheese.jpg να είναι το φόντο όλου του καμβά.

Έπειτα, υπάρχει μια επανάληψη for. Με αυτήν δημιουργούμε επτά αντικείμενα της κλάσης Mouse (που θα δούμε σε λίγο), δηλαδή όσα λέει η μεταβλητή MouseCount. Με την setPos καθορίζουμε την αρχική θέση (x, y) κάθε ποντικιού. Αυτό γίνεται με την γεννήτρια τυχαίων αριθμών qrand(). Το qrand()%400 σημαίνει “δώσε μου έναν αριθμό μέχρι το 400”. Ουσιαστικά, θέλουμε κάθε ποντίκι να μπορεί να βρίσκεται από το (-200, -200) έως το (200, 200). Τέλος, προσθέτουμε τον κάθε ποντικό στη σκηνή με την addItem και συνδέουμε το signal Destroyed() του με την showScore() του καμβά. Δηλαδή, κάθε φορά που θα χτυπάμε ένα ποντίκι, αυτό πριν εξαφανιστεί θα εκπέμπει το σινιάλο Destroyed() το οποίο θα ενεργοποιεί την showScore() για να ανανεωθεί το σκορ. Και όλα αυτά αυτόματα! Στην τελευταία γραμμή ορίζουμε το μέγεθος που θέλουμε να έχει ο καμβάς. Προσέξτε: ο καμβάς μπορεί να είναι μικρότερος ή μεγαλύτερος από την έκταση της σκηνής. Εμείς εδώ επιλέξαμε το (600, 600) ώστε να έχει ακριβώς το ίδιο μέγεθος με τη σκηνή!

Το σκορ

Περνάμε στην showScore(). Με αυτήν θα τυπώνουμε στο καμβά ένα μικρό κείμενο με το σκορ. Σημειώστε ότι κάθε κείμενο σε μια σκηνή γραφικών της Qt είναι πάντα ένα αντικείμενο της κλάσης QGraphicsTextItem. Η δουλειά της showScore είναι σχετικά απλή: θα διαγράφει το υπάρχον κείμενο με το παλιό σκορ από τον καμβά και μετά θα προσθέτει το νέο σκορ. Να ο κώδικας:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Canvas::showScore(){ 
  QList<QGraphicsTextItem *> texts; 
  foreach (QGraphicsItem *item, scene()->items()) { 
    if (QGraphicsTextItem *text = qgraphicsitem_cast<QGraphicsTextItem *>(item)) 
     texts << text; 
  } 
  foreach (QGraphicsTextItem *text, texts) 
    scene()->removeItem(text); 
  QString message("Shoot' em up! \nRats left: "); 
  message +=QString::number(scene()->items().count()); 
  QFont font; 
  font.setBold(true); 
  font.setPointSize(14); 
  QGraphicsTextItem *text; 
  text=scene()->addText(message, font); 
  text->setPos(-300,250); 
}

Πρώτα δημιουργούμε μια λίστα texts στην οποία θα κρατάμε όλα τα αντικείμενα κειμένου του καμβά. Για να τα ξεχωρίσουμε από τα sprites των…τρωκτικών, κάνουμε μια επανάληψη foreach σε όλα τα αντικείμενα της σκηνής και με μια μαγική διαδικασία της Qt που λέγεται casting ελέγχουμε αν το εκάστοτε αντικείμενο είναι QGraphicsTextItem ή όχι. Αν είναι, το περνάμε στην λίστα texts. Διαφορετικά, το αφήνουμε ήσυχο. Μόλις μαζέψουμε όλα τα παλιά κείμενα, τα αφαιρούμε από τη σκηνή (άρα και από τον καμβά) μέσω της scene()->removeItem. Στη συνέχεια, δημιουργούμε ένα νέο QString message με το κείμενο που θέλουμε, και το συνενώνουμε με τον αριθμό των υπόλοιπων αντικειμένων που υπάρχουν εκείνη τη στιγμή στην σκηνή (όπου βρίσκονται μόνο τα ποντίκια γιατί σβήσαμε το αντικείμενο με το σκορ). Έπειτα, δημιουργούμε ένα νέο QGraphicsText αντικείμενο (text), και το προσθέτουμε στην σκηνή με την addText όπου περνάμε το message και την επιθυμητή μορφή (font) της γραμματοσειράς του. Τέλος, με την setPos ορίζουμε τη θέση του νέου σκορ να είναι η κάτω αριστερή γωνία της σκηνής (και άρα του καμβά).

Το μόνο που μένει για να τελειώσουμε με τον καμβά μας είναι η runMiceRun(). Όπως είπαμε, αυτή θα καλείται με κλικ στο κουμπί startBt που ορίσαμε στην κλάση Window, για να ξεκινήσει το παιχνίδι. Η δουλειά της είναι να μαζεύει σε μια λίστα όλα τα spites των γραφικών, να προβάλλει το σκορ και, για κάθε ένα spite, να πυροδοτεί ένα χρονόμετρο το οποίο θα παράγει ‘γεγονότα’ ανά απειροελάχιστα χρονικά διαστήματα. Σε κάθε τέτοιο γεγονός, όπως θα δούμε στην κλάση Mouse, το κάθε ποντίκι θα υπολογίζει αυτόματα τη νέα θέση του. Να ο κώδικας:

1
2
3
4
5
6
7
8
9
10
void Canvas::runMiceRun(){ 
  QList<Mouse *> mice; 
  foreach (QGraphicsItem *item, scene()->items()) { 
    if (Mouse *mouse= qgraphicsitem_cast<Mouse *>(item)) 
      mice<< mouse; 
    } 
  showScore(); 
  foreach (Mouse *mouse, mice) 
    mouse->startTimer(1000 / 33); 
}

Δείτε το προσεκτικά. Οι πρώτες 5 γραμμές έχουν την λογική που είδαμε στην showScore(). Δημιουργούμε μια λίστα mice που θα αποθηκεύει δείκτες προς τα αντικείμενα της κλάσης Mouse. Μετά κάνουμε μια λούπα σε όλα τα αντικείμενα της σκηνής μας και, μέσω της μαγικής qgraphicsitem_cast κάνουμε cast κάθε αντικείμενο item σε ένα Mouse * για να ελέγξουμε αν το εν λόγω αντικείμενο είναι Mouse ή όχι. Αν είναι το προσθέτουμε στη λίστα. Μόλις τελειώσει η επανάληψη η λίστα mice περιέχει όλα τα αντικείμενα-ποντίκια που βρίσκονται στη σκηνή. Έπειτα, καλούμε την show Score για να προβάλλουμε το σκορ στην οθόνη και, τέλος, έχουμε μια ακόμα foreach λούπα. Με αυτήν καλούμε την μέθοδο StartTimer() κάθε αντικειμένου mouse, με την οποία του λέμε να εκπέμπει ένα ‘γεγονός’ QTimerEvent κάθε 1000/33 χιλιοστά του δευτερολέπτου. Μόλις το εκπέμπει, το παιχνίδι θα του δίνει το δικαίωμα να τρέξει την αντίστοιχη timerEvent() μέθοδό του για να υπολογίσει τη νέα θέση του. Ουσιαστικά, όλο το animation του παιχνιδιού γίνεται από αυτήν την εντολή. Προφανώς όσο μεγαλύτερο είναι το χρονικό διάστημα, τόσο πιο αργό το gameplay.

Τα τρωκτικά μας

Φτάσαμε στο τέλος σχεδόν. Το κακό είναι ότι δεν μας φτάνει ο χώρος. Ας τα δούμε εν τάχει. Η κλάση ορίζεται στο mouse.h ως εξής:

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
#ifndef MOUSE_H 
#define MOUSE_H 
 
#include <QGraphicsItem> 
#include <QObject> 
 
class QGraphicsSceneMouseEvent; 
 
class Mouse : public QObject, public QGraphicsItem { 
  Q_OBJECT 
public: 
  Mouse(); 
  QRectF boundingRect() const; 
  void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); 
signals: 
  void destroyed(); 
protected: 
  void timerEvent(QTimerEvent *event); 
  void mousePressEvent(QGraphicsSceneMouseEvent *event); 
private: 
  qreal angle; 
  qreal speed; 
}; 
 
#endif

Εδώ τα σημαντικά στοιχεία είναι τρία. Πρώτον, η κλάση παράγεται από δύο κλάσεις: από την QGraphicsItem (για τα γραφικά) και την QObject (για να μπορούμε να χρησιμοποιήσουμε την μέθοδο timerEvent που της ανήκει). Δεύτερον, δηλώνουμε τις μεθόδους boundingRect και paint. Στην πρώτη θα ορίσουμε ένα τετράπλευρο που θα περικλείει όλο το spite ενώ στη δεύτερη θα κάνουμε την ίδια τη σχεδίαση του (πάνω στο τετράπλευρο). Τρίτον, επαναυλοποιούμε τις protected μεθόδους timerEvent και mousePressEvent. Στην timerEvent, όπως είπαμε, κάθε sprite θα υπολογίζει τη νέα (τυχαία) θέση του, ενώ η mousePressEvent θα ενεργοποιείται κάθε φορά που ο χρήστης κάνει κλικ σε ένα ποντίκι και θα το καταστρέφει. Να οι υλοποιήσεις:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "mouse.h" 
#include <QGraphicsScene> 
#include <QGraphicsSceneMouseEvent> 
#include <QPainter> 
#include <QStyleOption> 
#include <math.h> 
#include <QtGlobal> 
#include <QApplication> 
static const double Pi = 3.14159265; 
 
Mouse::Mouse()  { 
  rotate(qrand() % (360 * 16)); 
  QImage image(":/images/cursor.png"); 
  setCursor(QCursor(QPixmap::fromImage(image))); 
}

Στην constructor της κλάσης, κάνουμε δύο μαγικά. Πρώτα το περιστρέφουμε κατά έναν τυχαίο αριθμό (ώστε να μην δείχνουν όλα προς τα πάνω αρχικά!) και έπειτα φορτώνουμε μια εικόνα PNG (ένα στόχαστρο είναι) με την οποία αντικαταθιστούμε τον δείκτη του ποντικιού, όταν αυτός βρίσκεται πάνω στο ποντίκι.

1
2
3
QRectF Mouse::boundingRect() const { 
  return QRectF(-20, -22, 100 , 90 ); 
}

Εδώ ορίζουμε και επιστρέφουμε το bounding rectangle. Δεν είναι κάτι ιδιαίτερο, απλά θέλει προσοχή ώστε να είναι μεγαλύτερο από το spite!

1
2
3
4
5
6
7
8
void Mouse::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) { 
  QImage image(":/images/rat.png"); 
  painter->drawPixmap(QPointF(0,0), QPixmap::fromImage(image)); 
  if (pos().x() <= -250) setPos ( 0,0); 
  if (pos().x() >= 300) setPos ( 0,0); 
  if (pos().y() <= -250) setPos ( 0,0); 
  if (pos().y() >= 300) setPos ( 0,0); 
}

Στην paint γίνεται όλη η σχεδίαση. Φορτώνουμε μια εικόνα PNG που έχουμε επιλέξει σε ένα QImage και μετά μέσω του αντικειμένου painter της QPainter (που κάνει όλη τη σχεδίαση στην Qt) σχεδιάζουμε ένα pixmap στον οποίο έχουμε φορτώσει αυτόματα την εικόνα. Επίσης, ελέγχουμε αν το spite βρίσκεται έξω από τα όρια της σκηνής, και αν συμβαίνει αυτό, το επιστρέφουμε στο κέντρο.

1
2
3
4
5
6
7
void Mouse::timerEvent(QTimerEvent *) { 
  speed += (-50 + qrand() % 100) / 100.0; 
  angle = (qrand() % 100) / 500.0; 
  qreal dx = ::sin(angle) * 10; 
  rotate(dx); 
  setPos(mapToParent(0, -(3 + sin(speed) * 3))); 
}

Αυτή η μέθοδος καλείται αυτόματα για κάθε sprite, κάθε 1000/33 χλτ του δευτερολέπτου, όπως ορίσαμε πρωτύτερα στην runMiceRun(). H δουλειά της, παρά τους πολύπλοκους υπολογισμούς, είναι απλή: βρίσκει τυχαία μια νέα κατεύθυνση και μια νέα ταχύτητα για το spiter, το περιστρέφει με την rotate και του αλλάζει θέση με τη setPos, έτσι ώστε να ποντίκια να έχουν φυσιολογική κίνηση!

1
2
3
4
5
void Mouse::mousePressEvent(QGraphicsSceneMouseEvent *event) {  
    scene()->removeItem(this); 
    emit destroyed(); 
    return; 
}

Εδώ πια, λέμε στο ποντίκι ότι αν του κάνουν κλικ, να πει στη σκηνή να το διαγράψει, να στείλει το signal destroyed (ώστε να ενημερωθεί το σκορ) και τέλος να επιστρέψει. Κάπου εδώ τελειώσαμε! Λόγω χώρου, δεν προλάβαμε να δούμε την Main() και το αρχείο mouseshooter.pro. Βρίσκονται όμως και τα δυο (μαζί με όλο τον κώδικα) στο συμπιεσμένο αρχείο ratshooter.tar.gz. Κατεβάστε τον κώδικα, κάντε compile (με qmake-qt4, make) και τρέξτε το! Αν έχετε απορίες, στείλτε μου email!

TIP

Προσέξτε τη διαφορά: είναι άλλο πράγμα το scene και άλλο το scene(). Το scene είναι το αντικείμενο της QGraphicsScene που δημιουργήσαμε στην contructor της κλάσης Canvas. Επειδή το ορίζουμε μόνο εκεί, είναι τοπική μεταβλητή. Αντίθετα, το scene() είναι μια μέθοδος της Canvas (που την έχει κληρονομήσει από την QGraphicsView) και η οποία επιστρέφει ένα δείκτη στην τρέχουσα σκηνή του καμβά. Ουσιαστικά, όταν θέλουμε να δουλεύουμε με τα αντικείμενα της σκηνής μας μέσα από μια οποιαδήποτε συνάρτησης της Canvas, πρέπει να χρησιμοποιούμε την scene(). Έτσι, διαγράφουμε ένα αντικείμενο δίνοντας: scene()->removeItem (item);

Κάντε περισσότερα!

Το MouseShooter παίζεται άνετα όπως είναι αλλά του λείπουν μερικά στοιχεία. Για παράδειγμα, μπορείτε να του βάλετε ένα κουμπί για “New Game” , να του προσθέσετε ήχο κάθε φορά που χτυπάτε τους στόχους, αλλά και να βελτιώσετε τη συμπεριφορά τους. Τώρα κινούνται εντελώς τυχαία και πέφτουν το ένα πάνω στο άλλο, αλλά αυτό αλλάζει εύκολα αν έχετε λίγο χρόνο. Και φυσικά, στη θέση των ποντικιών μπορείτε να βάλετε οποιονδήποτε, όπως φαίνεται στην εικ. 2 🙂


Εικόνα 2: Με μια δύο τροποποιήσεις, μπορείτε να πυροβολείτε οτιδήποτε στη θέση των ποντικιών: πολιτικούς, τη φίλη σας, κοκ 🙂

Previous

Samba_Τκ: Tcl/tk script for remote samba shares mounting

Next

SocNetV 0.51: changes, new logo, and RPM packages

3 Comments

  1. Nikos

    Hxous mporeis na xrisimopoihseis me tin Qt4?? dld na akous tous pyrovolismous k etsi!!

  2. Φυσικά μπορείς να προσθέσεις ήχους. Υπάρχει γι’ αυτό το λόγο η κλάση QSound, που χρησιμοποιεί WAV αρχεία. Δες περισσότερα εδώ:

    http://doc.trolltech.com/4.4/qsound.html

  3. Nikos

    Hallo!!
    Wraio project alla to ekana compile kai mou evgale 31 errors??
    Giati??
    To dokimasa me VS2008 kai Qtcreator alla eixa to idio apotelesma !!!
    thnx

Leave a Reply

Creative Commons License
Licensed under a Creative Commons Attribution-ShareAlike 4.0 International License - Powered by Linux