Python 憤怒的小鳥代碼實現(1):物理引擎pymunk使用

遊戲介紹

最近比較忙,國慶正好有時間寫了python版本的憤怒的小鳥,使用了物理引擎pymunk,圖片資源是從github上下載的,實現了一個可玩的簡單版本。

功能實現如下:

  • 支持小鳥類型:紅色小鳥,藍色小鳥,黃色小鳥。
  • 支持障礙物的類型:玻璃,木頭,石頭。
  • 支持障礙物的形狀:各種長度的長方形,正方形和圓形。
  • 使用json文件保存關卡信息,設置小豬和障礙物的位置。

遊戲截圖如下:
圖1demo1
圖2demo2
圖3demo3

完整代碼

遊戲實現代碼的github鏈接 憤怒的小鳥
這邊是csdn的下載鏈接 憤怒的小鳥

Pymunk介紹

pymunk是一個2D的物理引擎, 它實際是封裝了 c語言寫的2D物理引擎Chipmunk,可以實現碰撞,旋轉等物理運動。

安裝pymunk,可以直接使用pip工具,安裝最新的pymunk 5.5.0:

    pip install pymunk

介紹下在pymunk中會使用到的四個基本的類:

  • 剛體 (pymunk.Body):一個剛體具有物體的物理屬性(質量、座標、旋轉角度、速度等),它自己是沒有形狀的。
  • 碰撞形狀 (pymunk.Circle, pymunk.Segment and pymunk.Poly):通過將形狀附加到實體,你可以定義一個實體的形狀。你可以將多個形狀附加到單個實體上來定義一個複雜的形狀,如果不需要形狀,則可以不附加任何形狀。
  • 約束/關節 (pymunk.constraint.PinJoint, pymunk.constraint.SimpleMotor):你可以在兩個實體之間附加關節以約束它們的行爲。比如在兩個實體間保持一個固定的距離。
  • 空間 (pymunk.Space): 空間是pymunk中基本的模擬單元。你可以添加實體,形狀和關節到空間,然後整體更新空間。pymunk會控制空間中所有的實體,形狀和關節如何相互作用。

代碼實現

將物理引擎相關的代碼單獨放在了一個文件 (source\component\physics.py)中,減少代碼的耦合。
定義了一個Physics類,向外提供所有物理引擎相關的函數。
這篇文章只介紹physics.py 中pymunk相關的代碼。

pymunk相關初始化

reset 函數初始化了 空間類(pm.Space), 設置了兩個參數

  • gravity : 重力
  • dt (Time step length) : 表示pymunk中每次更新的時間段值,比如dt值是0.002,表示時間段是0.002秒。

setup_lines函數設置了一條直線,作爲地面。
Segment類創建了一條從點a 到 點b的直線。

class pymunk.Segment(body, a, b, radius)
Bases: pymunk.shapes.Shape
A line segment shape between two point. Meant mainly as a static shape.
import pymunk as pm

class Physics():
    def __init__(self):
        self.reset()

    def reset(self, level=None):
        self.level = level
        # init space: set gravity and dt
        self.space = pm.Space()
        self.space.gravity = (0.0, -700.0)
        self.dt = 0.002
        self.birds = []
        self.pigs = []
        self.blocks = []
        self.path_timer = 0
        self.check_collide = False
        self.setup_lines()
        self.setup_collision_handler()

    def setup_lines(self):
        # Static Ground
        x, y = to_pymunk(c.SCREEN_WIDTH, c.GROUND_HEIGHT)
        static_body = pm.Body(body_type=pm.Body.STATIC)
        static_lines = [pm.Segment(static_body, (0.0, y), (x, y), 0.0)]

        for line in static_lines:
            line.elasticity = 0.95
            line.friction = 1
            line.collision_type = COLLISION_LINE
        self.space.add(static_lines)
        self.static_lines = static_lines

