The Nature of Code閱讀心得與Python實作:5.2 Vehicles and Steering

更新 發佈閱讀 55 分鐘

轉向行為(steering behaviors)是Craig W. Reynolds所提出來的,其主要的目的,是要讓電腦動畫及互動媒體如電玩、虛擬實境中,能夠自主行動的角色,可以利用包括尋標(seeking)、逃跑(fleeing)、漫步(wandering)、抵達(arriving)、追補(pursuing)、迴避(evading),以及其他許多的策略,在他們的世界中,以更逼真、更像具有生命般的方式移動。

在Reynolds發表的論文〈Steering Behaviors For Autonomous Characters〉中,他選用了「vehicle」這個字來作為自主代理人的名稱。原書作者沿用這個名稱,在設計自主代理人時,就把類別名稱定為Vehicle

Reynolds假設vehicle是理想化的,也就是說,vehicle的質量會集中在一個點上。因此,不管vehicle的實際構造如何,也不管他的工作原理是什麼,更不管他是用輪子、用腳、用翅膀,還是用任何其他的方式移動,他就是會乖乖地工作,並對定義好的規則有所回應。

為了易於瞭解、討論,Reynolds以階層化的三層結構來拆解vehicle展現出自主移動行為的過程。這三層結構為:

  • 選擇行動(action selection):vehicle會根據從環境中取得的資訊來選擇可以達到目的的行動。如果vehicle的目標是避免撞壞自己,那當他在行進中發現前面有障礙物時,就會根據實際的狀況來選擇該採取哪種行動,以避免撞上去。繞過去?停止前進?不管採取哪種行動,都是為了達成避免撞壞自己這個目標。
  • 轉向(steering):當vehicle決定好要採取的行動之後,接下來就是要去算出執行這個行動所需的各項資料。如果決定要繞過去,那要從左邊或右邊繞過去?要花多少力氣?這個改變移動方向的力,就是轉向力(steering force)。在這一層中,vehicle需算出行動所需的轉向力。
  • 運轉移動(locomotion):在轉向層所得到的結果,要在這一層中付諸行動。因為vehicle是理想化的,所以在轉向層所算出來的轉向力就直接作用在vehicle上來改變移動方向,不需要去管那個力是怎麼來的,又是透過怎樣的傳動機制讓vehicle可以移動的。

The Steering Force

轉向力到底是個什麼樣的力呢?假設有個行進中的vehicle正在尋找目標。當這個vehicle發現目標不在他前進的方向上時,他必須改變行進的方向,這樣才能到達目標物所在處。要改變行進的方向,必須有股力量作用在vehicle上,如果這股力量是vehicle自己感知外在環境與自身狀態之後所決定產生的,那就稱這股力量為轉向力。

要讓vehicle朝目標物跑過去,我們可以把目標物設計成一個會散發出吸引力的物體,然後讓vehicle被吸過去。不過這樣子的話,vehicle就不是依照自己的意願主動接近目標物了;它是被動地被目標物吸過去的。在這種被動被吸過去的情況下,vehicle有可能會像是人造衛星繞著地球跑一樣,就在目標物周圍繞啊繞的,永遠到不了目標物所在地。所以,既然vehicle是個自主代理人,那就應該讓他心甘情願自己主動奔向目標物。這樣子,vehicle就會不斷地調整自己的移動方向,不斷地朝目標物接近,最後到達目標物所在處。要讓vehicle能夠主動地調整移動方向朝目標物接近,就必須讓他能夠產生調整移動方向所需要的轉向力。接下來,就來看看要怎麼賦予vehicle這樣子的能力。

raw-image

如圖,假設vehicle正以速度vcurrent前進,這時他發現目標物並不在前進的路線上。既然目標物的位置不在前方,那最快到達那裡的方式,就是原地調轉方向,對準目標物,然後以最快的速度vdesired前進。如果vehicle是在靜止狀態,原地調轉方向這個動作是可以做到的。可是,他現在正在錯誤的方向前進當中,想要調整成正確的方向,那就必須要靠適當的轉向力才能辦到。

既然vdesired是vehicle能到達目標物所在地最快的速度,那如果vehicle所產生的轉向力,能夠讓他的速度從vcurrent轉變成vdesired,這樣就再好不過了。速度從vcurrent轉變成vdesired,所以速度的改變量為

Δv = vdesired vcurrent

速度的轉變需要時間,而在我們的模擬世界中,時間的基本單位是幀,這也就是說,vehicle的速度,在1幀的時間內,會有Δv的變化。因此,vehicle實際上是以Δv的加速度,在1幀的時間內,讓他的速度由vcurrent轉變成vdesired。假設vehicle的質量為m,要產生這樣子的加速度來改變速度,由牛頓第二定律,所需的作用力,也就是轉向力,為

