
Tutorial 2: Pong
****************


Contents
^^^^^^^^

* Tutorial 2: Pong

  * Adding Game Logic

    * The Game Class

    * The Object Classes

      * Player

      * Ball

  * Starting the Game

    * Loading Sprites

    * Loading Backgrounds

    * Creating Objects

    * Creating Rooms

    * Making the Mouse Invisible

    * Starting the Game

  * The Final Result

Now that you've seen the basics of the SGE, it's time to create an
actual game. Although Pong might seem extremely simple, it will give
you a great foundation for developing more complex games in the
future.

Start out by setting up the project like we did in the Hello World
tutorial.


Adding Game Logic
=================


The Game Class
--------------

For our "sge.dsp.Game" class, we want to of course provide a way to
exit the game, and in this case, we are also going to provide a way to
pause the game.  Just for the heck of it, let's also allow the player
to take a screenshot by pressing F8 and toggle fullscreen by pressing
F11.

Let's take it one event at a time. Our close event is simple enough:

   def event_close(self):
       self.end()

Our key press event is slightly more involved.  To take a screenshot,
we simply use a combination of "sge.gfx.Sprite.from_screenshot()" and
"sge.gfx.Sprite.save()".  To toggle fullscreen, we simply change the
value of "sge.dsp.Game.fullscreen".  To pause the game, we use
"sge.dsp.Game.pause()".  We end up with this:

   def event_key_press(self, key, char):
       if key == 'f8':
           sge.gfx.Sprite.from_screenshot().save('screenshot.jpg')
       elif key == 'f11':
           self.fullscreen = not self.fullscreen
       elif key == 'escape':
           self.event_close()
       elif key in ('p', 'enter'):
           self.pause()

This is incomplete, though.  When "sge.dsp.Game.pause()" is called,
the game enters a special loop where normal events are ignored.  In
their place, we need to use "paused" events to give the player a
chance to unpause.  We also should allow the player to quit the game
while it is paused.  To achieve these goals, we add the special
events, "sge.dsp.Game.event_paused_key_pressed()" and
"sge.dsp.Game.event_paused_close()":

   def event_paused_key_press(self, key, char):
       if key == 'escape':
           # This allows the player to still exit while the game is
           # paused, rather than having to unpause first.
           self.event_close()
       else:
           self.unpause()

   def event_paused_close(self):
       # This allows the player to still exit while the game is paused,
       # rather than having to unpause first.
       self.event_close()

In this case, we are defining the paused key press event to unpause
the game when any key except for the Esc key is pressed.


The Object Classes
------------------

"sge.dsp.Object" objects are things in a game that we want to be
displayed in a room.  These objects tend to represent players,
enemies, tiles, decorations, and pretty much anything else you can
think of.

For Pong, we need three objects: the two players, and the ball.  We
will define two sub-classes of "sge.dsp.Object" for this purpose:
"Player" and "Ball".


Player
~~~~~~

"Player" is used for the paddles.  These are what the players control.

For "Player", the difference between different objects is which player
controls it. Every other difference (the position, the controls, and
the direction it hits the ball) can be easily derived from that.  We
are therefore going to define "Player.__init__()" to reflect this.

"Player.__init__()" will take a single argument, "player".  This
argument will indicate which player the object is for: "1" for player
1, or "2" for player 2.  We will set a few attributes based on this:

* "up_key" will indicate the key that moves the paddle up.  We will
  set it to ""w"" for player 1, or ""up"" for player 2.

* "down_key" will indicate the key that moves the paddle down.  We
  will set it to ""s"" for player 1, or ""down"" for player 2.

