👾 Make a Space Game · สร้างเกมอวกาศ · 做一个太空游戏

Step by step, in Python. You will learn classes, __init__, and the entry point by building something you can play. · ทีละขั้นตอน ด้วย Python คุณจะได้เรียนคลาส __init__ และ entry point จากการสร้างเกมที่เล่นได้จริง · 用 Python 一步一步来。你会在做出一个能玩的游戏的过程中,学会类、__init__ 和入口点。

The finished game: a space background, a player ship at the bottom, three kinds of alien creep flying in from the left, and bullets
This is what you will build. The creeps fly in from the left. Each one has its own speed, path, hit points and fire rate. นี่คือสิ่งที่คุณจะสร้าง ศัตรูบินเข้ามาจากทางซ้าย แต่ละตัวมีความเร็ว เส้นทาง พลังชีวิต และอัตราการยิงเป็นของตัวเอง 这就是你要做的东西。敌人从左边飞进来,每一个都有自己的速度、路线、血量和开火频率。

🇬🇧 English

We build the game in small steps. After almost every step you can run it and see something new.

Do not copy the whole thing at the end. Type each step. When it breaks, read the error. That is how people really learn to code.

You need Python first. If you do not have it, do Start Python on a Mac before this page.

🇹🇭 ไทย

เราจะสร้างเกมทีละขั้นเล็ก ๆ หลังจบเกือบทุกขั้น คุณสั่งรันแล้วจะเห็นอะไรใหม่ ๆ

อย่าเพิ่งคัดลอกทั้งไฟล์ตอนท้าย ให้พิมพ์ทีละขั้น พอมันพัง ให้อ่าน error นั่นคือวิธีที่คนเรียนเขียนโค้ดกันจริง ๆ

คุณต้องมี Python ก่อน ถ้ายังไม่มี ให้ทำ บทเริ่มต้น Python บน Mac ก่อนหน้านี้

🇨🇳 中文

我们一小步一小步地做。几乎每一步做完,你都可以运行一下,看到新的东西。

不要等到最后才整个复制。一步一步地敲。出错了就读错误信息 —— 真正学会写代码就是这么学的。

你需要先装好 Python。还没装的话,先做在 Mac 上开始学 Python

0 Get ready · เตรียมตัว · 做好准备

🇬🇧 English

We use a Python library called pygame. It draws the window, the pictures and the sounds.

Open the Terminal. Install it one time:

Then make a folder for the game, and open a new file called space_invaders.py.

🇹🇭 ไทย

เราจะใช้ไลบรารีของ Python ชื่อ pygame มันวาดหน้าต่าง รูปภาพ และเสียงให้เรา

เปิด Terminal แล้วติดตั้งหนึ่งครั้ง

จากนั้นสร้างโฟลเดอร์สำหรับเกม และเปิดไฟล์ใหม่ชื่อ space_invaders.py

🇨🇳 中文

我们要用一个叫 pygame 的 Python 库。它负责画窗口、图片和声音。

打开终端,安装一次就好:

然后建一个放游戏的文件夹,新建一个文件 space_invaders.py

pip3 install pygame

mkdir ~/Documents/Code/space
cd ~/Documents/Code/space
vim space_invaders.py

1 The entry point · จุดเริ่มต้นของโปรแกรม · 程序的入口

🇬🇧 English

Every program needs a place to start. We call it the entry point.

We put our game inside a function called main. Then we tell Python to run it.

The last line looks strange. It means: run main() only when I start this file myself. If another file imports this one, main() does not run.

Write these five lines. This is a real program. It works.

🇹🇭 ไทย

ทุกโปรแกรมต้องมีจุดเริ่มต้น เราเรียกมันว่า entry point

เราเอาเกมไว้ในฟังก์ชันชื่อ main แล้วบอก Python ให้รันมัน

บรรทัดสุดท้ายดูแปลก มันแปลว่า ให้รัน main() เฉพาะตอนที่เราสั่งรันไฟล์นี้เอง ถ้าไฟล์อื่นมา import ไฟล์นี้ main() จะไม่ทำงาน

เขียนห้าบรรทัดนี้ นี่คือโปรแกรมจริง มันทำงานได้