setup_collision_handler 函數用來設置在兩種類型的物體在碰撞發生時,可以由用戶使用的回調函數。
add_collision_handler 函數添加兩種類型物體a和b碰撞時會調用的handler。比如小豬和小鳥這兩種類型物體的註冊函數就是:add_collision_handler(COLLISION_PIG, COLLISION_BIRD)

add_collision_handler(collision_type_a, collision_type_b)
Return the CollisionHandler for collisions between objects of type collision_type_a and collision_type_b.

我們這裏只用到了 post_solve 回調函數,在兩個物體碰撞結束後,獲取碰撞衝擊力(collision impulse)。

post_solve
Two shapes are touching and their collision response has been processed.
func(arbiter, space, data)
You can retrieve the collision impulse or kinetic energy at this time if you want to use it to calculate sound volumes or damage amounts. See Arbiter for more info.

比如handle_pig_collide函數在小豬和障礙物碰撞後,會根據衝擊力的大小來相應減去小豬的生命。

COLLISION_BIRD = 1
COLLISION_PIG = 2
COLLISION_BLOCK = 3
COLLISION_LINE = 4

    def setup_collision_handler(self):
        def post_solve_bird_line(arbiter, space, data):
            if self.check_collide:
                bird_shape = arbiter.shapes[0]
                my_phy.handle_bird_collide(bird_shape, True)
        def post_solve_pig_bird(arbiter, space, data):
            if self.check_collide:
                pig_shape = arbiter.shapes[0]
                my_phy.handle_pig_collide(pig_shape, MAX_IMPULSE)
        def post_solve_pig_line(arbiter, space, data):
            if self.check_collide:
                pig_shape = arbiter.shapes[0]
                my_phy.handle_pig_collide(pig_shape, arbiter.total_impulse.length, True)
        def post_solve_pig_block(arbiter, space, data):
            if self.check_collide:
                if arbiter.total_impulse.length > MIN_DAMAGE_IMPULSE:
                    pig_shape = arbiter.shapes[0]
                    my_phy.handle_pig_collide(pig_shape, arbiter.total_impulse.length)
        def post_solve_block_bird(arbiter, space, data):
            if self.check_collide:
                block_shape, bird_shape = arbiter.shapes
                my_phy.handle_bird_collide(bird_shape)
                if arbiter.total_impulse.length > 1100:
                    my_phy.handle_block_collide(block_shape, arbiter.total_impulse.length)

        self.space.add_collision_handler(
            COLLISION_BIRD, COLLISION_LINE).post_solve = post_solve_bird_line

        self.space.add_collision_handler(
            COLLISION_PIG, COLLISION_BIRD).post_solve = post_solve_pig_bird

        self.space.add_collision_handler(
            COLLISION_PIG, COLLISION_LINE).post_solve = post_solve_pig_line

        self.space.add_collision_handler(
            COLLISION_PIG, COLLISION_BLOCK).post_solve = post_solve_pig_block

        self.space.add_collision_handler(
            COLLISION_BLOCK, COLLISION_BIRD).post_solve = post_solve_block_bird

    def handle_pig_collide(self, pig_shape, impulse, is_ground=False):
        for pig in self.pigs:
            if pig_shape == pig.phy.shape:
                if is_ground:
                    pig.phy.body.velocity = pig.phy.body.velocity * 0.8
                else:
                    damage = impulse // MIN_DAMAGE_IMPULSE
                    pig.set_damage(damage)

# must init as a global parameter to use in the post_solve handler
my_phy = Physics()

創建一個pymunk物體

創建物體一般有下面五個步驟

  1. moment_for_circle 函數根據質量(mass), 圓的半徑 來計算出剛體的轉動慣量(Moment Of Inertia),慣量就像剛體的旋轉質量。

    pymunk.moment_for_circle(mass, inner_radius, outer_radius, offset=(0, 0))
    Calculate the moment of inertia for a hollow circle
    inner_radius and outer_radius are the inner and outer diameters. (A solid circle has an inner diameter of 0)

  2. 根據質量和轉動慣量來創建一個剛體(pymunk.Body)。

    class pymunk.Body(mass=0, moment=0, body_type=<class 'CP_BODY_TYPE_DYNAMIC'>)
    
  3. 根據剛體,和形狀類型創建一個碰撞形狀,比如圓形就是 pymunk.Circle。
    class pymunk.Circle(body, radius, offset=(0, 0))
    Bases: pymunk.shapes.Shape
    A circle shape defined by a radius

  4. 設置形狀的一些屬性

