回到Silas S. Brown(赛乐思)的网站首页

以Python游戏介绍面向对象程序设计#2 Introducing OOP in a Python game 2

My previous post about a text adventure game is probably a better introduction than this one, but some youngsters'd rather write graphical action games so let's make one of those too.

Again I assume Python 3, and this time you'll need the Pygame library installed. I recommend Thonny 3 if you don't already have a Python setup (you could use Thonny 4 but it contains a pro-Ukraine message that might get you in trouble in some countries).

简单的乒乓游戏 Simple bat-and-ball game

我们写个简单的乒乓游戏,但我们会容许4个人做游戏(一个在左边,一个在右边,一个在上边和一个在下边,如果你缺乏4个人,电脑就会控制一些),面向对象程序设计会让我们更容易这样做。如果我们喜欢,我们甚至能把两多球放在显示器。
Let's write a simple bat-and-ball game for multiple players. To be a bit different, as our rectangular screen has 4 edges, we might as well make it a 4-player version (if you don't have 4 players, the computer can play some of them), with paddles (bats) on the left, right, top and bottom of the screen. The power of object-oriented programming will let us be flexible enough to do that. We can even have more than one ball on the screen if we want.

把任何东西放在显示器之前,我们应该先认识一点关于显示器怎样处理布局(直角座標)和颜色(红绿蓝模型)。别担心这其实不太难。
Before we put anything on the screen, we need to know a little bit about how screens do place (X-Y coordinates) and colour (RGB). Don't worry, it's not too hard.

座標 Coordinates

我们需要认识一些关于直角座標系(这是第4年的事):x是从左边缘的平距离,y是从下边缘的直立距离。如果我们设定xy然后看看这两个线在什么地方见面,我们可以具体规定本页或本显示器的任何正确地方。不过,Pygame很坏:它不服从数学的一般规则,它的y是从上边缘的垂直距离。我的意见是这是Pygame设计师的错误决定,因为Pygame图形是一般数学的相反,使我们的青少潜能程序员糊涂了。(80年代的英国广播公司微型计算机系统这次做的更好:它的图形在左下开始。)
We need to know about the Cartesian coordinate system (the UK National Curriculum currently introduces this in Year 4 i.e. age 9)---"x" is the horizontal distance from the left edge, and "y" is the vertical distance up from the bottom edge. We can put something at any place on a page or screen by setting the "x" and the "y" and finding where they meet. But Pygame is naughty---instead of following what mathematics normally does, its idea of "y" is the vertical distance down from the top edge. My personal opinion is this was an incredibly bad decision by the Pygame designers because it means Pygame graphics are done "backwards" from normal maths, confusing our young potential programmers. (The BBC Micro in the 1980s got this right: its graphics started at bottom left.)

这是另一件我不喜欢的Pygame事:它的xy的数目有赖于你屏幕的点,但不同屏幕有不同的点大小,所以,如果你在一个电脑上写游戏,你也许在另一个电脑发觉那个游戏根本太小了,所以把游戏发给朋友不一定有好结果。所以,我们得使我们的游戏考察全显示器有多少点才会计算该用多少点。(再次,80年代的英国广播公司微型计算机系统这次更好:它的坐标数字一直是显示器的某某分数,不被显示器点的大小影响。)
And here's another thing I don't like about the Pygame coordinate design: the number in the "x" or the "y" is the number of screen dots (called "picture elements" or pixels), but that depends on your screen: some screens have smaller dots than others, so if you write a game on one computer, you might find it looks way too small on another---no good if you want to send it to a friend! So we have to make the game check how many dots there are on the screen before it can work out how many dots to use. (Again the BBC Micro in the 1980s was more helpful: its numbers were always fixed fractions of the distance across the screen, no matter what dot size was being used.)

这些事我们不必太担忧,因为我们有面向对象程序设计。我们先告诉电脑“对象”如何举动,然后让电脑自己计算细节。有些人以为教孩子们面向对象程序设计太复杂了,最好只写简单的代码说显示器的哪个像素打开。那个方法也许首先更快,但我们越加多(比如更多做游戏的人)代码就越难调整。面向对象程序设计最终更容易。
We don't have to worry too much about these things, because we have object orientation. Once we tell the computer how the objects work, it can calculate some of the details itself. Some people think teaching object orientation to children is too complicated, and it's better to write simple code that just says which dot on the screen to light up. It's true their way might be quicker at the beginning, but it will get harder to adjust the code when we want to do things like add more players. This way will be easier in the long run.