* "x" is an attribute inherited from "sge.dsp.Object" which
  indicates the horizontal position of the object.  We will set this
  based on a constant we will define (technically just a variable,
  since Python doesn't support constants) called "PADDLE_XOFFSET":
  "PADDLE_XOFFSET" for player 1, or "sge.game.width - PADDLE_XOFFSET"
  for player 2.  We will define "PADDLE_XOFFSET" near the top of our
  code file, beneath imports, as "32".

* "hit_direction" will indicate the direction the paddle hits the
  ball.  We will set it to "1" for player 1, and "-1" for player 2.

Additionally, certain attributes inherited from "sge.dsp.Object" will
be the same for both "Player" objects.  "y" will always be
"sge.game.height / 2" (vertically centered).  "sprite" will always be
"paddle_sprite" (a sprite we will create later). "checks_collisions"
will always be "False", since player objects don't need to check for
collisions with each other; we can therefore leave all collision
checking to the ball object.

All attributes inherited from "sge.dsp.Object" will be defined by
passing their values to "sge.dsp.Object.__init__()", which we will
call with "super().__init__(*args, **kwargs)".  This makes our
"Player.__init__()" defintion an extension, rather than an override,
of "sge.dsp.Object.__init__()", which is important; overriding this
method would be likely to break something.

Our definition of "Player.__init__`()" ends up looking something like
this:

   def __init__(self, player):
       if player == 1:
           self.joystick = 0
           self.up_key = "w"
           self.down_key = "s"
           x = PADDLE_XOFFSET
           self.hit_direction = 1
       else:
           self.joystick = 1
           self.up_key = "up"
           self.down_key = "down"
           x = sge.game.width - PADDLE_XOFFSET
           self.hit_direction = -1

       y = sge.game.height / 2
       super().__init__(x, y, sprite=paddle_sprite, checks_collisions=False)

We need to allow the players to move the paddles.  We could do this by
using key press events, but since we would like the players to be able
to continuously move the paddles by holding down the key, the proper
way to do this is to check for the state of the keys every frame and
move accordingly.

"sge.keyboard.get_pressed()" returns the state of a key on the
keyboard.  We will check this in the step event to decide how the
paddle should move on any given frame.  The step event, defined by
"sge.dsp.Object.event_step()", is an event which always executes every
frame.

What we will do is subtract the state of "up_key" from the state of
"down_key".  This will give us "-1" if only "up_key" is pressed, "1"
if only "down_key" is pressed, and "0" if neither or both keys are
pressed.  We can multiply this result by a constant, which we will
call "PADDLE_SPEED", to get the amount that the paddle should move
this frame, and assign this value to the player's
"sge.dsp.Object.yvelocity", an attribute which indicates the number of
pixels an object will move vertically each frame.  We will define
"PADDLE_SPEED" as "4".

This isn't quite enough, though.  With just this, the paddle can be
moved off-screen!  To prevent this from happening, we will check the
player object's "bbox_top" and "bbox_bottom" values; these indicate
the current location of the object's bounding box.  If "bbox_top" is
less than "0", we will set it to "0".  If "bbox_bottom" is greater
than "sge.game.current_room.height", we will set it to
"sge.game.current_room.height". "sge.game.current_room", as its name
implies, indicates the currently running "sge.game.Room" object.

Our step event ends up looking something like this:

   def event_step(self, time_passed, delta_mult):
       # Movement
       key_motion = (sge.keyboard.get_pressed(self.down_key) -
                     sge.keyboard.get_pressed(self.up_key))

       self.yvelocity = key_motion * PADDLE_SPEED

       # Keep the paddle inside the window
       if self.bbox_top < 0:
           self.bbox_top = 0
       elif self.bbox_bottom > sge.game.current_room.height:
           self.bbox_bottom = sge.game.current_room.height


Ball
~~~~

"Ball" is the ball.  It is bounced back and forth by the players. If
it touches the top or bottom edge of the screen, it bounces off.  If
it passes one of the players, the other player gets a point and the
ball is returned to the playing field.

Any "Ball" object is always going to have the same initial attributes
as any other "Ball" object, so much like what we did with "Player", we
are going to define a custom "Ball.__init__()".

In this case, it's much simpler: "x" and "y" are going to start at the
center of the screen, and "sprite" is going to be "ball_sprite".
These are attributes inherited from "sge.dsp.Object", so we indicate
them in a call to "super().__init__".  "Ball.__init__()" ends up as:

   def __init__(self):
       x = sge.game.width / 2
       y = sge.game.height / 2
       super().__init__(x, y, sprite=ball_sprite)

Since we want to serve the ball both at the start of the game and
every time the ball passes a player, we should define a "Ball.serve()"
method.  This method needs to do two things: first, it needs to return
the ball to its original position in the center.  Second, it needs to
set the speed so that it moves either straight to the left or straight
to the right.  If a direction isn't specified, it needs to choose a
direction at random.

For the first task, we can use "sge.dsp.Object.xstart" and
"sge.dsp.Object.ystart".  These attributes indicate the original
position of an object when it was first created, which in the case of
"Ball" objects is in the center of the screen.

For the second task, we have an argument called "direction".  If it is
"None", it randomly becomes either "1" or "-1".  The value is then
multiplied by a constant called "BALL_START_SPEED", which we will set
to "2", and this becomes the ball's "sge.dsp.Object.xvelocity" value.
The ball's "sge.dsp.Object.yvelocity" value is then set to "0".

The result looks like this:

   def serve(self, direction=None):
       if direction is None:
           direction = random.choice([-1, 1])

       self.x = self.xstart
       self.y = self.ystart

       # Next round
       self.xvelocity = BALL_START_SPEED * direction
       self.yvelocity = 0

Note: Since we are now using the "random" module, we need to also
  import it at the top of our code file.

When the ball is created, we want to serve it immediately.  we will
put this in the create event, which is defined by
"sge.dsp.Object.event_create()".  The create event happens whenever
the object is created in the room.  This is the create event of
"Ball":

   def event_create(self):
       self.serve()

For "Ball"'s step event, we need to do two things: cause the ball to
bounce off of the top and bottom edges of the screen, and serve the
ball when it passes the left or right edge of the screen.

For the first task, we do the same thing we did with "Player", but we
also set whether "yvelocity" is positive or negative; we make it
negative when the ball touches the bottom, and positive when the ball
touches the top.

For the second task, we do a similar check, but we phrase the check
such that the ball needs to be completely outside of the room, rather
than just touching the edge.  We do this by checking "bbox_right"
against the left edge, and "bbox_left" against the right edge. When
the ball is outside the screen, we serve it in the direction of the
player it passed (so that the player who lost the round gets initial
control of the ball).

Our step event for "Ball" ends up looking something like this:

   def event_step(self, time_passed, delta_mult):
       # Scoring
       if self.bbox_right < 0:
           self.serve(-1)
       elif self.bbox_left > sge.game.current_room.width:
           self.serve(1)

       # Bouncing off of the edges
       if self.bbox_bottom > sge.game.current_room.height:
           self.bbox_bottom = sge.game.current_room.height
           self.yvelocity = -abs(self.yvelocity)
       elif self.bbox_top < 0:
           self.bbox_top = 0
           self.yvelocity = abs(self.yvelocity)

Now, we need to allow the players to repel the ball.  We will do this
with a collision event.  Collision events, controlled by
"sge.dsp.Object.event_collision()", occur when two objects touch each
other.

We first need to verify what type of object we're colliding with.  The
most straightforward way is to use "isinstance()" to check whether or
not the object being collided with, which is passed on to the "other"
argument, is an instance of "Player".  We write the collision code for
these two objects under this check.

The most straightforward way to do this is with directional collision
detection, but we are going to instead use "Player.hit_direction" to
determine what to do.  If the "other.hit_direction" is "1", we bounce
the ball to the right.  Otherwise, we bounce the ball to the left.

We need to make the ball accelerate each time the ball hits a paddle,
so that the round goes faster over time.  We will store the amount of
acceleration in a constant called "BALL_ACCELERATION", which we will
define as "0.2".  We will then set "self.xvelocity" to
"(abs(self.xvelocity) + BALL_ACCELERATION) * other.hit_direction".

We also need to make the ball's vertical movement change based on
where it hits the paddle.  To do this, we will subtract "other.y" from
"self.y" and multiply that by a constant called
"PADDLE_VERTICAL_FORCE", which we will define as "1 / 12"; this value
will be added to "self.yvelocity".

There is one problem left, though it is not particularly obvious.  The
way we have it set up at this point, the ball will eventually move so
fast that it will fail to collide with the paddles.  This is due to
how movement works; it's not actual movement, but rather a slight
change of position done every frame.  If that change of position is
too much, the ball can pass right over a paddle.

To prevent this, we need to set a limit for how fast the ball can move
horizontally.  Instead of just multiplying "(abs(self.xvelocity) +
BALL_ACCELERATION)" by "other.hit_direction", we multiply the smallest
out of that, and a new constant called "BALL_MAX_SPEED", by
"other.hit_direction".  We will define "BALL_MAX_SPEED" as "15".