摩擦係數(friction)

Friction coefficient.
Pymunk uses the Coulomb friction model, a value of 0.0 is frictionless.
A value over 1.0 is perfectly fine.

彈力 (elasticity)

Elasticity of the shape.
A value of 0.0 gives no bounce, while a value of 1.0 will give a ‘perfect’ bounce. 
  1. 最後將這個剛體和碰撞形狀都添加到空間中。

     pymunk.Space.add(*objs)
     Add one or many shapes, bodies or joints to the space
    
class PhyPig():
    def __init__(self, x, y, radius, space):
        mass = 5
        inertia = pm.moment_for_circle(mass, 0, radius, (0, 0))
        body = pm.Body(mass, inertia)
        body.position = x, y
        shape = pm.Circle(body, radius, (0, 0))
        shape.elasticity = 0.95
        shape.friction = 1
        shape.collision_type = COLLISION_PIG
        space.add(body, shape)
        self.body = body
        self.shape = shape

PhyPig 類的初始化函數創建了一個小豬物體,參數有物體的位置(x,y), 可以將小豬作爲一個圓形物體,所以參數有圓的半徑(radius), 參數space就是我們上面創建的空間類。

pymunk 狀態更新

update函數是更新函數,代碼只顯示了小豬相關的代碼。

step 函數的參數dt值就是上面設置的時間段值,表示這次調用 該空間經過了多少時間,pymunk 根據這個時間值更新空間中的所有物體的狀態(比如速度,位置等)。按照pymunk 文檔的說明,將dt值設小一點,每次調用多次會使得模擬更穩定和精確,所以這裏每次調用5次step函數。

pymunk.Space.step(dt)
Update the space for the given time step.

遍歷所有的小豬:

  • 檢查小豬的狀態,如果生命小於零或者y軸位置超出了範圍,刪除這個小豬。
  • 更新小豬的位置
    pygame 和 pymunk 中對於位置的值是不同的, y軸的座標需要進行轉換,具體看 to_pygame 函數,600是高度。pymunk 中 body.position的值是物體的中間位置,對應pygame 中 rect 的centerx 和 centery,所以需要轉成[left, top]位置。
    • pygame中,以左上角的位置爲(0,0)
    • pymunk中,以左下角的位置爲(0,0)
def to_pygame(p):
    """Convert position of pymunk to position of pygame"""
    return int(p.x), int(-p.y+600)

    def update(self, game_info, level, mouse_pressed):
        pigs_to_remove = []

        #From pymunk doc:Performing multiple calls with a smaller dt
        #                creates a more stable and accurate simulation
        #So make five updates per frame for better stability
        for x in range(5):
            self.space.step(self.dt)
        ...
        for pig in self.pigs:
            pig.update(game_info)
            if pig.phy.body.position.y < 0 or pig.life <= 0:
                pigs_to_remove.append(pig)
            poly = pig.phy.shape
            p = to_pygame(poly.body.position)
            x, y = p
            w, h = pig.image.get_size()
            # change to [left, top] position of pygame
            x -= w * 0.5
            y -= h * 0.5
            angle_degree = math.degrees(poly.body.angle)
            pig.update_position(x, y, angle_degree)

        for pig in pigs_to_remove:
            self.space.remove(pig.phy.shape, pig.phy.shape.body)
            self.pigs.remove(pig)
            level.update_score(c.PIG_SCORE)
        ...

編譯環境

python3.7 + pygame1.9 + pymunk 5.5.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章