Difference between revisions of "Development/Tutorials/Games/KGLEngine2d"

Jump to: navigation, search
m (Text replace - "</code>" to "</syntaxhighlight>")
 
(3 intermediate revisions by 3 users not shown)
Line 23: Line 23:
  
 
==Abstract==
 
==Abstract==
KGLEngine2d is a recent try to develop a 2D game engine using openGL, and powerful enough to deliver complex and beautiful games.
+
TUTORIAL NEED TO BE UPDATE
 +
KGLEngine2d is a recent try to develop a 2D game engine using OpenGL, and powerful enough to deliver complex and beautiful games.
 
As of now, version 1.0 is not even out, but the API is stable enough for anyone to start playing with the engine.
 
As of now, version 1.0 is not even out, but the API is stable enough for anyone to start playing with the engine.
Through this tutorial I will explain how to use a great part of the engine, included but not limited to : sprites creation, rotation and deplacement and collision detection.
+
Through this tutorial I will explain how to use a great part of the engine, included but not limited to : sprites creation, rotation and move and collision detection.
  
We will develop a simple pong game through this example. anyone who have followed the KGLEngine2d developement a bit will have seen some kglPong screenshot, as this is the way new features are shown, using this simple game to showcase them. So this will also be the way to introduce new developpers to the technology :)
+
We will develop a simple pong game through this example. anyone who have followed the KGLEngine2d development a bit will have seen some kglPong screenshot, as this is the way new features are shown, using this simple game to showcase them. So this will also be the way to introduce new developers to the technology :)
  
Here is a screenshot of what this tutorial leads to :  
+
Here is a screenshot of what this tutorial leads to :
 
[[Image:result.png|400px]]
 
[[Image:result.png|400px]]
  
 
==Starting Point : the main.cpp==
 
==Starting Point : the main.cpp==
 
Every application needs one, so let's start with it, as it is really simple.
 
Every application needs one, so let's start with it, as it is really simple.
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
 
#include <KApplication>
 
#include <KApplication>
 
#include <KAboutData>
 
#include <KAboutData>
Line 44: Line 45:
 
{
 
{
  
KAboutData aboutData( "kglpong", 0,
+
    KAboutData aboutData( "kglpong", 0,
      ki18n("kglpong"), "0.1",
+
                  ki18n("kglpong"), "0.1",
      ki18n("Classic pong game using KGLEngine2d"),
+
                  ki18n("Classic pong game using KGLEngine2d"),
      KAboutData::License_GPL,
+
                  KAboutData::License_GPL,
 
                               ki18n("Copyright (c) 2008 Developer") );
 
                               ki18n("Copyright (c) 2008 Developer") );
  
KCmdLineArgs::init( argc, argv, &aboutData );
+
    KCmdLineArgs::init( argc, argv, &aboutData );
KCmdLineOptions options;
+
    KCmdLineOptions options;
KCmdLineArgs::addCmdLineOptions(options);
+
    KCmdLineArgs::addCmdLineOptions(options);
KApplication app;
+
    KApplication app;
  
pongEngine pong;
+
    pongEngine pong;
pong.show();
+
    pong.show();
  
return app.exec();
+
    return app.exec();
 
}
 
}
</code>
+
</syntaxhighlight>
So, what are we doing ?
+
So, what's happening here?
Initializing the KApplication, and creating an instance of our main class, the one that subclasses KGLEngine2d. Then we tell him to show himself, and that's all. I told you there was nothing exciting in here :)
+
We initialize the KApplication, and create an instance of our main class, the one that will subclass KGLEngine2d. Then we tell the engine to show itself, and that's all. I told you there was nothing really exciting there :)
  
 +
*TODO: this paragraph needs to be updated*
 
==KGLEngine2d type system==
 
==KGLEngine2d type system==
  
KGLEngine uses user-defined objects types to refine the collision detection system. The default type is the parent object's className. The possibility to define the object's type at construction time allows for simple items to be created, wothout the need to put them in a class.
+
KGLEngine uses user-defined objects types to refine the collision detection system. The default type is the parent object's className. The possibility to define the object's type at construction time allows for simple items to be created, without the need to put them in a class.
  
 
==The KGLEngine2d pong engine==
 
==The KGLEngine2d pong engine==
  