🇨🇳 中文

每个程序都需要一个开始的地方。我们叫它入口点

我们把游戏放进一个叫 main 的函数里,然后告诉 Python 去运行它。

最后一行看起来很奇怪。它的意思是:只有当我自己运行这个文件时,才执行 main()。如果别的文件导入了这个文件,main() 不会运行。

写下这五行。这是一个真正的程序,它能跑。

space_invaders.py
def main():
    print("The game starts here!")


if __name__ == "__main__":
    main()
▶ RUN IT Type python3 space_invaders.py. You see one line of text. พิมพ์ python3 space_invaders.py คุณจะเห็นข้อความหนึ่งบรรทัด 输入 python3 space_invaders.py,你会看到一行文字。

2 Open a window · เปิดหน้าต่างเกม · 打开一个窗口

🇬🇧 English

Now we make the window. This is our canvas. We draw everything on it.

Three new ideas:

  • pygame.init() wakes pygame up.
  • set_mode makes the window. It gives us screen.
  • The while loop is the game loop. It runs about 60 times every second, forever, until you close the window.

Inside the loop we do the same three things every time: look at events, draw, show it.

🇹🇭 ไทย

ตอนนี้เราจะสร้างหน้าต่าง นี่คือ ผืนผ้าใบ ของเรา เราวาดทุกอย่างลงบนมัน

สามเรื่องใหม่

  • pygame.init() ปลุก pygame ให้ทำงาน
  • set_mode สร้างหน้าต่าง แล้วให้ screen กลับมา
  • ลูป while คือ game loop มันทำงานประมาณ 60 ครั้งต่อวินาที ไปเรื่อย ๆ จนกว่าคุณจะปิดหน้าต่าง

ในลูปเราทำสามอย่างเดิมทุกครั้ง ดู event วาด แสดงผล

🇨🇳 中文

现在我们来做窗口。这就是我们的画布,所有东西都画在它上面。

三个新东西:

  • pygame.init() 把 pygame 叫醒。
  • set_mode 创建窗口,并把 screen 交给我们。
  • while 循环就是游戏循环。它每秒大约运行 60 次,一直重复,直到你关掉窗口。

循环里每次都做同样的三件事:看事件显示出来

import pygame

WIDTH = 960
HEIGHT = 640


def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("Space Invaders")

    running = True
    while running:
        # 1. look at events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        # 2. draw
        screen.fill((10, 12, 30))

        # 3. show it
        pygame.display.flip()

    pygame.quit()


if __name__ == "__main__":
    main()
▶ RUN IT A dark blue window opens. Click the X to close it. หน้าต่างสีน้ำเงินเข้มจะเปิดขึ้นมา กดกากบาทเพื่อปิด 一个深蓝色的窗口打开了。点 X 关掉它。

3 Slow it down with a clock · ใส่นาฬิกาเพื่อคุมความเร็ว · 用时钟控制速度

🇬🇧 English

Right now the loop runs as fast as your computer can go. That is too fast, and the fan gets loud.

A clock keeps it at 60 frames every second. One turn of the loop is one frame.

Add two lines.

🇹🇭 ไทย

ตอนนี้ลูปทำงานเร็วเท่าที่เครื่องจะไหว ซึ่งเร็วเกินไป และพัดลมจะดัง

นาฬิกา จะคุมให้อยู่ที่ 60 เฟรมต่อวินาที ลูปหนึ่งรอบคือหนึ่ง เฟรม

เพิ่มสองบรรทัด

🇨🇳 中文

现在这个循环会用电脑的最快速度跑。太快了,风扇会很吵。

时钟把它控制在每秒 60 帧。循环转一圈就是一

加两行。

+ at the top of the file, with WIDTH and HEIGHT+ บนสุดของไฟล์ ตรงเดียวกับ WIDTH และ HEIGHT+ 放在文件最上面,和 WIDTH、HEIGHT 一起
FPS = 60
+ inside main(), next to set_mode+ ใน main() ข้าง ๆ set_mode+ 在 main() 里,set_mode 旁边
    clock = pygame.time.Clock()