有些人以为我们即将说“每个长方形对象都有x和y和高度和宽度”但慢下来!后来我们得做碰撞侦测(避免裁剪),2D或3D碰撞侦测可能看来一点难,不过,面向对象程序设计能这里帮我们: 我们只应该写如何做1D的碰撞侦测,然后说有两个尺寸(后来能说有三个)。
Now, some people might think we're about to say "every rectangular object has an 'x' and a 'y' and a height and a width" but hold on! Later on we'll have to do collision detection between objects (so they can bounce off of each other instead of going right through each other, sometimes known as "clipping"), and doing collision detection in two dimensions (or even three dimensions later) looks a bit hard. But we have object orientation: we just need to write how to do collision detection in one dimension, and then say have two of them (or maybe three of them later).

5月划船比賽 The May Bumps

每个6月,劍橋大學在康河舉行年度划船比賽,叫做“5月撞击比赛”。叫做“5月”因为以前是5月举行的,变成6月时他们看来忽略了改名。(那是个程序编制员的常见坏习惯:有个可变物保存某一件事,给它个对于那件事的正确名字,但后来修改它保存另一件事,太懒惰改名,后来糊涂了自己或其他程序编制员因为应该记得这是个错名的可变物。请别这样做:如果你修改保存什么,就改名,这避免混乱状态。)但我想我们集中精神看为什么划船比賽叫做“撞击”比赛。
Every June, Cambridge has a rowing-boat race on the River Cam called the May Bumps. It's called May because it used to be in May: maybe they forgot to change its name when it became June. (That's a common bad habit of programmers: they have a variable that stores one thing, so they give it a good name for that thing, but then they change the thing that's in it, but they're too lazy to change the name, and later programmers get confused because you have to remember the name is wrong. Please don't do this: if you're changing what it is, change its name. It's less confusing.) But what I want to focus on here is, why it's called Bumps.

康河有时窄,划艇与船桨有时宽,所以一个船超过另一个船有时不容易。所以他们有个规则:如果你后面船的前端碰撞你船的后端,你的船就得退赛,让那个船过。
The River Cam gets a bit narrow, and the rowing boats with their oars get a bit wide, so it's difficult for one boat to overtake another. So they have a rule that if the front of the boat behind you bumps into the back of your boat, then your boat must drop out and let that one pass.

让我们写程序计算一个船是否即将碰撞另一个船。我们应该知道两船的前端在哪里(每船的前端这只需要一个数目:从开始线到前端的距离),也应该知道两船的后端(船尾)在哪里,然后我们能看看一个船的前端有没有碰撞其他船的后端。
Let's write some code that works out if one boat is about to bump into another boat. We need to know where each boat-front is (each of which requires only one number: the distance from the starting line to where the boat-front is now), and we need to know where each boat-back is. (They have words like "bows" and "stern" but let's not get too worried about that now.) Then we can work out whether one boat-front is bumping into another boat-back.


class Boat:
    def __init__(self):
        # This is how to make a new Boat 这是如何建造新船
        self.front = 10
        self.back = 0
    def is_bumping(self, otherBoat):
        if otherBoat is self: # if they're the same Boat as us 如果我们和他们是同一个船,
            return False # we're not bumping them (we ARE them) 我们不碰撞他们(我们就是他们)
        else: return otherBoat.front >= self.front >= otherBoat.back