Fsteer = mΔv

推導出轉向力的計算式之後,接下來就可以開始設計Vehicle類別了。因為vehicle會在畫面上跑來跑去,所以先前設計MoverParticle這兩個類別時所使用的架構,也可以拿來這邊用。這也就是說,在Vehicle類別中,也會有下列屬性:

m:質量,純量
position:位置向量,pygame.Vector2物件
velocity:速度向量,pygame.Vector2物件
acceleration:加速度向量,pygame.Vector2物件

除此之外,因為我們希望自主代理人所表現出的行為,能夠像真實世界中會看到的情況一樣,所以有必要針對他的一些能力加以限制,讓他的行為能更加逼真。基於這樣子的考量,我們在Vehicle類別中,增加了下列兩個屬性來限制他的能力:

max_speed:最高移動速率,純量
max_force:最大出力,純量

規劃好主要的屬性之後,就可以來設計Vehicle類別中最核心的部分,也就是可以計算出需要的轉向力,來讓vehicle可以順利朝目標前進的方法;這個方法,就把它命名為seek

根據先前所推導出的計算式,在計算轉向力時,需要知道vcurrentvdesired這兩個速度向量。向量vcurrent是已知的,存放在屬性velocity中;而向量vdesired,則需根據vehicle及目標物的位置來計算。

要計算vdesired,必須知道vehicle及目標物的位置。vehicle的位置是已知的,存放在屬性position中;而目標物的位置,也會是已知的,因為vehicle可以感知到目標物在哪裡。那程式要怎麼寫,才能讓vehicle感知到目標物的位置呢?這其實挺簡單的,只要把目標物所在的位置傳進seek()方法中就可以了。

知道如何得知vehicle本身及目標物的位置之後,接下來就來看看計算轉向力的程式要怎麼寫。

假設pygame.Vector2物件target_position是目標物所在位置的位置向量,則由positiontarget_position這個向量的方向,就是vdesired的方向。至於vdesired的大小,則依實際上不同的應用,可以是vehicle原來的速度大小,也可以是它的最高移動速率。如果要讓vehicle全速朝目標物飛奔而去,那計算vdesired的程式可以這樣寫

desired = target_position - position
if desired.length() > 1.e-5:
desired.scale_to_length(max_speed)

因為scale_to_length()無法調整0向量的大小,所以要先判斷desired是否不是0向量,然後才能調整它的大小。判斷式中使用1.e-5這個數字而不是0,是因為當向量的長度小於某個數值時,scale_to_lenght()就會認定它是0向量;1.e-5這個數字應該夠小了。

算出vdesired之後,就可以利用先前所推導出的計算式來計算轉向力了。不過在計算時,可以稍微簡化一些,把式子當中的m這一項給拿掉,就直接把Δv當作是轉向力;這是因為不管有沒有乘上m,算出來的轉向力大小,通常都會超過max_force的限制,因而必須被降為max_force的緣故。根據簡化後的計算式,計算轉向力的程式可以這樣寫:

steer = desired - velocity
if steer.length() > max_force:
steer.scale_to_length(max_force)

當然,算出轉向力之後,還必須讓它作用在vehicle上頭,也就是

apply_force(steer)

設計完成的Vehicle類別程式碼如下:

class Vehicle:
def __init__(self, x, y, size=24, mass=1):
# 取得顯示畫面
self.screen = pygame.display.get_surface()

# 讓傳遞進來的數值來決定particle的質量
self.mass = mass

# vehicle的大小,長 x 寬 = size x size/2
self.size = size

# vehicle的初始位置、初始速度、初始加速度
self.position = pygame.Vector2(x, y)
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)

# vehicle的最大速率、最大出力
self.max_speed = 8
self.max_force = 0.2

