Arcade-游戏编程教程-三-

196 阅读54分钟

Arcade 游戏编程教程(三)

原文:Program Arcade Games

协议:CC BY-NC-SA 4.0

十一、控制器和屏幕

我们如何使用键盘、鼠标或游戏控制器来移动物体?

A978-1-4842-1790-0_11_Figa_HTML.jpg

到目前为止,我们已经展示了如何在屏幕上制作动画,但没有展示如何与它们交互。我们如何使用鼠标、键盘或游戏控制器来控制屏幕上的动作?谢天谢地,这很容易。

首先,有一个可以在屏幕上移动的物体是必要的。最好的方法是用一个函数获取 x 和 y 坐标,然后在那个位置绘制一个对象。所以回到第十章!让我们来看看如何编写一个函数来绘制一个对象。

所有的 pygame 绘制函数都需要一个screen参数来让 Pygame 知道在哪个窗口上绘制。我们需要将它传递给我们创建的在屏幕上绘制对象的任何函数。

该函数还需要知道在屏幕上的何处绘制对象。这个函数需要一个 x 和 y,我们将这个位置作为参数传递给这个函数。下面是定义一个函数的示例代码,该函数在被调用时将绘制一个雪人:

def draw_snowman(screen, x, y):

# Draw a circle for the head

pygame.draw.ellipse(screen, WHITE, [35+x, 0+y, 25, 25])

# Draw the middle snowman circle

pygame.draw.ellipse(screen, WHITE, [23+x, 20+y, 50, 50])

# Draw the bottom snowman circle

pygame.draw.ellipse(screen, WHITE, [0+x, 65+y, 100, 100])

然后在主程序循环中,可以绘制多个雪人,如下图所示。

A978-1-4842-1790-0_11_Figb_HTML.jpg

由函数绘制的雪人

# Snowman in upper left

draw_snowman(screen, 10, 10)

# Snowman in upper right

draw_snowman(screen, 300, 10)

# Snowman in lower left

draw_snowman(screen, 10, 300)

完整的工作示例可从以下网址在线获得:

ProgramArcadeGames.com/python_examples/f.php?file=functions_and_graphics.py

很有可能,从先前的实验室中,你已经有了一个可以画出很酷的东西的代码。但是你怎么把它变成一个函数呢?让我们举一个画简笔画的代码的例子:

# Head

pygame.draw.ellipse(screen, BLACK, [96,83,10,10], 0)

# Legs

pygame.draw.line(screen, BLACK, [100,100], [105,110], 2)

pygame.draw.line(screen, BLACK, [100,100], [95,110], 2)

# Body

pygame.draw.line(screen, RED, [100,100], [100,90], 2)

# Arms

pygame.draw.line(screen, RED, [100,90], [104,100], 2)

pygame.draw.line(screen, RED, [100,90], [96,100], 2)

A978-1-4842-1790-0_11_Figc_HTML.jpg

人物线条画

通过添加一个函数def并缩进其下的代码,可以很容易地将这段代码放入函数中。我们需要引入该函数绘制简笔画所需的所有数据。我们需要screen变量来告诉函数在哪个窗口上绘制,以及在哪里绘制简笔画的 x 和 y 坐标。

但是我们不能在程序循环中间定义函数!这段代码应该从程序的主要部分删除。函数声明应该在程序的开始。我们需要将代码移到顶部。见图有助于形象化。

def draw_stick_figure(screen,x,y):

# Head

pygame.draw.ellipse(screen, BLACK, [96,83,10,10], 0)

# Legs

pygame.draw.line(screen, BLACK, [100,100], [105,110], 2)

pygame.draw.line(screen, BLACK, [100,100], [95,110], 2)

# Body

pygame.draw.line(screen, RED, [100,100], [100,90], 2)

# Arms

pygame.draw.line(screen, RED, [100,90], [104,100], 2)

pygame.draw.line(screen, RED, [100,90], [96,100], 2)

A978-1-4842-1790-0_11_Figd_HTML.jpg

制作一个功能并把它放在正确的位置

现在,这段代码接受一个 x 和 y 坐标。不幸的是,它实际上对它们没有任何作用。您可以指定任何想要的坐标;简笔画总是画在同一个精确的位置。不是很有用。下一个代码示例将 x 和 y 坐标添加到我们之前的代码中。

def draw_stick_figure(screen, x, y):

# Head

pygame.draw.ellipse(screen, BLACK,[96+x,83+y,10,10],0)

# Legs

pygame.draw.line(screen, BLACK, [100+x,100+y], [105+x,110+y], 2)

pygame.draw.line(screen, BLACK, [100+x,100+y], [95+x,110+y], 2)

# Body

pygame.draw.line(screen, RED, [100+x,100+y], [100+x,90+y], 2)

# Arms

pygame.draw.line(screen, RED, [100+x,90+y], [104+x,100+y], 2)

pygame.draw.line(screen, RED, [100+x,90+y], [96+x,100+y], 2)

但问题是,图形已经画出了离原点一定的距离。它假设原点为(0,0 ),并向下绘制简笔画超过大约 100 个像素。看下一张图以及简笔画是如何不在传入的(0,0)坐标处绘制的。

A978-1-4842-1790-0_11_Fige_HTML.jpg

人物线条画

通过在函数中添加 x 和 y,我们将棒图的原点移动该量。例如,如果我们调用:

draw_stick_figure(screen, 50, 50)

代码没有将简笔画放在(50,50)处。它将原点下移超过 50 个像素。因为我们的简笔画已经在大约(100,100)处被绘制,随着原点的移动,该图形大约是(150,150)。我们如何解决这个问题,使图形实际绘制在函数调用请求的地方?

A978-1-4842-1790-0_11_Figf_HTML.jpg

寻找最小的 X 和 Y 值

如上图所示,求最小的 x 值和最小的 y 值。然后从函数中的每个 x 和 y 中减去这个值。不要弄乱高度和宽度值。这里有一个例子,我们减去了最小的 x 和 y 值:

def draw_stick_figure(screen, x, y):

# Head

pygame.draw.ellipse(screen, BLACK,[96-95+x,83-83+y,10,10],0)

# Legs

pygame.draw.line(screen, BLACK, [100-95+x,100-83+y], [105-95+x,110-83+y], 2)

pygame.draw.line(screen, BLACK, [100-95+x,100-83+y], [95-95+x,110-83+y], 2)

# Body

pygame.draw.line(screen, RED, [100-95+x,100-83+y], [100-95+x,90-83+y], 2)

# Arms

pygame.draw.line(screen, RED, [100-95+x,90-83+y], [104-95+x,100-83+y], 2)

pygame.draw.line(screen, RED, [100-95+x,90-83+y], [96-95+x,100-83+y], 2)

或者,为了使程序更简单,自己做减法:

def draw_stick_figure(screen, x, y):

# Head

pygame.draw.ellipse(screen, BLACK, [1+x,y,10,10], 0)

# Legs

pygame.draw.line(screen, BLACK,[5+x,17+y], [10+x,27+y], 2)

pygame.draw.line(screen, BLACK, [5+x,17+y], [x,27+y], 2)

# Body

pygame.draw.line(screen, RED, [5+x,17+y], [5+x,7+y], 2)

# Arms

pygame.draw.line(screen, RED, [5+x,7+y], [9+x,17+y], 2)

pygame.draw.line(screen, RED, [5+x,7+y], [1+x,17+y], 2)

老鼠

太好了,现在我们知道如何写一个函数来画一个物体在特定的坐标。我们怎么得到那些坐标?最容易操作的是鼠标。获得坐标只需要一行代码:

pos = pygame.mouse.get_pos()

诀窍是坐标作为列表返回,或者更具体地说,作为不可修改的元组返回。x 和 y 值都存储在同一个变量中。所以如果我们做一个print(pos)我们会得到下图所示的结果。

A978-1-4842-1790-0_11_Figg_HTML.jpg

协调

变量pos是一个由两个数字组成的元组。x 坐标在数组的位置 0,y 坐标在位置 1。这些可以很容易地取出并传递给绘制该项目的函数:

# Game logic

pos = pygame.mouse.get_pos()

x = pos[0]

y = pos[1]

# Drawing section

draw_stick_figure(screen, x, y)

获取鼠标应该进入主程序循环的游戏逻辑部分。函数调用应该在主程序循环的绘图部分进行。

唯一的问题是鼠标指针画在简笔画的正上方,很难看到,如下图所示。

A978-1-4842-1790-0_11_Figh_HTML.jpg

鼠标光标位于顶部的简笔画

在主程序循环之前,可以使用下面的代码隐藏鼠标:

# Hide the mouse cursor

pygame.mouse.set_visible(False)

完整的工作示例可在此处找到:

ProgramArcadeGames.com/python_examples/f.php?file=move_mouse.py

键盘