+ the last line inside the while loop+ บรรทัดสุดท้ายในลูป while+ while 循环里的最后一行
        clock.tick(FPS)

4 Our first class: the player · คลาสแรกของเรา: ผู้เล่น · 第一个类:玩家

🇬🇧 English

A class is a plan for making things. From one plan we can make many things.

__init__ runs one time, when we make a new player. It gives the player its first numbers.

self means this one player. self.x is this player's x. Every method takes self first.

Our player has an x, a y, a speed, and 3 lives. That is all.

🇹🇭 ไทย

คลาส คือแบบแปลนสำหรับสร้างสิ่งของ จากแบบเดียวเราสร้างของได้หลายชิ้น

__init__ ทำงานหนึ่งครั้ง ตอนที่เราสร้างผู้เล่นใหม่ มันใส่ตัวเลขชุดแรกให้ผู้เล่น

self แปลว่า ผู้เล่นคนนี้ self.x คือค่า x ของผู้เล่นคนนี้ ทุกเมธอดต้องรับ self เป็นตัวแรก

ผู้เล่นของเรามี x, y, ความเร็ว และ 3 ชีวิต แค่นั้นเอง

🇨🇳 中文

是造东西的图纸。用一张图纸可以造出很多个东西。

__init__ 只在我们新建一个玩家时运行一次,给这个玩家最初的那些数字。

self 的意思是这一个玩家self.x 就是这个玩家的 x。每个方法的第一个参数都是 self

我们的玩家有 x、y、速度和 3 条命。就这些。

WHITE = (255, 255, 255)


class Player:
    def __init__(self):
        self.x = WIDTH // 2
        self.y = HEIGHT - 70
        self.speed = 6
        self.lives = 3
        self.score = 0

    def draw(self, screen):
        pygame.draw.rect(screen, WHITE, (self.x - 20, self.y - 10, 40, 20))
+ inside main(), before the while loop+ ใน main() ก่อนเข้าลูป while+ 在 main() 里,while 循环之前
    player = Player()
+ inside the while loop, after screen.fill(...)+ ในลูป while หลังบรรทัด screen.fill(...)+ 在 while 循环里,screen.fill(...) 之后
        player.draw(screen)
▶ RUN IT A white box sits at the bottom. That is your ship. กล่องสีขาวอยู่ด้านล่าง นั่นคือยานของคุณ 屏幕下方出现一个白色方块。那就是你的飞船。

5 Make the player move · ทำให้ผู้เล่นเคลื่อนที่ · 让玩家动起来

🇬🇧 English

We add a method called update. It looks at the keyboard and changes self.x.

The last two lines stop the ship leaving the screen.

🇹🇭 ไทย

เราเพิ่มเมธอดชื่อ update มันดูแป้นพิมพ์แล้วเปลี่ยนค่า self.x

สองบรรทัดสุดท้ายกันไม่ให้ยานออกนอกจอ

🇨🇳 中文

我们加一个叫 update 的方法。它看键盘,然后改变 self.x

最后两行让飞船不会跑出屏幕。

    def update(self, keys):
        if keys[pygame.K_LEFT]:
            self.x -= self.speed
        if keys[pygame.K_RIGHT]:
            self.x += self.speed

        if self.x < 20:
            self.x = 20
        if self.x > WIDTH - 20:
            self.x = WIDTH - 20
+ inside the while loop, before you draw+ ในลูป while ก่อนจะวาดภาพ+ 在 while 循环里,画之前
        keys = pygame.key.get_pressed()
        player.update(keys)
▶ RUN IT Press the LEFT and RIGHT arrow keys. The box moves. กดปุ่มลูกศรซ้ายและขวา กล่องจะเลื่อน 按左右方向键,方块会移动。

6 A class for bullets · คลาสสำหรับกระสุน · 子弹的类

🇬🇧 English

A bullet is very simple. It has a place, a speed and a colour.

A negative speed goes up. A positive speed goes down. We will use the same class for the player's bullets and the creeps' bullets. Only the number changes.

We keep all the bullets in a list. Each frame we move every bullet in the list, and throw away the ones that left the screen.

🇹🇭 ไทย

กระสุนง่ายมาก มันมีตำแหน่ง ความเร็ว และสี

