這個功能困擾的我很久,困難點在於如何判斷是哪個面發生碰撞。期間一直無法做出理想中的反彈效果。後來拋出問題向朋友求救,討論後終於得到解答,雖然想出來的演算法不盡完美,但大部分的情況下都能有預想中的反彈行為。
判斷碰撞發生面
當碰撞發生時,如果可以知道兩的矩形是從哪個面碰撞的,就可以決定反彈方向。但是卻不能夠以碰撞發生當下的狀態來判斷碰撞發生面。如下圖:
左圖中間的藍色矩形與白色矩形是碰撞發生時的狀態,藍色矩形是會反彈的物體。直覺上會想要利用藍色矩形與白色矩形重疊的邊,來判斷藍色矩形應該是往下反彈。然而事情沒有那麼美好,如右圖所示:藍色矩形也有可能從上方移動過來,如果做了往下反彈,就會出現神奇現象了。
要是其中一個矩形太大,連哪個邊有碰到都無法判定。
跟朋友討論出來的解法是,要是知道碰撞前一刻兩個矩形的相對位置,就可以判定碰撞發生在哪一個面。接下來解說設定碰撞時的兩個矩形:會反彈的矩形稱為球(像是打磚塊遊戲的球)、不會反彈的矩形稱為平台(像是打磚塊遊戲的平台)。
決定相對位置
延續前一篇,矩形由四個值 top、bottom、left、right 訂出它在場景中的位置,藉由這四個值可以很容易地決定相對位置:
- 球在平台的上方:ball.bottom < platform.top
- 球在平台的下方:ball.top > platform.bottom
- 球在平台的左方:ball.right < platform.left
- 球在平台的右方:ball.left > platform.right
由於整個演算法觸發的時機為碰撞發生時,所以會取得碰撞當下的資訊。而這一刻的位置為這一刻的速度加上前一刻的位置,則碰撞前一刻的位置為:$$Pos_{t-1} = Pos_t - V_t$$以此撰寫對應函式,要注意球是可以同時在平台的左方與上方的(即左上方):
def ball_bounce_off(ball_rect, ball_speed, platform_rect, platform_speed):
"""
計算並回傳球撞擊平台後球的新速度
@param ball_rect 撞擊當下球的位置。為 pygame.Rect
@param ball_speed 撞擊當下球的速度。為一個 list [x, y]
@param platform_rect 撞擊當下平台的位置。為 pygame.Rect
@param platform_speed 撞擊當下平台的速度。為一個 list [x, y]
"""
# 計算前一刻的位置
ball_rect_last = ball_rect.move(-ball_speed[0], -ball_speed[1])
platform_rect_last = platform_rect.move(-platform_speed[0], -platform_speed[1])
ball_speed_new = ball_speed.copy()
# 判定相對位置
if ball_rect_last.bottom < platform_rect_last.top or \
ball_rect_last.top > platform_rect_last.bottom:
ball_speed_new[1] *= -1
if ball_rect_last.right < platform_rect_last.left or \
ball_rect_last.left > platform_rect_last.right:
ball_speed_new[0] *= -1
return ball_speed_new
位置修正
在碰撞發生時,除了決定反彈的方向之外,還有位置需要修正。由於球與平台在碰撞時不會嵌入其中,但是碰撞發生時得到的是兩個互相交疊的矩形,因此必須要把球從平台「排出」。
排出的方向一樣要由碰撞前一刻的相對位置來決定。如果撞擊前一刻球在平台的上方,則球排出後的位置一定會在平台的上方(因為球是往下撞擊平台的上平面,反彈後的位置一定在此之上)。排出的方法有兩種,一是簡單的將球移到平台的邊緣與其相切,二是考慮球嵌入平台的距離。後者在物理上比較正確。
差異
兩種演算法在物件移動比較快的時候會有明顯的視覺差異。下圖是單純的移到與平台相切,看起來像球被平台拖住了。
再與考慮嵌入平台的距離的算法比較,球看起來反彈的比較乾脆:
兩者除了視覺上的差異之外,因為位置修正算法的不同,反彈後球的路徑也會有所不同。
移到與平台相切
這個做法就像是把球剛好擠到平台外,前一刻球的相對位置在哪裡,就直接往那個方向擠,例如:如果碰撞前一刻球在平台的上方,則要往上移動,讓球的下方與平台的上方相切。下圖橘色表示上一刻球的位置,圖左表示移動的過程,圖右則是看到的移動結果:
依照不同的相對位置,有不同的移動方向:
def ball_bounce_off(ball_rect, ball_speed, platform_rect, platform_speed):
"""
計算並回傳球撞擊平台後球的新位置與速度
@param ball_rect 撞擊當下球的位置。為 pygame.Rect
@param ball_speed 撞擊當下球的速度。為一個 list [x, y]
@param platform_rect 撞擊當下平台的位置。為 pygame.Rect
@param platform_speed 撞擊當下平台的速度。為一個 list [x, y]
"""
# 計算前一刻的位置
ball_rect_last = ball_rect.move(-ball_speed[0], -ball_speed[1])
platform_rect_last = platform_rect.move(-platform_speed[0], -platform_speed[1])
ball_rect_new = ball_rect.copy()
ball_speed_new = ball_speed.copy()
# 判定相對位置,將球移動到剛好與平台相切
if ball_rect_last.bottom < platform_rect_last.top:
# 球在平台的上方
ball_rect_new.bottom = platform_rect.top
ball_speed_new[1] *= -1
elif ball_rect_last.top > platform_rect_last.bottom:
# 球在平台的下方
ball_rect_new.top = platform_rect.bottom
ball_speed_new[1] *= -1
if ball_rect_last.right < platform_rect_last.left:
# 球在平台的左方
ball_rect_new.right = platform_rect.left
ball_speed_new[0] *= -1
elif ball_rect_last.left > platform_rect_last.right:
# 球在平台的右方
ball_rect_new.left = plarform_rect.right
ball_speed_new[0] *= -1
return ball_rect_new, ball_speed_new
考慮嵌入的距離
以連續的移動過程來看,照理來說球一碰到平台就要立刻反彈,但是在遊戲中,每一幀之間都是一下子移動一段距離,當球意識到自己碰撞到平台了(也就是檢查碰撞的時候),球早已嵌入平台之中,所以必須要把多跑的距離補還給球,才會符合常理。
如下圖,假設球移動的速度是 7 單位 / 幀,圖左是連續的移動過程,深橘色代表球碰撞到平台要開始反彈的位置,最後實線框的矩形是球最後的位置。右圖則是遊戲中實際移動的情況,每一幀會直接移動 7 單位,實線藍色框是球移動後的位置,可以看到球比應該要反彈的位置(深藍色)晚了一段距離,而要補償的距離與嵌入的距離有關。
要計算嵌入距離會與碰撞發生面有關,例如:球碰到平台的上平面,球理應往上彈(y 方向速度反轉),但球會繼續往下跑,所以球往下跑多少距離,就從碰撞發生面往上補多少距離。
所以相比於上一個方法,除了「排出」之外,還要再加上嵌入的距離:
def ball_bounce_off(ball_rect, ball_speed, platform_rect, platform_speed):
"""
計算並回傳球撞擊平台後球的新位置與速度
@param ball_rect 撞擊當下球的位置。為 pygame.Rect
@param ball_speed 撞擊當下球的速度。為一個 list [x, y]
@param platform_rect 撞擊當下平台的位置。為 pygame.Rect
@param platform_speed 撞擊當下平台的速度。為一個 list [x, y]
"""
# 計算前一刻的位置
ball_rect_last = ball_rect.move(-ball_speed[0], -ball_speed[1])
platform_rect_last = platform_rect.move(-platform_speed[0], -platform_speed[1])
ball_rect_new = ball_rect.copy()
ball_speed_new = ball_speed.copy()
# 判定相對位置,並考慮嵌入的距離
if ball_rect_last.bottom < platform_rect_last.top:
# 球在平台的上方
ball_rect_new.bottom = platform_rect.top \
- (ball_rect.bottom - platform_rect.top)
ball_speed_new[1] *= -1
elif ball_rect_last.top > platform_rect_last.bottom:
# 球在平台的下方
ball_rect_new.top = platform_rect.bottom \
+ (platform_rect.bottom - ball_rect.top)
ball_speed_new[1] *= -1
if ball_rect_last.right < platform_rect_last.left:
# 球在平台的左方
ball_rect_new.right = platform_rect.left \
- (ball_rect.right - platform_rect.left)
ball_speed_new[0] *= -1
elif ball_rect_last.left > platform_rect_last.right:
# 球在平台的右方
ball_rect_new.left = plarform_rect.right \
+ (platform_rect.right - ball_rect.left)
ball_speed_new[0] *= -1
return ball_rect_new, ball_speed_new
結語
上述介紹的方法其實還可以更好,就是當球前一刻的位置判定是同時在平台的左方與上方時,到底是先碰到左平面還是上平面,這會影響到球反彈的方向。但是原本的演算法在遊戲運作中看起來沒有明顯的錯誤,就沒有繼續把它改得更好了。
沒有留言:
張貼留言