這一節的標題是
3.4 Pointing in the Direction of Movement
因為方格子標題字數限制,所以沒完整顯現
在模擬運動中的物體時,如果物體是圓形,那就不需要考慮旋轉的問題,畢竟不管怎麼轉,圓還是圓,看起來都一樣。但是,如果物體不是圓形而是其他形狀呢?模擬如螞蟻、汽車、太空船等不是圓形物體時,除非是要讓它們倒著走,不然不管是直線前進、轉彎,乃至於回頭,都需要讓它們一直面朝運動方向。那要怎麼做,才能讓物體一直面朝運動方向呢?
所謂的「面朝運動方向」,指的其實是「把物體轉到和速度向量一樣的角度」。要達到這個目的,就必須算出速度向量的角度。假設速度向量v=(vx, vy)$,從前一節的圖以及分析可以知道
tanθ = vy / vx
這裡的θ就是速度向量的角度。所以
θ = tan-1(vy / vx)
也就是說,利用tan的反三角函數tan-1,就可以算出v的角度。
在python中,math.atan(x)
和math.atan2(y, x)
都可以用來計算tan-1,但是它們有什麼不同,又該用哪一個呢?
math.atan(x)
只有一個參數,算出來的角度範圍介於-π/2到π/2之間。所以,從math.atan(x)
所得到的向量角度,沒有辦法得知向量到底是位於哪一個象限。例如,向量(4, 3)和(-4, -3)分別位於第一、三象限,在算角度時,傳給math.atan(x)
的數值分別是
3 / 4 = 0.75
及
(-3) / (-4) = 0.75
這是完全一樣的數值,算出來的角度當然是一樣的。又例如,向量(-4, 3)和(4, -3)分別位於第二、四象限,傳給math.atan(x)
的數值分別是
3 / (-4) = -0.75
及
(-3) / 4 = -0.75
也是完全一樣的數值,最後算出來的角度,當然也會一樣。因此,使用math.atan(x)
時,要想知道向量真正的角度,還必須要搭配兩個分量的正負號來判斷才有辦法辦到。
至於math.atan2(y, x)
,算出的角度範圍,會介於-π到π之間。因此,從math.atan2(y, x)
所得到的角度,是向量真正的角度,可以清楚地知道向量到底是位於哪個象限。
從上述的分析就可以知道,選用math.atan2(y, x)
來計算向量的角度,是比較好的做法,可以省去不少功夫。
下面這個例子,是把Example 1.10中的mover
圖案,由圓形改成長方形,並加入旋轉的功能,讓mover
可以自動轉向,永遠面向滑鼠游標的方向。
Example 3.3: Pointing in the Direction of Motion
class Mover:
def __init__(self):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 起始位置位於畫面中央
self.position = pygame.Vector2(self.width//2, self.height//2)
# 最高速限
self.top_speed = 4
# 初始速度
self.velocity = pygame.Vector2(0, 0)
# mover的大小
self.size = (30, 10)
# 設定mover所在surface的格式為per-pixel alpha
self.surface = pygame.Surface(self.size, pygame.SRCALPHA)
def update(self):
# 計算加速度,其方向為,由mover所在位置指向滑鼠游標位置的方向
mouse = pygame.Vector2(pygame.mouse.get_pos())
acceleration = mouse - self.position
# 調整加速度大小,呈現比較好的模擬效果
acceleration.scale_to_length(0.5)
self.velocity += acceleration
# 限速
if self.velocity.length() > self.top_speed:
self.velocity.scale_to_length(self.top_speed)
self.position += self.velocity
def show(self):
# 在surface上畫出mover
rect = pygame.Rect((0, 0), self.size)
pygame.draw.rect(self.surface, (0, 0, 0), rect)
# 讓mover面朝運動方向
angle = math.atan2(self.velocity.y, self.velocity.x)
# 旋轉角度的單位需由弳度轉換為度
rotated_surface = pygame.transform.rotate(self.surface, -math.degrees(angle))
rect_new = rotated_surface.get_rect(center=self.position)
# 把mover所在的surface貼到最後要顯示的畫面上
self.screen.blit(rotated_surface, (rect_new.x, rect_new.y))
def check_edges(self):
if self.position.x > self.width:
self.position.x = 0
elif self.position.x < 0:
self.position.x = self.width
if self.position.y > self.height:
self.position.y = 0
elif self.position.y < 0:
self.position.y = self.height
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 3.3: Pointing in the Direction of Motion")
WHITE = (255, 255, 255)
screen_size = (640, 360)
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
mover = Mover()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
mover.update()
mover.check_edges()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
在旋轉surface
時需加上負號,這是因為surface
的原點是在左上角,而y軸的方向是向下,剛好與一般我們所熟悉的,y軸方向向上的直角座標系統相反。所以,必須加上負號來調整,使其方向一致。
Exercise 3.4
按下向左鍵時,加速度向量的方向,是速度向量逆時針轉90度的方向;按下向右鍵時,則是順時針轉90度。寫程式時要注意在畫面上呈現出來的旋轉方向是相反的,必須加上負號來調整。這是因為畫面的y軸方向是向下,跟直角座標系統的y軸方向相反。
class Vehicle:
def __init__(self):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 起始位置位於畫面中央
self.position = pygame.Vector2(self.width//2, self.height//2)
# 最高速限
self.top_speed = 10
# 初始速度
self.velocity = pygame.Vector2(1, 0)
# vehicle的大小
self.size = (30, 10)
# 設定vehicle所在surface的格式為per-pixel alpha
self.surface = pygame.Surface(self.size, pygame.SRCALPHA)
def update(self):
keys = pygame.key.get_pressed()
# 按鍵盤向左鍵左轉灣;向右鍵右轉灣
if keys[pygame.K_LEFT]:
acceleration = self.velocity.rotate(-90)
# 調整加速度大小,呈現比較好的模擬效果
acceleration.scale_to_length(0.05)
elif keys[pygame.K_RIGHT]:
acceleration = self.velocity.rotate(90)
# 調整加速度大小,呈現比較好的模擬效果
acceleration.scale_to_length(0.05)
else:
acceleration = pygame.Vector2(0, 0)
self.velocity += acceleration
# 限速
if self.velocity.length() > self.top_speed:
self.velocity.scale_to_length(self.top_speed)
self.position += self.velocity
def show(self):
# 在surface上畫出vehicle
rect = pygame.Rect((0, 0), self.size)
pygame.draw.rect(self.surface, (0, 0, 0), rect)
# 讓vehicle面朝運動方向
angle = math.atan2(self.velocity.y, self.velocity.x)
# 旋轉角度的單位需由弳度轉換為度
rotated_surface = pygame.transform.rotate(self.surface, -math.degrees(angle))
rect_new = rotated_surface.get_rect(center=self.position)
# 把vehicle所在的surface貼到最後要顯示的畫面上
self.screen.blit(rotated_surface, (rect_new.x, rect_new.y))
def check_edges(self):
if self.position.x > self.width:
self.position.x = 0
elif self.position.x < 0:
self.position.x = self.width
if self.position.y > self.height:
self.position.y = 0
elif self.position.y < 0:
self.position.y = self.height
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 3.4")
WHITE = (255, 255, 255)
screen_size = (640, 360)
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
vehicle = Vehicle()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
vehicle.update()
vehicle.check_edges()
vehicle.show()
pygame.display.update()
frame_rate.tick(FPS)