1.. _pong-tutorial: 2 3The Pong Game 4============= 5The following tutorial will show you some capabilities of the component-based 6approach, PySDL2 features. We will create the basics of a simple Pong game 7implementation here. The basics of creating a event loop, dealing with 8user input, moving images around and creating a rendering function are 9covered in this tutorial. 10 11Getting started 12--------------- 13We start with creating the window and add a small event loop, so we are able 14to close the window and exit the game. :: 15 16 import sys 17 import sdl2 18 import sdl2.ext 19 20 21 def run(): 22 sdl2.ext.init() 23 window = sdl2.ext.Window("The Pong Game", size=(800, 600)) 24 window.show() 25 running = True 26 while running: 27 events = sdl2.ext.get_events() 28 for event in events: 29 if event.type == sdl2.SDL_QUIT: 30 running = False 31 break 32 window.refresh() 33 return 0 34 35 if __name__ == "__main__": 36 sys.exit(run()) 37 38The import statements, video initialisation and window creation were 39discussed previously in the :ref:`hello_world` tutorial. We import everything 40from the :mod:`sdl2` package here, too, to have all SDL2 functions available. 41 42Instead of some integrated event processor, a new code fragment is 43introduced, though. :: 44 45 running = True 46 while running: 47 events = sdl2.ext.get_events() 48 for event in events: 49 if event.type == sdl2.SDL_QUIT: 50 running = False 51 break 52 window.refresh() 53 54The while loop above is the main event loop of our application. It deals with 55all kinds of input events that can occur when working with the window, such as 56mouse movements, key strokes, resizing operations and so on. SDL handles a lot 57for us when it comes to events, so all we need to do is to check, if there are 58any events, retrieve each event one by one, and handle it, if necessary. For 59now, we will just handle the ``sdl2.SDL_QUIT`` event, which is raised when the 60window is about to be closed. 61 62In any other case we will just refresh the window's graphics buffer, so 63it is updated and visible on-screen. 64 65Adding the game world 66--------------------- 67The window is available and working. Now let's take care of creating the 68game world, which will manage the player paddles, ball, visible elements 69and everything else. We are going to use an implementation layout loosely 70based on a COP [#f1]_ pattern, which separates data structures and 71functionality from each other. This allows us to change or enhance functional 72parts easily without having to refactor all classes we are implementing. 73 74We start with creating the two player paddles and the rendering engine 75that will display them. :: 76 77 [...] 78 79 WHITE = sdl2.ext.Color(255, 255, 255) 80 81 class SoftwareRenderer(sdl2.ext.SoftwareSpriteRenderSystem): 82 def __init__(self, window): 83 super(SoftwareRenderer, self).__init__(window) 84 85 def render(self, components): 86 sdl2.ext.fill(self.surface, sdl2.ext.Color(0, 0, 0)) 87 super(SoftwareRenderer, self).render(components) 88 89 90 class Player(sdl2.ext.Entity): 91 def __init__(self, world, sprite, posx=0, posy=0): 92 self.sprite = sprite 93 self.sprite.position = posx, posy 94 95 96 def run(): 97 ... 98 99 world = sdl2.ext.World() 100 101 spriterenderer = SoftwareRenderer(window) 102 world.add_system(spriterenderer) 103 104 factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE) 105 sp_paddle1 = factory.from_color(WHITE, size=(20, 100)) 106 sp_paddle2 = factory.from_color(WHITE, size=(20, 100)) 107 108 player1 = Player(world, sp_paddle1, 0, 250) 109 player2 = Player(world, sp_paddle2, 780, 250) 110 111 running = True 112 while running: 113 events = sdl2.ext.get_events() 114 for event in events: 115 if event.type == sdl2.SDL_QUIT: 116 running = False 117 break 118 world.process() 119 120 if __name__ == "__main__": 121 sys.exit(run()) 122 123The first thing to do is to enhance the 124:class:`sdl2.ext.SoftwareSpriteRenderSystem` so that it will paint 125the whole window screen black on every drawing cycle, before drawing all 126sprites on the window. 127 128Afterwards, the player paddles will be implemented, based on an 129:class:`sdl2.ext.Entity` data container. The player paddles are 130simple rectangular sprites that can be positioned anywhere on the 131window. 132 133In the main program function, we put those things together by creating a 134:class:`sdl2.ext.World`, in which the player paddles and the renderer 135can live and operate. 136 137Within the main event loop, we allow the world to process all attached 138systems, which causes it to invoke the ``process()`` methods for all 139:class:`sdl2.ext.System` instances added to it. 140 141Moving the ball 142--------------- 143We have two static paddles centred vertically on the left and right of 144our window. The next thing to do is to add a ball that can move around 145within the window boundaries. :: 146 147 [...] 148 class MovementSystem(sdl2.ext.Applicator): 149 def __init__(self, minx, miny, maxx, maxy): 150 super(MovementSystem, self).__init__() 151 self.componenttypes = Velocity, sdl2.ext.Sprite 152 self.minx = minx 153 self.miny = miny 154 self.maxx = maxx 155 self.maxy = maxy 156 157 def process(self, world, componentsets): 158 for velocity, sprite in componentsets: 159 swidth, sheight = sprite.size 160 sprite.x += velocity.vx 161 sprite.y += velocity.vy 162 163 sprite.x = max(self.minx, sprite.x) 164 sprite.y = max(self.miny, sprite.y) 165 166 pmaxx = sprite.x + swidth 167 pmaxy = sprite.y + sheight 168 if pmaxx > self.maxx: 169 sprite.x = self.maxx - swidth 170 if pmaxy > self.maxy: 171 sprite.y = self.maxy - sheight 172 173 174 class Velocity(object): 175 def __init__(self): 176 super(Velocity, self).__init__() 177 self.vx = 0 178 self.vy = 0 179 180 181 class Player(sdl2.ext.Entity): 182 def __init__(self, world, posx=0, posy=0): 183 [...] 184 self.velocity = Velocity() 185 186 187 class Ball(sdl2.ext.Entity): 188 def __init__(self, world, sprite, posx=0, posy=0): 189 self.sprite = sprite 190 self.sprite.position = posx, posy 191 self.velocity = Velocity() 192 193 194 def run(): 195 [...] 196 sp_ball = factory.from_color(WHITE, size=(20, 20)) 197 [...] 198 movement = MovementSystem(0, 0, 800, 600) 199 spriterenderer = SoftwareRenderer(window) 200 201 world.add_system(movement) 202 world.add_system(spriterenderer) 203 204 [...] 205 206 ball = Ball(world, sp_ball, 390, 290) 207 ball.velocity.vx = -3 208 209 [...] 210 211Two new classes are introduced here, ``Velocity`` and 212``MovementSystem``. The ``Velocity`` class is a simple data bag. It 213does not contain any application logic, but consists of the relevant 214information to represent the movement in a certain direction. This 215allows us to mark in-game items as being able to move around. 216 217The ``MovementSystem`` in turn takes care of moving the in-game items around 218by applying the velocity to their current position. Thus, we can simply enable 219any ``Player`` instance to be movable or not by adding or removing a 220velocity attribute to them, which is a ``Velocity`` component instance. 221 222.. note:: 223 224 The naming is important here. The EBS implementation as described in 225 :ref:`ref-ebs` requires every in-application or in-game item attribute 226 bound to a :class:`sdl2.ext.Entity` to be the lowercase class name of its 227 related component. :: 228 229 Player.vel = Velocity(10, 10) 230 231 for example would raise an exception, since the system expects 232 ``Player.vel`` to be an instance of a ``Vel`` component. 233 234The ``MovementSystem`` is a specialised :class:`sdl2.ext.System`, a 235:class:`sdl2.ext.Applicator`, which can operate on combined sets of 236data. When the :meth:`sdl2.ext.Applicator.process()` method is 237called, the passed ``componentsets`` iterable will contain tuples of 238objects that belong to an instance and feature a certain type. The 239``MovementSystem``'s ``process()`` implementation hence will loop over 240sets of ``Velocity`` and ``Sprite`` instances that belong to the same 241:class:`sdl2.ext.Entity`. Since we have a ball and two players 242currently available, it typically would loop over three tuples, two for 243the individual players and one for the ball. 244 245The :class:`sdl2.ext.Applicator` thus enables us to process combined 246data of our in-game items, without creating complex data structures. 247 248.. note:: 249 250 Only entities that contain *all* attributes (components) are taken 251 into account. If e.g. the ``Ball`` class would not contain a 252 ``Velocity`` component, it would not be processed by the 253 ``MovementSystem``. 254 255Why do we use this approach? The :class:`sdl2.ext.Sprite` objects carry a 256position, which defines the location at which they should be rendered, when 257processed by the ``SoftwareRenderer``. If they should move around (which is 258a change in the position), we need to apply the velocity to them. 259 260We also define some more things within the ``MovementSystem``, such as a 261simple boundary check, so that the players and ball cannot leave the 262visible window area on moving around. 263 264Bouncing 265-------- 266We have a ball that can move around as well as the general game logic 267for moving things around. In contrast to a classic OO approach we do not 268need to implement the movement logic within the ``Ball`` and ``Player`` 269class individually, since the basic movement is the same for all (yes, 270you could have solved that with inheriting ``Ball`` and ``Player`` from 271a ``MovableObject`` class in OO). 272 273The ball now moves and stays within the bounds, but once it hits the 274left side, it will stay there. To make it *bouncy*, we need to add a 275simple collision system, which causes the ball to change its direction 276on colliding with the walls or the player paddles. :: 277 278 [...] 279 class CollisionSystem(sdl2.ext.Applicator): 280 def __init__(self, minx, miny, maxx, maxy): 281 super(CollisionSystem, self).__init__() 282 self.componenttypes = Velocity, sdl2.ext.Sprite 283 self.ball = None 284 self.minx = minx 285 self.miny = miny 286 self.maxx = maxx 287 self.maxy = maxy 288 289 def _overlap(self, item): 290 pos, sprite = item 291 if sprite == self.ball.sprite: 292 return False 293 294 left, top, right, bottom = sprite.area 295 bleft, btop, bright, bbottom = self.ball.sprite.area 296 297 return (bleft < right and bright > left and 298 btop < bottom and bbottom > top) 299 300 def process(self, world, componentsets): 301 collitems = [comp for comp in componentsets if self._overlap(comp)] 302 if collitems: 303 self.ball.velocity.vx = -self.ball.velocity.vx 304 305 306 def run(): 307 [...] 308 world = World() 309 310 movement = MovementSystem(0, 0, 800, 600) 311 collision = CollisionSystem(0, 0, 800, 600) 312 spriterenderer = SoftwareRenderer(window) 313 314 world.add_system(movement) 315 world.add_system(collision) 316 world.add_system(spriterenderer) 317 318 [...] 319 collision.ball = ball 320 321 running = True 322 while running: 323 events = sdl2.ext.get_events() 324 for event in events: 325 if event.type == sdl2.SDL_QUIT: 326 running = False 327 break 328 sdl2.SDL_Delay(10) 329 world.process() 330 331 if __name__ == "__main__": 332 sys.exit(run()) 333 334The ``CollisionSystem`` only needs to take care of the ball and objects 335it collides with, since the ball is the only unpredictable object within our 336game world. The player paddles will only be able to move up and down 337within the visible window area and we already dealt with that within the 338``MovementSystem`` code. 339 340Whenever the ball collides with one of the paddles, its movement 341direction (velocity) should be inverted, so that it *bounces* back. 342 343Additionally, we won't run at the full processor speed anymore in the 344main loop, but instead add a short delay, using the 345:func:`sdl2.SDL_Delay` function. This reduces the overall load on the 346CPU and makes the game a bit slower. 347 348Reacting on player input 349------------------------ 350We have a moving ball that bounces from side to side. The next step 351would be to allow moving one of the paddles around, if the player presses a 352key. The SDL event routines allow us to deal with a huge variety of user and 353system events that could occur for our application, but right now we are only 354interested in key strokes for the Up and Down keys to move one of the player 355paddles up or down. :: 356 357 [...] 358 def run(): 359 [...] 360 running = True 361 while running: 362 events = sdl2.ext.get_events() 363 for event in events: 364 if event.type == sdl2.SDL_QUIT: 365 running = False 366 break 367 if event.type == sdl2.SDL_KEYDOWN: 368 if event.key.keysym.sym == sdl2.SDLK_UP: 369 player1.velocity.vy = -3 370 elif event.key.keysym.sym == sdl2.SDLK_DOWN: 371 player1.velocity.vy = 3 372 elif event.type == sdl2.SDL_KEYUP: 373 if event.key.keysym.sym in (sdl2.SDLK_UP, sdl2.SDLK_DOWN): 374 player1.velocity.vy = 0 375 sdl2.SDL_Delay(10) 376 world.process() 377 378 if __name__ == "__main__": 379 sys.exit(run()) 380 381Every event that can occur and that is supported by SDL2 can be identified by a 382static event type code. This allows us to check for a key stroke, mouse button 383press, and so on. First, we have to check for ``sdl2.SDL_KEYDOWN`` and 384``sdl2.SDL_KEYUP`` events, so we can start and stop the paddle movement on 385demand. Once we identified such events, we need to check, whether the pressed 386or released key is actually the Up or Down key, so that we do not start or stop 387moving the paddle, if the user presses R or G or whatever. 388 389Whenever the Up or Down key are pressed down, we allow the left player 390paddle to move by changing its velocity information for the vertical 391direction. Likewise, if either of those keys is released, we stop moving 392the paddle. 393 394Improved bouncing 395----------------- 396We have a moving paddle and we have a ball that bounces from one side to 397another, which makes the game ... quite boring. If you played Pong before, 398you know that most variations of it will cause the ball to bounce in a 399certain angle, if it collides with a paddle. Most of those 400implementations achieve this by implementing the paddle collision as if 401the ball collides with a rounded surface. If it collides with the center 402of the paddle, it will bounce back straight, if it hits the paddle near 403the center, it will bounce back with a pointed angle and on the corners 404of the paddle it will bounce back with some angle close to 90 degrees to 405its initial movement direction. :: 406 407 class CollisionSystem(sdl2.ext.Applicator): 408 [...] 409 410 def process(self, world, componentsets): 411 collitems = [comp for comp in componentsets if self._overlap(comp)] 412 if collitems: 413 self.ball.velocity.vx = -self.ball.velocity.vx 414 415 sprite = collitems[0][1] 416 ballcentery = self.ball.sprite.y + self.ball.sprite.size[1] // 2 417 halfheight = sprite.size[1] // 2 418 stepsize = halfheight // 10 419 degrees = 0.7 420 paddlecentery = sprite.y + halfheight 421 if ballcentery < paddlecentery: 422 factor = (paddlecentery - ballcentery) // stepsize 423 self.ball.velocity.vy = -int(round(factor * degrees)) 424 elif ballcentery > paddlecentery: 425 factor = (ballcentery - paddlecentery) // stepsize 426 self.ball.velocity.vy = int(round(factor * degrees)) 427 else: 428 self.ball.velocity.vy = - self.ball.velocity.vy 429 430The reworked processing code above simulates a curved paddle by 431creating segmented areas, which cause the ball to be reflected in 432different angles. Instead of doing some complex trigonometry to 433calculate an accurate angle and transform it on a x/y plane, we simply 434check, where the ball collided with the paddle and adjust the vertical 435velocity. 436 437If the ball now hits a paddle, it can be reflected at different angles, 438hitting the top and bottom window boundaries... and will stay there. If it 439hits the window boundaries, it should be reflected, too, but not with a 440varying angle, but with the exact angle, it hit the boundary with. 441This means that we just need to invert the vertical velocity, once the 442ball hits the top or bottom. :: 443 444 class CollisionSystem(sdl2.ext.Applicator): 445 [...] 446 447 def process(self, world, componentsets): 448 [...] 449 450 if (self.ball.sprite.y <= self.miny or 451 self.ball.sprite.y + self.ball.sprite.size[1] >= self.maxy): 452 self.ball.velocity.vy = - self.ball.velocity.vy 453 454 if (self.ball.sprite.x <= self.minx or 455 self.ball.sprite.x + self.ball.sprite.size[0] >= self.maxx): 456 self.ball.velocity.vx = - self.ball.velocity.vx 457 458Creating an enemy 459----------------- 460Now that we can shoot back the ball in different ways, it would be nice 461to have an opponent to play against. We could enhance the main event 462loop to recognise two different keys and manipulate the second paddle's 463velocity for two people playing against each other. We also could 464create a simple computer-controlled player that tries to hit the ball 465back to us, which sounds more interesting. :: 466 467 class TrackingAIController(sdl2.ext.Applicator): 468 def __init__(self, miny, maxy): 469 super(TrackingAIController, self).__init__() 470 self.componenttypes = PlayerData, Velocity, sdl2.ext.Sprite 471 self.miny = miny 472 self.maxy = maxy 473 self.ball = None 474 475 def process(self, world, componentsets): 476 for pdata, vel, sprite in componentsets: 477 if not pdata.ai: 478 continue 479 480 centery = sprite.y + sprite.size[1] // 2 481 if self.ball.velocity.vx < 0: 482 # ball is moving away from the AI 483 if centery < self.maxy // 2: 484 vel.vy = 3 485 elif centery > self.maxy // 2: 486 vel.vy = -3 487 else: 488 vel.vy = 0 489 else: 490 bcentery = self.ball.sprite.y + self.ball.sprite.size[1] // 2 491 if bcentery < centery: 492 vel.vy = -3 493 elif bcentery > centery: 494 vel.vy = 3 495 else: 496 vel.vy = 0 497 498 499 class PlayerData(object): 500 def __init__(self): 501 super(PlayerData, self).__init__() 502 self.ai = False 503 504 505 class Player(sdl2.ext.Entity): 506 def __init__(self, world, sprite, posx=0, posy=0, ai=False): 507 self.sprite = sprite 508 self.sprite.position = posx, posy 509 self.velocity = Velocity() 510 self.playerdata = PlayerData() 511 self.playerdata.ai = ai 512 513 514 def run(): 515 [...] 516 aicontroller = TrackingAIController(0, 600) 517 518 world.add_system(aicontroller) 519 world.add_system(movement) 520 world.add_system(collision) 521 world.add_system(spriterenderer) 522 523 player1 = Player(world, sp_paddle1, 0, 250) 524 player2 = Player(world, sp_paddle2, 780, 250, True) 525 [...] 526 aicontroller.ball = ball 527 528 [...] 529 530We start by creating a component ``PlayerData`` that flags a player as 531being AI controlled or not. Afterwards, a ``TrackingAIController`` is 532implemented, which, depending on the information of the ``PlayerData`` 533component, will move the specific player paddle around by manipulating 534its velocity information. 535 536The AI is pretty simple, just following the ball's vertical movement, 537trying to hit it at its center, if the ball moves into the direction of 538the AI-controlled paddle. As soon as the ball moves away from the 539paddle, the paddle will move back to the vertical center. 540 541.. tip:: 542 543 Add ``True`` as last parameter to the first ``Player()`` constructor to 544 see two AIs playing against each other. 545 546Next steps 547---------- 548We created the basics of a Pong game, which can be found in the 549examples folder. However, there are some more things to do, such as 550 551 * resetting the ball to the center with a random vertical velocity, if 552 it hits either the left or right window bounds 553 554 * adding the ability to track the points made by either player, if the 555 ball hit the left or right side 556 557 * drawing a dashed line in the middle to make the game field look 558 nicer 559 560 * displaying the points made by each player 561 562It is your turn now to implement these features. Go ahead, it is not as 563complex as it sounds. 564 565 * you can reset the ball's position in the ``CollisionSystem`` code, 566 by changing the code for the ``minx`` and ``maxx`` test 567 568 * you could enhance the ``CollisionSystem`` to process ``PlayerData`` 569 components and add the functionality to add points there (or write a 570 small processor that keeps track of the ball only and processes only 571 the ``PlayerData`` and ``video.SoftSprite`` objects of each player for 572 adding points). Alternatively, you could use the 573 :class:`sdl2.ext.EventHandler` class to raise a score count 574 function within the ``CollisionSystem``, if the ball collides with 575 one of the paddles. 576 577 * write an own render sytem, based on :class:`sdl2.ext.Applicator`, 578 which takes care of position and sprite sets :: 579 580 StaticRepeatingSprite(Entity): 581 ... 582 self.positions = Positions((400, 0), (400, 60), (400, 120), ...) 583 ... 584 585 * draw some simple images for 0-9 and render them as sprites, 586 depending on the points a player made. 587 588.. rubric:: Footnotes 589 590.. [#f1] Component-Oriented Programming 591