Our collision event ends up looking something like this:

   def event_collision(self, other, xdirection, ydirection):
       if isinstance(other, Player):
           if other.hit_direction == 1:
               self.bbox_left = other.bbox_right + 1
           else:
               self.bbox_right = other.bbox_left - 1

           self.xvelocity = min(abs(self.xvelocity) + BALL_ACCELERATION,
                                BALL_MAX_SPEED) * other.hit_direction
           self.yvelocity += (self.y - other.y) * PADDLE_VERTICAL_FORCE


Starting the Game
=================

It's time to get our game started.

We are going to pass some arguments to the creation of our "Game"
object: we are going to define "width" as "640", "height" as "480",
"fps" as "120", and "window_text" as ""Pong"".  Specify them as
keyword arguments.


Loading Sprites
---------------

We need two sprites: a paddle sprite and a ball sprite.  We also need
a black background with a line down the middle.  We could draw these
in an image editor and load them, but since they are so simple, we are
going to generate them dynamically instead.

Sprites are stored as "sge.gfx.Sprite" objects, so we are going to
create two of them:

   paddle_sprite = sge.gfx.Sprite(width=8, height=48, origin_x=4, origin_y=24)
   ball_sprite = sge.gfx.Sprite(width=8, height=8, origin_x=4, origin_y=4)