ความเร็วเป็น ลบ คือขึ้นข้างบน เป็น บวก คือลงข้างล่าง เราจะใช้คลาสเดียวกันนี้ทั้งกระสุนผู้เล่นและกระสุนศัตรู เปลี่ยนแค่ตัวเลข

เราเก็บกระสุนทั้งหมดไว้ใน ลิสต์ ทุกเฟรมเราขยับกระสุนทุกนัดในลิสต์ แล้วทิ้งนัดที่ออกนอกจอไป

🇨🇳 中文

子弹非常简单。它有位置、速度和颜色。

速度是负数就往上飞,是正数就往下飞。玩家的子弹和敌人的子弹用同一个类,只是数字不同。

我们把所有子弹放在一个列表里。每一帧移动列表里的每颗子弹,再把飞出屏幕的扔掉。

YELLOW = (255, 220, 60)


class Bullet:
    def __init__(self, x, y, speed, colour):
        self.x = x
        self.y = y
        self.speed = speed          # up is negative, down is positive
        self.colour = colour

    def update(self):
        self.y += self.speed

    def is_gone(self):
        return self.y < -20 or self.y > HEIGHT + 20

    def rect(self):
        return pygame.Rect(self.x - 2, self.y - 8, 4, 16)

    def draw(self, screen):
        pygame.draw.rect(screen, self.colour, self.rect())
+ inside main(), before the while loop+ ใน main() ก่อนเข้าลูป while+ 在 main() 里,while 循环之前
    my_bullets = []
+ inside the event loop+ ในลูปที่อ่าน event+ 在处理事件的循环里
        for event in pygame.event.get():
            if event.type == pygame.QUIT:          # you already have this
                running = False
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                my_bullets.append(Bullet(player.x, player.y - 24, -10, YELLOW))
+ inside the while loop+ ในลูป while+ 在 while 循环里
        for bullet in my_bullets:
            bullet.update()
            bullet.draw(screen)
        my_bullets = [b for b in my_bullets if not b.is_gone()]
▶ RUN IT Press SPACE. Yellow bullets fly up. กด SPACE กระสุนสีเหลืองจะพุ่งขึ้น 按空格键,黄色子弹往上飞。

7 The creep class — the important one · คลาสของศัตรู — คลาสสำคัญ · 敌人的类 —— 最重要的一个

🇬🇧 English

Now the best part. Look at __init__. It takes six numbers:

  • y — its line across the screen.
  • speed — how fast it flies right.
  • path"straight" or "curve".
  • hp — hit points: 1, 2 or 3.
  • fire_rate — shoot every N frames. A small number means it shoots often.
  • image — its picture.

Every creep starts at x = -50. That is just off the left edge, so it flies in from outside the screen.

A curved creep uses math.sin. Sine goes up and down, up and down, forever. That makes a wave.

🇹🇭 ไทย

มาถึงส่วนที่ดีที่สุด ดูที่ __init__ มันรับตัวเลข หกตัว

  • y — เส้นทางแนวนอนของมัน
  • speed — บินไปทางขวาเร็วแค่ไหน
  • path"straight" หรือ "curve"
  • hp — พลังชีวิต 1, 2 หรือ 3
  • fire_rate — ยิงทุก ๆ กี่เฟรม ตัวเลขน้อยแปลว่ายิงบ่อย
  • image — รูปของมัน

ศัตรูทุกตัวเริ่มที่ x = -50 ซึ่งอยู่นอกขอบ ซ้าย พอดี มันจึงบินเข้ามาจากนอกจอ

ตัวที่บินโค้งใช้ math.sin ค่า sine จะขึ้นแล้วลง ขึ้นแล้วลง ไปเรื่อย ๆ ทำให้เกิดคลื่น

🇨🇳 中文

现在是最精彩的部分。看 __init__,它接收六个数字:

  • y —— 它横穿屏幕的那条线。
  • speed —— 往右飞得多快。
  • path —— "straight"(直线)或 "curve"(曲线)。
  • hp —— 血量:1、2 或 3。
  • fire_rate —— 每隔多少帧开一枪。数字小 = 开枪频繁。
  • image —— 它的图片。