问题 Questions:
  1. 解释那代码的else:部分,或者画图解。为什么应该考虑otherBoat.front而不只是otherBoat.back?
    Explain the else: part of the above, or draw a picture. Why did we need to check otherBoat.front as well as otherBoat.back?
  2. 如果我们不只摸前面的船但稍微推过它的后端,代码仍然能计算我们碰撞他们吗?为什么这在程序里可能有用?(暗示:电脑是数字的,也许我们应该以步骤搬动。)
    If we don't just touch the boat in front but push slightly into it, will the code still say we're bumping them? Why might this be useful in a program? (Hint: the computer is digital and we might have to move in steps.)
  3. 不是每一个船都有前端离开始线10单位而后端离开始线0单位。如果__init__行被修改为def __init__(self, start, length):start是开始,length是长度),此后两行怎么修改使用这些?(暗示:船后端是从船前端和船长度计算的)
    Not all boats will have their fronts 10 units away from the starting line and their backs 0 units away from the starting line. If the __init__ line is changed to def __init__(self, start, length): how do the 2 lines after it need to change to use start and length? (Hint: the line that sets back needs to use both of these new things.)
  4. 这个代码计算什么:
    What does this code calculate: self.is_bumping(otherBoat) or otherBoat.is_bumping(self)

让我们现给每一个船一个速度(每个时间单位动多少距离单位),然后改变规则所以无论有任何碰撞,船就返回。
Let's give each boat a speed (units travelled per unit of time), and change the rules so that if it bumps into anything it reverses.


class Boat:
    def __init__(self, start, length, speed):
        self.front = start
        self.back = start - length
        self.speed = speed
    def touching(self, otherBoat):
        if otherBoat is self: return False
        return otherBoat.front >= self.front >= otherBoat.back or self.front >= otherBoat.front >= self.back
    def move(self, allBoats):
        self.front += self.speed
        self.back += self.speed
        if any(self.touching(b) for b in allBoats):
            self.front -= self.speed # bounce back
            self.back -= self.speed
            self.speed = - self.speed # and turn around

boats = [
    Boat(10, 5, 0.2),
    Boat(20, 7, -0.3)]
for timeUnit in range(100):
    for b in boats: b.move(boats)
    print (boats[0].front, boats[1].back)
问题 Questions:
  1. 这个touching做什么?
    What does touching do?
  2. 为什么move需要知道所有船的列出?能现看我们为什么之前用了id
    Why does move need a list of all the boats? Can you now see why we checked id before?
  3. 试试输入以上代码,包括后面的测试行。我们还没使用Pygame只显示船地位的数字。能不能从这些数字解释两个船发生了什么事?
    Try out the above code, including the test lines at the end. We're not using Pygame yet: it's showing boat positions as just numbers. Can you explain from the numbers what happened to the two boats?

现在我们可以把Boat重命名为ObjectDimension(物体尺寸),删除测试行,而做二维碰撞侦测:
Now let's rename our boats into "object dimensions" (and delete the test lines) and do 2D collision checking:


class GameObject:
    def __init__(self, x, y, height, width, speedX=0, speedY=0):
        self.xDim = ObjectDimension(x, width, speedX)
        self.yDim = ObjectDimension(y, height, speedY)
    def move(self, allObjects):
        self.xDim.move(
          o.xDim for o in allObjects if self.yDim.touching(o.yDim))
        self.yDim.move(
          o.yDim for o in allObjects if self.xDim.touching(o.xDim))