"sge.gfx.Sprite.origin_x" and "sge.gfx.Sprite.origin_y" indicate the
origin of the sprite.  In this case, we are setting the origins to the
center of the sprites.  This is necessary for our method of
determining how the paddles affect vertical speed to work, and it also
makes symmetry easier.

Currently, both of these sprites are blank.  We need to draw the
images on them.  In this case, we will just draw white rectangles that
fill the entirety of the sprites, which can be done with
"sge.gfx.Sprite.draw_rectangle()":

   paddle_sprite.draw_rectangle(0, 0, paddle_sprite.width,
                                paddle_sprite.height, fill=sge.gfx.Color("white"))
   ball_sprite.draw_rectangle(0, 0, ball_sprite.width, ball_sprite.height,
                              fill=sge.gfx.Color("white"))


Loading Backgrounds
-------------------

Now we need a background.  Our sprites are white, so we need a black
background.  We could of course leave it just at that, but that would
be boring, so we are also going to also have a white line in the
middle. We can do this easily by using the paddle sprite as a
background layer. Background layers are special objects that indicate
sprites that are used in a background.  We create the layer, put it in
a list, and pass that list onto "sge.gfx.Background.__init__()"'s
"layers" argument:

   layers = [sge.gfx.BackgroundLayer(paddle_sprite, sge.game.width / 2, 0, -10000,
                                     repeat_up=True, repeat_down=True)]
   background = sge.gfx.Background(layers, sge.gfx.Color("black"))

The fourth argument of "sge.BackgroudLayer.__init__()" is the layer's
Z-axis value.  The Z-axis is used to determine what objects are in
front of what other objects; objects with a higher Z-axis value are
closer to the viewer.  The default Z-axis value is "0".  Since we want
all objects to be in front of the layer, we set its Z-axis value to a
very low negative value.


Creating Objects
----------------

Don't forget to create our objects!  In "player1", store a "Player"
object with the "player" argument specified as "1". In "player2",
store a "Player" object with the "player" argument specified as "2".
Finally, create a "Ball" object and store it in "ball".  Put all of
these objects in a list and assign this list to a variable called
"objects".


Creating Rooms
--------------

Create a "Room" object.  Specify the first argument as "objects", and
specify the keyword argument "background" as "background".  Don't
forget to assign it to "sge.game.start_room"!


Making the Mouse Invisible
--------------------------

Since we don't need to see the mouse cursor, we will hide it.  To do
this, set "sge.game.mouse.visible" to "False".


Starting the Game
-----------------

Add a call to "sge.game.start()" at the end, under a check for the
value of "__name__".


The Final Result
================

