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

閱讀時間約 28 分鐘

角運動(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)





15會員
129內容數
寫點東西自娛娛人
留言0
查看全部
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
角度有兩種單位,一個就是大家耳熟能詳的「度」(degree),記做 °;另一個是「弳度」(radian),也翻譯成「弧度」,記做rad。當使用弳度來度量角度的大小時,通常會把rad省略,不寫出來。
這一章主要在探討,如何模擬震盪(oscillation)相關的物理現象如波、單擺、彈簧等。
到目前為止,我們所模擬的萬有引力,是一個物體吸引另一個物體,或者是一個物體吸引多個物體。然而,在真實世界中,每個物體都會互相吸引,所以在這一節中,就來把模擬的情境,擴展成多個物體互相吸引。
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
角度有兩種單位,一個就是大家耳熟能詳的「度」(degree),記做 °;另一個是「弳度」(radian),也翻譯成「弧度」,記做rad。當使用弳度來度量角度的大小時,通常會把rad省略,不寫出來。
這一章主要在探討,如何模擬震盪(oscillation)相關的物理現象如波、單擺、彈簧等。
到目前為止,我們所模擬的萬有引力,是一個物體吸引另一個物體,或者是一個物體吸引多個物體。然而,在真實世界中,每個物體都會互相吸引,所以在這一節中,就來把模擬的情境,擴展成多個物體互相吸引。
模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Nature is a profound source of inspiration and tranquility. From the vibrant colors of blooming flowers to the soothing sounds of flowing rivers, the
Thumbnail
"You live for yourselves, not for humans." About trees and their feelings of sadness due to human interference.
我有幸與自然的奇蹟相遇,這次的經歷讓我深受觸動和感動。我想和大家分享這段美好的回憶。 這次的邂逅發生在一個寧靜的山間湖泊。當我來到這個美麗的地方時,我被周圍的自然景色所震撼。湖水清澈見底,山峰環繞著湖泊,形成壯麗的景觀。 我決定在湖邊散步,欣賞著湖水的漣漪和山的倒影。這一切讓
Now I understand: there is no need for futile treatment or ineffective care, allowing our mother nature to take over may be the kindest approach for t
In the morning, the sunlight pours into my bedroom through the window, like a natural alarm clock. I opened my eyes to feel the fresh air and the
Thumbnail
接受我跟雙生的命運之後,我內在愛的能量就在源源不絕地跑出來,靈感下載也每天的量超大,這些我想要寫出來的「情書」系列,我發現也許比起理論性的文章更能讓大家「感受」我內在感覺到的愛的能量。老實說我不太在乎我的雙生火焰是否真的看到這些情書,我感覺自己也不是真的想要寫給他,而這是我的「任務」的一部分。雖然我
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Nature is a profound source of inspiration and tranquility. From the vibrant colors of blooming flowers to the soothing sounds of flowing rivers, the
Thumbnail
"You live for yourselves, not for humans." About trees and their feelings of sadness due to human interference.
我有幸與自然的奇蹟相遇,這次的經歷讓我深受觸動和感動。我想和大家分享這段美好的回憶。 這次的邂逅發生在一個寧靜的山間湖泊。當我來到這個美麗的地方時,我被周圍的自然景色所震撼。湖水清澈見底,山峰環繞著湖泊,形成壯麗的景觀。 我決定在湖邊散步,欣賞著湖水的漣漪和山的倒影。這一切讓
Now I understand: there is no need for futile treatment or ineffective care, allowing our mother nature to take over may be the kindest approach for t
In the morning, the sunlight pours into my bedroom through the window, like a natural alarm clock. I opened my eyes to feel the fresh air and the
Thumbnail
接受我跟雙生的命運之後,我內在愛的能量就在源源不絕地跑出來,靈感下載也每天的量超大,這些我想要寫出來的「情書」系列,我發現也許比起理論性的文章更能讓大家「感受」我內在感覺到的愛的能量。老實說我不太在乎我的雙生火焰是否真的看到這些情書,我感覺自己也不是真的想要寫給他,而這是我的「任務」的一部分。雖然我