Now, as we want a bit of sophisticated features, we will have to subclass the KGLEngine2d.
+
Now, as we want some sophisticated features, we will have to subclass the KGLEngine2d.
  
Namely, those sophisticated actions, that forces us to subclass are the keyboard interactions. As of now, those are managed in the engine's mainLoop, and there are no alternatives to use them otherwise.
+
Mainly, those sophisticated actions that forces us to subclass are the keyboard interactions. As of now, they're managed in the engine's mainLoop(), and there is no other way to use them.
Everything else could have been done in the main, or anywhere else, using a simple slot and the engine power, but I will use the Object-Oriented conception in this tutorial.
+
Everything else could have been done in the main, or anywhere else, using a simple slot and the engine power, but this tutorial takes the object-oriented approach.
  
 +
<syntaxhighlight lang="cpp-qt">
 +
#include "pongengine.h"
 +
#include "ball.h"
 +
#include "racket.h"
  
<code cppqt>
+
#include <KGLEnhancedTextureManager>
setWidth(640);
+
#include "KGLPhysicsEngine>
setHeight(480);
+
#include <KGLTextItem>
 +
#include <KAction>
  
const double wallThickness = 10;
+
#include <QString>
KGLItem *NWall = new KGLItem(WALL_TYPE, QRectF(-width()/2, height()/2-wallThickness, width(), wallThickness));
+
#include <QColor>
NWall->setColor(Qt::red);
+
#include <QTime>
addGLItem(NWall);
+
 
 +
const QString BACKGROUND_TYPE = "00Background";
 +
 
 +
