Python 超級瑪麗代碼實現:人物行走和碰撞檢測

功能介紹

人物行走

人物的行走速度這邊分成水平方向(X軸)和豎直方向(Y軸),水平方向的速度要考慮加速度和摩擦力,豎直方向的速度要考慮重力加速度。

  • 水平方向:設定X軸向右走的速度為大於0,向左走的速度為小於0
  • 豎直方向:設定Y軸向下的速度為大於0,向上的速度為小於0

遊戲中的人物有下面幾個主要的狀態:

  • 站立不動:水平方向速度為0,且豎直方向站在某個物體上。
  • 向左或向右走:水平方向速度的絕對值大於0,且豎直方向站在某個物體上。
  • 向上跳:豎直方向方向速度小於0,且上方沒有碰到某個物體,同時需要玩家按住jump鍵。
  • 向下降落:豎直方向方向速度大於0或者玩家沒有按住jump鍵,且下方沒有碰到某個物體。

向上跳和向下降落的狀態判斷可能一開始比較難理解,可以看後面的具體實現,目的是如果玩家長按jump鍵時,可以讓人物跳的更高。

上面的判斷是否站在某個物體上,或者是否碰到某個物體,就需要用到物體之間的碰撞檢測。

碰撞檢測

對於遊戲中出現的每一樣東西,比如磚塊,箱子,水管,地面,還有人物都可以看成是一個獨立的物體,所以每個物體類都繼承了pygame的精靈類pg.sprite.Sprite,可以使用精靈類提供的碰撞檢測函數來判斷。

設置source\\constants.py 中的變量DEBUG值為True,可以看到圖1的遊戲截圖,比如最簡單的地面,可以看成是一個長方形的物體。

  • 下方紅色的長方形物體就是地面(ground)
  • 右邊的幾個紅色小方塊是階梯(step)
  • 左邊空中的像牆一樣的是磚塊(brick)
  • 帶問號的是箱子(box)

因為人物是站在地面上,且水平速度為0,所以當前的人物狀態就是站立不動。

Python 超級瑪麗代碼實現:人物行走和碰撞檢測

遊戲代碼

遊戲實現代碼的github鏈接 超級瑪麗

這邊是csdn的下載鏈接 超級瑪麗

代碼介紹

人物行走代碼

有一個單獨的人物類,在source\\components\\player.py 中,其中有個handle_state 函數,根據人物當前的狀態執行不同的函數。

為了簡潔下面所有函數中將不相關的代碼都省略掉了。

<code>    def handle_state(self, keys, fire_group):
if self.state == c.STAND:
self.standing(keys, fire_group)
elif self.state == c.WALK:
self.walking(keys, fire_group)
elif self.state == c.JUMP:
self.jumping(keys, fire_group)
elif self.state == c.FALL:
self.falling(keys, fire_group)/<code>

人物的狀態就是上面說的4個狀態:

  • 站立不動:c.STAND
  • 向左或向右走:c.WALK
  • 向上跳:c.JUMP
  • 向下降落:c.FALL

人物類關於行走速度的成員變量先了解下:

水平方向相關的

  • x_accel:水平方向的加速度,值大於0,不區別方向。
  • max_x_vel:水平方向的最大速度,值大於0,不區別方向。
  • x_vel:水平方向的速度,值大於0表示向右走,值小於0表示向左走。
  • 初始值:max_run_vel和max_walk_vel 表示最大速度,run_accel和walk_accel表示加速度。
  • facing_right:值為True表示當前是向右走,值為False表示當前是向左走,這個是用來設置人物的圖像。

  • 豎直方向相關的
  • gravity:重力加速度,值大於0,表示方向向下。
  • jump_vel:起跳時豎直方向的初始速度,值小於0,表示方向向上。
  • y_vel:豎直方向的速度。
  • 看下最複雜的 walking 函數,keys數組是當前按下的鍵盤輸入,tools.keybinding中值的含義如下
<code>keybinding = {
'action':pg.K_s,
'jump':pg.K_a,
'left':pg.K_LEFT,
'right':pg.K_RIGHT,
'down':pg.K_DOWN
}/<code>
  • 先根據當前是否有按下 keybinding[‘action’] 鍵來設置不同的最大水平方向速度和水平方向加速度。
  • 如果有按下 keybinding[‘jump’] 鍵,則設置人物狀態為c.JUMP,初始化豎直方向的速度
  • 如果有按下keybinding[‘left’]鍵,表示要向左走,如果 x_vel 大於0,表示之前是向右走的,所以設置一個轉身的加速度為SMALL_TURNAROUND,然後調用cal_vel 函數根據之前的速度和加速度,計算出當前的速度。
  • 如果有按下keybinding[‘right’]鍵,表示要向右走,和上面類似
  • 如果沒有按下keybinding[‘left’]鍵和keybinding[‘right’]鍵,就像有摩擦力的存在,則水平方向的速度會慢慢變成0,如果 x_vel 值為0,則設置人物狀態為c.STAND。