每个敌人都从 x = -50 开始,也就是边缘外面一点,所以它是从屏幕外飞进来的。

走曲线的敌人用了 math.sin。正弦值会上上下下、来回摆动,这就画出了一条波浪。

import math
import random

RED = (255, 80, 80)


class Creep:
    def __init__(self, image, y, speed, path, hp, fire_rate):
        self.image = image
        self.x = -50                # every creep starts off the left edge
        self.start_y = y
        self.y = y
        self.speed = speed
        self.path = path            # "straight" or "curve"
        self.hp = hp                # 1, 2 or 3
        self.fire_rate = fire_rate  # small number = shoots often
        self.timer = random.randint(0, fire_rate)
        self.wave = 0.0

    def update(self):
        self.x += self.speed

        if self.path == "curve":
            self.wave += 0.04
            self.y = self.start_y + math.sin(self.wave) * 70

        self.timer += 1

    def wants_to_shoot(self):
        if self.timer >= self.fire_rate:
            self.timer = 0
            return True
        return False

    def is_gone(self):
        return self.x > WIDTH + 60

8 Many creeps, all different · ศัตรูหลายตัว ต่างกันหมด · 很多敌人,各不相同

🇬🇧 English

This is the whole idea of a class.

We wrote Creep one time. Now we make many creeps, and we give each one different numbers. Same class. Different behaviour.

One creep is slow, tough and flies straight. The next one is fast, weak and flies in a wave. We did not write new code for them. We only changed the numbers in __init__.

A new creep arrives every 45 frames — about every three quarters of a second.

🇹🇭 ไทย

นี่คือหัวใจของการใช้คลาส

เราเขียน Creep ครั้งเดียว แล้วสร้างศัตรูหลายตัว โดยให้ ตัวเลขต่างกัน คลาสเดียวกัน แต่พฤติกรรมต่างกัน

ตัวหนึ่งช้า อึด และบินตรง อีกตัวเร็ว บอบบาง และบินเป็นคลื่น เราไม่ได้เขียนโค้ดใหม่ให้มันเลย เราแค่เปลี่ยนตัวเลขใน __init__

ศัตรูตัวใหม่จะมาทุก 45 เฟรม ประมาณสามในสี่วินาที

🇨🇳 中文

这就是「类」的全部意义。

我们只写了一次 Creep。现在造出很多个敌人,每个给不同的数字。同一个类,不同的行为。

有的敌人慢、结实、走直线;下一个又快、又脆、走波浪线。我们没有为它们写新代码,只改了 __init__ 里的数字。

每 45 帧来一个新敌人 —— 大约每四分之三秒。