pongEngine::pongEngine(QWidget *parent)
 +
        : KGLEngine2d(QRectF(-400, -300, 800, 600), 20, parent),
 +
        m_collection(new KActionCollection(this)),
 +
        isStarted(false),
 +
        player1Score(0),
 +
        player2Score(0)
 +
{
 +
    setTextureManager(new KGLEnhancedTextureManager("./sprites/"));
  
KGLItem *SWall = new KGLItem(WALL_TYPE, QRectF(-width()/2, -height()/2, width(), wallThickness));
+
    const double wallThickness = 10;
SWall->setColor(Qt::red);
+
    KGLItem *NWall = new KGLItem(QRectF(-width() / 2, height() / 2 - wallThickness, width(), wallThickness));
addGLItem(SWall);
+
    NWall->setObjectName(WALL_TYPE);
 +
    addItem(NWall);
  
KGLItem *player1Goal = new KGLItem(GOAL_TYPE, QRectF(-width()/2, -height()/2, wallThickness, height()));
+
    KGLItem *SWall = new KGLItem(QRectF(-width() / 2, -height() / 2, width(), wallThickness));
player1Goal->setColor(Qt::black);
+
    SWall->setObjectName(WALL_TYPE);
addGLItem(player1Goal);
+
    addItem(SWall);
  
KGLItem *player2Goal = new KGLItem(GOAL_TYPE, QRectF(width()/2-wallThickness, -height()/2, wallThickness, height()));
+
    KGLItem *player1Goal = new KGLItem(QRectF(-width() / 2, -height() / 2, wallThickness, height()));
player2Goal->setColor(Qt::black);
+
    player1Goal->setObjectName(GOAL_TYPE);
addGLItem(player2Goal);
+
    addItem(player1Goal);
  
</code>
+
    KGLItem *player2Goal = new KGLItem(QRectF(width() / 2 - wallThickness, -height() / 2, wallThickness, height()));
 +
    player2Goal->setObjectName(GOAL_TYPE);
 +
    addItem(player2Goal);
 +
</syntaxhighlight>
  
 
So, what happens there ?
 
So, what happens there ?
Line 104: Line 125:
  
 
Then we create and add items to the engine, no more.
 
Then we create and add items to the engine, no more.
The KGLItem constructor can be called without any argument, but can also be given two : the item type, used for collision detection, and the dimensions of the item.
+
The most common way to create a KGLItem is by specifying it's shape using a QRectF.
Here, we change the items' dimensions according to what we want them to represent : The walls have to be wide but thin, same for the goals, except they're vertical and not horizontal.
+
Here, we change the items dimensions according to what we want them to represent: The walls have to be wide but thin, same for the goals, except they're vertical and not horizontal.
 
The first two items are there so the ball will bounce when it reaches the top or the bottom of the screen.
 
The first two items are there so the ball will bounce when it reaches the top or the bottom of the screen.
 
The third and fourth ones are the goals, so we can detect when the ball's gone past a player's racket. The goals are set to be black, this should not be done this way, they should be given an image instead, but, hey, this is a code example and i wanted to show you how to draw a simple colored rectangle.
 
The third and fourth ones are the goals, so we can detect when the ball's gone past a player's racket. The goals are set to be black, this should not be done this way, they should be given an image instead, but, hey, this is a code example and i wanted to show you how to draw a simple colored rectangle.
  
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
const double thickness = 0.05;
+
    player1Racket = new Racket();
const double wallDistance = 0.2;
+
    player1Racket->setPosition(-width() / 2, -wallThickness - 1);
const double height = 0.25;
+
    player1Racket->setZIndex(2);
 +
    addItem(player1Racket);
  
player1Racket = new Racket();
 
player1Racket->setPosition(-hwidth()+wallDistance, 0);
 
addGLItem(player1Racket);
 
  
player2Racket = new Racket();
+
    player2Racket = new Racket();
player2Racket->setPosition(hwidth()-wallDistance-player2Racket->width(), 0);
+
    player2Racket->rotate(M_PI);
addGLItem(player2Racket);
+
    player2Racket->setPosition(width() / 2 - player2Racket->width(), -wallThickness - 1);
 +
    player2Racket->setZIndex(2);
 +
    addItem(player2Racket);
  
ball = new Ball();
+
    ball = new Ball();
ball->setPosition(0, 0);
+
    ball->setPosition(0, 0);
connect(ball, SIGNAL(enterGoal(int)), this, SLOT(score(int)));
+
    ball->setZIndex(2);
addGLItem(ball);
+
    addItem(ball);
</code>
+
  
Now we create the players' rackets and the ball. These are special classes that will be explained a bit later. Sprites get their size directly from the image you give them as a parameter, so unlike KGLItem, you do not need to specify the size of a KGLSpriteitem. The POINT passed as argument (optionally) is the repeat pattern of the texture, eg POINT(1,2) means your texture will be drawn two times ihorizontally, in the rectanle defined by the image's size.
+
    connect(ball, SIGNAL(enterGoal(int)), this, SLOT(score(int)));
The picture below show how a sprite created with the default point (1,1) would like with a POINT(1,2) :  
+
</syntaxhighlight>
 +
 
 +
Here we create the rackets and the ball. These are special classes that will be explained a bit later. Sprites get their size directly from the image you give them as a parameter, so unlike KGLItem, you do not need to specify the size of a KGLSpriteitem. The QPointF passed as argument (optionally) is the repeat pattern of the texture, eg QpointF(1,2) means your texture will be drawn two times horizontally, in the rectanle defined by the image's size.
 +
The picture below shosw how a sprite created with the default QPointF(1,1) would like with a QPointF(1,2) :
  
 
[[image:bloc-example.png]]
 
[[image:bloc-example.png]]
  
  
They might have been simple KGLSprites, and the collisions would have been managed in another function connected to the KGLItem::colliding(PhysicsItem*) function. But, again, let's take the Object Oriented way.
+
They might have been simple KGLSprites, and the collisions would have been managed in another function connected to the KGLItem::colliding(PhysicsItem*) function. But, again, let's take the object oriented way.
We also connect the ball's enterGoal signal with the score slot, so when the ball enters a goal, the engine will be told, and will update the score.
+
We also set the items ZIndex which will determine the drawing orders of the items, the higher it is, the later they will be drawn.
 +
Finally, we connect() the ball's enterGoal signal with the score slot, so when the ball enters a goal, the engine will be notified, and will update the score.
  
Then let's define which keys we will be using:  
+
Now, let's define which keys we'll be using:
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
KAction *up = m_collection->addAction("p1_up");
+
    KAction *up = m_collection->addAction("p1_up");
up->setShortcut(Qt::Key_Z);
+
    up->setShortcut(Qt::Key_Z);
KAction *down = m_collection->addAction("p1_down");
+
    KAction *down = m_collection->addAction("p1_down");
down->setShortcut(Qt::Key_S);
+
    down->setShortcut(Qt::Key_S);
KAction *left = m_collection->addAction("p2_up");
+
    KAction *left = m_collection->addAction("p2_up");
left->setShortcut(Qt::Key_Up);
+
    left->setShortcut(Qt::Key_Up);
KAction *right = m_collection->addAction("p2_down");
+
    KAction *right = m_collection->addAction("p2_down");
right->setShortcut(Qt::Key_Down);
+
    right->setShortcut(Qt::Key_Down;
  
KAction *start = m_collection->addAction("start");
+
    KAction *start = m_collection->addAction("start");
start->setShortcut(Qt::Key_Space);
+
    start->setShortcut(Qt::Key_Space);
</code>
+
</syntaxhighlight>
 
We add every action we are going to use, using their names, and then we define their keys.
 
We add every action we are going to use, using their names, and then we define their keys.
  
Now, the collision detection and automatic moving items system :
+
Next is the collision detection and automatic moving of items system:
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
setItemTypeMovable(Racket::staticMetaObject.className());
+
    physicsEngine()->setItemTypeMovable(player1Racket);
setItemTypeMovable(Ball::staticMetaObject.className());
+
    physicsEngine()->setItemTypeMovable(ball);
  
addItemCollisionRule(Ball::staticMetaObject.className(), Racket::staticMetaObject.className());
+
    physicsEngine()->addCollisionRule(ball, player1Racket);
addItemCollisionRule(Ball::staticMetaObject.className(), WALL_TYPE);
+
    physicsEngine()->addCollisionRule(ball, NWall);
addItemCollisionRule(Ball::staticMetaObject.className(), GOAL_TYPE);
+
    physicsEngine()->addCollisionRule(ball, player1Goal);
  
addItemCollisionRule(Racket::staticMetaObject.className(), WALL_TYPE);
+
    physicsEngine()->addCollisionRule(player1Racket, NWall);
</code>
+
</syntaxhighlight>
  
 
First, we tell the engine to call the move() function of each and every item of type RACKET_TYPE and BALL_TYPE, at each call of the mainLoop.
 
First, we tell the engine to call the move() function of each and every item of type RACKET_TYPE and BALL_TYPE, at each call of the mainLoop.
Line 171: Line 195:
 
Then, we add collision detection rules. We say that an item of type BALL_TYPE can only collide with items of type RACKET_TYPE, WALL_TYPE, and GOAL_TYPE (which, in this case, is pretty much everything).
 
Then, we add collision detection rules. We say that an item of type BALL_TYPE can only collide with items of type RACKET_TYPE, WALL_TYPE, and GOAL_TYPE (which, in this case, is pretty much everything).
  
Then, we add the scoreBoards :  
+
Then, we add the scoreBoards :
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
player1ScoreBoard = new KGLTextItem("0");
+
    player1ScoreBoard = new KGLTextItem("0");
player1ScoreBoard->setPosition(200, hheight() - wallThickness - 60);
+
    player1ScoreBoard->setPosition(200, height() / 2 - wallThickness - 60);
player1ScoreBoard->font().setPointSize(60);
+
    player1ScoreBoard->font().setPointSize(60);
addGLItem(player1ScoreBoard);
+
    player1ScoreBoard->setZIndex(2);
 +
    addItem(player1ScoreBoard);
  
player2ScoreBoard = new KGLTextItem("0");
+
    player2ScoreBoard = new KGLTextItem("0");
player2ScoreBoard->setPosition(-200, hheight() - wallThickness - 60);
+
    player2ScoreBoard->setPosition(-200, height() / 2 - wallThickness - 60);
player2ScoreBoard->font().setPointSize(60);
+
    player2ScoreBoard->font().setPointSize(60);
addGLItem(player2ScoreBoard);
+
    player2ScoreBoard->setZIndex(2);
</code>
+
    addItem(player2ScoreBoard);
 +
</syntaxhighlight>
  
 
These are simple text items, displaying each player's score.
 
These are simple text items, displaying each player's score.
  
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
qsrand(QTime::currentTime().msec());
+
    qsrand(QTime::currentTime().msec());
lastScorer = (qrand() > RAND_MAX/2);
+
    lastScorer = (qrand() > RAND_MAX / 2);
</code>
+
  
Lastly, we define randomly who scored last, which defines the direction the ball will take (left or right).
+
    startGame();
 +
</syntaxhighlight>
 +
 
 +
Finally, we use a random value to determine who scored last, which defines the direction the ball will take (left or right), and we call startGame() which will repeatedly calls the mainLoop() function.
  
 
==The Ball and Racket Classes==
 
==The Ball and Racket Classes==
  
Let's take a look at the Ball and Racket headers :  
+
Let's take a look at the Ball and Racket headers :
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
 +
#ifndef KGL_BALL_H
 +
#define KGL_BALL_H
 +
 
 +
#include <KGLSpriteItem>
 +
 
 +
const QString WALL_TYPE = "01Wall";
 +
const QString GOAL_TYPE = "04Goal";
 +
 
 
class Ball : public KGLItem
 
class Ball : public KGLItem
 
{
 
{
Q_OBJECT
+
    Q_OBJECT
 
public:
 
public:
Ball();
+
    Ball();
void collidingWith(const SET_COLLIDABLE_ITEMS &);
+
    void collidingWith(KGLItemBase*);
 
signals:
 
signals:
void enterGoal(int);
+
    void enterGoal(int);
 
};
 
};
</code>
 
  
<code cppqt>
+
#endif //KGL_BALL_H
 +
</syntaxhighlight>
 +
 
 +
<syntaxhighlight lang="cpp-qt">
 +
#ifndef KGL_RACKET_H
 +
#define KGL_RACKET_H
 +
 
 +
#include <KGLSpriteItem>
 +
 
 
class Racket : public KGLItem
 
class Racket : public KGLItem
 
{
 
{
Q_OBJECT
+
    Q_OBJECT
 
public:
 
public:
Racket();
+
    Racket();
void collidingWith(const SET_COLLIDABLE_ITEMS &);
+
    void collidingWith(KGLItemBase*);
 
};
 
};
</code>
+
 
 +
#endif //KGL_RACKET_H
 +
</syntaxhighlight>
  
 
We just define KGLItems with nothing really special, but when we will change the ball and rackets from simple white squares to sprites, this will prove to be useful.
 
We just define KGLItems with nothing really special, but when we will change the ball and rackets from simple white squares to sprites, this will prove to be useful.
 
Now let's take a look at the constructors:
 
Now let's take a look at the constructors:
  
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
 
Ball::Ball()
 
Ball::Ball()
:KGLItem(Ball::staticMetaObject.className(),, RECT(0, 0, 10, 10))
+
        : KGLItem(QRectF(0, 0, 10, 10))
 
{
 
{
 
}
 
}
</code>
+
</syntaxhighlight>
  
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
Racket::Racket()
+
Ball::Ball()
:KGLItem(Racket::staticMetaObject.className(),, RECT(0, 0, 10, 30))
+
        : KGLItem(QRectF(0, 0, 10, 10))
 
{
 
{
 
}
 
}
</code>
+
</syntaxhighlight>
  
So we just define the width and height of the item,as we don't need anything more, yet.
+
So we just define the width and height of the item, as we don't need anything more yet.
  
The interesting thing right now is in the collidingWith function :  
+
The interesting thing right now is in the collidingWith function :
  
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
void Ball::collidingWith(const SET_COLLIDABLE_ITEMS &items)
+
void Ball::collidingWith(KGLItemBase *item)
 
{
 
{
 
+
    if (item->objectName() == WALL_TYPE) {
SET_COLLIDABLE_ITEMS::iterator it = items.begin();
+
        setSpeed(speed().x(), -speed().y());
while(it != items.end())
+
    }
{
+
    if (item->objectName() == Racket::staticMetaObject.className()) {
if((*it)->typeName() == WALL_TYPE)
+
        setSpeed(-speed().x(), speed().y());
setSpeed(POINT(speed().x(), -speed().y()));
+
    }
if((*it)->typeName() == RACKET_TYPE)
+
    if (item->objectName() == GOAL_TYPE) {
setSpeed(POINT(-speed().x(), speed().y()));
+
        emit(enterGoal((item->position().x() > 0) ? 1 : 2));
if((*it)->typeName() == GOAL_TYPE)
+
    }
emit(enterGoal(((*it)->position().x() > 0) ? 1 : 2));
+
++it;
+
}
+
 
}
 
}
</code>
+
</syntaxhighlight>
  
We iterate on the colliding items, and depending on their type, we act. If we are colliding with a wall, we invert our vertical speed. If we are colliding with a racket, we invert our horizontal speed. If we are colliding a goal, then let the engine know it.
+
We iterate on the colliding items, and depending on their type, we act. If we are colliding with a wall, we invert our vertical speed. If we are colliding with a racket, we invert our horizontal speed. If we are colliding with a goal, then we emit a signal to let the engine know it.
  
<code cppqt>
+
<syntaxhighlight lang="cpp-qt">
SET_COLLIDABLE_ITEMS::iterator it = items.begin();
+
void Racket::collidingWith(KGLItemBase* item)
while(it != items.end())
+
 
{
 
{
if((*it)->typeName() == WALL_TYPE)
+
    if (item->objectName() == WALL_TYPE) {
{
+
        moveBy(0, -speed().y());
moveBy(0, -speed().y());
+
        setSpeed(0, 0);
setSpeed(POINT(0, 0));
+
    }
}
+
 
}
 
}
</code>
+
</syntaxhighlight>
  
Here, we iterate again, but there is only one possible reaction : if we hit a wall, then stop moving (and move back to somewhere away from the wall).
+
Here, we iterate again, but there is only one possible reaction: if we hit a wall, then stop moving (and move back to somewhere away from the wall).
  
 
</noinclude>
 
</noinclude>

Latest revision as of 21:51, 29 June 2011

{{{KGLEngine2d tutorial}}}
Tutorial Series   KGLEngine2d developement
Prerequisites   None
What's Next   Nothing at the moment
Further Reading   KGLEngine2d's code

Contents

[edit] Abstract

TUTORIAL NEED TO BE UPDATE KGLEngine2d is a recent try to develop a 2D game engine using OpenGL, and powerful enough to deliver complex and beautiful games. As of now, version 1.0 is not even out, but the API is stable enough for anyone to start playing with the engine. Through this tutorial I will explain how to use a great part of the engine, included but not limited to : sprites creation, rotation and move and collision detection.

We will develop a simple pong game through this example. anyone who have followed the KGLEngine2d development a bit will have seen some kglPong screenshot, as this is the way new features are shown, using this simple game to showcase them. So this will also be the way to introduce new developers to the technology :)

Here is a screenshot of what this tutorial leads to : Result.png

[edit] Starting Point : the main.cpp

Every application needs one, so let's start with it, as it is really simple.

#include <KApplication>
#include <KAboutData>
#include <KCmdLineArgs>
 
#include "pongengine.h"
 
int main( int argc, char **argv)
{
 
    KAboutData aboutData( "kglpong", 0,
                  ki18n("kglpong"), "0.1",
                  ki18n("Classic pong game using KGLEngine2d"),
                  KAboutData::License_GPL,
                              ki18n("Copyright (c) 2008 Developer") );
 
    KCmdLineArgs::init( argc, argv, &aboutData );
    KCmdLineOptions options;
    KCmdLineArgs::addCmdLineOptions(options);
    KApplication app;
 
    pongEngine pong;
    pong.show();
 
    return app.exec();
}

So, what's happening here? We initialize the KApplication, and create an instance of our main class, the one that will subclass KGLEngine2d. Then we tell the engine to show itself, and that's all. I told you there was nothing really exciting there :)

  • TODO: this paragraph needs to be updated*