# 設定vehicle所在surface的格式為per-pixel alpha,並在上面畫出vehicle
self.surface = pygame.Surface((self.size, self.size/2), pygame.SRCALPHA)
body = [(0, 0), (0, self.size/2), (self.size, self.size//4)]
pygame.draw.polygon(self.surface, (0, 0, 0), body)

def apply_force(self, force):
self.acceleration += force/self.mass

def seek(self, target_position):
desired = target_position - self.position
if desired.length() > 1.e-5:
desired.scale_to_length(self.max_speed)

steer = desired - self.velocity
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)

self.apply_force(steer)

def update(self):
self.velocity += self.acceleration
if self.velocity.length() > self.max_speed:
self.velocity.scale_to_length(self.max_speed)

self.position += self.velocity

self.acceleration *= 0

def show(self):
# 旋轉surface,讓vehicle面朝前進方向
heading = math.atan2(self.velocity.y, self.velocity.x)
rotated_surface = pygame.transform.rotate(self.surface, -math.degrees(heading))
rect_new = rotated_surface.get_rect(center=self.position)

# 把vehicle所在的surface貼到最後要顯示的畫面上
self.screen.blit(rotated_surface, rect_new)

下面這個例子,是讓vehicle把滑鼠游標當作搜尋目標,所展現出來的轉向行為。

Example 5.1: Seeking a Target

# python version 3.10.9
import math
import sys

import pygame # version 2.3.0

pygame.init()

pygame.display.set_caption("Example 5.1: Seeking a Target")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

vehicle = Vehicle(WIDTH//2, HEIGHT//2)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

screen.fill(WHITE)

mouse = pygame.Vector2(pygame.mouse.get_pos())
pygame.draw.circle(screen, (0, 0, 0), mouse, 24, 2)

vehicle.seek(mouse)
vehicle.update()
vehicle.show()

pygame.display.update()
frame_rate.tick(FPS)

Exercise 5.1

seek()方法中,計算desired向量的部分,加個負號即可:

desired = -(target_position - self.position)

Exercise 5.2

修改seek()方法,加入調整max_speedmax_force的機制。vehicle越靠近目標物,max_speedmax_force會越低。另外,因為max_speedmax_force有可能會變成0,導致desiredsteervelocity等向量也變成0,所以包括update()方法在內,有使用scale_to_length()調整向量大小的部分,都必須加上判斷向量大小是否為0的判斷式,以避免出現錯誤。

def seek(self, target_position):
desired = target_position - self.position

# 依據接近目標的程度,動態調整最大速率及最大力量
proximity = desired.length()/100
self.max_speed = 8*proximity
self.max_force = 0.2*proximity

if desired.length() > 1.e-5:
desired.scale_to_length(self.max_speed)

steer = desired - self.velocity
if self.max_force > 1.e-5:
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)

self.apply_force(steer)

def update(self):
self.velocity += self.acceleration
if self.max_speed > 1.e-5:
if self.velocity.length() > self.max_speed:
self.velocity.scale_to_length(self.max_speed)

self.position += self.velocity

self.acceleration *= 0

Exercise 5.3

以繼承Vehicle類別的方式來設計Pursuer類別及Target類別。在Pursuer類別的pursue()方法中,以外插方式來預測target在未來的位置P,計算式如下:

P = P0 + T*vtarget

其中P0target目前的位置;T是預測區間,也就是要預測T個單位時間之後的位置;vtargettarget目前的速度。

class Pursuer(Vehicle):
def __init__(self, x, y, size=24, mass=1):
super().__init__(x, y, size=24, mass=1)

self.prediction_interval = 10

def pursue(self, target):
# 以外插方式來預測target在未來的位置
prediction = target.position + self.prediction_interval*target.velocity

desired = prediction - self.position
if desired.length() > 1.e-5:
desired.scale_to_length(self.max_speed)

steer = desired - self.velocity
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)

self.apply_force(steer)


class Target(Vehicle):
def __init__(self, x, y, size=24, mass=1):
super().__init__(x, y, size=24, mass=1)

# 取得顯示畫面的大小
self.width, self.height = self.screen.get_size()

# target的大小
self.size = size
self.radius = self.size/2

# target的移動速度
velocity = random.choice([-1, 1])*pygame.Vector2(random.randint(1, WIDTH), random.randint(1, HEIGHT))
velocity.scale_to_length(3)
self.velocity = velocity

# 設定target所在surface的格式為per-pixel alpha,並在上面畫出target
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
self.center = pygame.Vector2(self.radius, self.radius)
pygame.draw.circle(self.surface, (0, 0, 0), self.center, self.radius)

def update(self):
self.position += self.velocity

def show(self):
self.screen.blit(self.surface, self.position-self.center)

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 random
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 5.3")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 840, 560
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