用键盘控制有点复杂。我们不能只从鼠标中抓取 x 和 y。键盘没有给出 x 和 y,我们需要:

  • 为我们的起始位置创建一个初始 x 和 y。
  • 设置按下箭头键时每帧的像素速度。(按键)
  • 释放箭头键时,将速度重置为零。(击键)
  • 根据速度调整每帧的 x 和 y。

这看起来很复杂,但这就像我们之前做的弹跳矩形一样,只是速度是由键盘控制的。

首先,在主循环开始之前设置位置和速度:

# Speed in pixels per frame

x_speed = 0

y_speed = 0

# Current position

x_coord = 10

y_coord = 10

在程序的主while循环中,我们需要向事件处理循环中添加一些项目。除了寻找一个pygame.QUIT事件,程序还需要寻找键盘事件。用户每次按键都会生成一个事件。

当一个键被按下时,产生一个pygame.KEYDOWN事件。当用户放开某个键时,会生成一个pygame.KEYUP事件。当用户按下一个键时,速度向量被设置为每帧 3 或-3 像素。当用户放开一个键时,速度向量被重置为零。最后通过矢量调整对象的坐标,然后绘制对象。请参见下面的代码示例:

for event in pygame.event.get():

if event.type == pygame.QUIT:

done = True

# User pressed down on a key

elif event.type == pygame.KEYDOWN:

# Figure out if it was an arrow key. If so

# adjust speed.

if event.key == pygame.K_LEFT:

x_speed = -3

elif event.key == pygame.K_RIGHT:

x_speed = 3

elif event.key == pygame.K_UP:

y_speed = -3

elif event.key == pygame.K_DOWN:

y_speed = 3

# User let up on a key

elif event.type == pygame.KEYUP:

# If it is an arrow key, reset vector back to zero

if event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT:

x_speed = 0

elif event.key == pygame.K_UP or event.key == pygame.K_DOWN:

y_speed = 0

# Move the object according to the speed vector.

x_coord += x_speed

y_coord += y_speed

# Draw the stick figure

draw_stick_figure(screen, x_coord, y_coord)

有关完整示例,请参见:

ProgramArcadeGames.com/python_examples/f.php?file=move_keyboard.py

注意,这个例子并没有阻止角色离开屏幕边缘。为此,在游戏逻辑部分,需要一组if语句来检查x_coordy_coord的值。如果它们在屏幕的边界之外,则将坐标重置为边缘。具体的代码留给读者作为练习。

下表显示了 Pygame 中可以使用的键码的完整列表:

| Pygame Code(游戏代码) | 美国信息交换标准代码 | 普通名词 | | --- | --- | --- | | k _ 退格键 | \b | 退格键 | | k _ 返回 | \r | 返回 | | K_TAB | \t | 标签 | | K_ESCAPE | [ | 逃跑 | | k _ 空间 |   | 空间 | | k _ 逗号 | , | 逗号符号 | | k 减 | - | 负的 | | k _ 周期 | 。 | 句点斜线 | | k _ 斜线 | / | 向前 | | k0 | Zero | Zero | | k1 | one | one | | k2 | Two | Two | | k3 | three | three | | k4 | four | four | | K5 | five | five | | K6 | six | six | | k7 | seven | seven | | k8 | eight | eight | | K9 | nine | nine | | k _ 分号 | ; | 分号符号 | | k _ 等于 | = | 等号 | | k _ 左括号 | [ | 左边的 | | K_RIGHTBRACKET | ] | 正确 | | k _ 反斜杠 | \ | 反斜杠括号 | | k _ 反引号 | ` | 坟墓 | | K_a | a | a | | K_b | b | b | | K_c | c | c | | K_d | d | d | | K_e | e | e | | K_f | f | f | | K_g | g | g | | K_h | h | h | | K_i | 我 | 我 | | kj | j | j | | K_k | k | k | | K_l | l | l | | K_m | m | m | | K_n | n | n | | k0 | o | o | | K_p | p | p | | K_q | q | q | | K_r | r | r | | K_s | s | s | | K_t | t | t | | K_u | u | u | | K_v | v | v | | K_w | w | w | | K_x | x | x | | K_y | y | y | | kz | z | z | | k _ 删除 | 删除 |   | | K_KP0 | 小键盘 | Zero | | K_KP1 | 小键盘 | one | | K_KP2 | 小键盘 | Two | | K_KP3 | 小键盘 | three | | K_KP4 | 小键盘 | four | | K_KP5 | 小键盘 | five | | K_KP6 | 小键盘 | six | | K_KP7 | 小键盘 | seven | | K_KP8 | 小键盘 | eight | | K_KP9 | 小键盘 | 9 期 | | kkp 周期 | 。 | 键盘划分 | | K _ KP _ 除 | / | 键盘乘法 | | kkp 乘 | * | 键盘减号 | | K _ KP _ 减 | - | 键盘加号 | | S7-1200 可编程控制器 | + | 键盘输入 | | K_KP_ENTER | \r | 键盘等于 | | KP 等于 | = | 小键盘 | | K_UP | 起来 | 箭 | | K_DOWN | 向下 | 箭 | | 爵士 | 正确 | 箭 | | k _ 左 | 左边的 | 箭 | | k _ 插入 | 插入 |   | | K_HOME | 家 |   | | K_END | 目标 |   | | K_PAGEUP | 页,面,张,版 | 起来 | | K_PAGEDOWN | 页,面,张,版 | 向下 | | K_F1 | 子一代 |   | | K_F2 | 第二子代 |   | | K_F3 | 第三子代 |   | | F4 k | 法乐四联症 |   | | K_F5 | F5 |   | | F6 k | F6 |   | | F7 k | F7 |   | | F8 k | F8 |   | | K_F9 | F9 |   | | F10 k | F10 |   | | K_F11 | F11 |   | | K_F12 | F12 |   | | K_NUMLOCK | 键盘上的数字锁定键 |   | | K_CAPSLOCK | 帽锁 |   | | K_RSHIFT | 正确 | 变化 | | 克 _LSHIFT | 左边的 | 变化 | | S7-1200 可编程控制器 | 正确 | 计算机的 ctrl 按键 | | S7-1200 可编程控制器 | 左边的 | 计算机的 ctrl 按键 | | K_RALT | 正确 | 中高音 | | k _ lalt(消歧义) | 左边的 | 中高音 |

游戏控制器

游戏控制器需要一套不同的代码,但想法仍然很简单。

首先,检查计算机是否有操纵杆,并在使用前进行初始化。这应该只做一次。在主程序循环之前完成:

# Current position

x_coord = 10

y_coord = 10

# Count the joysticks the computer has

joystick_count = pygame.joystick.get_count()

if joystick_count == 0:

# No joysticks!

print("Error, I didn’t find any joysticks.")

else:

# Use joystick #0 and initialize it

my_joystick = pygame.joystick.Joystick(0)

my_joystick.init()

操纵杆将返回两个浮点值。如果操纵杆完全居中,它将返回(0,0)。如果操纵杆向左完全抬起,它将返回(-1,-1)。如果操纵杆向下向右,它将返回(1,1)。如果操纵杆介于两者之间,数值会相应地缩放。从下图开始查看控制器图像,了解其工作原理。

A978-1-4842-1790-0_11_Figi_HTML.jpg

居中(0,0)

A978-1-4842-1790-0_11_Figj_HTML.jpg

左上(-1,-1)

A978-1-4842-1790-0_11_Figk_HTML.jpg

向上(0,-1)

A978-1-4842-1790-0_11_Figl_HTML.jpg

向右上方(1,-1)

A978-1-4842-1790-0_11_Figm_HTML.jpg

右(1,0)

A978-1-4842-1790-0_11_Fign_HTML.jpg

右下(1,1)

A978-1-4842-1790-0_11_Figo_HTML.jpg

向下(0,1)

A978-1-4842-1790-0_11_Figp_HTML.jpg

左下方(-1,1)

A978-1-4842-1790-0_11_Figq_HTML.jpg

向左(-1,0)

在主程序循环中,操纵杆返回的值可能会根据对象应该移动的距离而增加。在下面的代码中,在一个方向上完全移动操纵杆会使其每帧移动 10 个像素,因为操纵杆值被乘以 10。

# This goes in the main program loop!

# As long as there is a joystick

if joystick_count != 0:

# This gets the position of the axis on the game controller

# It returns a number between -1.0 and +1.0

horiz_axis_pos = my_joystick.get_axis(0)

vert_axis_pos = my_joystick.get_axis(1)

# Move x according to the axis. We multiply by 10 to speed up the movement.

# Convert to an integer because we can’t draw at pixel 3.5, just 3 or 4.

x_coord = x_coord + int(horiz_axis_pos * 10)

y_coord = y_coord + int(vert_axis_pos * 10)

# Clear the screen

screen.fill(WHITE)

# Draw the item at the proper coordinates

draw_stick_figure(screen, x_coord, y_coord)

有关完整示例,请参见

ProgramArcadeGames.com/python_examples/f.php?file=move_game_controller.py

控制器有很多操纵杆、按钮,甚至帽子开关。下面是一个示例程序和屏幕截图,它将所有内容打印到屏幕上,显示每个游戏控制器正在做什么。注意,游戏控制器必须在程序启动前插上电源,否则程序无法检测到它们。

A978-1-4842-1790-0_11_Figr_HTML.jpg

操纵杆调用程序

"""

Sample Python/Pygame Programs

http://programarcadegames.com/

Show everything we can pull off the joystick

"""

import pygame

# Define some colors

BLACK = (0, 0, 0)

WHITE = (255, 255, 255)

class TextPrint(object):

"""

This is a simple class that will help us print to the screen

It has nothing to do with the joysticks, just outputting the

information.

"""

def __init__(self):

""" Constructor """

self.reset()

self.x_pos = 10

self.y_pos = 10

self.font = pygame.font.Font(None, 20)

def print(self, my_screen, text_string):

""" Draw text onto the screen. """

text_bitmap = self.font.render(text_string, True, BLACK)

my_screen.blit(text_bitmap, [self.x_pos, self.y_pos])

self.y_pos += self.line_height

def reset(self):

""" Reset text to the top of the screen. """

self.x_pos = 10

self.y_pos = 10

self.line_height = 15

def indent(self):

""" Indent the next line of text """

self.x_pos += 10

def unindent(self):

""" Unindent the next line of text """

self.x_pos -= 10

pygame.init()

# Set the width and height of the screen [width,height]

size = [500, 700]

screen = pygame.display.set_mode(size)

pygame.display.set_caption("My Game")

# Loop until the user clicks the close button.

done = False

# Used to manage how fast the screen updates

clock = pygame.time.Clock()

# Initialize the joysticks

pygame.joystick.init()

# Get ready to print

textPrint = TextPrint()

# -------- Main Program Loop -----------

while not done:

# EVENT PROCESSING STEP

for event in pygame.event.get():

if event.type == pygame.QUIT:

done = True

# Possible joystick actions: JOYAXISMOTION JOYBALLMOTION JOYBUTTONDOWN

# JOYBUTTONUP JOYHATMOTION

if event.type == pygame.JOYBUTTONDOWN:

print("Joystick button pressed.")

if event.type == pygame.JOYBUTTONUP:

print("Joystick button released.")

# DRAWING STEP

# First, clear the screen to white. Don’t put other drawing commands

# above this, or they will be erased with this command.

screen.fill(WHITE)

textPrint.reset()

# Get count of joysticks

joystick_count = pygame.joystick.get_count()

textPrint.print(screen, "Number of joysticks: {}".format(joystick_count))

textPrint.indent()

# For each joystick:

for i in range(joystick_count):

joystick = pygame.joystick.Joystick(i)

joystick.init()

textPrint.print(screen, "Joystick {}".format(i))

textPrint.indent()

# Get the name from the OS for the controller/joystick

name = joystick.get_name()

textPrint.print(screen, "Joystick name: {}".format(name))

# Usually axis run in pairs, up/down for one, and left/right for

# the other.

axes = joystick.get_numaxes()

textPrint.print(screen, "Number of axes: {}".format(axes))

textPrint.indent()

for i in range(axes):

axis = joystick.get_axis(i)

textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis))

textPrint.unindent()

buttons = joystick.get_numbuttons()

textPrint.print(screen, "Number of buttons: {}".format(buttons))

textPrint.indent()

for i in range(buttons):

button = joystick.get_button(i)

textPrint.print(screen, "Button {:>2} value: {}".format(i, button))

textPrint.unindent()

# Hat switch. All or nothing for direction, not like joysticks.

# Value comes back in an array.

hats = joystick.get_numhats()

textPrint.print(screen, "Number of hats: {}".format(hats))

textPrint.indent()

for i in range(hats):

hat = joystick.get_hat(i)

textPrint.print(screen, "Hat {} value: {}".format(i, str(hat)))

textPrint.unindent()

textPrint.unindent()

# ALL CODE TO DRAW SHOULD GO ABOVE THIS COMMENT

# Go ahead and update the screen with what we’ve drawn.

pygame.display.flip()

# Limit to 60 frames per second

clock.tick(60)

pygame.quit()

回顾

多项选择测验

What code will draw a circle at the specified x and y locations? def draw_circle(screen, x, y): pygame.draw.ellipse(screen, WHITE, [x, y, 25, 25])   def draw_circle(screen,x,y): pygame.draw.ellipse(screen, WHITE, [x, y, 25 + x, 25 + y])   def draw_circle(screen, x, y): pygame.draw.ellipse(screen, WHITE, [0, 0, 25 + x, 25 + y])     The following code draws an “X.” What would the code look like if it was moved from the main program loop to a function, with the ability to specify how the coordinates of X appear? pygame.draw.line(screen, RED, [80, 80], [100, 100], 2) pygame.draw.line(screen, RED, [80, 100], [100, 80], 2) def draw_x(screen, x, y):     pygame.draw.line(screen, RED, [80, 80], [100, 100], 2)     pygame.draw.line(screen, RED, [80, 100], [100, 80], 2)   def draw_x(screen, x, y):     pygame.draw.line(screen, RED, [80+x, 80+y], [100, 100], 2)     pygame.draw.line(screen, RED, [80+x, 100+y], [100, 80], 2)   def draw_x(screen, x, y):     pygame.draw.line(screen, RED, [x, y], [20+x, 20+y], 2)     pygame.draw.line(screen, RED, [x, 20+y], [20+x, y], 2)   def draw_x(screen, x, y):     pygame.draw.line(screen, RED, [x, y], [20, 20], 2)     pygame.draw.line(screen, RED, [x, 20+y], [20, 0], 2)   def draw_x(screen, x, y):     pygame.draw.line(screen, RED, [80+x, 80+y], [100+x, 100+y], 2)     pygame.draw.line(screen, RED, [80+x, 100+y], [100+x, 80+y], 2)     code will get the x and y position of the mouse? pos = pygame.mouse.get_pos() x = pos[0] y = pos[1]   pos = pygame.mouse.get_pos() x = pos[x] y = pos[y]   pos = pygame.mouse.get_pos() x = pos(x) y = pos(y)   x = pygame.mouse.get_pos(x) y = pygame.mouse.get_pos(y)   x = pygame.mouse.get_pos(0) y = pygame.mouse.get_pos(1)     In the keyboard example, if x_speed and y_speed were both set to 3, then: The object would be set to location (3, 3).   The object would move down and to the right at 3 pixels per frame.   The object would move down and to the right at 3 pixels per second.   The object would move up and to the right at 3 pixels per second.   The object would move up and to the left 3 pixels per frame.     The call axes = joystick.get_numaxes() will return how many axes for a game controller? 2   4   One for each analog joystick on the game controller.   Two for each analog joystick on the game controller.   One for each button on the game controller.     Depending on the button state, what value will the variable button be assigned using this code? button = joystick.get_button( 0 ) 0 or 1   On or Off   Up or Down   True or False     What is the difference between a hat on a game controller and a joystick? Nothing, they are just different names for the same thing.   A hat can be moved in small amounts; an analog joystick is all or nothing.   An analog joystick can be moved in small amounts; a hat is all or nothing.     What axis values will be returned when the joystick is moved up and to the left? (-1, -1)   (1, 1)   (0, 0)     What axis values will be returned when the joystick is centered? (-1, -1)   (1, 1)   (0, 0)     What code would move an object based on the position of the joystick on the game controller? horiz_axis_pos = my_joystick.get_axis(0) vert_axis_pos = my_joystick.get_axis(1)   x_coord = int(x_coord + horiz_axis_pos * 10) y_coord = int(y_coord + vert_axis_pos * 10)   x_coord = my_joystick.get_axis(0) y_coord = my_joystick.get_axis(1)   x_coord = my_joystick.get_axis(0)*10 y_coord = my_joystick.get_axis(1)*10    

简答工作表

What’s wrong with this code that uses a function to draw a stick figure? Assume the colors are already defined and the rest of the program is OK. What is wrong with the code in the function? def draw_stick_figure(screen, x, y):     # Head     pygame.draw.ellipse(screen, BLACK, [96,83,10,10], 0)     # Legs     pygame.draw.line(screen, BLACK, [100,100], [105,110], 2)     pygame.draw.line(screen, BLACK, [100,100], [95,110], 2)     # Body     pygame.draw.line(screen, RED, [100,100], [100,90], 2)     # Arms     pygame.draw.line(screen, RED, [100,90], [104,100], 2)     pygame.draw.line(screen, RED, [100,90], [96,100], 2)   Show how to only grab the x coordinate of where the mouse is.   Why is it important to keep the event processing loop together and only have one of them? It is more than organization; there will be subtle hard-to-detect errors. What are they and why will they happen without the event processing loop together? (Review “The Event Processing Loop” in Chapter 5 if needed.)   When we created a bouncing rectangle, we multiplied the speed times -1 when the rectangle hit the edge of the screen. Explain why that technique won’t work for moving an object with the keyboard.   Why does movement with the keyboard or game controller need to have a starting x, y location, but the mouse doesn’t?   What values will a game controller return if it is held all the way down and to the right?  

锻炼

请查看本章附带的练习“功能和用户控制”的附录。

十二、位图图形和声音

为了超越绘制圆形和矩形所提供的简单形状,我们的程序需要处理位图图形的能力。位图图形可以是从绘图程序创建和保存的照片或图像。

但是图形是不够的。游戏也需要声音!这一章展示了如何在你的游戏中加入图像和声音。

将程序存储在文件夹中

到目前为止,我们制作的程序只涉及一个文件。现在我们包括图像和声音,有更多的文件是我们程序的一部分。很容易将这些文件与我们正在制作的其他程序混在一起。保持一切整洁和独立的方法是将这些程序放入各自的文件夹中。在开始任何这样的项目之前,点击“新建文件夹”按钮,并使用该新文件夹作为存放所有新文件的地方,如下图所示。

A978-1-4842-1790-0_12_Figa_HTML.jpg

创建新文件夹

设置背景图像

需要为你的游戏设置背景图片?找到如下图所示的图像。如果你在网络浏览器上在线浏览,你通常可以右键点击一张图片,然后把它保存到电脑上。将图像保存到我们刚刚为游戏创建的文件夹中。

确保不使用有版权的图片!使用反向图像搜索可以很容易地再次确认你没有复制它。

A978-1-4842-1790-0_12_Figb_HTML.jpg

背景图像

游戏中使用的任何位图图像都应该已经根据它在屏幕上的显示方式进行了调整。不要从高分辨率相机拍摄 5000x5000 像素的图像,然后试图将其加载到只有 800x600 的窗口中。在 Python 程序中使用图像之前,使用图形程序(甚至 MS Paint 也可以)并调整图像大小/裁剪图像。

加载图像是一个简单的过程,只需要一行代码。在那一行代码中发生了很多事情,所以对这一行的解释将分为三个部分。我们的load命令的第一个版本将加载一个名为saturn_family1.jpg的文件。该文件必须位于 Python 程序所在的同一目录中,否则计算机将找不到它:

pygame.image.load("saturn_family1.jpg")

该代码可以加载图像,但是我们没有办法引用和显示该图像!我们需要一个与load()命令返回的值相等的变量集。在 load 命令的下一个版本中,我们创建了一个名为background_image的新变量。第二版见下文:

background_image = pygame.image.load("saturn_family1.jpg")

最后,需要将图像转换成 pygame 更容易处理的格式。为此,我们将.convert()添加到命令中来调用 convert 函数。函数.convert()Image类中的一个方法。我们将在第十三章中详细讨论类、对象和方法。

所有的图片都应该使用类似下面的代码来加载。只需根据需要更改变量名和文件名。

background_image = pygame.image.load("saturn_family1.jpg").convert()

加载图像应该在主程序循环之前完成。虽然有可能在主程序循环中加载它,但这将导致程序每秒钟从磁盘获取图像 20 次左右。这完全没有必要。只需要在程序启动时做一次。

使用blit命令显示图像。这将图像位传送到屏幕上。在第六章中,我们已经在游戏窗口显示文本时使用过这个命令。

blit命令是screen变量中的一个方法,所以我们需要通过screen.blit来启动我们的命令。接下来,我们需要将图像传递给 blit,以及在哪里 blit 它。这个命令应该在循环内部执行,这样图像就可以在每一帧中绘制出来。见下文:

screen.blit(background_image, [0, 0])

这段代码从(0,0)开始将保存在background_image中的图像传送到屏幕上。

移动图像

现在我们想加载一个图像并在屏幕上移动它。我们将从一艘简单的橙色宇宙飞船开始。你可以从 http://kenney.nl/ 那里得到这个和许多其他伟大的资产。见下图。这艘船的图片可以从这本书的网站上下载,或者你也可以在。你喜欢的白色或黑色背景的 png。不要用. jpg。

A978-1-4842-1790-0_12_Figc_HTML.jpg

玩家形象

为了加载图像,我们需要使用与背景图像相同的命令。在这种情况下,我假设文件保存为player.png

player_image = pygame.image.load("player.png").convert()

在主程序循环中,检索鼠标坐标,并将其传递给另一个blit函数作为绘制图像的坐标:

# Get the current mouse position. This returns the position

# as a list of two numbers.

player_position = pygame.mouse.get_pos()

x = player_position[0]

y = player_position[1]

# Copy image to screen:

screen.blit(player_image, [x, y])

这说明了一个问题。该图像是一艘带有纯黑背景的宇宙飞船。所以当图像被绘制时,程序显示下图。

A978-1-4842-1790-0_12_Figd_HTML.jpg

这张图片的纯黑背景很明显

我们只要飞船,不要长方形背景!但是我们可以加载的所有图像都是矩形,那么我们如何只显示图像中我们想要的部分呢?解决这个问题的方法是告诉程序让一种颜色透明而不显示。这可以在装载后立即完成。下面的代码使黑色(假设黑色已经被定义为一个变量)透明:

player_image.set_colorkey(BLACK)

这适用于大多数以结尾的文件。这对于大多数人来说并不太好。jpg 文件。jpeg 图像格式非常适合保存照片,但作为使图像变小的算法的一部分,它确实会微妙地改变图像。图像输入。gif 和。png 也是压缩的,但是这些格式中使用的算法不会改变图像。格式。bmp 根本没有压缩,它会产生巨大的文件。因为。jpg 格式改变了格式,这意味着不是所有的背景颜色都会完全一样。在下一张图中,飞船被保存为白色背景的 jpeg 格式。船周围的白色并不完全是(255,255,255),而是非常接近。

A978-1-4842-1790-0_12_Fige_HTML.jpg

JPEG 压缩伪像

如果你要挑选一个透明的图像,选择. gif 或. png。这些是图形艺术类型图像的最佳格式。照片应该是. jpg。请记住,仅仅通过将文件扩展名重命名为. png 是不可能将. jpg 更改为另一种格式的。即使您将其命名为其他格式,它仍然是. jpg。它需要在图形程序中进行转换,以将其转换为不同的格式。但是一旦转换成. jpg 格式,它就被修改了,转换成. png 格式不会修复这些修改。

这里有三个很好的地方可以找到在您的程序中使用的免费图像:

声音

在这一节中,我们将在点击鼠标按钮时播放激光声音。这个声音最初来自 Kenney.nl,你可以在这里下载并保存声音:ProgramArcadeGames.com/python_examples/en/laser5.ogg

像图像一样,声音在使用前必须加载。这应该在主程序循环之前进行一次。以下命令加载一个声音文件,并创建一个名为click_sound的变量来引用它:

click_sound = pygame.mixer.Sound("laser5.ogg")

我们可以使用以下命令来播放声音:

click_sound.play()

但是我们把这个命令放在哪里呢?如果我们把它放在主程序循环中,它将每秒播放 20 次左右。真的很烦。我们需要一个触发器。一些动作发生,然后我们播放声音。例如,当用户用下面的代码点击鼠标按钮时,可以播放这个声音:

for event in pygame.event.get():

if event.type == pygame.QUIT:

done = True

elif event.type == pygame.MOUSEBUTTONDOWN:

click_sound.play()

未压缩的声音文件通常以.wav结尾。这些文件比其他格式的文件大,因为没有对它们运行算法来使它们变小。还有曾经流行的.mp3格式,尽管这种格式有专利,可能使它不适合某些应用。另一种可以免费使用的格式是以.ogg结尾的 OGG Vorbis 格式。

Pygame 并不播放互联网上能找到的所有.wav文件。如果你有一个不工作的文件,你可以试着用程序 http://sourceforge.net/projects/audacity 把它转换成以.ogg结尾的 ogg-vorbis 类型的声音文件。这种文件格式对于 pygame 来说很小也很可靠。

如果你想在你的节目中播放背景音乐,请查看在线示例部分:

ProgramArcadeGames.com/en/python_examples/f.php?file=background_music.py

请注意,您不能将受版权保护的音乐与您的程序一起再分发。即使你制作了一个以受版权保护的音乐为背景的视频,YouTube 和类似的视频网站也会标记你侵犯版权。

找到在您的程序中使用的免费声音的好地方:

完整列表

"""

Sample Python/Pygame Programs

http://programarcadegames.com/

Explanation video:http://youtu.be/4YqIKncMJNs

Explanation video:http://youtu.be/ONAK8VZIcI4

Explanation video:http://youtu.be/_6c4o41BIms

"""

import pygame

# Define some colors

WHITE = (255, 255, 255)

BLACK = (0, 0, 0)

# Call this function so the Pygame library can initialize itself

pygame.init()

# Create an 800x600 sized screen

screen = pygame.display.set_mode([800, 600])

# This sets the name of the window

pygame.display.set_caption(’CMSC 150 is cool’)

clock = pygame.time.Clock()

# Before the loop, load the sounds:

click_sound = pygame.mixer.Sound("laser5.ogg")

# Set positions of graphics

background_position = [0, 0]

# Load and set up graphics.

background_image = pygame.image.load("saturn_family1.jpg").convert()

player_image = pygame.image.load("playerShip1_orange.png").convert()

player_image.set_colorkey(BLACK)

done = False

while not done:

for event in pygame.event.get():

if event.type == pygame.QUIT:

done = True

elif event.type == pygame.MOUSEBUTTONDOWN:

click_sound.play()

# Copy image to screen:

screen.blit(background_image, background_position)

# Get the current mouse position. This returns the position

# as a list of two numbers.

player_position = pygame.mouse.get_pos()

x = player_position[0]

y = player_position[1]

# Copy image to screen:

screen.blit(player_image, [x, y])

pygame.display.flip()

clock.tick(60)

pygame.quit()

回顾

多项选择测验

Should the following line go inside, or outside of the main program loop? background_image = pygame.image.load("saturn_family1.jpg").convert() Outside the loop, because it isn’t a good idea to load the image from the disk 20 times per second.   Inside the loop, because the background image needs to be redrawn every frame.     In the following code, what does the [0, 0] do? screen.blit(background_image, [0, 0]) Default dimensions of the bitmap.   Specifies the x and y of the top left coordinate of where to start drawing the bitmap on the screen.   Draw the bitmap in the center of the screen.     Should the following line go inside or outside of the main program loop? screen.blit(background_image, [0, 0]) Outside the loop, because it isn’t a good idea to load the image from the disk 20 times per second.   Inside the loop, because the background image needs to be redrawn every frame.     Given this line of code, what code will get the x value of the current mouse position? player_position = pygame.mouse.get_pos() x = player_position[x]   x = player_position[0]   x = player_position.x   x[0] = player_position     What types of image file formats are loss-less (i.e., they do not change the image)? Choose the best answer. png, jpg, gif   png, gif   png   jpg   gif   jpg, gif     What does this code do? player_image.set_colorkey(WHITE) Makes the bitmap background white.   Sets all the white pixels to be transparent instead.   Sets the next color to be drawn to white.   Clears the screen to a white color.   Draws the player image in white.     What is wrong with section of code? for event in pygame.event.get():     if event.type == pygame.QUIT:         done=True     if event.type == pygame.MOUSEBUTTONDOWN:         click_sound = pygame.mixer.Sound("click.wav")         click_sound.play() Pygame doesn’t support .wav files.   The colorkey hasn’t been set for click_sound yet.   Sounds should be loaded at the start of the program, not in the main program loop.   Sounds should not be played in a main program loop.    

简答工作表

This is about the time that many people learning to program run into problems with Windows hiding file extensions. Briefly explain how to make Windows show file extensions. If you don’t remember, go back to Chapter 1 to see the details.   For the following file extensions:

  • 。使用 jpeg 文件交换格式存储的编码图像文件扩展名
  • 。声音资源文件
  • 。可交换的图像格式
  • 。巴布亚新几内亚
  • 。格式
  • 。位图文件的扩展名
  • . mp3

…match the extension to the category it best fits:

  • 照片
  • 图形艺术
  • 未压缩图像
  • 歌曲和音效
  • 未压缩的声音

  Should an image be loaded inside the main program loop or before it? Should the program blit the image in the main program loop or before it?   How can a person change an image from one format to another? For example, how do you change a .jpg to a .gif? Why does changing the file extension not really work? (Ask if you can’t figure it out.)   Explain why an image that was originally saved as a .jpg doesn’t work with setting a background color even after it is converted to a .png.   Briefly explain how to play background music in a game and how to automatically start playing a new song when the current song ends.  

锻炼

查看附录中本章附带的练习“位图图形和用户控制”。

十三、类简介

类和对象是非常强大的编程工具。它们使编程更容易。事实上,您已经熟悉了类和对象的概念。类是对象的分类。比如人或者形象。对象是一个类的特定实例。就像玛丽是一个人的实例。

对象有属性,比如一个人的名字、身高和年龄。对象也有方法。方法定义了一个对象能做什么:比如跑、跳或坐。

为什么要学习课程?

冒险游戏中的每个角色都需要数据:名字、位置、实力;他们举起他们的手臂;他们朝哪个方向前进;等等。再加上那些角色做事。他们跑,跳,打,和说话。

如果没有类,我们存储这些数据的 Python 代码可能看起来像这样:

name = "Link"

sex = "Male"

max_hit_points = 50

current_hit_points = 50

为了处理这个字符,我们需要将数据传递给一个函数:

def display_character(name, sex, max_hit_points, current_hit_points):

print(name, sex, max_hit_points, current_hit_points)

现在想象一下,创建一个程序,为我们游戏中的每个角色、怪物和物品设置一组这样的变量。然后我们需要创建处理这些项目的函数。我们现在已经陷入了数据的泥潭。突然间,这听起来一点也不好玩了。

但是等等,更糟的是!随着我们游戏的扩展,我们可能需要添加新的领域来描述我们的角色。在这种情况下,我们添加了max_speed:

name = "Link"

sex = "Male"

max_hit_points = 50

current_hit_points = 50

max_speed = 10

def display_character(name, sex, max_hit_points, current_hit_points, max_speed):

print(name, sex, max_hit_points, current_hit_points)

在上面的例子中,只有一个函数。但是在一个大型的视频游戏中,我们可能有数百个函数来处理主角。添加一个新的字段来帮助描述一个角色有什么和能做什么,这将要求我们遍历这些函数中的每一个,并将其添加到参数列表中。那将是大量的工作。也许我们需要给不同类型的角色添加max_speed,比如怪物。需要有更好的方法。不知何故,我们的程序需要将这些数据字段打包,以便于管理。

定义和创建简单的类

管理多个数据属性的更好方法是定义一个包含所有信息的结构。然后我们可以给这些信息起一个名字,比如字符或地址。在 Python 和任何其他现代语言中,通过使用一个类可以很容易地做到这一点。

例如,我们可以定义一个代表游戏中角色的类:

class Character():

""" This is a class that represents the main character in a game. """

def __init__(self):

""" This is a method that sets up the variables in the object. """

self.name = "Link"

self.sex = "Male"

self.max_hit_points = 50

self.current_hit_points = 50

self.max_speed = 10

self.armor_amount = 8

这是另一个例子。我们定义一个类来保存地址的所有字段:

class Address():

""" Hold all the fields for a mailing address. """

def __init__(self):

""" Set up the address fields. """

self.name = ""

self.line1 = ""

self.line2 = ""

self.city = ""

self.state = ""

self.zip = ""

在上面的代码中,Address是类名。类中的变量,如namecity,称为属性或字段。(注意声明一个类和声明一个函数的异同。)

与函数和变量不同,类名应该以大写字母开头。虽然可以用小写字母开始一个类,但这不被认为是好的做法。

被称为构造函数的特殊函数中的def __init__(self):在创建类时自动运行。我们稍后会详细讨论构造函数。

这个self.有点像代词 my。在课堂上,我们谈论我的名字、我的城市等等。我们不想在Address的类定义之外使用self.,来引用一个Address字段。为什么?因为就像代词“我的”一样,当由不同的人说时,它的意思是完全不同的人!

为了更好地可视化类以及它们之间的关系,程序员经常制作图表。Address类的图表如下图所示。查看类名是如何显示在顶部的,下面列出了每个属性的名称。每个属性的右边是数据类型,如字符串或整数。

A978-1-4842-1790-0_13_Figa_HTML.jpg

类图

类代码定义了一个类,但它实际上并没有创建一个类的实例。代码告诉计算机一个地址有哪些字段,初始默认值是什么。虽然我们还没有地址。我们可以定义一个类而不创建它,就像我们可以定义一个函数而不调用它一样。要创建一个类并设置字段,请查看下面的示例:

# Create an address

home_address = Address()

# Set the fields in the address

home_address.name = "John Smith"

home_address.line1 = "701 N. C Street"

home_address.line2 = "Carver Science Building"

home_address.city = "Indianola"

home_address.state = "IA"

home_address.zip = "50125"

在第 2 行用Address()创建了一个 address 类的实例。注意类名Address的用法,后跟括号。变量名可以是遵循正常命名规则的任何名称。

要设置类中的字段,程序必须使用点运算符。该运算符是位于home_address和字段名之间的句点。查看最后 6 行如何使用点运算符来设置每个字段值。

使用类时一个非常常见的错误是忘记指定要使用的类的实例。如果只创建了一个地址,很自然地认为计算机会知道使用你所说的那个地址。然而,事实并非如此。请参见下面的示例:

class Address():

def __init__(self):

self.name = ""

self.line1 = ""

self.line2 = ""

self.city = ""

self.state = ""

self.zip = ""

# Create an address

my_address = Address()

# Alert! This does not set the address’s name!

name = "Dr. Craven"

# This doesn’t set the name for the address either

Address.name = "Dr. Craven"

# This does work:

my_address.name = "Dr. Craven"

可以创建第二个地址,并且可以使用来自两个实例的字段。请参见下面的示例(为便于阅读,添加了行号):

001 class Address():

002     def __init__(self):

003         self.name = ""

004         self.line1 = ""

005         self.line2 = ""

006         self.city = ""

007         self.state = ""

008         self.zip = ""

009

010 # Create an address

011 home_address = Address()

012

013 # Set the fields in the address

014 home_address.name = "John Smith"

015 home_address.line1 = "701 N. C Street"

016 home_address.line2 = "Carver Science Building"

017 home_address.city = "Indianola"

018 home_address.state = "IA"

019 home_address.zip = "50125"

020

021 # Create another address

022 vacation_home_address = Address()

023

024 # Set the fields in the address

025 vacation_home_address.name = "John Smith"

026 vacation_home_address.line1 = "1122 Main Street"

027 vacation_home_address.line2 = ""

028 vacation_home_address.city = "Panama City Beach"

029 vacation_home_address.state = "FL"

030 vacation_home_address.zip = "32407"

031

032 print("The client’s main home is in " + home_address.city)

033 print("His vacation home is in " + vacation_home_address.city)

第 11 行创建了第一个Address实例;第 22 行创建了第二个实例。变量home_address指向第一个实例,vacation_home_address指向第二个实例。

第 25–30 行设置了这个新类实例中的字段。第 32 行打印家庭地址的城市,因为home_address出现在点运算符之前。第 33 行打印假期地址,因为vacation_home_address出现在点运算符之前。

在示例中,Address被称为类,因为它为数据对象定义了一个新的分类。变量home_addressvacation_home_address引用对象,因为它们引用了类Address的实际实例。一个对象的简单定义是它是一个类的实例。像鲍勃和南希是人类类的实例。

通过使用www.pythontutor.com,我们可以可视化代码的执行(见下文)。有三个变量在起作用。一个指向Address的类定义。另外两个变量指向不同的地址对象和它们的数据。

A978-1-4842-1790-0_13_Figb_HTML.jpg

两个地址

将大量数据字段放入一个类中,可以很容易地将数据传入和传出一个函数。在下面的代码中,该函数接受一个地址作为参数,并将其打印在屏幕上。没有必要为地址的每个字段传递参数。

# Print an address to the screen

def print_address(address):

print(address.name)

# If there is a line1 in the address, print it

if len(address.line1) > 0:

print(address.line1)

# If there is a line2 in the address, print it

if len(address.line2) > 0:

print( address.line2 )

print(address.city + ", " + address.state + " " + address.zip)

print_address(home_address)

print()

print_address(vacation_home_address)

向类中添加方法

除了属性,类还可以有方法。方法是存在于类内部的函数。扩展前面回顾问题 1 中的Dog类的例子,下面的代码增加了一个狗叫的方法。

class Dog():

def __init__(self):

self.age = 0

self.name = ""

self.weight = 0

def bark(self):

print("Woof")

方法定义包含在上面的第 7–8 行中。类中的方法定义看起来几乎和函数定义一模一样。最大的不同是在第 7 行增加了一个参数self。类中任何方法的第一个参数必须是self。即使函数不使用该参数,它也是必需的。

以下是为类创建方法时要记住的重要事项:

  • 应该先列出属性,后列出方法。
  • 任何方法的第一个参数都必须是self
  • 方法定义恰好缩进一个制表位。

调用方法的方式类似于引用对象的属性。请参见下面的示例代码。

001 my_dog = Dog()

002

003 my_dog.name = "Spot"

004 my_dog.weight = 20

005 my_dog.age = 3

006

007 my_dog.bark()

第 1 行创建了狗。第 3–5 行设置对象的属性。第 7 行调用了bark函数。注意,即使bark函数有一个参数self,调用也不会传入任何东西。这是因为第一个参数被假定为对 dog 对象本身的引用。在幕后,Python 发出了一个看起来像这样的调用:

# Example, not actually legal

Dog.bark(my_dog)

如果bark函数需要引用任何属性,那么它就使用self引用变量。例如,我们可以改变Dog类,这样当狗叫的时候,它也能打印出狗的名字。在下面的代码中,使用点运算符和self引用来访问 name 属性。

def bark(self):

print("Woof says", self.name)

属性是形容词,方法是动词。该类的绘图如下图所示。

A978-1-4842-1790-0_13_Figc_HTML.jpg

狗类

示例:球类

这个示例代码可以在 Python/pygame 中用来画一个球。将所有参数包含在一个类中会使数据管理更容易。下图显示了Ball类的图表。

A978-1-4842-1790-0_13_Figd_HTML.jpg

球类

class Ball():

def __init__(self):

# --- Class Attributes ---

# Ball position

self.x = 0

self.y = 0

# Ball’s vector

self.change_x = 0

self.change_y = 0

# Ball size

self.size = 10

# Ball color

self.color = [255,255,255]

# --- Class Methods ---

def move(self):

self.x += self.change_x

self.y += self.change_y

def draw(self, screen):

pygame.draw.circle(screen, self.color, [self.x, self.y], self.size )

下面是在主程序循环之前创建一个球并设置其属性的代码:

theBall = Ball()

theBall.x = 100

theBall.y = 100

theBall.change_x = 2

theBall.change_y = 1

theBall.color = [255,0,0]

这段代码将进入主循环来移动和绘制球:

theBall.move()

theBall.draw(screen)

参考

这就是我们区分真正的程序员和想要成为程序员的地方。理解类引用。看一下下面的代码:

class Person():

def __init__(self):

self.name = ""

self.money = 0

bob = Person()

bob.name = "Bob"

bob.money = 100

nancy = Person()

nancy.name = "Nancy"

print(bob.name, "has", bob.money, "dollars.")

print(nancy.name, "has", nancy.money, "dollars.")

上面的代码创建了 Person()类的两个实例,使用www.pythontutor.com我们可以在下图中可视化这两个类。

A978-1-4842-1790-0_13_Fige_HTML.jpg

两个人

上面的代码没有什么新意。但是下面的代码可以:

class Person():

def __init__(self):

self.name = ""

self.money = 0

bob = Person()

bob.name = "Bob"

bob.money = 100

nancy = bob

nancy.name = "Nancy"

print(bob.name, "has", bob.money, "dollars.")

print(nancy.name, "has", nancy.money, "dollars.")

看到第 10 行和nancy = bob的区别了吗?

处理对象时一个常见的误解是假设变量bob是 Person 对象。事实并非如此。变量bob是对 Person 对象的引用。也就是说,它存储的是对象所在的内存地址,而不是对象本身。

如果bob实际上是对象,那么第 9 行可以创建对象的副本,这样就会有两个对象存在。程序的输出将显示 Bob 和 Nancy 都有 100 美元。但是当运行时,程序输出以下内容:

Nancy has 100 dollars.

Nancy has 100 dollars.

bob存储的是对对象的引用。除了引用,我们还可以称之为地址、指针或句柄。引用是计算机内存中存储对象的地址。这个地址是一个十六进制数,如果打印出来,可能看起来像 0x1e504。运行第 9 行时,复制的是地址,而不是地址指向的整个对象。见下图。

A978-1-4842-1790-0_13_Figf_HTML.jpg

类别引用

我们也可以在www.pythontutor.com中运行它,看看这两个变量是如何指向同一个对象的。

A978-1-4842-1790-0_13_Figg_HTML.jpg

一个人,两个指针

功能和参考

请看下面的代码示例。第 1 行创建了一个接受一个数字作为参数的函数。变量money是包含传入数据副本的变量。将该数字加 100 不会改变存储在第 11 行bob.money中的数字。因此,第 14 行的 print 语句打印出 100 而不是 200。

def give_money1(money):

money += 100

class Person():

def __init__(self):

self.name = ""

self.money = 0

bob = Person()

bob.name = "Bob"

bob.money = 100

give_money1(bob.money)

print(bob.money)

http://www.pythontutor.com/visualize.html#mode=display上运行,我们看到money变量有两个实例。一个是对give_money1函数的复制和本地。

A978-1-4842-1790-0_13_Figh_HTML.jpg

函数引用

看看下面的附加代码。这段代码确实导致bob.money增加,并且打印语句打印 200。

def give_money2(person):

person.money += 100

give_money2(bob)

print(bob.money)

这是为什么呢?因为person包含的是对象的内存地址的副本,而不是实际的对象本身。你可以把它想象成一个银行账号。该函数有一个银行帐号的副本,而不是整个银行帐户的副本。因此,使用银行账号的副本存入 100 美元会导致 Bob 的银行账户余额上升。

A978-1-4842-1790-0_13_Figi_HTML.jpg

函数引用

数组以同样的方式工作。接受一个数组(列表)作为参数并修改该数组中的值的函数将修改调用代码创建的同一数组。复制的是数组的地址,而不是整个数组。

复习问题

Create a class called Cat. Give it attributes for name, color, and weight. Give it a method called meow.   Create an instance of the cat class, set the attributes, and call the meow method.   Create a class called Monster. Give it an attribute for name and an integer attribute for health. Create a method called decrease_health that takes in a parameter amount and decreases the health by that much. Inside that method, print that the animal died if its health goes below zero.  

构造器

下面列出的Dog我们班有一个可怕的问题。当我们创建一只狗时,默认情况下,这只狗没有名字。狗应该有名字!我们不应该允许狗出生后就不给它取名字。然而,下面的代码允许这种情况发生,那只狗永远不会有名字。

class Dog()

def __init__(self):

self.name = ""

my_dog = Dog()

Python 不希望这种情况发生。这就是 Python 类有一个特殊函数的原因,每当创建该类的实例时都会调用这个函数。通过添加一个称为构造函数的函数,程序员可以添加每次创建类的实例时自动运行的代码。请参见下面的示例构造函数代码:

class Dog():

def __init__(self):

""" Constructor. Called when creating an object of this type. """

self.name = ""

print("A new dog is born!")

# This creates the dog

my_dog = Dog()

构造函数从第 2 行开始。必须取名__init__init前有两个下划线,后有两个下划线。一个常见的错误是只使用一个。

构造函数必须接受self作为第一个参数,就像类中的其他方法一样。当程序运行时,它将打印:

A new dog is born!

当在第 8 行创建一个Dog对象时,自动调用__init__函数,并将消息打印到屏幕上。

避免这个错误

将一个方法的所有内容放入一个定义中。不要定义两次。例如:

# Wrong:

class Dog():

def __init__(self):

self.age = 0

self.name = ""

self.weight = 0

def __init__(self):

print("New dog!")

计算机会忽略第一个__init__并使用最后一个定义。请改为这样做:

# Correct:

class Dog():

def __init__(self):

self.age = 0

self.name = ""

self.weight = 0

print("New dog!")

构造函数可用于初始化和设置对象的数据。上面的示例Dog类仍然允许在创建 dog 对象后将name属性留空。我们如何防止这种情况发生?许多对象在创建时就需要有正确的值。可以使用构造函数来实现这一点。参见下面的代码:

001 class Dog():

002

003     def __init__(self, new_name):

004         """ Constructor. """

005         self.name = new_name

006

007 # This creates the dog

008 my_dog = Dog("Spot")

009

010 # Print the name to verify it was set

011 print(my_dog.name)

012

013 # This line will give an error because

014 # a name is not passed in.

015 herDog = Dog()

在第 3 行,构造函数现在有了一个名为new_name的额外参数。该参数的值用于设置第 8 行的Dog类中的name属性。不再可能创建一个没有名字的狗类。第 15 行的代码尝试这样做。这将导致 Python 错误,并且无法运行。一个常见的错误是将__init__函数的参数命名为与属性相同的名称,并假设这些值会自动同步。这种情况不会发生。

复习问题

Should class names begin with an upper or lowercase letter?   Should method names begin with an upper or lowercase letter?   Should attribute names begin with an upper or lowercase letter?   Which should be listed first in a class, attributes or methods?   What are other names for a reference?   What is another name for an instance variable?   What is the name for an instance of a class?   Create a class called Star that will print out “A star is born!” every time it is created.   Create a class called Monster with attributes for health and a name. Add a constructor to the class that sets the health and name of the object with data passed in as parameters.  

遗产

使用类和对象的另一个强大特性是利用继承的能力。创建一个类并继承父类的所有属性和方法是可能的。

例如,一个程序可能会创建一个名为Boat的类,它拥有代表游戏中一艘船所需的所有属性:

class Boat():

def __init__(self):

self.tonnage = 0

self.name = ""

self.isDocked = True

def dock(self):

if self.isDocked:

print("You are already docked.")

else:

self.isDocked = True

print("Docking")

def undock(self):

if not self.isDocked:

print("You aren’t docked.")

else:

self.isDocked = False

print("Undocking")

要测试我们的代码:

b = Boat()

b.dock()

b.undock()

b.undock()

b.dock()

b.dock()

产出:

You are already docked.

Undocking

You aren’t docked.

Docking

You are already docked.

我们的项目也需要一艘潜艇。我们的潜艇能做一艘船能做的一切,另外我们需要一个指挥submerge。没有继承,我们有两个选择。

  • 一,给我们的船添加submerge()命令。这不是一个好主意,因为我们不想给人一种我们的船通常在水下的印象。
  • 第二,我们可以创建一个Boat类的副本,并将其命名为Submarine。在这个类中,我们将添加submerge()命令。这一开始很容易,但是如果我们改变了Boat类,事情就变得更难了。程序员需要记住,我们不仅需要修改Boat类,还要对Submarine类做同样的修改。保持这些代码的同步既费时又容易出错。

幸运的是,有一个更好的方法。我们的程序可以创建继承父类所有属性和方法的子类。然后,子类可以添加符合其需求的字段和方法。例如:

class Submarine(Boat):

def submerge(self):

print("Submerge!")

第 1 行是重要的部分。仅仅通过在类声明期间将Boat放在括号之间,我们就已经自动选择了Boat类中的每个属性和方法。如果我们更新Boat,那么子类Submarine将自动获得这些更新。传承就是这么简单!

下一个代码示例如下图所示。

A978-1-4842-1790-0_13_Figj_HTML.jpg

类图

001 class Person():

002     def __init__(self):

003         self.name = ""

004

005 class Employee(Person):

006     def __init__(self):

007         # Call the parent/super class constructor first

008         super().__init__()

009

010         # Now set up our variables

011         self.job_title = ""

012

013 class Customer(Person):

014     def __init__(self):

015         super().__init__()

016         self.email = ""

017

018 john_smith = Person()

019 john_smith.name = "John Smith"

020

021 jane_employee = Employee()

022 jane_employee.name = "Jane Employee"

023 jane_employee.job_title = "Web Developer"

024

025 bob_customer = Customer()

026 bob_customer.name = "Bob Customer"

027 bob_customer.email = "send_me@spam.com"

通过将Person放在第 5 行和第 13 行的括号之间,程序员告诉计算机PersonEmployeeCustomer的父类。这允许程序在第 19 和 22 行设置name属性。

方法也是继承的。父类拥有的任何方法,子类也会拥有。但是如果我们在子类和父类中都有一个方法呢?

我们有两个选择。我们可以用关键字super()运行它们。使用super()后跟一个点操作符,最后是一个方法名,允许您调用该方法的父版本。

上面的代码显示了使用super的第一个选项,其中我们不仅运行子构造函数,还运行父构造函数。

如果您正在为子方法编写方法,并且想要调用父方法,通常它将是子方法中的第一条语句。请注意上面的例子。

所有的构造函数都应该调用父构造函数,因为那样你就会有一个没有父构造函数的子构造函数,这很可悲。事实上,有些语言强制执行这个规则,但是 Python 没有。

第二种选择?方法可以被子类重写以提供不同的功能。以下示例显示了这两个选项。Employee.report覆盖了Person.report,因为它从不调用和运行父报告方法。Customer报告确实调用了父类,Customer中的report方法增加了Person功能。

class Person():

def __init__(self):

self.name = ""

def report(self):

# Basic report

print("Report for", self.name)

class Employee(Person):

def __init__(self):

# Call the parent/super class constructor first

super().__init__()

# Now set up our variables

self.job_title = ""

def report(self):

# Here we override report and just do this:

print("Employee report for", self.name)

class Customer(Person):

def __init__(self):

super().__init__()

self.email = ""

def report(self):

# Run the parent report:

super().report()

# Now add our own stuff to the end so we do both

print("Customer e-mail:", self.email)

john_smith = Person()

john_smith.name = "John Smith"

jane_employee = Employee()

jane_employee.name = "Jane Employee"

jane_employee.job_title = "Web Developer"

bob_customer = Customer()

bob_customer.name = "Bob Customer"

bob_customer.email = "send_me@spam.com"

john_smith.report()

jane_employee.report()

bob_customer.report()

Is-A 和 Has-A 关系

类有两种主要类型的关系。他们是“是一个”和“有一个”的关系。

父类应该总是子类的更一般、更抽象的版本。这种类型的父子关系称为 is 关系。例如,一个父类动物可以有一个子类狗。Dog 类可以有一个子类 Poodle。另一个例子,海豚是哺乳动物。反之亦然:哺乳动物不一定是海豚。所以海豚类永远不会成为哺乳动物类的父母。同样,教室桌子不应该是教室椅子的父代,因为椅子不是桌子。

另一种关系是有关系。这些关系通过类属性在代码中实现。一只狗有一个名字,因此 dog 类有一个 name 属性。同样,一个人可以有一只狗,这可以通过让 person 类有一个 dog 属性来实现。Person 类不会从 Dog 派生,因为那会是一种侮辱。

查看前面的代码示例,我们可以看到:

  • 员工是一个人。
  • 客户是一个人。
  • 人有名字。
  • 员工有职称。
  • 客户有一封电子邮件。

静态变量与实例变量

静态变量和实例变量之间的区别令人困惑。谢天谢地,现在没有必要完全理解这种区别。但是如果你坚持编程,它将是。因此我们在这里简单介绍一下。

在我出版这本书的最初几年,Python 还有一些奇怪的地方让我感到困惑。所以你可能会看到我弄错的旧视频和例子。

实例变量是我们到目前为止使用的类变量的类型。该类的每个实例都有自己的值。例如,在一个挤满人的房间里,每个人都有自己的年龄。有些年龄可能是相同的,但我们仍然需要单独跟踪每个年龄。

有了实例变量,就不能和一屋子人只说“年龄”了。我们需要具体说明我们谈论的是谁的年龄。此外,如果房间里没有人,那么在没有人的情况下提及年龄是没有意义的。

对于静态变量,类的每个实例的值都是相同的。即使没有实例,静态变量仍然有一个值。例如,我们可能有一个静态变量来表示现有的Human类的数量。没有人类?值为零,但仍然存在。

在下面的例子中,ClassA创建了一个实例变量。ClassB创建一个静态变量。

001 # Example of an instance variable

002 class ClassA():

003     def __init__(self):

004         self.y = 3

005

006 # Example of a static variable

007 class ClassB():

008     x = 7

009

010 # Create class instances

011 a = ClassA()

012 b = ClassB()

013

014 # Two ways to print the static variable.

015 # The second way is the proper way to do it.

016 print(b.x)

017 print(ClassB.x)

018

019 # One way to print an instance variable.

020 # The second generates an error, because we don’t know what instance

021 # to reference.

022 print(a.y)

023 print(ClassA.y)

在上面的例子中,第 16 行和第 17 行打印出静态变量。第 17 行是这样做的正确方式。与以前不同,我们可以在使用静态变量时引用类名,而不是指向特定实例的变量。因为我们使用的是类名,所以通过查看第 17 行,我们可以立即看出我们使用的是静态变量。第 16 行可以是实例或静态变量。这种混乱使得第 17 行成为更好的选择。

第 22 行打印出实例变量,就像我们在前面的例子中所做的一样。第 23 行将产生一个错误,因为y的每个实例都是不同的(毕竟它是一个实例变量),并且我们没有告诉计算机我们在谈论的是ClassA的哪个实例。

实例变量隐藏静态变量

这是我不喜欢 Python 的一个特性。静态变量和实例变量可以同名。请看下面的例子:

# Class with a static variable

class ClassB():

x = 7

# Create a class instance

b = ClassB()

# This prints 7

print(b.x)

# This also prints 7

print(ClassB.x)

# Set x to a new value using the class name

ClassB.x = 8

# This also prints 8

print(b.x)

# This prints 8

print(ClassB.x)

# Set x to a new value using the instance.

# Wait! Actually, it doesn’t set x to a new value!

# It creates a brand new variable, x. This x

# is an instance variable. The static variable is

# also called x. But they are two different

# variables. This is super-confusing and is bad

# practice.

b.x = 9

# This prints 9

print(b.x)

# This prints 8\. NOT 9!!!

print(ClassB.x)

允许实例变量隐藏静态变量让我困惑了很多年!

回顾

多项选择测验

Select the best class definition for an alien: class Alien(): def __init__(self): self.name = "" self.height = 7.2 self.weight = 156   class alien(): def __init__(self): self.name = "" self.height = 7.2 self.weight = 156   class alien.name = "" class alien.height = 7.2 class alien.weight = 156   class alien( def __init__(self): self.name = "" self.height = 7.2 self.weight = 156 )     What does this code do? d1 = Dog() d2 = Dog() Creates two objects, of type Dog.   Creates two classes, of type Dog.   Creates one object, of type Dog.     What does this code do? d1 = Dog() d2 = d1 Creates two objects, of type Dog.   Creates two classes, of type Dog.   Creates one object, of type Dog.     What is wrong with the following code: class Book():     def open(self):         print("You opened the book")     def __init__(self):         self.pages = 347 There should be a self. in front of pages.   Book should not be capitalized.   The __init__ with attributes should be listed first.   open should be capitalized.     What is wrong with the following code: class Ball():     def __init__(self):         self.x = 0         self.y = 0         self.change_x = 0         self.change_y = 0     x += change_x     y += change_y The ball should not be at location 0, 0   The variables should be set equal to "".   The code to add to x and y must be in a method.   The code to set the variables to zero should be inside a method.   All classes must have at least one method   self should be in between the parentheses.     What is wrong with the following code: class Ball():     def __init__(self):         self.x = 0         self.y = 0 Ball.x = 50 Ball.y = 100 Lines 3 and 4 should not have self. in front.   Ball. does not refer to an instance of the class.   Lines 3 and 5 should be used to set x and y to 50 and 100.   Ball. on lines 6 and 7 should be lowercase.     What is wrong with the following code: class Ball():     def __init__(self):         self.x = 0         self.y = 0         b = Ball()         b.x = 50         b.y = 100 Lines 6–8 should be in a method.   Lines 6–8 should not be indented.   Lines 7 and 8 should have self. instead of b.   Line 6 should have self in between the parentheses.     What will this print? class Ball():     def __init__(self):         self.x = 0         self.y = 0 b1 = Ball() b2 = b1 b1.x = 40 b2.x = 50 b1.x += 5 b2.x += 5 print(b1.x, b2.x) 40 40   60 60   45 55   55 55   40 50     What will this print? class Account():     def __init__(self):         self.money = 0     def deposit(self, amount):         self.money += amount account = Account() money = 100 account.deposit(50) print(money, account.money) 150 150   100 50   100 100   50 100   50 50   100 150     What is wrong with the following: class Dog():     def __init__(self, new_name):         """ Constructor.         Called when creating an object of this type """         name = new_name         print("A new dog is born!") # This creates the dog my_dog = Dog("Rover") On line 6, there should be a self. in front of new_name   On line 6, there should be a self. in front of name   Line 10 has 1 parameter, yet in line 2 we can see __init__ takes two parameters.   Lines 9 and 10 should be indented.   Lines 6 to 7 should not be indented.    

简答工作表

第一部分:

What is the difference between a class and an object?   What is the difference between a function and a method?   Write code to create an instance of this class and set its attributes: class Dog():     def __init__(self):         self.age = 0         self.name = ""         self.weight = 0   Write code to create two different instances of this class and set attributes for both objects: class Person():     def __init__(self):         self.name = ""         self.cell_phone = ""         self.email = ""   For the code below, write a class that has the appropriate class name and attributes that will allow the code to work. my_bird = Bird() my_bird.color = "green" my_bird.name = "Sunny" my_bird.breed = "Sun Conure"   Define a class that would represent a character in a simple 2D game. Include attributes for the position, name, and strength.   The following code runs, but it is not correct. What did the programmer do wrong? class Person():     def __init__(self):         self.name = ""         self.money = 0 nancy = Person() name = "Nancy" money = 100   Take a look at the code. It does not run. What is the error that prevents it from running? class Person():     def __init__(self):         self.name = ""         self.money = 0 bob = Person() print(bob.name, "has", money, "dollars.")   Even with that error fixed, the program will not print out: Bob has 0 dollars. Instead it just prints out: has 0 dollars. Why is this the case?   Take pairs of the following items, and list some of the “has-a” relationships, and the “is-a” relationships between them.

  • 往来账户
  • 抵押账户
  • 顾客
  • 撤退
  • 银行存款
  • severely subnormal 智力严重逊常
  • 交易
  • 地址
  • 存款

  In Python, how is an “is-a” relationship implemented? Give an example.   In Python, how is a “has-a” relationship implemented? Give an example.   How does this change if an object is allowed more than one item of a given type? (Ask if you aren’t sure.)  

第二部分:

要回答接下来的四个问题,创建一个程序。在那个程序中会有所有四个问题的答案。确保程序运行,然后从程序中复制/粘贴来回答下面的每个问题。

你应该有一个以三个类定义开始的程序,前三个问题各有一个。然后你应该有代码来创建每个类的实例,这将是最后一个问题的答案。

Write code that defines a class named Animal:

  • 为动物名称添加一个属性。
  • 为打印“Munch munch”的Animal添加一个eat()方法
  • 打印“Grrr 表示[动物名称]”的Animalmake_noise()方法
  • 为打印“一只动物出生了”的Animal类添加一个构造函数

  A class named Cat:

  • 使Animal成为父级。
  • 打印“喵说[动物名]”的Catmake_noise()方法
  • 打印“一只猫出生了”的Cat的构造函数
  • 修改构造函数,使它也调用父构造函数。

  A class named Dog:

  • 使Animal成为父级。
  • 一个Dogmake_noise()方法,打印“树皮说【动物名称】”
  • 打印“一只狗出生了”的Dog的构造函数
  • 修改构造函数,使它也调用父构造函数。

  A main program with:

  • 创建一只猫、两只狗和一只动物的代码。
  • 设定每种动物的名称。
  • 为每种动物调用eat()make_noise()的代码。(这个别忘了!)

锻炼

查看附录中本章附带的练习“类和图形”。