[edit] KGLEngine2d type system

KGLEngine uses user-defined objects types to refine the collision detection system. The default type is the parent object's className. The possibility to define the object's type at construction time allows for simple items to be created, without the need to put them in a class.

[edit] The KGLEngine2d pong engine

Now, as we want some sophisticated features, we will have to subclass the KGLEngine2d.

Mainly, those sophisticated actions that forces us to subclass are the keyboard interactions. As of now, they're managed in the engine's mainLoop(), and there is no other way to use them. Everything else could have been done in the main, or anywhere else, using a simple slot and the engine power, but this tutorial takes the object-oriented approach.

#include "pongengine.h"
#include "ball.h"
#include "racket.h"
 
#include <KGLEnhancedTextureManager>
#include "KGLPhysicsEngine>
#include <KGLTextItem>
#include <KAction>
 
#include <QString>
#include <QColor>
#include <QTime>
 
const QString BACKGROUND_TYPE = "00Background";
 
pongEngine::pongEngine(QWidget *parent)
        : KGLEngine2d(QRectF(-400, -300, 800, 600), 20, parent),
        m_collection(new KActionCollection(this)),
        isStarted(false),
        player1Score(0),
        player2Score(0)
{
    setTextureManager(new KGLEnhancedTextureManager("./sprites/"));
 
    const double wallThickness = 10;
    KGLItem *NWall = new KGLItem(QRectF(-width() / 2, height() / 2 - wallThickness, width(), wallThickness));
    NWall->setObjectName(WALL_TYPE);
    addItem(NWall);
 
    KGLItem *SWall = new KGLItem(QRectF(-width() / 2, -height() / 2, width(), wallThickness));
    SWall->setObjectName(WALL_TYPE);
    addItem(SWall);
 
    KGLItem *player1Goal = new KGLItem(QRectF(-width() / 2, -height() / 2, wallThickness, height()));
    player1Goal->setObjectName(GOAL_TYPE);
    addItem(player1Goal);
 
    KGLItem *player2Goal = new KGLItem(QRectF(width() / 2 - wallThickness, -height() / 2, wallThickness, height()));
    player2Goal->setObjectName(GOAL_TYPE);
    addItem(player2Goal);

So, what happens there ? First, we set the coordinates system. We tell the engine to show us a region of 640 pixels of width, and 480 of height. This only affects the rendered view, not the window size. This means we are creating a region in which the user will see what will be drawn. Any item outside the square of width and height specified, and centered around 0 will be drawn out of the screen. //NEEDS AN IMAGE TO EXPLAIN EASILY

Then we create and add items to the engine, no more. The most common way to create a KGLItem is by specifying it's shape using a QRectF. Here, we change the items dimensions according to what we want them to represent: The walls have to be wide but thin, same for the goals, except they're vertical and not horizontal. The first two items are there so the ball will bounce when it reaches the top or the bottom of the screen. The third and fourth ones are the goals, so we can detect when the ball's gone past a player's racket. The goals are set to be black, this should not be done this way, they should be given an image instead, but, hey, this is a code example and i wanted to show you how to draw a simple colored rectangle.

    player1Racket = new Racket();
    player1Racket->setPosition(-width() / 2, -wallThickness - 1);
    player1Racket->setZIndex(2);
    addItem(player1Racket);
 
 
    player2Racket = new Racket();
    player2Racket->rotate(M_PI);
    player2Racket->setPosition(width() / 2 - player2Racket->width(), -wallThickness - 1);
    player2Racket->setZIndex(2);
    addItem(player2Racket);
 
    ball = new Ball();
    ball->setPosition(0, 0);
    ball->setZIndex(2);
    addItem(ball);
 
    connect(ball, SIGNAL(enterGoal(int)), this, SLOT(score(int)));

Here we create the rackets and the ball. These are special classes that will be explained a bit later. Sprites get their size directly from the image you give them as a parameter, so unlike KGLItem, you do not need to specify the size of a KGLSpriteitem. The QPointF passed as argument (optionally) is the repeat pattern of the texture, eg QpointF(1,2) means your texture will be drawn two times horizontally, in the rectanle defined by the image's size. The picture below shosw how a sprite created with the default QPointF(1,1) would like with a QPointF(1,2) :

Bloc-example.png


They might have been simple KGLSprites, and the collisions would have been managed in another function connected to the KGLItem::colliding(PhysicsItem*) function. But, again, let's take the object oriented way. We also set the items ZIndex which will determine the drawing orders of the items, the higher it is, the later they will be drawn. Finally, we connect() the ball's enterGoal signal with the score slot, so when the ball enters a goal, the engine will be notified, and will update the score.

Now, let's define which keys we'll be using:

    KAction *up = m_collection->addAction("p1_up");
    up->setShortcut(Qt::Key_Z);
    KAction *down = m_collection->addAction("p1_down");
    down->setShortcut(Qt::Key_S);
    KAction *left = m_collection->addAction("p2_up");
    left->setShortcut(Qt::Key_Up);
    KAction *right = m_collection->addAction("p2_down");
    right->setShortcut(Qt::Key_Down;
 
    KAction *start = m_collection->addAction("start");
    start->setShortcut(Qt::Key_Space);

We add every action we are going to use, using their names, and then we define their keys.

Next is the collision detection and automatic moving of items system:

    physicsEngine()->setItemTypeMovable(player1Racket);
    physicsEngine()->setItemTypeMovable(ball);
 
    physicsEngine()->addCollisionRule(ball, player1Racket);
    physicsEngine()->addCollisionRule(ball, NWall);
    physicsEngine()->addCollisionRule(ball, player1Goal);
 
    physicsEngine()->addCollisionRule(player1Racket, NWall);

First, we tell the engine to call the move() function of each and every item of type RACKET_TYPE and BALL_TYPE, at each call of the mainLoop. This way, we only will have to set these item's speed for them to move.

By default, an item can't collide with other items, so we need to define the collision rules in order to use the physics engine. Then, we add collision detection rules. We say that an item of type BALL_TYPE can only collide with items of type RACKET_TYPE, WALL_TYPE, and GOAL_TYPE (which, in this case, is pretty much everything).

Then, we add the scoreBoards :

    player1ScoreBoard = new KGLTextItem("0");
    player1ScoreBoard->setPosition(200, height() / 2 - wallThickness - 60);
    player1ScoreBoard->font().setPointSize(60);
    player1ScoreBoard->setZIndex(2);
    addItem(player1ScoreBoard);
 
    player2ScoreBoard = new KGLTextItem("0");
    player2ScoreBoard->setPosition(-200, height() / 2 - wallThickness - 60);
    player2ScoreBoard->font().setPointSize(60);
    player2ScoreBoard->setZIndex(2);
    addItem(player2ScoreBoard);

These are simple text items, displaying each player's score.

    qsrand(QTime::currentTime().msec());
    lastScorer = (qrand() > RAND_MAX / 2);
 
    startGame();

Finally, we use a random value to determine who scored last, which defines the direction the ball will take (left or right), and we call startGame() which will repeatedly calls the mainLoop() function.

[edit] The Ball and Racket Classes

Let's take a look at the Ball and Racket headers :

#ifndef KGL_BALL_H
#define KGL_BALL_H
 
#include <KGLSpriteItem>
 
const QString WALL_TYPE = "01Wall";
const QString GOAL_TYPE = "04Goal";
 
class Ball : public KGLItem
{
    Q_OBJECT
public:
    Ball();
    void collidingWith(KGLItemBase*);
signals:
    void enterGoal(int);
};
 
#endif //KGL_BALL_H
#ifndef KGL_RACKET_H
#define KGL_RACKET_H
 
#include <KGLSpriteItem>
 
class Racket : public KGLItem
{
    Q_OBJECT
public:
    Racket();
    void collidingWith(KGLItemBase*);
};
 
#endif //KGL_RACKET_H

We just define KGLItems with nothing really special, but when we will change the ball and rackets from simple white squares to sprites, this will prove to be useful. Now let's take a look at the constructors:

Ball::Ball()
        : KGLItem(QRectF(0, 0, 10, 10))
{
}
Ball::Ball()
        : KGLItem(QRectF(0, 0, 10, 10))
{
}

So we just define the width and height of the item, as we don't need anything more yet.

The interesting thing right now is in the collidingWith function :

void Ball::collidingWith(KGLItemBase *item)
{
    if (item->objectName() == WALL_TYPE) {
        setSpeed(speed().x(), -speed().y());
    }
    if (item->objectName() == Racket::staticMetaObject.className()) {
        setSpeed(-speed().x(), speed().y());
    }
    if (item->objectName() == GOAL_TYPE) {
        emit(enterGoal((item->position().x() > 0) ? 1 : 2));
    }
}

We iterate on the colliding items, and depending on their type, we act. If we are colliding with a wall, we invert our vertical speed. If we are colliding with a racket, we invert our horizontal speed. If we are colliding with a goal, then we emit a signal to let the engine know it.

void Racket::collidingWith(KGLItemBase* item)
{
    if (item->objectName() == WALL_TYPE) {
        moveBy(0, -speed().y());
        setSpeed(0, 0);
    }
}

Here, we iterate again, but there is only one possible reaction: if we hit a wall, then stop moving (and move back to somewhere away from the wall).



This page was last modified on 29 June 2011, at 21:51. This page has been accessed 8,069 times. Content is available under Creative Commons License SA 3.0 as well as the GNU Free Documentation License 1.2.
KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V.Legal