3、Python基础回顾:面向对象编程、时间模块与数学运算
好,咱们进入第三章。在正式开始搭建动画刷新系统之前,我得先带你把 Python 的几个核心基础模块过一遍。你可能会想:「这些基础东西,我早就会了。」嗯,我理解。但动画刷新系统对时间精度和对象管理的要求,跟写个爬虫或者数据处理脚本完全不一样。我当年第一次做实时渲染工具时,就是因为对 time.sleep() 的精度理解不到位,导致动画一卡一卡的,被测试同事追着骂了好几天。
所以这一章,咱们重点看三个东西:面向对象编程、时间模块、数学运算。它们是你后面所有动画逻辑的基石。
3.1 面向对象编程:把动画元素当成「对象」来管
面向对象编程,说白了就是把你的数据和对数据的操作打包在一起。在动画系统里,每一个小球、每一帧画面、每一个动画效果,都是一个对象。
3.1.1 类与对象的基本概念
类就是模板,对象就是根据模板造出来的具体实例。我习惯把类想象成「月饼模具」,对象就是「做出来的月饼」。模具只有一个,但你可以做出无数个月饼,每个月饼的馅料(数据)可以不同。
class Ball:
"""一个小球类"""
def __init__(self, x, y, radius, color):
self.x = x
self.y = y
self.radius = radius
self.color = color
self.speed_x = 0
self.speed_y = 0
def move(self):
"""移动小球"""
self.x += self.speed_x
self.y += self.speed_y
def __str__(self):
return f"Ball({self.x}, {self.y}, r={self.radius})"
# 创建两个小球对象
ball_a = Ball(100, 200, 20, "red")
ball_b = Ball(300, 150, 30, "blue")
你看,ball_a 和 ball_b 是两个独立的对象。它们有各自的坐标、半径、颜色,互不干扰。这就是面向对象最直观的好处——封装。
核心要点:__init__ 方法是构造函数,在创建对象时自动调用。self 代表对象本身,所有实例方法第一个参数必须是 self。
3.1.2 继承:让动画元素「复用」公共逻辑
在动画系统里,你会有很多种可动元素:小球、方块、文字、图片……它们都有共同的行为(移动、绘制),但具体实现不同。这时候继承就派上用场了。
class AnimatedObject:
"""所有动画对象的基类"""
def __init__(self, x, y):
self.x = x
self.y = y
self.visible = True
def update(self):
"""更新状态,子类重写"""
raise NotImplementedError("子类必须实现 update 方法")
def draw(self, screen):
"""绘制到屏幕,子类重写"""
raise NotImplementedError("子类必须实现 draw 方法")
class Ball(AnimatedObject):
def __init__(self, x, y, radius):
super().__init__(x, y) # 调用父类构造函数
self.radius = radius
def update(self):
# 小球的更新逻辑
self.x += 1
def draw(self, screen):
# 小球的绘制逻辑
pass # 具体绘制代码后面再写
这里有个坑,我当年踩过:子类一定要调用 super().__init__()。如果你忘了,父类的 x 和 y 就永远不会被初始化。我曾经因为这个 bug 排查了整整一个下午,最后发现是少写了一行 super() 调用。
避坑指南:我曾经在写一个粒子系统时,把所有粒子都继承自同一个基类,但忘记在子类中调用 super().__init__(),结果所有粒子的坐标都是 None,画面上一片空白。记住:子类初始化时,先调父类的 __init__。
3.1.3 多态:同一接口,不同行为
多态让代码更灵活。你想想看,在动画循环里,你只需要遍历所有对象,调用它们的 update() 和 draw() 方法,而不需要关心它们具体是什么类型。这就是多态的魅力。
# 多态示例
objects = [Ball(100, 200, 20), Box(300, 150, 50, 50), Text(400, 300, "Hello")]
for obj in objects:
obj.update() # 每个对象执行自己的更新逻辑
obj.draw(screen) # 每个对象执行自己的绘制逻辑
我个人习惯在基类里用 raise NotImplementedError 来强制子类实现方法。这样如果你忘了实现某个方法,程序会立刻报错,而不是静默失败。
3.2 时间模块:动画的「心跳」
动画的本质,就是随时间变化画面。所以时间模块是动画系统的核心。Python 提供了 time 和 datetime 两个模块,咱们重点看 time。
3.2.1 time 模块:高精度计时
time.time() 返回当前时间戳(从 1970 年 1 月 1 日开始的秒数),精度很高。在动画里,我们用它来计算帧间隔。
import time
# 获取当前时间戳
start_time = time.time()
print(f"开始时间:{start_time}")
# 模拟动画循环
for i in range(5):
time.sleep(0.016) # 约 60 FPS
elapsed = time.time() - start_time
print(f"第 {i+1} 帧,已过 {elapsed:.3f} 秒")
小技巧:time.perf_counter() 比 time.time() 精度更高,适合做性能测量。在动画系统里,我通常用 perf_counter 来测量帧耗时。
3.2.2 帧率控制:别让动画跑飞了
如果不控制帧率,动画会跑得飞快,根本看不清。我们需要一个「帧率控制器」。
class FrameRateController:
"""帧率控制器"""
def __init__(self, target_fps=60):
self.target_fps = target_fps
self.frame_duration = 1.0 / target_fps
self.last_time = time.perf_counter()
def wait_for_next_frame(self):
"""等待到下一帧应该开始的时间"""
current_time = time.perf_counter()
elapsed = current_time - self.last_time
sleep_time = self.frame_duration - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
self.last_time = time.perf_counter()
return time.perf_counter() - self.last_time # 返回实际帧耗时
这个控制器会保证每帧之间的间隔稳定在 1/60 秒左右。如果某帧处理时间过长,它会自动跳过等待,避免动画卡死。
注意:time.sleep() 的精度受操作系统影响。在 Windows 上,它的最小精度大约是 15 毫秒。如果你需要更高精度的定时,可以考虑用 pygame 的时钟或者 time.perf_counter 做忙等待。但忙等待会占用 CPU,别在移动设备上这么干。
3.2.3 datetime 模块:处理「人类可读」的时间
datetime 主要用于日志记录、时间戳格式化等场景。在动画系统里,你可能需要记录动画开始时间、计算动画持续时间等。
from datetime import datetime, timedelta
# 获取当前时间
now = datetime.now()
print(f"当前时间:{now.strftime('%Y-%m-%d %H:%M:%S')}")
# 计算动画结束时间
animation_duration = timedelta(seconds=5)
end_time = now + animation_duration
print(f"动画结束时间:{end_time.strftime('%H:%M:%S')}")
# 计算时间差
start = datetime.now()
# ... 执行动画 ...
end = datetime.now()
elapsed = end - start
print(f"动画耗时:{elapsed.total_seconds():.2f} 秒")
3.3 数学运算:动画的「物理引擎」
动画离不开数学。位置计算、速度变化、碰撞检测……全都依赖数学运算。Python 的 math 模块提供了我们需要的所有数学函数。
3.3.1 常用数学函数
| 函数 | 说明 | 动画应用场景 |
|---|---|---|
math.sin() |
正弦函数 | 制作波浪运动、弹性效果 |
math.cos() |
余弦函数 | 圆周运动、摆动动画 |
math.sqrt() |
平方根 | 计算距离、碰撞检测 |
math.atan2() |
反正切(带象限) | 计算两点之间的角度 |
math.radians() |
角度转弧度 | 将角度转换为弧度制 |
math.degrees() |
弧度转角度 | 将弧度转换为角度制 |
3.3.2 实战:让小球做圆周运动
咱们来写一个实际例子。让一个小球围绕中心点做圆周运动。
import math
class OrbitingBall:
"""做圆周运动的小球"""
def __init__(self, center_x, center_y, radius, speed):
self.center_x = center_x
self.center_y = center_y
self.orbit_radius = radius # 轨道半径
self.speed = speed # 角速度(弧度/秒)
self.angle = 0 # 当前角度
def update(self, dt):
"""更新位置,dt 是时间增量(秒)"""
self.angle += self.speed * dt
# 计算当前位置
self.x = self.center_x + self.orbit_radius * math.cos(self.angle)
self.y = self.center_y + self.orbit_radius * math.sin(self.angle)
def get_position(self):
return (self.x, self.y)
# 使用示例
ball = OrbitingBall(400, 300, 150, 1.5) # 半径150,角速度1.5弧度/秒
ball.update(0.016) # 更新一帧(约60 FPS)
print(f"小球位置:{ball.get_position()}")
这里用到了 math.cos 和 math.sin。你想想看,角度从 0 到 2π 变化时,cos 和 sin 的值正好构成一个圆。这就是圆周运动的数学本质。
3.3.3 向量运算:自己动手实现
Python 标准库没有内置的向量类,但我们可以自己写一个。在动画系统里,向量运算太常用了。
import math
class Vector2:
"""二维向量"""
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __add__(self, other):
return Vector2(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector2(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector2(self.x * scalar, self.y * scalar)
def length(self):
"""向量长度"""
return math.sqrt(self.x ** 2 + self.y ** 2)
def normalize(self):
"""归一化"""
length = self.length()
if length == 0:
return Vector2(0, 0)
return Vector2(self.x / length, self.y / length)
def dot(self, other):
"""点积"""
return self.x * other.x + self.y * other.y
def distance_to(self, other):
"""计算到另一个向量的距离"""
return (self - other).length()
def __str__(self):
return f"({self.x:.2f}, {self.y:.2f})"
为什么向量重要? 在动画系统里,位置、速度、加速度都是向量。有了向量类,你可以用 position += velocity * dt 这样的代码来更新位置,非常直观。我所有的动画项目都离不开这个基础类。
3.4 本章知识体系
下面这张图帮你梳理了本章的核心知识点和它们之间的关系。
3.5 本章小结
这一章的内容,说白了就是给后面的动画系统打地基。咱们过了三个东西:
- 面向对象编程:用类来封装动画元素,用继承来复用代码,用多态来统一接口。这是你管理几十上百个动画对象的基础。
- 时间模块:
time.time()和time.perf_counter()用来计时,time.sleep()用来控制帧率。记住,datetime更适合做日志,不适合做高精度计时。 - 数学运算:三角函数做波形和圆周运动,
sqrt算距离,向量运算做位置和速度管理。自己实现一个Vector2类,后面你会感谢我的。
我的建议:别急着往后看。花 20 分钟把上面的代码都敲一遍,尤其是那个 Vector2 类和 FrameRateController。代码这东西,看十遍不如敲一遍。我在带新人时发现,凡是动手敲过代码的,后面学动画逻辑都特别快。