问题 Questions:
  1. GameObjectmove为什么需要两个if部分?(暗示:我们不再在窄康河)
    Why are the if parts required in GameObject's move? (Hint: we're not on the narrow river anymore.)
  2. 目前的xy指出物体的右下角落。为什么?如果我们想指出左下或左上或(更难)中间,该如何改变ObjectDimension
    At the moment, the x,y position points to the bottom right-hand corner of the object. Why? How should the ObjectDimension class change if we want to make it the bottom left, the top left, or (harder) the middle?
  3. 这里所说的=0等于什么?(叫做“默认选项”)
    What does the =0 do here? (The fancy wording for it is "default value" but can you work out what that means? Hint: what happens if we don't set a speed when we make a GameObject?)

颜色 Colours

现在我们几乎能把我们的物体放在显示器,但我们仍然应该知道如何具体规定他们的颜色。
We are now very close to drawing our objects on the screen, but before we do so, we need to know how to say which colour they are.

不好意思我还没做完这个翻译
(Sorry I've not finished translating this yet)

Visible light is made up of extremely tiny particles that behave like waves. To give you some idea how small those waves are, look at a ruler. It probably has centimetres (1/100 of a metre) and perhaps millimetres (1/1,000 of a metre). You may think a millimetre is small, but a light wave is between 1,500 and 2,500 times smaller---if you blew up a light wave to the size of a whole millimetre, your 30-centimetre ruler could stretch past the top of the Shanghai Tower, which is more than twice the height of the London Shard. And yet, most people's eyes can tell that not all light is the same wavelength (that's why I said it's "between" two numbers)---we see different wavelengths as different colours.

Every TV or monitor I ever saw can make no more than three different wavelengths---red, green and blue. You might remember last time you watched TV you saw many more colours than just red, green and blue---if it can make only three, why do we see more?

The answer is in the back of our eyes. Humans have 3 different types of colour-sensitive cells called cone cells. (Dogs have 2 types, some fish have 4 types, and some scientists think pigeons have 5 types but they're still working to prove it.) The 3 cones in humans can each start working for any light, but they work more for light close to a specific wavelength: one is most sensitive to red, another is most sensitive to green, and another is most sensitive to blue.

When a normally-sighted human sees orange light, both their red-sensitive cone cells and their green-sensitive cone cells start working, but because orange is closer to red than to green, the green-sensitive cones are working only about 64% as hard as the red-sensitive cones. So we can trick the person into thinking they are seeing orange if we give them 100% red light plus 64% green light, because this makes the cones work in the same proportion as they would with real orange. That's how a TV or monitor "makes" orange: it mixes 100% red with 64% green to make a "fake" orange that looks like the real thing to most people's eyes.

Now I put quotes around "makes" because it's not really true. Some people do say 100% red plus 64% green "makes orange", but that happens only in the brain of a normally-sighted human. Cats, dogs and fish see differently, and people with colour blindness see differently. And if you use a prism to break the TV's "orange" into parts, you'll see that in reality it's still red and green, unlike light from other sources. Your TV is tricking you into thinking you're seeing colours that aren't there!

To set a colour in Pygame, we need to tell Pygame what mixture of red, green and blue we need to fake the colour on a TV or monitor. We do this by giving Pygame three numbers, with 0 meaning "none of this colour" and 255 meaning "as much as possible of this colour"---the highest is 255 because that's the biggest number that can fit into 8 digits of the binary code that the computer uses to tell its graphics circuit what mixture to use. Pygame also uses American English spelling, so we have to write "colour" without the U (as a Brit I stubbornly continue to add the U in normal writing and drop it only when I have to for an American computer system). Here are some colour mixtures to get you started---you can experiment to find others:


import pygame
red    = pygame.Color(255,   0,   0)
orange = pygame.Color(255, 163,   0)
yellow = pygame.Color(255, 255,   0)
green  = pygame.Color(  0, 255,   0)
blue   = pygame.Color(  0,   0, 255)
cyan   = pygame.Color(  0, 255, 255)
pink   = pygame.Color(255, 200, 220)
white  = pygame.Color(255, 255, 255)
black  = pygame.Color(  0,   0,   0)
Questions:
  1. Change the __init__ part of the GameObject class, adding an extra parameter called colour, and say it's set to red if not given. Make it set self.colour = colour to keep it for later.
  2. Add a new method to the GameObject class called draw which will actually draw it on the screen. It can start with def draw(self): and one way to do it is pygame.draw.rect(display, colour, pygame.Rect(self.xDim.back, self.yDim.back, self.xDim.front-self.xDim.back, self.yDim.front-self.yDim.back)) but if you're clever you can make this a bit shorter (hint: can we set a temporary x and y first?)
  3. Add a new method to the GameObject class called erase which is like draw but erases the object by drawing over it in black (we'll need to do this before moving if we're not clearing the whole screen every time unit). Can you combine erase and draw so they both call a common service method with only the "colour or black" part changed?

Setting up the screen

We are now very close to putting something on screen. Here's how to get Pygame to open a nearly full-screen window and read off its height and width in dots: we will use * which means multiply (times, usually written × but that's hard to type so we use * in most programming languages), and we'll multiply by a decimal fraction less than 1 to make it smaller, but not too much less than 1.0 because we still want the window to take most of the screen (we just want to leave some space for desktop things around the edges so it's easier to quit if we get something wrong):

pygame.init()
screenW, screenH = pygame.display.get_desktop_sizes()[0]
screenW,screenH = screenW*0.9, screenH*0.8
display = pygame.display.set_mode((screenW,screenH))

Then, after putting in the ObjectDimension class (renamed from Boat), and the GameObject class (with the extra draw and erase methods from the above question), we can set the starting positions:


players = [
    GameObject(screenW*0.06, screenH*0.5,
               screenH*0.15, screenW*0.02,
               0, 0, yellow),
    GameObject(screenW*0.97, screenH*0.5,
               screenH*0.15, screenW*0.02,
               0, 0, blue),
    GameObject(screenW*0.5, screenH*0.97,
               screenH*0.02, screenW*0.15,
               0, 0, green)]
balls = [
    GameObject(screenW*0.5, screenH*0.5,
               screenH*0.02, screenH*0.02,
               screenW*0.001, screenH*0.0007)]
walls = [
    GameObject(1, screenH, screenH, 1), # left
    GameObject(screenW, 1, 1, screenW), # top
    GameObject(screenW, screenH, screenH, 1), # right
    GameObject(screenW, screenH, 1, screenW)] # bottom

everything = players + balls + walls
while True:
    for obj in everything:
        obj.erase()
        obj.move(everything)
        obj.draw()
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit() ; break
    pygame.display.update()
    pygame.time.wait(1)
Questions:
  1. Can you make the ball white instead of red just by adding ,white somewhere?
  2. Add a fourth player. (Which side has not yet been used? Can we base the fourth player on the third player but with a different starting place?)
  3. Add a second ball (start it in a different place)---look carefully at how commas work in lists
  4. Why do we need the walls? (If you're not sure, try taking them out and see what happens)
  5. If you fancy it, make an extra obstacle in the middle of the playing area by adding to walls

Moving the players' bats

We now have one or more balls bouncing off the walls of the screen and bouncing off the bats (and even perhaps bouncing off each other)---we didn't have to code for each of these bounces separately, because we have object orientation: we just wrote out how to handle one object and then had the computer do the same for all of them. But the bats still aren't moving---we've not yet added any code to let the players control them.

There are ways of asking the computer to tell us when someone types something on the keyboard, but "typing" something is not what we're interested in here. For one thing, some computers are set to different keyboard layouts---I often set mine to a layout called Dvorak that's easier on my wrists when I'm typing fast, and you might be using a computer that can be switched into a Chinese input method where several keys have to be pressed to get one character: imagine what could happen if that gets accidentally switched on during a game. And for another thing, if this game is going to be for 2, 3 or even 4 players all crowding around one keyboard, they won't manage to take it in turns to press one key at a time. So, this isn't normal typing: we need to go to the more basic level of "which actual keys are being held down" (possibly several at once).

Pygame sends us "events" to tell us what's happening. At the moment, we just check if event.type == pygame.QUIT to see if someone closed our window (which is very important to act on), but we can also check for pygame.KEYDOWN and pygame.KEYUP to find out when keys start to be pressed down, and when they spring back up (not being pressed down anymore).

When we get one of those, we need to find out which key it is, using special "key codes" or "scan codes" which can be different on different types of computer---but thankfully Pygame gives us some pre-set variables we can check against if we want to make sure our game will work on all the kinds of computer Pygame can work on.

(Scan codes are very flexible: you can even respond to keys like Ctrl and Shift, with the left-hand one being different from the right-hand one, if you want. Just remember to use the pre-set variables if you want to make sure your game works on other types of computer.)

When we set up the players, right now we're just setting the starting position, height, width, speed (all 0) and colour. Let's add four more things to each player: the keys to go up, down, left and right. Except two of the players can go only left and right, and the other two can go only up and down, so some of these things will be None. And we're getting rather a lot of things in the settings list for each player, so it'll be more readable if we add more thing= before each one to label what it is, which also helps us miss out stuff we don't want (like the starting speed, or the keys to move in directions we can't go):


players = [
    Player(x=screenW*0.06, y=screenH*0.5,
           height=screenH*0.15, width=screenW*0.02,
           colour=yellow,
           up=pygame.KSCAN_W, down=pygame.KSCAN_S),
    
    Player(x=screenW*0.97, y=screenH*0.5,
           height=screenH*0.15, width=screenW*0.02,
           colour=blue,
           up=pygame.KSCAN_UP, down=pygame.KSCAN_DOWN),
    
    Player(x=screenW*0.5, y=screenH*0.97,
           height=screenH*0.02, width=screenW*0.15,
           colour=green,
           left=pygame.KSCAN_J, right=pygame.KSCAN_K),

    Player(x=screenW*0.5, y=screenH*0.06,
           height=screenH*0.02, width=screenW*0.15,
           colour=cyan,
           left=pygame.KSCAN_F1, right=pygame.KSCAN_F2)]

The player on the right uses the up and down arrow keys, the player on the left uses W and S, the player at the bottom uses J and K and pity the player at the top who has to crowd around and use F1 and F2---feel free to change these if you have better suggestions: you can get a list of all Pygame scan codes by saying print('\n'.join(sorted(k for k,v in pygame.__dict__.items() if k.startswith("KSCAN"))))

If you run the above now, you'll get an error, because we changed GameObject into Player but we haven't yet said what a Player is. We need to say that a Player is a special kind of GameObject that doesn't just sit there like a wall or bounce around by itself like a ball---it gets controlled by the keyboard:


class Player(GameObject):
    def __init__(self, x, y, height, width, colour,
                 up=None, down=None, left=None, right=None):
        GameObject.__init__(self, x, y, height, width, 0, 0, colour)
        self.up, self.down = up, down
        self.left, self.right = left, right
    def check_keydown(self, scancode):
        if scancode==self.up:
            self.yDim.speed = -screenH*0.002
        if scancode==self.down:
            self.yDim.speed = +screenH*0.002
        if scancode==self.left:
            self.xDim.speed = -screenW*0.002
        if scancode==self.right:
            self.xDim.speed = +screenW*0.002
    def check_keyup(self, scancode):
        if scancode in [self.up, self.down, self.left, self.right]:
            self.xDim.speed = self.yDim.speed = 0
Questions:
  1. Will these bats move faster or slower than the ball? What do you need to change to change that?
  2. I don't like having to change the same number in 4 different places. Please fix the code so that it uses a variable that would need to be changed only once if we want to change the player speed.
  3. The last line has two equals signs in different places: what does that do?

It's not quite working yet because we still need to actually call our new check_keydown and check_keyup methods. Let's change the event handler so it looks like this:


    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:
            for p in players:
                p.check_keydown(event.scancode)
        if event.type == pygame.KEYUP:
            for p in players:
                p.check_keyup(event.scancode)
        if event.type == pygame.QUIT:
            pygame.quit() ; break

and you probably want to go and play it now so I won't put more questions here. Don't be surprised though if, when you try to run your bat into a wall, or even the ball or another bat, your bat might bounce off and start moving in the other direction until you release the key and press it again---that's because we gave every GameObject the "bounce" logic, even the players, so your bats will bounce off of things as well. If this isn't what we want, we can override the move method of Player (so it doesn't just take the one from GameObject but does something different) but that can be for later.

You might like to try adding a basic "computer player" that just keeps moving its bat from end to end---you can do that by putting the right speedX or speedY value into the GameObject.__init__ call and letting the bounce logic do the rest. You probably want to have a class ComputerPlayer that's a special type of Player (hint: check how we made Player a special type of GameObject---can we do that kind of thing again?) and just give it a new version of __init__ that puts in the speed. Doing it this way, you can even assign keys to the computer player so that it starts off being controlled by the computer but then a real person can take over by pressing its keys. Hopefully you're starting to see the power of object orientation now---just imagine how much more complicated it would have been if we'd had to write separate code for each player, wall and ball!

Keeping score

I wasn't sure how score was supposed to work in a 4-player bat-and-ball game, so I asked a 10-year-old and his suggestion was "the last player to hit a ball scores whenever it hits any wall" so let's code that.

(You see I get it that different people are well-practised at different things. I may have coded a network translator used by two enormous phone companies plus some stuff for the weather forecasts, but if the task is thinking up game rules, children are probably better than me at it.)

So we'll want to keep track of which player last hit the ball. As there might be more than one ball, let's say a ball can have a hidden label saying which player hit it.

Now, this might get slightly tricky because currently our actual "bounce" logic is in the move method of ObjectDimension (our old Boat class), and that thing doesn't even "know" which GameObject it's working for, let alone what thing it hit---it responds only to hitting something. But we can change it:

  1. Change the constructor (the __init__) of ObjectDimension to add an extra item after speed called controller. (Don't forget to say self.controller = controller below so it's kept for later.)
  2. In the __init__ of GameObject, add ,self after the speedX and speedY when constructing self.xDim and self.yDim. That'll make sure the X and the Y dimensions of a GameObject are able to refer back to their 'parent' GameObject via their self.controller.
  3. The line that starts if any(self.touching needs changing, because now we no longer just want to say "are we touching anything" but we want to know what things are being touched. Try writing it like this:
    
            touching = [b for b in allObjectDimensions
                        if self.touching(b)]
            if touching:
                for t in touching:
                    self.controller.touched(t.controller)
                    t.controller.touched(self.controller)
    
    and then the self.front -= self.speed as before (don't change the indentation of that part: it still goes inside the if touching block, not inside the for t in touching block).
  4. In class GameObject add a method def touched(self, otherObject): pass (the pass means do nothing for now---we just want to make sure everything has a touched method, to stop Python from saying there's no such method as touched when the ObjectDimension tries to call it on something).
  5. Run the game to check it still works. (It still won't do scoring, but we can at least check we didn't just make a mistake that's bad enough to crash it.)
  6. In class Player, write:
    
        def touched(self, otherObject):
            otherObject.last_played_by = self
    
    ---this will set last_played_by on any object a player touches (even another player or a wall), but that won't really matter because we'll check it only when it's on a ball.
  7. In the constructor (__init__) of class Player, put self.score = 0 (that'll make each player start with 0 points)
  8. Before the class Player, write class Goal(GameObject): pass and nothing else. That just says we want Goal to be a special type of GameObject, but we don't yet want to change any of the behaviour---we just want to be able to recognise if something is a goal when we hit it.
  9. Go to the part that sets up walls and change the four main GameObjects (top, bottom, left and right) into Goals. (If you added any extra walls in the middle, don't change those into Goals, just leave them as normal objects. And if you only want to play against one opponent, you might want to leave the top and bottom walls as normal objects so nobody scores by hitting those. Remember, a Goal is a special object that will cause the last person who hit the ball to score a point when the ball hits it---choose which objects are Goals carefully.)
  10. Make balls special---let me help you out with this one:
    
    class Ball(GameObject):
        def __init__(self,*a,**k):
            GameObject.__init__(self,*a,**k)
            self.last_played_by = None
        def touched(self, otherObject):
            if self.last_played_by and type(otherObject)==Goal:
                self.last_played_by.score += 1
                pygame.display.set_caption(
                    "-".join(f"{p.score}" for p in players))
    
    ---don't worry about the *a,**k stuff: it's a Python shorthand that lets us pass all the details about the new ball back up to the underlying GameObject without our having to fret about what those details are. And the set_caption part takes the score from each player and joins them together to put onto the window title---which is easier than putting them onto the game screen, because to do that we'd first need to learn about fonts, and I'm trying to get you up and running quickly so let's just use the window title as score for now. The window title does have the slight advantage that screen-reading software for blind people can read it out---we haven't yet made this game actually playable by blind people without assistance, but at least you can start a reader for a blind friend to know the score if you want.
  11. Don't forget to go to the balls setup and change the GameObject there into a Ball (if you have more than one ball, do this for all of them)

Extra challenge: by adding just one more line of code in the right place, make it so that, whenever a player hits a ball, the colour of that ball changes to the colour of the player's bat. (But do check that the other object really is a ball---we don't want to paint the walls or the other players here! Look at how we used type().)

Can you also add another one line to change a goal into the colour of the ball whenever a point is scored? (You might want to make the goals a bit thicker than 1 to see this more easily.)


All material © Silas S. Brown unless otherwise stated.