更新於 2024/08/30閱讀時間約 16 分鐘

The Nature of Code閱讀心得與Python實作:3.4 Pointing in the...

這一節的標題是
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)


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