You should now have a script that looks something like this:

   #!/usr/bin/env python3

   # Pong Example
   # Written in 2013-2015 by Julie Marchant <onpon4@riseup.net>
   #
   # To the extent possible under law, the author(s) have dedicated all
   # copyright and related and neighboring rights to this software to the
   # public domain worldwide. This software is distributed without any
   # warranty.
   #
   # You should have received a copy of the CC0 Public Domain Dedication
   # along with this software. If not, see
   # <http://creativecommons.org/publicdomain/zero/1.0/>.

   import random

   import sge

   PADDLE_XOFFSET = 32
   PADDLE_SPEED = 4
   PADDLE_VERTICAL_FORCE = 1 / 12
   BALL_START_SPEED = 2
   BALL_ACCELERATION = 0.2
   BALL_MAX_SPEED = 15


   class Game(sge.dsp.Game):

       def event_key_press(self, key, char):
           global game_in_progress

           if key == 'f8':
               sge.gfx.Sprite.from_screenshot().save('screenshot.jpg')
           elif key == 'f11':
               self.fullscreen = not self.fullscreen
           elif key == 'escape':
               self.event_close()
           elif key in ('p', 'enter'):
               self.pause()

       def event_close(self):
           self.end()

       def event_paused_key_press(self, key, char):
           if key == 'escape':
               # This allows the player to still exit while the game is
               # paused, rather than having to unpause first.
               self.event_close()
           else:
               self.unpause()

       def event_paused_close(self):
           # This allows the player to still exit while the game is paused,
           # rather than having to unpause first.
           self.event_close()


   class Player(sge.dsp.Object):

       def __init__(self, player):
           if player == 1:
               self.up_key = "w"
               self.down_key = "s"
               x = PADDLE_XOFFSET
               self.hit_direction = 1
           else:
               self.up_key = "up"
               self.down_key = "down"
               x = sge.game.width - PADDLE_XOFFSET
               self.hit_direction = -1

           y = sge.game.height / 2
           super().__init__(x, y, sprite=paddle_sprite, checks_collisions=False)

       def event_step(self, time_passed, delta_mult):
           # Movement
           key_motion = (sge.keyboard.get_pressed(self.down_key) -
                         sge.keyboard.get_pressed(self.up_key))

           self.yvelocity = key_motion * PADDLE_SPEED

           # Keep the paddle inside the window
           if self.bbox_top < 0:
               self.bbox_top = 0
           elif self.bbox_bottom > sge.game.current_room.height:
               self.bbox_bottom = sge.game.current_room.height


   class Ball(sge.dsp.Object):

       def __init__(self):
           x = sge.game.width / 2
           y = sge.game.height / 2
           super().__init__(x, y, sprite=ball_sprite)

       def event_create(self):
           self.serve()

       def event_step(self, time_passed, delta_mult):
           # Scoring
           if self.bbox_right < 0:
               self.serve(-1)
           elif self.bbox_left > sge.game.current_room.width:
               self.serve(1)

           # Bouncing off of the edges
           if self.bbox_bottom > sge.game.current_room.height:
               self.bbox_bottom = sge.game.current_room.height
               self.yvelocity = -abs(self.yvelocity)
           elif self.bbox_top < 0:
               self.bbox_top = 0
               self.yvelocity = abs(self.yvelocity)

       def event_collision(self, other, xdirection, ydirection):
           if isinstance(other, Player):
               if other.hit_direction == 1:
                   self.bbox_left = other.bbox_right + 1
               else:
                   self.bbox_right = other.bbox_left - 1

               self.xvelocity = min(abs(self.xvelocity) + BALL_ACCELERATION,
                                    BALL_MAX_SPEED) * other.hit_direction
               self.yvelocity += (self.y - other.y) * PADDLE_VERTICAL_FORCE

       def serve(self, direction=None):
           if direction is None:
               direction = random.choice([-1, 1])

           self.x = self.xstart
           self.y = self.ystart

           # Next round
           self.xvelocity = BALL_START_SPEED * direction
           self.yvelocity = 0


   # Create Game object
   Game(width=640, height=480, fps=120, window_text="Pong")

   # Load sprites
   paddle_sprite = sge.gfx.Sprite(width=8, height=48, origin_x=4, origin_y=24)
   ball_sprite = sge.gfx.Sprite(width=8, height=8, origin_x=4, origin_y=4)
   paddle_sprite.draw_rectangle(0, 0, paddle_sprite.width, paddle_sprite.height,
                                fill=sge.gfx.Color("white"))
   ball_sprite.draw_rectangle(0, 0, ball_sprite.width, ball_sprite.height,
                              fill=sge.gfx.Color("white"))

   # Load backgrounds
   layers = [sge.gfx.BackgroundLayer(paddle_sprite, sge.game.width / 2, 0, -10000,
                                     repeat_up=True, repeat_down=True)]
   background = sge.gfx.Background(layers, sge.gfx.Color("black"))

   # Create objects
   player1 = Player(1)
   player2 = Player(2)
   ball = Ball()
   objects = [player1, player2, ball]

   # Create rooms
   sge.game.start_room = sge.dsp.Room(objects, background=background)

   sge.game.mouse.visible = False


   if __name__ == '__main__':
       sge.game.start()

This is a basically complete Pong game, but it lacks some features.
First, this game doesn't keep track of the score.  It is left up to
the players to keep track of who is winning.  Second, there is no
sound.  We should fix both of these problems.

Additionally, it would be nice if our game could support joystick
input.

In the next tutorial, we will improve on these points to make a Pong
game more on par with Atari's original Pong.