<code> def walking(self, keys, fire_group):        
if keys[tools.keybinding['action']]:
self.max_x_vel = self.max_run_vel
self.x_accel = self.run_accel
else:
self.max_x_vel = self.max_walk_vel
self.x_accel = self.walk_accel

if keys[tools.keybinding['jump']]:
if self.allow_jump:
self.state = c.JUMP
if abs(self.x_vel) > 4:
self.y_vel = self.jump_vel - .5
else:
self.y_vel = self.jump_vel

if keys[tools.keybinding['left']]:
self.facing_right = False
if self.x_vel > 0:
self.frame_index = 5
self.x_accel = c.SMALL_TURNAROUND

self.x_vel = self.cal_vel(self.x_vel, self.max_x_vel, self.x_accel, True)
elif keys[tools.keybinding['right']]:
self.facing_right = True
if self.x_vel < 0:
self.frame_index = 5
self.x_accel = c.SMALL_TURNAROUND

self.x_vel = self.cal_vel(self.x_vel, self.max_x_vel, self.x_accel)
else:
if self.facing_right:
if self.x_vel > 0:
self.x_vel -= self.x_accel
else:
self.x_vel = 0
self.state = c.STAND
else:
if self.x_vel < 0:
self.x_vel += self.x_accel
else:
self.x_vel = 0
self.state = c.STAND

def cal_vel(self, vel, max_vel, accel, isNegative=False):
""" max_vel and accel must > 0 """
if isNegative:
new_vel = vel * -1

else:
new_vel = vel
if (new_vel + accel) < max_vel:
new_vel += accel
else:
new_vel = max_vel
if isNegative:
return new_vel * -1
else:
return new_vel/<code>

再看下jumping 函數,

  • 開始gravity 設為 c.JUMP_GRAVITY,可以看到JUMP_GRAVITY 比GRAVITY值小很多,如果玩家長按jump鍵時,可以讓人物跳的更高。
  • 如果豎直方向速度y_vel 大於0,表示方向向下,則設置人物狀態為c.FALL
  • 如果按下 keybinding[‘left’]鍵或 keybinding[‘right’]鍵,則計算水平方向的速度。
  • 如果沒有按 keybinding[‘jump’]鍵,則設置人物狀態為c.FALL
<code>JUMP_GRAVITY = .31
GRAVITY = 1.01

def jumping(self, keys, fire_group):
""" y_vel value: positive is down, negative is up """
self.allow_jump = False
self.frame_index = 4
self.gravity = c.JUMP_GRAVITY
self.y_vel += self.gravity

if self.y_vel >= 0 and self.y_vel < self.max_y_vel:
self.gravity = c.GRAVITY
self.state = c.FALL

if keys[tools.keybinding['right']]:

self.x_vel = self.cal_vel(self.x_vel, self.max_x_vel, self.x_accel)
elif keys[tools.keybinding['left']]:
self.x_vel = self.cal_vel(self.x_vel, self.max_x_vel, self.x_accel, True)

if not keys[tools.keybinding['jump']]:
self.gravity = c.GRAVITY
self.state = c.FALL/<code>

standing函數和 falling 函數比較簡單,就省略了。碰撞檢測代碼人物的碰撞檢測代碼在 source\\states\\level.py 中的入口是update_player_position函數 ,可以看到這邊分成水平方向和豎直方向:

  • 根據人物的水平方向速度x_vel 更新人物的X軸位置,同時人物的X軸位置不能超出遊戲地圖的X軸範圍,然後調用check_player_x_collisions函數進行水平方向的碰撞檢測。
  • 根據人物的水平方向速度x_vel 更新人物的X軸位置,同時人物的X軸位置不能超出遊戲地圖的X軸範圍,然後調用check_player_x_collisions函數進行水平方向的碰撞檢測。
  • 根據人物的豎直方向速度y_vel 更新人物的Y軸位置,然後調用check_player_y_collisions函數進行豎直方向的碰撞檢測
