2024-08-23|閱讀時間 ‧ 約 49 分鐘

The Nature of Code閱讀心得與Python實作:3.2 Angular Motion

角運動(angular motion)指的是物體的旋轉運動。物體進行直線運動時,可以用速度和加速度來描述它的運動。速度指的是單位時間內,物體的位移量;而加速度則是單位時間內,速度的改變量。同樣的做法也可以運用在描述角運動上,我們可以用角速度(angular velocity)和角加速度(angular acceleration)來描述角運動。角速度指的是單位時間內,物體旋轉的角度;而角加速度,則是單位時間內角速度的改變量。

在第1、2章處理位置、速度、加速度時,是使用下列做法:

position += velocity
velocity += acceleration

處理物體的旋轉,也可以用相同的方式:

angle += angular_velocity
angular_velocity += angular_acceleration

不過要注意的是,positionvelocityacceleration是向量,但是angleangular_velocityangular_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)





分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.