def make_creep(images):
    hp = random.choice([1, 1, 2, 2, 3])

    return Creep(
        image=images[hp],
        y=random.randint(70, HEIGHT // 2),
        speed=random.uniform(1.0, 3.2),
        path=random.choice(["straight", "curve"]),
        hp=hp,
        fire_rate=random.randint(70, 200),
    )
+ inside main(), before the while loop+ ใน main() ก่อนเข้าลูป while+ 在 main() 里,while 循环之前
    creeps = []
    spawn_timer = 0
+ inside the while loop+ ในลูป while+ 在 while 循环里
        spawn_timer += 1
        if spawn_timer >= 45:
            spawn_timer = 0
            creeps.append(make_creep(creep_images))

        for creep in creeps:
            creep.update()
            creep.draw(screen)
        creeps = [c for c in creeps if not c.is_gone()]
▶ RUN IT Creeps fly in from the left. Watch them. They all move differently. ศัตรูบินเข้ามาจากทางซ้าย ลองดูสิ ทุกตัวเคลื่อนที่ไม่เหมือนกัน 敌人从左边飞进来。仔细看,每一个的动法都不一样。

9 The creeps shoot back · ศัตรูยิงตอบ · 敌人开始还击

🇬🇧 English

Now fire_rate does its job.

Each frame we ask every creep: are you ready to shoot? A creep with fire_rate = 70 says yes often. A creep with fire_rate = 200 says yes rarely.

Their bullets have a positive speed, so they go down.

🇹🇭 ไทย

ตอนนี้ fire_rate ได้ทำงานของมันแล้ว

ทุกเฟรมเราถามศัตรูทุกตัวว่า พร้อมยิงหรือยัง ตัวที่ fire_rate = 70 จะตอบว่าพร้อมบ่อย ส่วนตัวที่ fire_rate = 200 จะนาน ๆ ครั้ง

กระสุนของศัตรูมีความเร็วเป็น บวก มันจึงพุ่งลงล่าง

🇨🇳 中文

现在轮到 fire_rate 发挥作用了。

每一帧我们问每个敌人:你准备好开枪了吗? fire_rate = 70 的敌人经常说「好了」,fire_rate = 200 的很少说。

它们的子弹速度是正数,所以往下飞。

+ inside main(), before the while loop+ ใน main() ก่อนเข้าลูป while+ 在 main() 里,while 循环之前
    their_bullets = []
+ in the while loop — replace the creep loop you already have+ ในลูป while — แทนที่ลูปของ creep ที่มีอยู่แล้ว+ 在 while 循环里 —— 替换你原来那段 creep 循环
        for creep in creeps:
            creep.update()
            if creep.wants_to_shoot():
                their_bullets.append(Bullet(creep.x, creep.y + 20, 5, RED))

10 Hits and hit points · การโดนยิงและพลังชีวิต · 命中与血量

🇬🇧 English

A Rect is a box. colliderect asks: do these two boxes touch?

When your bullet touches a creep, the creep loses one hit point. A creep with hp = 3 needs three hits. A creep with hp = 1 dies at once.

We use list(...) to make a copy. You must not remove things from a list while you walk through it.

🇹🇭 ไทย

Rect คือกล่องสี่เหลี่ยม ส่วน colliderect ถามว่า กล่องสองใบนี้ชนกันไหม

เมื่อกระสุนของคุณโดนศัตรู ศัตรูจะเสียพลังชีวิตไป หนึ่ง แต้ม ตัวที่ hp = 3 ต้องยิงสามครั้ง ส่วนตัวที่ hp = 1 ตายทันที

เราใช้ list(...) เพื่อทำสำเนา ห้ามลบของออกจากลิสต์ขณะที่กำลังวนอ่านลิสต์นั้นอยู่

🇨🇳 中文

Rect 就是一个方框。colliderect 问的是:这两个框碰到了吗?

你的子弹碰到敌人时,敌人掉点血。hp = 3 的敌人要打三下,hp = 1 的一下就没了。

我们用 list(...) 做一份副本。正在遍历一个列表的时候,不能同时从里面删东西。

+ inside the while loop, after everything has moved+ ในลูป while หลังจากทุกอย่างขยับแล้ว+ 在 while 循环里,所有东西都移动完之后
        for bullet in list(my_bullets):
            for creep in list(creeps):
                if bullet.rect().colliderect(creep.rect()):
                    my_bullets.remove(bullet)
                    creep.hp -= 1                  # take one hit point away
                    if creep.hp <= 0:
                        creeps.remove(creep)
                        player.score += 10
                    break

        for bullet in list(their_bullets):
            if bullet.rect().colliderect(player.rect()):
                their_bullets.remove(bullet)
                player.lives -= 1

11 Score, lives and game over · คะแนน ชีวิต และจบเกม · 分数、生命和游戏结束

🇬🇧 English

The player already has score and lives from __init__. Now we show them on the screen.

When lives reach zero, we set playing = False. The game still draws, but nothing moves. Press R and we make a new Player() — a fresh object, with 3 lives again.

🇹🇭 ไทย

ผู้เล่นมี score และ lives อยู่แล้วจาก __init__ ตอนนี้เราแค่เอามาแสดงบนจอ

เมื่อชีวิตเหลือศูนย์ เราตั้ง playing = False เกมยังวาดภาพอยู่ แต่ไม่มีอะไรขยับ กด R แล้วเราสร้าง Player() ตัวใหม่ เป็นออบเจกต์ใหม่ที่มี 3 ชีวิตอีกครั้ง

🇨🇳 中文

玩家在 __init__ 里已经有 scorelives 了。现在把它们显示在屏幕上。

生命归零时,我们把 playing 设为 False。画面还在画,但什么都不动了。按 R,我们就新建一个 Player() —— 一个全新的对象,又有 3 条命。

+ inside main(), next to set_mode+ ใน main() ข้าง ๆ set_mode+ 在 main() 里,set_mode 旁边
    font = pygame.font.SysFont(None, 30)
    playing = True
+ inside the while loop, when you draw+ ในลูป while ตอนวาดภาพ+ 在 while 循环里,画的时候
        screen.blit(font.render(f"Score {player.score}", True, WHITE), (16, 14))
        screen.blit(font.render(f"Lives {player.lives}", True, WHITE), (WIDTH - 110, 14))
+ just after you take a life away+ ตรงหลังจากที่หักชีวิตผู้เล่น+ 在扣掉一条命之后
        for bullet in list(their_bullets):         # you already have this
            if bullet.rect().colliderect(player.rect()):
                their_bullets.remove(bullet)
                player.lives -= 1
                if player.lives <= 0:              # add these two lines
                    playing = False
+ inside the event loop+ ในลูปที่อ่าน event+ 在处理事件的循环里
        for event in pygame.event.get():
            if event.type == pygame.QUIT:          # you already have this
                running = False
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_r and not playing:
                player = Player()                  # a brand new object
                creeps = []
                playing = True

12 Add the pictures · ใส่รูปภาพ · 加上图片

🇬🇧 English

Download the five pictures. Put them either in a folder called images next to your game file, or in the same folder as the game file. Our code looks in both places.

find_image_dir checks images first, then the folder itself. Downloads usually land next to the game, not in a subfolder.

If a picture is missing, load_image makes a coloured box instead. So the game always works. But it also tells you — a game that quietly shows boxes is confusing.

Load the pictures after set_mode. Before the window exists, pygame cannot prepare a picture.

🇹🇭 ไทย

ดาวน์โหลดรูปทั้งห้า แล้ววางไว้ หรือ ในโฟลเดอร์ชื่อ images ข้าง ๆ ไฟล์เกม หรือ ในโฟลเดอร์เดียวกับไฟล์เกมก็ได้ โค้ดของเราหาให้ทั้งสองที่

find_image_dir จะดูใน images ก่อน แล้วค่อยดูในโฟลเดอร์นั้นเอง เพราะไฟล์ที่ดาวน์โหลดมามักจะอยู่ข้าง ๆ ตัวเกม ไม่ได้อยู่ในโฟลเดอร์ย่อย

ถ้าหารูปไม่เจอ load_image จะสร้างกล่องสีแทน เกมจึงทำงานได้เสมอ แต่มันจะบอกคุณด้วย เพราะเกมที่ขึ้นกล่องสีเงียบ ๆ ทำให้สับสน

โหลดรูป หลัง set_mode เพราะถ้ายังไม่มีหน้าต่าง pygame จะเตรียมรูปไม่ได้

🇨🇳 中文

下载那五张图片。可以放进游戏文件旁边一个叫 images 的文件夹,也可以直接和游戏文件放在同一个文件夹里。我们的代码两个地方都会找。

find_image_dir 先看 images,再看文件夹本身。下载下来的文件通常就落在游戏旁边,而不是子文件夹里。

如果图片不在,load_image 就改画一个彩色方块,所以游戏永远能跑。但它也会告诉你 —— 一个默默显示方块的游戏很让人困惑。

要在 set_mode 之后加载图片。窗口还没建好时,pygame 没办法准备图片。

from pathlib import Path

PICTURES = ["background.png", "player.png", "creep1.png", "creep2.png", "creep3.png"]


def find_image_dir():
    here = Path(__file__).resolve().parent
    for folder in (here / "images", here):
        if (folder / "background.png").exists():
            return folder
    return here / "images"          # nothing found - report this one


IMAGE_DIR = find_image_dir()


def report_images():
    found = [n for n in PICTURES if (IMAGE_DIR / n).exists()]
    print(f"Looking for pictures in: {IMAGE_DIR}")
    print(f"Found {len(found)} of {len(PICTURES)}.")
    if len(found) < len(PICTURES):
        print("The game still works. It draws coloured boxes instead.")


def load_image(name, size, colour):
    path = IMAGE_DIR / name
    if path.exists():
        picture = pygame.image.load(str(path)).convert_alpha()
        return pygame.transform.smoothscale(picture, size)
    box = pygame.Surface(size, pygame.SRCALPHA)
    box.fill(colour)
    return box
+ the first line inside main()+ บรรทัดแรกใน main()+ main() 里的第一行
    report_images()
+ inside main(), straight after set_mode+ ใน main() ต่อจาก set_mode เลย+ 在 main() 里,紧跟着 set_mode
    background = load_image("background.png", (WIDTH, HEIGHT), DARK)
    player_image = load_image("player.png", (54, 54), WHITE)
    creep_images = {
        1: load_image("creep1.png", (48, 48), (90, 220, 230)),
        2: load_image("creep2.png", (56, 56), (240, 160, 60)),
        3: load_image("creep3.png", (64, 64), (190, 110, 230)),
    }
▶ RUN IT Run it. The first two lines say where it looked and how many it found. If it says Found 0 of 5, copy the .png files to the folder it names. สั่งรัน สองบรรทัดแรกจะบอกว่ามันหาที่ไหนและเจอกี่รูป ถ้าขึ้นว่า Found 0 of 5 ให้คัดลอกไฟล์ .png ไปไว้ในโฟลเดอร์ที่มันบอก 运行一下。开头两行会说它去哪里找、找到了几张。如果显示 Found 0 of 5,就把 .png 复制到它指出的那个文件夹。

13 The whole game · เกมทั้งหมด · 完整的游戏

🇬🇧 English

Here is the finished file, with everything joined together and tidied up. Download it and compare it with your own.

Put space_invaders.py in a folder. Make an images folder next to it. Put the five pictures inside. Then run it.

🇹🇭 ไทย

นี่คือไฟล์ที่เสร็จแล้ว รวมทุกอย่างเข้าด้วยกันและจัดให้เรียบร้อย ดาวน์โหลดไปเทียบกับของคุณเองได้

วาง space_invaders.py ไว้ในโฟลเดอร์ สร้างโฟลเดอร์ images ข้าง ๆ แล้วเอารูปทั้งห้าใส่ไว้ จากนั้นสั่งรัน

🇨🇳 中文

这是完成后的文件,所有东西都拼在一起并整理好了。下载下来,和你自己写的对照一下。

space_invaders.py 放进一个文件夹,旁边建一个 images 文件夹,把五张图片放进去,然后运行。

cd ~/Documents/Code/space
mkdir images          # put the five pictures in here
python3 space_invaders.py

14 Now change it · ลองแก้ดู · 现在动手改它

🇬🇧 English

The game is yours now. Change one number. Run it. See what happens.

Most of these change only the numbers we put into __init__. That is the lesson: the class stays the same, the numbers make the difference.

🇹🇭 ไทย

เกมนี้เป็นของคุณแล้ว ลองเปลี่ยนตัวเลขสักตัว แล้วสั่งรัน ดูว่าเกิดอะไรขึ้น

ส่วนใหญ่แค่เปลี่ยนตัวเลขที่เราใส่เข้าไปใน __init__ นี่แหละคือบทเรียน คลาสยังเหมือนเดิม แต่ตัวเลขทำให้ทุกอย่างต่างออกไป

🇨🇳 中文

这个游戏现在是你的了。改一个数字,运行一下,看看会发生什么。

下面大部分只是改我们传给 __init__ 的数字。这正是这一课的重点:类没有变,是数字造成了差别。

Try thisHowLevel
Make the creeps fasterIn make_creep, change random.uniform(1.0, 3.2) to (2.0, 6.0).easy
Make a creep with 5 hit pointsChange random.choice([1, 1, 2, 2, 3]) and add a picture for hp = 5.easy
Make the wave biggerIn Creep.update, change * 70 to * 150.easy
Give the player 5 livesOne number in Player.__init__.easy
Add a third pathAdd "zigzag" to path and write what it does in update.harder
Make hard creeps worth moreGive player.score 10 * creep.hp instead of 10.harder