<code>    def update_player_position(self):
self.player.rect.x += round(self.player.x_vel)
if self.player.rect.x < self.start_x:
self.player.rect.x = self.start_x
elif self.player.rect.right > self.end_x:
self.player.rect.right = self.end_x
self.check_player_x_collisions()

if not self.player.dead:
self.player.rect.y += round(self.player.y_vel)
self.check_player_y_collisions()/<code>

具體實現時將同一類物體放在一個pygame.sprite.Group類中,

<code>pygame.sprite.Group
A container class to hold and manage multiple Sprite objects.
Group(*sprites) -> Group/<code>

這樣每次調用pg.sprite.spritecollideany 函數就能判斷人物和這一類物體是否有碰撞。

<code>pygame.sprite.spritecollideany()
Simple test if a sprite intersects anything in a group.
spritecollideany(sprite, group, collided = None) -> Sprite Collision with the returned sprite.
/<code>

不同物體的group如下,另外敵人,金幣和蘑菇等物體的碰撞檢測先忽略。

  • ground_step_pipe_group:地面,階梯和水管的group。
  • brick_group:磚塊的group, 如果是金幣磚塊,從下面碰撞會獲取金幣。
  • box_group:箱子的group,從下面碰撞箱子可以出現金幣,蘑菇,花等的獎勵。

因為不同種類group撞擊時,後續產生的結果會有區別,所有需要對每一類group分別進行碰撞檢測。

X軸方向上面3類group如果檢測到有碰撞時,會調用adjust_player_for_x_collisions 函數,來調整人物的X軸位置。

<code>    def check_player_x_collisions(self):
ground_step_pipe = pg.sprite.spritecollideany(self.player, self.ground_step_pipe_group)
brick = pg.sprite.spritecollideany(self.player, self.brick_group)
box = pg.sprite.spritecollideany(self.player, self.box_group)
\t\t...
if box:
self.adjust_player_for_x_collisions(box)
elif brick:
self.adjust_player_for_x_collisions(brick)
elif ground_step_pipe:
if (ground_step_pipe.name == c.MAP_PIPE and
ground_step_pipe.type == c.PIPE_TYPE_HORIZONTAL):
return
self.adjust_player_for_x_collisions(ground_step_pipe)
elif powerup:
...
elif enemy:
...
elif coin:
.../<code>

adjust_player_for_x_collisions 函數先根據人物和碰撞物體的X軸相對位置,判斷人物在碰撞物體的左邊還是右邊,來調整人物的X軸位置,然後設置人物水平方向的速度為0。

<code>    def adjust_player_for_x_collisions(self, collider):
if collider.name == c.MAP_SLIDER:
return

if self.player.rect.x < collider.rect.x:
self.player.rect.right = collider.rect.left
else:
self.player.rect.left = collider.rect.right
self.player.x_vel = 0/<code>

check_player_y_collisions 函數也是對不同group分別進行碰撞檢測,Y軸方向這3類group如果檢測到有碰撞時,會調用adjust_player_for_y_collisions 函數,來調整人物的Y軸位置。 最後調用check_is_falling函數判斷人物是否要設成 向下降落 的狀態。


<code>    def check_player_y_collisions(self):
ground_step_pipe = pg.sprite.spritecollideany(self.player, self.ground_step_pipe_group)

# decrease runtime delay: when player is on the ground, don't check brick and box
if self.player.rect.bottom < c.GROUND_HEIGHT:
brick = pg.sprite.spritecollideany(self.player, self.brick_group)
box = pg.sprite.spritecollideany(self.player, self.box_group)
brick, box = self.prevent_collision_conflict(brick, box)
else:
brick, box = False, False

if box:
self.adjust_player_for_y_collisions(box)
elif brick:
self.adjust_player_for_y_collisions(brick)
elif ground_step_pipe:
self.adjust_player_for_y_collisions(ground_step_pipe)
elif enemy:
...
elif shell:
...
self.check_is_falling(self.player)/<code>

adjust_player_for_y_collisions 函數先根據人物和碰撞物體的Y軸相對位置,判斷人物在碰撞物體的下邊還是上邊,來調整人物的Y軸位置

  • 如果人物在碰撞物體的下邊,則有一個反彈的效果,設置人物的豎直方向速度為7,調整人物的Y軸位置,設置人物狀態為c.FALL。如果碰撞物體為磚塊或箱子,還要進行後續處理。
  • 如果人物在碰撞物體的上邊,設置人物的豎直方向速度為0,調整人物的Y軸位置,一般情況下設置人物狀態為c.WALK。