pursuer = Pursuer(WIDTH//2, HEIGHT//2)

target = Target(random.randint(0, WIDTH), random.randint(0, HEIGHT), 50)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

screen.fill(WHITE)

# 擊中目標,重新開始
if (target.position-pursuer.position).length() <= target.radius:
pursuer.position = pygame.Vector2(WIDTH//2, HEIGHT//2)
target = Target(random.randint(0, WIDTH), random.randint(0, HEIGHT), 50)

pursuer.pursue(target)
pursuer.update()
pursuer.show()

target.update()
target.check_edges()
target.show()

pygame.display.update()
frame_rate.tick(FPS)

The Arrive Behavior

vehicle利用seek()方法來尋找目標時,即便知道目標物在哪裡,但在抵達目標物所在位置時,總是會衝過頭,然後回頭重新向跑向目標物,然後又衝過頭又再回頭,不斷地重複這個過程,完全沒法停在目標物所在位置。為什麼會這樣呢?這是因為不管距離目標是遠是近,vehicle無時無刻就只知道要全速衝過去,而不知道這樣做是會衝過頭的。

那要怎麼讓vehicle不會陷入那種衝過頭,永遠沒法停在目標物所在位置的無窮輪迴呢?想想看火車要進站時會怎麼做就知道了。當火車離站還很遠時,它會以很快的速度前進。隨著越來越接近停靠站,火車的速度會越來越慢、越來越慢,最後準確地停在要停的地方。火車進站的這種做法,就是可以讓vehicle擺脫不斷衝過頭再回頭的無窮輪迴,真正的抵達目的地的方法。接下來,就來看看這個抵達(arriving)行為的程式要怎麼寫。

要讓vehicle越接近目標物速度越慢,可以把seek()方法中計算desired的部分改成這樣:

slowing_distance = 100
c = 0.5

desired = target_position - self.position
if desired.length() < slowing_distance:
desired *= c
else:
desired.scale_to_length(self.max_speed)

這樣子,當vehicle與目標物的距離比slowing_distance設定的數字小時,就代表他已經進入減速區,而desired的大小,也就會隨著他越來越靠近目標物而越來越小,從而達到減速的目的。

雖然上述的做法可以讓vehicle越接近目標物時速度越慢,但並不是個很好的做法。怎麼說呢?雖然看起來我們可以藉由調整c的大小,來調整減速的效果,但卻不一定有用。例如,假設

max_speed = 5
slowing_distance = 100
c = 0.5

vehicle距離目標物50個像素時,算出來的desired向量,大小會是25,遠大於max_speed的值。因此,雖然vehicle已在減速區內,但desired的大小,還是max_speed,沒有任何改變,所以根本不會減速。

那有沒有更好的做法呢?Reynolds的做法就不會有上述的問題。他的做法其實挺簡單的,就是當vehicle進入減速區後,desired向量的大小,就會隨著離目標物越來越近,而從max_speed降為0;其計算式為

max_speed*desired.length()/slowing_distance

根據Reynolds提出的方法,讓vehicle能展現抵達行為的方法可以這麼寫:

def arrive(self, target_position):
slowing_distance = 100

desired = target_position - self.position
if desired.length() < slowing_distance:
if desired.length() > 1.e-5:
reduced_speed = self.max_speed*desired.length()/slowing_distance
desired.scale_to_length(reduced_speed)

else:
desired.scale_to_length(self.max_speed)

steer = desired - self.velocity
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)

self.apply_force(steer)

arrive()方法的效果,可以從下面這個例子看出來。

Example 5.2: Arriving at a Target

# python version 3.10.9
import math
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 5.2: Arriving at a Target")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

vehicle = Vehicle(WIDTH//2, HEIGHT//2)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

screen.fill(WHITE)

mouse = pygame.Vector2(pygame.mouse.get_pos())
pygame.draw.circle(screen, (0, 0, 0), mouse, 24, 2)

vehicle.arrive(mouse)
vehicle.update()
vehicle.show()

pygame.display.update()
frame_rate.tick(FPS)

vehicle的抵達行為,充分展現出了自主代理人的特性。當vehicle進入減速區之後,會根據他及目標物的位置,以及他和目標物之間的距離,計算出可順利抵達目標物所在處的速度vdesired,然後再根據這個可行的速度與他目前速度之間的差異,計算出需施加的轉向力。這整個過程,都有賴於他能感知到外部環境,也就是目標物的位置,以及內部狀態,也就是他自己的位置、速度,並根據這些資訊計算出該採取的行動。這些都是vehicle自主完成的,沒有任何人指揮他該怎麼做。

另一個vehicle的抵達行為所展現出的自主代理人特性,就在於他所產生的轉向力。當vehicle在減速區外時,他所產生的轉向力,其方向是指向目標物的方向,這樣他才能夠朝目標物接近。然而,當進入減速區之後,他所產生的轉向力,其方向卻是指向遠離目標物的方向,這樣才能降低接近目標物的速度,以避免因為速度太快而衝過頭。與萬有引力這類由非生命體所產生的作用力相較之下,轉向力這種會根據環境而調整的作用力,能讓vehicle的行為更加的逼真、有生命力;反觀在目標物的萬有引力作用之下,不管vehicle距離目標物是遠是近,作用力的方向永遠會指向目標物,而讓其行為看起來比較呆板、單調、沒生命力。

Your Own Behaviors

Reynolds設計了許多不同的轉向行為。在設計這些轉向行為時,最終都會歸結到一個相同的問題:怎樣的vdesired計算方式,才能夠讓自主代理人展現出我們想要看到的轉向行為?這也就是說,當我們要設計新的轉向行為時,其實就是要設計出新的vdesired計算方式。接下來,就用兩個例子來看看這種設計模式是怎麼進行的。這兩個例子,一個是Reynolds設計的漫步(wander)轉向行為,另一個是新的、原書提出來的,叫做「stay within walls」的轉向行為。

漫步是隨機轉向的一種類型,最簡單的實作方式,就是在每一幀中,都產生隨機的轉向力。不過,這樣子的做法,會讓自主代理人的行動看起來神經兮兮的,實在不怎麼有趣。

針對漫步這種轉向行為,Reynolds設計了一個巧妙的方式,來避免自主代理人出現神經兮兮的行為。Reynolds的設計,會讓自主代理人移動的方式,看起來就像是朝某個方向移動一會兒,然後轉頭朝另一個方向移動一會兒,然後又轉頭朝另一個方向移動一會兒,如此不斷地重複這樣子的行為。接下來,就來看看這個巧妙的方式是怎麼做的。不過,要注意的是,接下來所描述的,並不完全是Reynolds論文中所提出的原始做法,而是經過原書稍微簡化過之後的做法。

先前提到過,在設計轉向行為時,最重要的關鍵在於設計vdesired的計算方式;而在計算vdesired時,唯一的未知數,就是目標物的位置。Reynolds就是藉由設計目標物位置的改變模式,來設計漫步這種轉向行為。接下來就來看看詳細的做法。

raw-image

如圖,在vehicle前進方向距離d的地方,畫一個半徑為r的圓,然後在圓上任意取一個點作為目標物的位置。假設目標物的位置和vehicle前進方向間的夾角是θ,而且Δθ是個介於−ϕ和ϕ之間的隨機數值。在下一幀時,在圓上那個和vehicle前進方向間的夾角為θ+Δθ的點,就是目標物新的位置。利用這樣子的做法,就可以讓目標物在一個有限的範圍內,隨機地改變位置;而vehicle在尋標的過程中所展現出來的移動方式,也會是隨機的,但又不至於看起來神經兮兮的。

Exercise 5.4

class Wanderer(Vehicle):
def __init__(self, x, y, size=24, mass=1):
super().__init__(x, y, size=24, mass=1)

# 取得顯示畫面的大小
self.width, self.height = self.screen.get_size()

# wanderer的最大速率、最大出力
self.max_speed = 2
self.max_force = 0.05

# 前方大圓距離d、半徑r、圓心位置center
self.d = 80
self.r = 25
self.center = pygame.Vector2()

# 目標物偏移角度theta、角度變動範圍: -phi ~ phi
# 角度單位:度
self.theta = 0
self.phi = 15

# 目標物位置
self.target = pygame.Vector2()

def wander(self):
direction = self.velocity.normalize() if self.velocity.length()>0 else pygame.Vector2(1, 0)
self.center = self.position + self.d*direction

heading = math.degrees(math.atan2(self.velocity.y, self.velocity.x))
self.theta += random.uniform(-self.phi, self.phi)
self.target = self.center + pygame.Vector2.from_polar((self.r, self.theta+heading))

self.seek(self.target)

def show_target(self):
# 在顯示畫面上畫出wanderer前方的大圓、wanderer到大圓之間的直線、大圓圓心到目標物之間的直線
pygame.draw.circle(self.screen, (0, 0, 0), self.center, self.r, 1)
pygame.draw.line(self.screen, (0, 0, 0), self.position, self.center)
pygame.draw.line(self.screen, (0, 0, 0), self.center, self.target)
pygame.draw.circle(self.screen, (0, 0, 0), self.target, 3)

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 random
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 5.4")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

wanderer = Wanderer(WIDTH//2, HEIGHT//2)

show_target_position = True

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
show_target_position = not show_target_position

screen.fill(WHITE)

wanderer.wander()
wanderer.update()
wanderer.check_edges()
wanderer.show()
if show_target_position:
wanderer.show_target()

pygame.display.update()
frame_rate.tick(FPS)

接下來,再來看另一個例子。在這個例子中,當vehicle發現自己太靠近牆壁時,就會轉個彎,快速遠離牆壁。例如,當他跟某個垂直方向的牆壁之間的距離太近時,就會產生轉向力,在保持速度的垂直分量不變的情況下,讓速度的水平分量,轉變成能遠離牆壁方向的最大速率,以遠離牆壁。

Example 5.3: "Stay Within Walls” Steering Behavior

Vehicle類別新增一個boundaries()方法來產生前述之轉向力。假設顯示畫面的四個邊就是四面牆,而vehicle跟牆壁之間的距離小於offset時,就會產生能遠離牆壁的轉向力;程式如下:

def boundaries(self, offset):
desired = None

# 進入垂直邊界,以最大的水平方向速率遠離
if self.position.x > self.width - offset:
desired = pygame.Vector2(-self.max_speed, self.velocity.y)
elif self.position.x < offset:
desired = pygame.Vector2(self.max_speed, self.velocity.y)

# 進入水平邊界,以最大的垂直方向速率分量遠離
if self.position.y > self.height - offset:
desired = pygame.Vector2(self.velocity.x, -self.max_speed)
elif self.position.y < offset:
desired = pygame.Vector2(self.velocity.x, self.max_speed)

if desired is not None:
desired.scale_to_length(self.max_speed)
steer = desired - self.velocity
if steer.length() > 1.e-5:
steer.scale_to_length(self.max_force)
self.apply_force(steer)

在這個方法的開頭,為了要知道vehicle是不是因為跟牆壁之間的距離小於offset而需要產生轉向力,所以設定

desired = None

並在後面藉由檢查desired是不是仍然為None來判斷vehicle是否需產生轉向力。那為什麼不把desired設定成0向量,然後檢查desired是否為0向量呢?根據原書的說明,這會讓vehicle最後靜止不動。不過,實際用原書提供的程式以及python來測試,如果把desired設定成0向量,然後藉由檢查desired的大小是否為0來判斷是否需產生轉向力,就可以避免這種狀況發生。因此,boundaries()方法,也可以寫成

def boundaries(self, offset):
desired = pygame.Vector2(0, 0)
:
:
if desired.length() != 0:
:
:

不過,這種寫法並沒有比較好,畢竟去檢查變數是不是等於某個數值,就是個會讓人沒有安全感的寫法。

主程式部分如下:

# python version 3.10.9
import math
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 5.3: “Stay Within Walls” Steering Behavior")

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

vehicle = Vehicle(WIDTH//2, HEIGHT//2)

wall_offset = 25

bound = pygame.Rect(wall_offset, wall_offset, WIDTH-2*wall_offset, HEIGHT-2*wall_offset)

show_boundaries = True

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
show_boundaries = not show_boundaries

screen.fill(WHITE)

vehicle.boundaries(wall_offset)
vehicle.update()
vehicle.show()
if show_boundaries:
pygame.draw.rect(screen, BLACK, bound, 1)

pygame.display.update()
frame_rate.tick(FPS)

Exercise 5.5

讓vehicle像魚一樣扭動著游向滑鼠指標;距離越近,扭動速度越慢。程式寫法很簡單,修改wander()方法中,用來計算theta的式子就可以了。

class Fish(Vehicle):
def __init__(self, x, y, size=24, mass=1):
super().__init__(x, y, size=24, mass=1)

# 最大速率、最大出力
self.max_speed = 2
self.max_force = 0.2

# 前方大圓距離d、半徑r、圓心位置center
self.d = 80
self.r = 50
self.center = pygame.Vector2()

# 前方大圓上之假想目標物偏移角度,單位「度」
self.theta = 0

def swim(self, food_position):
direction = self.velocity.normalize() if self.velocity.length()>0 else pygame.Vector2(1, 0)
self.center = self.position + self.d*direction

heading = math.degrees(math.atan2(self.velocity.y, self.velocity.x))
# 依據與食物之間的距離來控制扭動速度,距離越近,扭動速度越慢
self.theta += (self.position-food_position).length()/10
if self.theta > 360:
self.theta -= 360

target = food_position + pygame.Vector2.from_polar((self.r, self.theta+heading))
self.seek(target)


# python version 3.10.9
import math
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 5.5")

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

fish = Fish(WIDTH//2, HEIGHT//2)

show_target_position = True

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

screen.fill(WHITE)

mouse = pygame.Vector2(pygame.mouse.get_pos())
pygame.draw.circle(screen, (0, 0, 0), mouse, 5, 2)

fish.swim(mouse)
fish.update()
fish.show()

pygame.display.update()
frame_rate.tick(FPS)


留言
avatar-img
留言分享你的想法!
avatar-img
ysf的沙龍
19會員
159內容數
寫點東西自娛娛人
ysf的沙龍的其他內容
2025/03/10
在模擬群聚行為時,隨著boid的數量越來越多,需要的計算量也會越來越多,導致程式的執行速度也跟著越來越慢,最後甚至於動彈不得。要克服這個問題,在寫程式時使用效率比較好的演算法,就是個不錯的主意。
Thumbnail
2025/03/10
在模擬群聚行為時,隨著boid的數量越來越多,需要的計算量也會越來越多,導致程式的執行速度也跟著越來越慢,最後甚至於動彈不得。要克服這個問題,在寫程式時使用效率比較好的演算法,就是個不錯的主意。
Thumbnail
2025/02/03
到目前為止,我們所設計出來的自主代理人都是孤鳥,既不知道有其他自主代理人的存在,也不會跟其他自主代理人有任何互動。在這一節,我們將讓自主代理人能感知到其他自主代理人的存在,並且與其他自主代理人互動,最後形成由自主代理人所組成的複雜系統(complex system)。
2025/02/03
到目前為止,我們所設計出來的自主代理人都是孤鳥,既不知道有其他自主代理人的存在,也不會跟其他自主代理人有任何互動。在這一節,我們將讓自主代理人能感知到其他自主代理人的存在,並且與其他自主代理人互動,最後形成由自主代理人所組成的複雜系統(complex system)。
2024/12/30
不同於用來找出兩點間最短距離演算法的路徑搜尋(path finding),路徑循行(path following),指的是依循已經設定好的路徑來移動的轉向行為。這一節就要來研究Reynolds所設計的路徑循行轉向行為。
2024/12/30
不同於用來找出兩點間最短距離演算法的路徑搜尋(path finding),路徑循行(path following),指的是依循已經設定好的路徑來移動的轉向行為。這一節就要來研究Reynolds所設計的路徑循行轉向行為。
看更多
你可能也想看
Thumbnail
想開始學塔羅卻不知道要準備哪些工具?這篇整理塔羅新手必備好物清單,從塔羅牌、塔羅布到收納袋與香氛噴霧一次入手。趁蝦皮雙11優惠打造專屬占卜空間,還能加入蝦皮分潤計畫,用分享創造收入。
Thumbnail
想開始學塔羅卻不知道要準備哪些工具?這篇整理塔羅新手必備好物清單,從塔羅牌、塔羅布到收納袋與香氛噴霧一次入手。趁蝦皮雙11優惠打造專屬占卜空間,還能加入蝦皮分潤計畫,用分享創造收入。
Thumbnail
今天不只要分享蝦皮分潤計畫,也想分享最近到貨的魔法少年賈修扭蛋開箱,還有我的雙11購物清單,漫畫、文具、Switch2、後背包......雙11優惠真的超多,如果有什麼一直想買卻遲遲還沒下手的東西,最適合趁這個購物季趕緊下單!
Thumbnail
今天不只要分享蝦皮分潤計畫,也想分享最近到貨的魔法少年賈修扭蛋開箱,還有我的雙11購物清單,漫畫、文具、Switch2、後背包......雙11優惠真的超多,如果有什麼一直想買卻遲遲還沒下手的東西,最適合趁這個購物季趕緊下單!
Thumbnail
接下來要來看看Reynolds所設計的「流場循行(flow-field following)」轉向行為。
Thumbnail
接下來要來看看Reynolds所設計的「流場循行(flow-field following)」轉向行為。
Thumbnail
轉向行為(steering behaviors)是Craig W. Reynolds所提出來的,其主要的目的,是要讓電腦動畫及互動媒體如電玩、虛擬實境中,能夠自主行動的角色,可以利用許多的策略,在他們的世界中,以更逼真、更像具有生命般的方式移動。
Thumbnail
轉向行為(steering behaviors)是Craig W. Reynolds所提出來的,其主要的目的,是要讓電腦動畫及互動媒體如電玩、虛擬實境中,能夠自主行動的角色,可以利用許多的策略,在他們的世界中,以更逼真、更像具有生命般的方式移動。
Thumbnail
GT跨界玩家是一齣從頭到尾都在心跳加速的影片,影片裡有許多真實世界的賽道,競速的畫面感與奔馳的速度感讓人一同身歷其境,連影片中主角翻車的意外也跟著摔的咪咪貓貓,但是在每一個超車或是技術翻越的時候,右腳忍不住也會想要大踩油門超車,看完之後就是「腳癢」+「過癮」! 故事是關於... 這是一部由真
Thumbnail
GT跨界玩家是一齣從頭到尾都在心跳加速的影片,影片裡有許多真實世界的賽道,競速的畫面感與奔馳的速度感讓人一同身歷其境,連影片中主角翻車的意外也跟著摔的咪咪貓貓,但是在每一個超車或是技術翻越的時候,右腳忍不住也會想要大踩油門超車,看完之後就是「腳癢」+「過癮」! 故事是關於... 這是一部由真
Thumbnail
這個過程的開始是當Mind能夠認識到制約。這就是這個過程的開始。它基本上是從你第一次學習你的設計和你有一個開放的中心時開始的。是你的Mind認識到你正在被制約。這是你的Mind開始學習一個新的任務、角色,以不同的方式執行,成為你的見證者而不是你的導演,開始看到制約的力量。
Thumbnail
這個過程的開始是當Mind能夠認識到制約。這就是這個過程的開始。它基本上是從你第一次學習你的設計和你有一個開放的中心時開始的。是你的Mind認識到你正在被制約。這是你的Mind開始學習一個新的任務、角色,以不同的方式執行,成為你的見證者而不是你的導演,開始看到制約的力量。
Thumbnail
今年有幸參與沉浸劇場、VR影像等新媒材的劇本編寫,加上之前開發過互動影視,想從幾個角度對這些新媒材提出粗淺看法。首先是被動性與主動性。無論是影視結合互動,還是劇場加入沉浸,為的都是增加觀者的主動性,換個角度講是要吸引對主動性有興趣的未觸及觀眾。VR有穿戴限制,互動影視模仿電玩,沉浸劇場最無可取代
Thumbnail
今年有幸參與沉浸劇場、VR影像等新媒材的劇本編寫,加上之前開發過互動影視,想從幾個角度對這些新媒材提出粗淺看法。首先是被動性與主動性。無論是影視結合互動,還是劇場加入沉浸,為的都是增加觀者的主動性,換個角度講是要吸引對主動性有興趣的未觸及觀眾。VR有穿戴限制,互動影視模仿電玩,沉浸劇場最無可取代
Thumbnail
圖卡筆記【老喻的人生演算法課】03 大腦的兩種模式 人的大腦有兩種系統,一個是本能、直覺的自動導航系統;一個是會思考分析的主動控制系統。一般情況,我們會混合使用這兩種系統。 舉例來說,開始學開車會很緊張,那時我們用的是主動控制系統,隨著對開車越來越熟悉,開車技能會交由自動導航系統,讓我們可以一邊
Thumbnail
圖卡筆記【老喻的人生演算法課】03 大腦的兩種模式 人的大腦有兩種系統,一個是本能、直覺的自動導航系統;一個是會思考分析的主動控制系統。一般情況,我們會混合使用這兩種系統。 舉例來說,開始學開車會很緊張,那時我們用的是主動控制系統,隨著對開車越來越熟悉,開車技能會交由自動導航系統,讓我們可以一邊
Thumbnail
  舊文《來自探路客       今天看了一本「體驗設計」的書,得到一點啟發,它以知名的『超級瑪莉喔』遊戲來作為例子,來說明它會一直紅下去的原因 原因是你覺得它會很好玩嗎?它有趣嗎?還是它很有特色?結果都不是答案.... 是個出乎我意外之料的答案,是玩家無意識遵守它的規則,很自然、但從不會注意到
Thumbnail
  舊文《來自探路客       今天看了一本「體驗設計」的書,得到一點啟發,它以知名的『超級瑪莉喔』遊戲來作為例子,來說明它會一直紅下去的原因 原因是你覺得它會很好玩嗎?它有趣嗎?還是它很有特色?結果都不是答案.... 是個出乎我意外之料的答案,是玩家無意識遵守它的規則,很自然、但從不會注意到
Thumbnail
遊戲化是什麼? 作者裡面認為比起遊戲化,更好的名詞其實是「人本設計」。相較於功能取向的優化,人本設計優化系統中的「人類動機」,將遊戲中常見的樂趣與參與元素應用在現實世界中,像是產品、工作場所、行銷活動,甚至是人生。
Thumbnail
遊戲化是什麼? 作者裡面認為比起遊戲化,更好的名詞其實是「人本設計」。相較於功能取向的優化,人本設計優化系統中的「人類動機」,將遊戲中常見的樂趣與參與元素應用在現實世界中,像是產品、工作場所、行銷活動,甚至是人生。
Thumbnail
Youtube上逛著逛著看到techwithtim的線上教學,這是一個pygame的模組練習,只\是我想了解深一點的是物件導向的寫法應用。影片大約兩小時,實際邊動手coding,一邊看著影片的講解,結果花在這上面的時間遠遠超過我的預期。
Thumbnail
Youtube上逛著逛著看到techwithtim的線上教學,這是一個pygame的模組練習,只\是我想了解深一點的是物件導向的寫法應用。影片大約兩小時,實際邊動手coding,一邊看著影片的講解,結果花在這上面的時間遠遠超過我的預期。
Thumbnail
如果要找一個詞來說明程式的價值,那就是「自動化」;機器雖然愚笨,但不會疲憊,能讓人類的生產力得到解放。我期待的程式教育不只是教導技能,而是教導學生在該不滿的時候就不滿,也教導學生懂得叛逆、而且知道還有「自己動手」這條途徑。
Thumbnail
如果要找一個詞來說明程式的價值,那就是「自動化」;機器雖然愚笨,但不會疲憊,能讓人類的生產力得到解放。我期待的程式教育不只是教導技能,而是教導學生在該不滿的時候就不滿,也教導學生懂得叛逆、而且知道還有「自己動手」這條途徑。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News