角運動(angular motion)指的是物體的旋轉運動。物體進行直線運動時,可以用速度和加速度來描述它的運動。速度指的是單位時間內,物體的位移量;而加速度則是單位時間內,速度的改變量。同樣的做法也可以運用在描述角運動上,我們可以用角速度(angular velocity)和角加速度(angular acceleration)來描述角運動。角速度指的是單位時間內,物體旋轉的角度;而角加速度,則是單位時間內角速度的改變量。
在第1、2章處理位置、速度、加速度時,是使用下列做法:
position += velocity
velocity += acceleration
處理物體的旋轉,也可以用相同的方式:
angle += angular_velocity
angular_velocity += angular_acceleration
不過要注意的是,position
、velocity
、acceleration
是向量,但是angle
、angular_velocity
、angular_acceleration
則是純量。之所以如此,是因為我們現在是在2D平面上,物體就只會繞著一個軸旋轉;如果是在3D空間中,因為有兩個軸,則它們也會是向量。
另一個需要注意的是,這樣子的做法,是假設模擬的時間間隔delta time,也就是第二章提到過的時間步長,等於顯示1幀畫面的時間。
利用上述的做法,可以把Exercise 3.1改寫成下列Example 3.1的寫法。
Example 3.1: Angular Motion Using transform.rotate()
圖案一開始的時候是靜止不動的,但是在角加速度的作用下,旋轉的速度會越來越快。
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 3.1: Angular Motion Using transform.rotate()")
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
baton_length = 200 # 指揮棒長度
radius = 10 # 指揮棒端點球半徑
# 建造用來繪製指揮棒圖案的surface並以白色清空
surface_size = (baton_length, baton_length)
surface = pygame.Surface(surface_size)
surface.fill(WHITE)
# 在surface上繪製指揮棒圖案
endpoint1 = (radius, baton_length//2)
endpoint2 = (baton_length-radius, baton_length//2)
pygame.draw.circle(surface, BLACK, endpoint1, radius)
pygame.draw.circle(surface, BLACK, endpoint2, radius)
pygame.draw.line(surface, BLACK, endpoint1, endpoint2, 5)
# 角度的單位是度
angle = 0
angular_velocity = 0
angular_acceleration = 0.001 # 逆時針旋轉
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
angular_velocity += angular_acceleration
angle += angular_velocity
rotated_surface = pygame.transform.rotate(surface, angle)
rect = rotated_surface.get_rect(center=(WIDTH//2, HEIGHT//2))
screen.blit(rotated_surface, (rect.x, rect.y))
pygame.display.update()
frame_rate.tick(FPS)
Exercise 3.2
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 3.2")
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
baton_length = 200 # 指揮棒長度
radius = 10 # 指揮棒端點球半徑
# 建造用來繪製指揮棒圖案的surface並以白色清空
surface_size = (baton_length, baton_length)
surface = pygame.Surface(surface_size)
surface.fill(WHITE)
# 在surface上繪製指揮棒圖案
endpoint1 = (radius, baton_length//2)
endpoint2 = (baton_length-radius, baton_length//2)
pygame.draw.circle(surface, BLACK, endpoint1, radius)
pygame.draw.circle(surface, BLACK, endpoint2, radius)
pygame.draw.line(surface, BLACK, endpoint1, endpoint2, 5)
mass = 2 # 指揮棒質量
Cd = 0.01 # 阻力係數
# 角度的單位是度
angle = 0
angular_velocity = 0
angular_acceleration = 0
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 按下滑鼠左鍵時施加給指揮棒的力量
f = 3 if pygame.mouse.get_pressed()[0] else 0
# 計算阻力,並加入避免指揮棒旋轉方向改變的限制條件
direction = 1 if angular_velocity >= 0 else -1
drag_force = -direction*angular_velocity**2*Cd
print(angular_velocity, drag_force)
limit = mass*abs(angular_velocity)
# 阻力最多只能使角速度降至0,無法使其反向
if abs(drag_force) > limit:
drag_force = -direction*limit
angular_acceleration = (drag_force+f)/mass
angular_velocity += angular_acceleration
angle += angular_velocity
rotated_surface = pygame.transform.rotate(surface, angle)
rect = rotated_surface.get_rect(center=(WIDTH/2, HEIGHT/2))
screen.blit(rotated_surface, (rect.x, rect.y))
pygame.display.update()
frame_rate.tick(FPS)
這個旋轉物體的做法也可以加到Mover
物件中。要讓Mover
物件旋轉,我們可以把角加速度寫死,例如
angular_acceleration = 0.01
不過我們也可以根據環境中作用在物體上的力量,來動態地產生角加速度,這樣子模擬出來的效果,會更有趣一些。
要模擬出讓物體旋轉的物理現象,可以用一個雖然比較取巧,但產生的結果還算符合常理的方法來達到目的。這個方法其實挺簡單的,那就是把角加速度當成是物體加速度向量的函數,例如
angular_acceleration = acceleration.x
這樣子的設定,會讓物體的加速度向右時,產生逆時針旋轉的角加速度;而當物體的加速度向左時,則會產生順時針旋轉的角加速度。
雖然上述的設定可以達到目的,不過要注意的是,x方向的加速度如果很大,物體可能會因為過大的角加速度,而以看起來不怎麼真實的方式旋轉。要避免這類情況發生,可以把計算方式改成
angular_acceleration = acceleration.x/n # n是數值
或者幫角速度設一個上限值,都可以達到目的。當然啦,這裡的n和上限值,都必須根據實際的狀況來調整
Example 3.2: Forces with (Arbitrary) Angular Motion
Mover
類別的__init__()
、update()
、show()
需修改。修改後的程式碼為:
def __init__(self, x, y, mass):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 讓傳遞進來的數值來決定物體的質量
self.mass = mass
# 物體的質量越大,尺寸就會越大
self.size = 16*self.mass
self.radius = self.size/2
# 讓傳遞進來的數值來決定物體的起始位置
self.position = pygame.Vector2(x, y)
# 物件的初始速度、初始加速度
self.velocity = pygame.Vector2(random.uniform(-1, 1), random.uniform(-1, 1))
self.acceleration = pygame.Vector2(0, 0)
# 物件的初始角度、初始角速度、初始角加速度
self.angular_acceleration = 0
self.angular_velocity = 0
self.angle = 0
# 設定mover所在surface的格式為per-pixel alpha
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.angular_acceleration = self.acceleration.x/10
self.angular_velocity += self.angular_acceleration
# 限定角速度在一個範圍內
if self.angular_velocity > 0.1:
self.angular_velocity = 0.1
elif self.angular_velocity < -0.1:
self.angular_velocity = -0.1
self.angle += self.angular_velocity
self.acceleration *= 0
def show(self):
circle_center = (self.radius, self.radius)
# 畫出具有透明度的mover
pygame.draw.circle(self.surface, (0, 0, 0, 50), circle_center, self.radius)
# 在圓裡面畫一條直線,這樣才能看得出來是在旋轉
pygame.draw.line(self.surface, (0, 0, 0, 255), circle_center, (2*self.radius, self.radius))
# 旋轉角度的單位需由弳度轉換為度
rotated_surface = pygame.transform.rotate(self.surface, math.degrees(self.angle))
rect_new = rotated_surface.get_rect(center=circle_center)
# 把mover所在的surface貼到最後要顯示的畫面上
x, y = self.position
self.screen.blit(rotated_surface, (x+rect_new.x, y+rect_new.y))
主程式如下:
# python version 3.10.9
import math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 3.2: Forces with (Arbitrary) Angular Motion")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
movers = [Mover(random.randint(0, WIDTH), random.randint(0, HEIGHT),
random.uniform(1, 2))
for i in range(20)]
attractor = Attractor(0.4, 20, WIDTH/2, HEIGHT/2)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
attractor.show()
for mover in movers:
force = attractor.attract(mover)
mover.apply_force(force)
mover.update()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 3.3
按滑鼠左鍵可以發射砲彈。
class Projectile:
def __init__(self, x, y, mass):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 讓傳遞進來的數值來決定物體的質量
self.mass = mass
# 物體的質量越大,尺寸就會越大
self.size = 16*self.mass
# 讓傳遞進來的數值來決定物體的起始位置
self.position = pygame.Vector2(x, y)
# 物件的初始速度、初始加速度
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
# 物件的初始角度、初始角速度、初始角加速度
self.angular_acceleration = 0
self.angular_velocity = 0
self.angle = 0
# 設定projectile所在surface的格式為per-pixel alpha
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
def apply_force(self, force):
self.acceleration += force/self.mass
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.angular_acceleration = -self.velocity.x
self.angular_velocity += self.angular_acceleration
# 限定角速度在一個範圍內
if self.angular_velocity > 0.2:
self.angular_velocity = 0.2
elif self.angular_velocity < -0.2:
self.angular_velocity = -0.2
self.angle += self.angular_velocity
self.acceleration *= 0
def show(self):
# 畫出具有透明度的projectile
rect = pygame.Rect(0, 0, self.size, self.size)
pygame.draw.rect(self.surface, (0, 0, 0, 50), rect)
# 旋轉角度的單位需由弳度轉換為度
rotated_surface = pygame.transform.rotate(self.surface, math.degrees(self.angle))
rect_new = rotated_surface.get_rect(center=self.position)
# 把projectile所在的surface貼到最後要顯示的畫面上
self.screen.blit(rotated_surface, (rect_new.x, rect_new.y))
def check_edges(self):
if self.position.x + self.size/2 > self.width:
self.position.x = self.width - self.size/2
self.velocity.x = -0.9*self.velocity.x
elif self.position.x - self.size/2 < 0:
self.position.x = self.size/2
self.velocity.x = -0.9*self.velocity.x
if self.position.y + self.size/2 > self.height:
self.position.y = self.height - self.size/2
self.velocity.y = 0
self.angular_velocity = 0
self.angle = 0
elif self.position.y - self.size/2 < 0:
self.position.y = self.size/2
self.velocity.y = -0.9*self.velocity.y
def friction_force(mu, velocity, N=1):
# 速度小於某個數值時,就設定為0,讓物體停止移動
if velocity.length() >= 0: #0.001:
v_hat = velocity.normalize()
else:
v_hat = pygame.Vector2(0, 0)
return -mu*N*v_hat
# python version 3.10.9
import math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 3.3")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 大砲發射砲彈的力道
cannon_force = pygame.Vector2(6, -8)
projectiles = []
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
# 按一下滑鼠左鍵會發射一顆砲彈
if event.button == 1:
new_projectile = Projectile(0, HEIGHT-50, random.uniform(1, 3))
new_projectile.apply_force(cannon_force)
new_projectile.update()
projectiles.append(new_projectile)
screen.fill(WHITE)
for projectile in projectiles:
# 砲彈持續受到重力的作用
gravity = pygame.Vector2(0, 0.1*projectile.mass)
projectile.apply_force(gravity)
# 掉到地上移動時,會有摩擦力
if projectile.position.y+projectile.size >= HEIGHT:
friction = friction_force(0.05, projectile.velocity, N=1)
projectile.apply_force(friction)
projectile.update()
projectile.check_edges()
projectile.show()
pygame.display.update()
frame_rate.tick(FPS)