<code>    def adjust_player_for_y_collisions(self, sprite):
if self.player.rect.top > sprite.rect.top:
if sprite.name == c.MAP_BRICK:
...
elif sprite.name == c.MAP_BOX:
...
elif (sprite.name == c.MAP_PIPE and
sprite.type == c.PIPE_TYPE_HORIZONTAL):
return

self.player.y_vel = 7
self.player.rect.top = sprite.rect.bottom
self.player.state = c.FALL
else:
self.player.y_vel = 0
self.player.rect.bottom = sprite.rect.top
if self.player.state == c.FLAGPOLE:
self.player.state = c.WALK_AUTO
elif self.player.state == c.END_OF_LEVEL_FALL:
self.player.state = c.WALK_AUTO
else:
self.player.state = c.WALK/<code>

check_is_falling函數 判斷人物下方是否有物體,有個小技巧,就是先將人物的Y軸位置向下移動1,然後判斷和上面三類group是否有碰撞:

  • 如果沒有碰撞,表示人物下方沒有物體,這時候如果人物狀態不是 c.JUMP 和一些特殊狀態,就設置人物狀態為 c.FALL。
  • 如果有碰撞,則不用管。

最後將人物的Y軸位置恢復(向上移動1)。

<code>    def check_is_falling(self, sprite): 

sprite.rect.y += 1
check_group = pg.sprite.Group(self.ground_step_pipe_group,
self.brick_group, self.box_group)

if pg.sprite.spritecollideany(sprite, check_group) is None:
if (sprite.state == c.WALK_AUTO or
sprite.state == c.END_OF_LEVEL_FALL):
sprite.state = c.END_OF_LEVEL_FALL
elif (sprite.state != c.JUMP and
sprite.state != c.FLAGPOLE and
not self.in_frozen_state()):
sprite.state = c.FALL
sprite.rect.y -= 1/<code>
  • 先根據當前是否有按下 keybinding[‘action’] 鍵來設置不同的最大水平方向速度和水平方向加速度。
  • 如果有按下 keybinding[‘jump’] 鍵,則設置人物狀態為c.JUMP,初始化豎直方向的速度
  • 如果有按下keybinding[‘left’]鍵,表示要向左走,如果 x_vel 大於0,表示之前是向右走的,所以設置一個轉身的加速度為SMALL_TURNAROUND,然後調用cal_vel 函數根據之前的速度和加速度,計算出當前的速度。
  • 如果有按下keybinding[‘right’]鍵,表示要向右走,和上面類似
  • 如果沒有按下keybinding[‘left’]鍵和keybinding[‘right’]鍵,就像有摩擦力的存在,則水平方向的速度會慢慢變成0,如果 x_vel 值為0,則設置人物狀態為c.STAND。
<code>  def walking(self, keys, fire_group):        
if keys[tools.keybinding['action']]:
self.max_x_vel = self.max_run_vel
self.x_accel = self.run_accel
else:

self.max_x_vel = self.max_walk_vel
self.x_accel = self.walk_accel

if keys[tools.keybinding['jump']]:
if self.allow_jump:
self.state = c.JUMP
if abs(self.x_vel) > 4:
self.y_vel = self.jump_vel - .5
else:
self.y_vel = self.jump_vel

if keys[tools.keybinding['left']]:
self.facing_right = False
if self.x_vel > 0:
self.frame_index = 5
self.x_accel = c.SMALL_TURNAROUND

self.x_vel = self.cal_vel(self.x_vel, self.max_x_vel, self.x_accel, True)
elif keys[tools.keybinding['right']]:
self.facing_right = True
if self.x_vel < 0:
self.frame_index = 5
self.x_accel = c.SMALL_TURNAROUND

self.x_vel = self.cal_vel(self.x_vel, self.max_x_vel, self.x_accel)
else:
if self.facing_right:
if self.x_vel > 0:
self.x_vel -= self.x_accel
else:
self.x_vel = 0
self.state = c.STAND
else:
if self.x_vel < 0:
self.x_vel += self.x_accel
else:
self.x_vel = 0
self.state = c.STAND

def cal_vel(self, vel, max_vel, accel, isNegative=False):
""" max_vel and accel must > 0 """
if isNegative:
new_vel = vel * -1
else:
new_vel = vel
if (new_vel + accel) < max_vel:
new_vel += accel
else:
new_vel = max_vel
if isNegative:

return new_vel * -1
else:
return new_vel/<code>

代碼已打包,學習python關注公眾號python社區營、分享學習視頻、及更多精彩文章


分享到:


相關文章: