اذهب إلى المحتوى

إضافة خاصية القفز والركض إلى لعبة في بايثون


رشا سعد

تناول المقال السابق من هذه السلسلة محاكاةً برمجية لقوى الجاذبية وتأثيرها على تحركات بطل اللعبة، وفي هذا المقال سيتوفر للبطل ما يعينه على مواجهة أثرها ولو للحظات معدودة عبر منحه خاصية القفز.

سيَقفز اللاعب وعندما يصل لذروة قفزته ستُطبق عليه الجاذبية من جديد فيعود للسقوط، فما هي الآلية التي تحكم ذلك وكيف يميز البرنامج بين القفز والسقوط؟

الآلية المستخدمة هنا تعتمد على المتغيرات، إذ سنعرف بدايةً متغير خاص يراقب حالة اللاعب ويخبر بايثون بأن اللاعب يقفز الآن ليطبق عليه الجاذبية فيسقط بتأثيرها على أقرب كائن لموقعه من كائنات اللعبة.

قبل أن نبدأ نذكرك بمقالات السلسلة:

  1. بناء لعبة نرد بسيطة بلغة بايثون.
  2. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame.
  3. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame.
  4. تحريك شخصية اللعبة باستخدام PyGame.
  5. إضافة شخصية العدو للعبة.
  6. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame.
  7. محاكاة أثر الجاذبية في لعبة بايثون.
  8. إضافة خاصية القفز والركض إلى لعبة بايثون.
  9. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون.
  10. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة.
  11. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون.

ضبط متغيرات القفز

سنضيف متغيرين جديدين إلى صنف اللاعب.

  • يتتبع المتغير الأول حالة القفز عند اللاعب، ويحدد في كل لحظة إن كان الكائن يقفز أم يقف على أرضٍ صلبة.
  • أما المتغير الثاني فيسحب كائن اللاعب إلى أرضية اللعبة.

بناءً على ما سبق عرّف المتغيرات المنطقية التالية ضمن صنف اللاعب علمًا أن أول سطرين لتبيان السياق فقط ولا حاجة لإعادة كتابتها:

        self.frame = 0
        self.health = 10
        # أدناه المتغيرات الخاصة بالقفز
        self.is_jumping = True
        self.is_falling = False

إنه الاستخدام الأول للمتغيرات المنطقية في هذه السلسلة والتي تسمى أيضًا بالمتغيرات البوليانية نسبةً إلى عالم الرياضيات جورج بول، تأخذ هذه المتغيرات إحدى قيمتين فقط لا ثالث لهما إما صحيح True أو خاطئ False، أي أن اللاعب يقفز أو لا يقفز وفقًا للمتغير الأول، ويسقط أو لا يسقط وفقًا للمتغير الثاني.

لاحظ أننا أعطينا المتغير الأول is_jumping الذي يشير إلى القفز قيمةً ابتدائية هي True، ذلك أن كائن البطل في مثالنا يظهر بدايةً في سماء عالم اللعبة وينبغي سحبه إلى الأسفل مباشرةً ليبدأ اللعب، وكانت هذه أسهل الطرق لإنجاز ذلك رغم أنها لا توظف المتغير المنطقي توظيفًا مثاليًا لكنها تغنينا عن كتابة تعليمات خاصة تحدد موقع إنتاج البطل في كل مستوى، زد على أنها تُذكرنا بالألعاب الكلاسيكية التي يقفز فيها اللاعب إلى عالم اللعبة في لحظة البدء، فاعتمد هذه الخدعة البسيطة وسهّل برنامجك لا بأس في ذلك.

أما المتغير الثاني is_falling فقد أُسندت له القيمة الابتدائية False ليهبط اللاعب إلى الأرضية فور بدء اللعبة.

شروط تفعيل أثر الجاذبية

القفز في العالم الحقيقي هو فعل مقاوم للجاذبية، وفي لعبتنا ستعمل الجاذبية في حالة واحدة فقط هي قفز اللاعب -أي عدم وقوفه على أرضٍ صلبة- وتتوقف في بقية الحالات، أما إن عملت باستمرار كما في المقال السابق فإن اللاعب سيعاني ارتدادًا دائمًا بسبب سحبه للاصطدام بالأرض.

الجاذبية في ألعاب الفيديو هي دالة تُبنى خصيصًا في بعض الألعاب لتحاكي الجاذبية الأرضية الحقيقية، وليست ميزة مضمنة في محركات الألعاب مثل Pygame وهي بطبيعة الحال لا تُدار من قبلها، فبعض الألعاب المطورة باستعمال Pygame لا تحتاج للجاذبية مطلقًا وفقًا لقصتها وتصميمها، والسقوط ليس سقوطًا بالمعنى الفعلي للكلمة بل مجرد تحريك للكائن تفرضه الدالة، وحتى تمنح البطل قدرة القفز ومقاومة الجاذبية أو الاصطدام بالمنصات العائمة عليك بالتأكيد تعديل هذه الدالة وجعلها تنشط فقط عندما يقفز.

عدّل إذًا دالة الجاذبية التي استخدمناها في المقال السابق لتصبح على الشكل التالي:

    def gravity(self):
        if self.is_jumping:
            self.movey += 3.2

تتسبب هذه الدالة في سقوط البطل إلى أسفل الشاشة مباشرةً متجاوزًا الأرضية والفقرة التالية ستُعالج ذلك من خلال اكتشاف تصادمه مع أرضية اللعبة.

برمجة الأرضية الصلبة

طبقنا في المقال السابق حيلةً بسيطة لمنع كائن البطل من السقوط بعيدًا إلى أسفل الشاشة وجعلناه يرتد إلى مسافة معينة تبقيه ضمن الشاشة، يمكننا تشبيه هذه الحيلة بإنشاء جدار غير مرئي بارتفاعٍ معين يمتد على عرض الشاشة من الأسفل، لا يسعنا فعليًا التمسك بهذه الطريقة فهي لا تحقق معيار الشيفرة النظيفة في البرمجة، ففي الشيفرة النظيفة تُمثل الكائنات بكائنات برمجية فعلية (ونعني بذلك الجدار الوهمي الذي ذكرناه)، أضف إلى ذلك أننا لابد سنحتاج في بعض الحالات إلى إسقاط بطلنا حتى أسفل عالم اللعبة ربما عقبًا له على قفزة خاطئة أو عند خسارة كل نقاطه أو ما شابه فهذه الحركة شائعة في تصميم الألعاب.

اكتب التعليمات التالية في الدالة update ضمن صنف اللاعب:

ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.movey = 0
            self.rect.bottom = g.rect.top
            self.is_jumping = False  # إيقاف القفز

        # السقوط عبر عالم اللعبة
        if self.rect.y > worldy:
            self.health -=1
            print(self.health)
            self.rect.x = tx
            self.rect.y = ty

تتحقق هذه التعليمات من حدوث التصادم بين كائن الأرضية وكائن البطل وفق الأسلوب نفسه الذي استخدمناه للتحقق من التصادم بين البطل والعدو.

وفي حال حدوث التصادم ستعدّل هذه التعليمات موضع البطل عبر تعديل قيمة self.rect.bottom التي تعني أخفض نقطة في صورة كائن البطل لتصبح مساوية لقيمة g.rect.top التي تعني قمة كائن الأرضية أو أعلى نقطة فيه، وبذلك يبدو البطل كأنه يقف تمامًا فوق أرضية اللعبة ويمنعه ذلك من السقوط إلى الأرضية فهو يقف عليها.

علمًا أن هذه التفاصيل (مثل أخفض نقطة وأعلى نقطة في صورة الكائن) هي معلومات توفرها Pygame عن الكائنات عمومًا ويمكنك دائمًا الاستفادة منها.

بعد تعديل موضع كائن البطل ستُغَيّر هذه التعليمات قيمة self.is_jumping إلى False ليعرف برنامج اللعبة أن البطل لم يعد في حالة قفز بعد الآن، وقيمة self.movey أيضًا إلى 0 حتى لا يخضع البطل لتأثير الجاذبية (في الواقع هي طرافة في فيزياء اللعبة أن تتوقف عن جذب الكائن إلى الأرض طالما أنه يقف عليها).

أما مهمة if الشرطية في الجزء الأخير من التعليمات فهي اكتشاف القفزات الخاطئة لبطل اللعبة التي ينزل بموجبها تحت مستوى الأرضية، لتَخصم بناءً عليها نقطًا من صحته ومن ثم تعيد إنتاجه في أعلى يسار الشاشة باستخدام القيم ty و tx التي تمثل أبعاد بلاط الأرضية وقد استخدمناها للسهولة فقط في حين تستطيع تحديد الموضع الذي تريد، ويمكنك طبعًا عدم خصم نقاط من صحة اللاعب لقاء السقوط دون مستوى الأرضية فأنت من يحدد شروط اللعبة في نهاية المطاف.

القفز في Pygame

أنشئ أولًا دالة القفز jump لتقلب قيم المتغيرات is_falling و is_jumping.

    def jump(self):
        if self.is_jumping is False:
            self.is_falling = False
            self.is_jumping = True

عمليًا الانطلاق الفعلي للقفز يحدث في الدالة update ضمن صنف اللاعب.

        if self.is_jumping and self.is_falling is False:
            self.is_falling = True
            self.movey -= 33  # مقدار علو القفزة

تعمل هذه التعليمات بشرط تحقق أمرين أن تكون قيمة المتغير is_jumping هي True وقيمة المتغير is_falling هي False، وستُعَدّل عندها إحداثيات كائن البطل على المحور العمودي Y لتصبح 33- بكسل في فضاء عالم اللعبة وقيمتها سالبة بالطبع لأن الصفر في الأعلى والقيم الأصغر هي الأقرب إلى أعلى الشاشة، يعطيك هذا الرقم قفزةً جيدة لكن يمكنك تعديله لتحظى بقفزاتٍ أعلى أو أخفض.

بعد تعديل الإحداثي العمودي تُغَيّر هذه التعليمات قيمة is_falling لتصبح True حتى لا تتراكم القفزات فيُطلق الكائن عاليًا في فضاء اللعبة.

استدعاء دالة القفز

أشرنا للقفز سابقًا في مقال تحريك شخصية اللعبة عن طريق PyGame وربطناه بالضغط على السهم العلوي من أسهم الاتجاهات على لوحة المفاتيح قبل إنشاء دالة القفز ودون استدعائها وقد اكتفينا حينها بطباعة عبارة تشير للقفز في نافذة الطرفية باستخدام print، والآن يمكننا استدعاء دالة القفز في الحلقة الرئيسية بدلًا من الطباعة.

عدّل القيمة المقابلة للضغط على السهم العلوي في الحلقة الرئيسية لتصبح على الشكل التالي:

            if event.key == pygame.K_UP or event.key == ord('w'):
                player.jump()

يمكنك استخدام زر آخر بدل السهم العلوي مثل زر المسافة إن رغبت بذلك أو الجمع بين الخيارين باستخدام تعليمة if بسيطة، علمًا أن الرمز الذي يعبر عن المسافة في Pygame هو:

pygame.K_SPACE 

الهبوط على المنصات العائمة

تعاملنا في فقرةٍ سابقة مع اصطدام اللاعب بأرضية اللعبة وما نتج عنه من إيقاف لعمل الجاذبية، والآن سنعيد الخطوات نفسها مع بقية المنصات العائمة، إذ إن أرضية اللعبة والمنصات في مثالنا لا تنتميان لمجموعة الكائنات نفسها بل لمجموعتين مختلفتين، مع العلم أنك تستطيع دمجها معًا في مجموعةٍ واحدة، والتعامل مع الأرضية بصفتها منصة عادية، لكن المقال اعتمد الخيار الأوسع الذي يسمح بتفضيلات إضافية، في النتيجة السماح لكائن البطل بالوقوف على إحدى المنصات يعني أن يكتشف البرنامج تصادمه مع كائن المنصة ويمنع الجاذبية من سحبه إلى الأسفل.

اكتب التعليمات التالية ضمن الدالة update:

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.is_jumping = False  # إيقاف القفز
            self.movey = 0

            # اكتشاف محاولة الوصول للمنصة من الأسفل
            if self.rect.bottom <= p.rect.bottom:
               self.rect.bottom = p.rect.top
            else:
               self.movey += 3.2

تفحص هذه التعليمات كافة المنصات العائمة في اللعبة وتتحقق إن حدث أي تصادم بينها وبين كائن البطل، وفي حال ثبت حدوث التصادم تُعدّل قيمة متغير القفز إلى False وتُعطل حركة البطل على المحور العمودي.

منصات اللعبة عائمة في الهواء ما يعطي الأريحية للاعب بالوصول إليها من الأعلى أو من الأسفل، عادةً ما تمنع الألعاب وصول البطل للمنصات من الأسفل وتسمح به من الأعلى فقط أي عندما يقفز البطل أعلى من مستوى المنصة، يمكنك تجربة الحالتين لكن هذا المثال تعامل مع المنصات على أنها سقوف والجزء الثاني من التعليمات يسمح للاعب بالقفز على المنصة طالما أن قفزته بلغت مسافةً أعلى من قمة المنصة ويمنعه عندما يحاول القفز عليها من الأسفل.

يتحقق السطر الأول من التعليمة if الشرطية فيما إذا كانت قيمة أخفض نقطة من كائن البطل أقل من موقع المنصة ما يعني أن البطل أعلى منها على الشاشة لأن القيم سالبة، وإن تحقق ذلك يُسمح له بالهبوط على المنصة وتتعدل قيمة أخفض نقطة فيه لتصبح مساوية لقيمة قمة كائن المنصة، وإن لم يتحقق الشرط فيزداد الإحداثي Y لموقع لبطل مؤديًا إلى سقوطه نحو الأسفل بعيدًا عن المنصة العائمة التي كان يحاول الوصول إليها.

السقوط

إذا شغلت اللعبة الآن ستجد أن دالة القفز تعمل وبطلك يتنقل بين المنصات لكنه لا يسقط أرضًا بعد أن يهبط على أي منصة بل يبقى معلقًا في الهواء، والسبب في ذلك أننا عطلنا الجاذبية عند الهبوط على المنصة ولم نكتب أي تعليمة تعيدها للعمل ثانيةً عندما يصل البطل إلى حافة المنصة.

عدّل دالة التحديث update في صنف اللاعب لتصبح مسؤولة عن تشغيل وإيقاف تشغيل الجاذبية أثناء حركة الكائن وفق التالي انتبه أنك ستحتاج لإضافة الأسطر التي تتضمن تعليقات فقط.

        if self.movex < 0:
            self.is_jumping = True  # إيقاف تشغيل الجاذبية
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = pygame.transform.flip(self.images[self.frame // ani], True, False)

        if self.movex > 0:
            self.is_jumping = True  # تشغيل الجاذبية
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = self.images[self.frame // ani]

إذا فشل التحقق من حدوث تصادم مع إحدى المنصات خلال فترة تشغيل الجاذبية فإن البطل سيسقط أرضًا.

شغل اللعبة الآن، ولاحظ أن الوظائف كافة تعمل جيدًا، يمكنك تغيير قيم بعض المتغيرات لتجرب تأثيرها.

إليك شيفرة اللعبة كاملًا حتى هذه النقطة، أما في القسم التالي سنمنح البطل القدرة على الركض بحرية عبر تمرير عالم اللعبة جانبيًا.

#!/usr/bin/env python3
# by Seth Kenlon

# GPLv3
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import pygame
import sys
import os

'''
Variables
'''

worldx = 960
worldy = 720
fps = 40
ani = 4
world = pygame.display.set_mode([worldx, worldy])

BLUE = (25, 25, 200)
BLACK = (23, 23, 23)
WHITE = (254, 254, 254)
ALPHA = (0, 255, 0)

'''
Objects
'''

# x location, y location, img width, img height, img file
class Platform(pygame.sprite.Sprite):
    def __init__(self, xloc, yloc, imgw, imgh, img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images', img)).convert()
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc


class Player(pygame.sprite.Sprite):
    """
    Spawn a player
    """

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.is_jumping = True
        self.is_falling = True
        self.images = []
        for i in range(1, 5):
            img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect = self.image.get_rect()

    def gravity(self):
        if self.is_jumping:
            self.movey += 3.2

    def control(self, x, y):
        """
        control player movement
        """
        self.movex += x

    def jump(self):
        if self.is_jumping is False:
            self.is_falling = False
            self.is_jumping = True

    def update(self):
        """
        Update sprite position
        """

        # moving left
        if self.movex < 0:
            self.is_jumping = True
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = pygame.transform.flip(self.images[self.frame // ani], True, False)

        # moving right
        if self.movex > 0:
            self.is_jumping = True
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = self.images[self.frame // ani]

        # collisions
        enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in enemy_hit_list:
            self.health -= 1
            # print(self.health)

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.movey = 0
            self.rect.bottom = g.rect.top
            self.is_jumping = False  # stop jumping

        # fall off the world
        if self.rect.y > worldy:
            self.health -=1
            print(self.health)
            self.rect.x = tx
            self.rect.y = ty

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.is_jumping = False  # stop jumping
            self.movey = 0
            if self.rect.bottom <= p.rect.bottom:
               self.rect.bottom = p.rect.top
            else:
               self.movey += 3.2

        if self.is_jumping and self.is_falling is False:
            self.is_falling = True
            self.movey -= 33  # how high to jump

        self.rect.x += self.movex
        self.rect.y += self.movey

class Enemy(pygame.sprite.Sprite):
    """
    Spawn an enemy
    """

    def __init__(self, x, y, img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images', img))
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

    def move(self):
        """
        enemy movement
        """
        distance = 80
        speed = 8

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance * 2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1


class Level:
    def ground(lvl, gloc, tx, ty):
        ground_list = pygame.sprite.Group()
        i = 0
        if lvl == 1:
            while i < len(gloc):
                ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png')
                ground_list.add(ground)
                i = i + 1

        if lvl == 2:
            print("Level " + str(lvl))

        return ground_list

    def bad(lvl, eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0], eloc[1], 'enemy.png')
            enemy_list = pygame.sprite.Group()
            enemy_list.add(enemy)
        if lvl == 2:
            print("Level " + str(lvl))

        return enemy_list

    # x location, y location, img width, img height, img file
    def platform(lvl, tx, ty):
        plat_list = pygame.sprite.Group()
        ploc = []
        i = 0
        if lvl == 1:
            ploc.append((200, worldy - ty - 128, 3))
            ploc.append((300, worldy - ty - 256, 3))
            ploc.append((550, worldy - ty - 128, 4))
            while i < len(ploc):
                j = 0
                while j <= ploc[i][2]:
                    plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png')
                    plat_list.add(plat)
                    j = j + 1
                print('run' + str(i) + str(ploc[i]))
                i = i + 1

        if lvl == 2:
            print("Level " + str(lvl))

        return plat_list


'''
Setup
'''

backdrop = pygame.image.load(os.path.join('images', 'stage.png'))
clock = pygame.time.Clock()
pygame.init()
backdropbox = world.get_rect()
main = True

player = Player()  # spawn player
player.rect.x = 0  # go to x
player.rect.y = 30  # go to y
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10

eloc = []
eloc = [300, 0]
enemy_list = Level.bad(1, eloc)

gloc = []
tx = 64
ty = 64

i = 0
while i <= (worldx / tx) + tx:
    gloc.append(i * tx)
    i = i + 1

ground_list = Level.ground(1, gloc, tx, ty)
plat_list = Level.platform(1, tx, ty)

'''
Main Loop
'''

while main:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            try:
                sys.exit()
            finally:
                main = False

        if event.type == pygame.KEYDOWN:
            if event.key == ord('q'):
                pygame.quit()
                try:
                    sys.exit()
                finally:
                    main = False
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(-steps, 0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(steps, 0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                player.jump()

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps, 0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps, 0)

    world.blit(backdrop, backdropbox)
    player.update()
    player.gravity()
    player_list.draw(world)
    enemy_list.draw(world)
    ground_list.draw(world)
    plat_list.draw(world)
    for e in enemy_list:
        e.move()
    pygame.display.flip()
    clock.tick(fps)

تفعيل الركض ذهابًا وإيابا في عالم اللعبة

سنعطي البطل في هذه المرحلة القدرة على الركض ذهابًا وإيابًا عبر تمرير عالم اللعبة جانبيًا.

خططت في مقالٍ سابق مستوى للعبة يتضمن عدة منصات وعندما نفذته لم تظهر كافة المنصات على شاشة العرض فقد تجاوزت أبعادها وأعدادها مساحة الشاشة الواحدة، والتمرير الجانبي لعالم اللعبة أو ما يسمى side-scroller هو الحل الأمثل لهذه الحالة ولإظهار المستوى المخطط له كاملًا.

تمامًا كما يوحي الاسم، ستتحرك المنصات جانبيًا كلما اقترب البطل من حافة الشاشة لتعطي المستخدم شعورًا يشبه مشاهدة الفيديو وكأن كاميرا متحركة تتنقل في عالم اللعبة وتصوره، يتطلب التمرير حجز منطقة ميتة على جانبي الشاشة الأيمن والأيسر وعندما يصل البطل إلى هذه المناطق سيبقى ثابتًا بينما يُمرر العالم جانبيًا من حوله وتتحرك المنصات.

تثبيت نقاط التمرير الجانبي على الشاشة

إن رغبت بالسماح للاعب بالتحرك إلى الأمام والخلف فستحتاج إلى نقطتي تمرير على جانبي الشاشة، أما لو سمحت له بالتقدم للأمام فقط تكفيك عندها نقطةٌ واحدة.

سنحدد نقاط التمرير باستخدام المتغيرات بمسافة 100 أو 200 بيكسل عن كل جانب من جوانب الشاشة.

عرّف إذًا متغيرات التمرير في مقطع المتغيرات ضمن برنامجك على الشكل التالي:

forwardx  = 600
backwardx = 230

والآن تحقق من اللحظة التي يصل فيها بطلك إلى نقاط التمرير ضمن الحلقة الرئيسية للبرنامج وذلك عبر تعليمة if الشرطية، وإذا وصل اللاعب إلى إحدى هذه النقاط حرك عندها عالم اللعبة يمينًا أو يسارًا.

اكتب التعليمات التالية في حلقة التكرار الرئيسية للبرنامج وانتبه أن لا تتداخل مع الحلقة for الخاصة باكتشاف الضغط على أزرار لوحة المفاتيح، الأسطر الثلاثة الأخير هي للسياق فقط لن تحتاج لإعادة كتابتها:

        # تمرير عالم اللعبة إلى الأمام
        if player.rect.x >= forwardx:
                scroll = player.rect.x - forwardx
                player.rect.x = forwardx
                for p in plat_list:
                        p.rect.x -= scroll

        # تمرير عالم اللعبة إلى الخلف
        if player.rect.x <= backwardx:
                scroll = backwardx - player.rect.x
                player.rect.x = backwardx
                for p in plat_list:
                        p.rect.x += scroll

    # تعليمات التمرير أعلى هذا السطر
    world.blit(backdrop, backdropbox)
    player.gravity() # فحص الجاذبية
    player.update()

شغل اللعبة الآن وجرب التمرير.

pygame-scroll

ستلاحظ بعد التشغيل أن التمرير الجانبي يعمل جيدًا والمنصات تتحرك مع البطل إلى الأمام والخلف أما العنصر الوحيد الذي لا يستجيب للتمرير فهو كائن شخصية العدو.

اكتب التعليمات اللازمة لتمرير إن أردت أن يُمرر مثلهم، وتستطيع طبعًا تمريره للأمام فقط أو للخلف فقط أو بكلا الاتجاهين ليلاحق بطل اللعبة.

تمرير العدو مع عالم اللعبة

طبق التعليمات نفسها الخاصة بتمرير المنصات على شخصية العدو، والأفضل لك أن تطبقها على المجموعة التي تضم جميع كائنات العدو إذ إن اللعبة تتضمن أكثر من عدو، هذه ميزة جمع العناصر المتشابهة في مجموعات فهي تسهل على المبرمج تطبيق التعليمات عليها جميعًا دفعةً واحدة.

أضف السطرين الأخيرين من التعليمات التالية ضمن الحلقة الرئيسية في كتلة التمرير إلى الأمام لتمرير العدو إلى الأمام:

    # تمرير عالم اللعبة إلى الأمام
    if player.rect.x >= forwardx:
        scroll = player.rect.x - forwardx
        player.rect.x = forwardx
        for p in plat_list:
            p.rect.x -= scroll
        for e in enemy_list:    # تمرير العدو
            e.rect.x -= scroll  # تمرير العدو

ومن ثم السطرين الأخيرين من هذه التعليمات إلى كتلة التمرير إلى الخلف ضمن الحلقة الرئيسية لتمرير العدو بالاتجاه الآخر:

    # تمرير عالم اللعبة إلى الخلف
    if player.rect.x <= backwardx:
        scroll = backwardx - player.rect.x
        player.rect.x = backwardx
        for p in plat_list:
            p.rect.x += scroll
        for e in enemy_list:    # تمرير العدو
            e.rect.x += scroll  # تمرير العدو

والآن شغل اللعبة وتفقد النتائج.

إليك الشكل النهائي لبرنامج اللعبة حتى هذه المرحلة ليبقى مرجعًا لديك:

#!/usr/bin/env python3
# by Seth Kenlon

# GPLv3
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import pygame
import sys
import os

'''
Variables
'''

worldx = 960
worldy = 720
fps = 40
ani = 4
world = pygame.display.set_mode([worldx, worldy])
forwardx  = 600
backwardx = 230

BLUE = (25, 25, 200)
BLACK = (23, 23, 23)
WHITE = (254, 254, 254)
ALPHA = (0, 255, 0)

'''
Objects
'''

# x location, y location, img width, img height, img file
class Platform(pygame.sprite.Sprite):
    def __init__(self, xloc, yloc, imgw, imgh, img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images', img)).convert()
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc


class Player(pygame.sprite.Sprite):
    """
    Spawn a player
    """

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.is_jumping = True
        self.is_falling = True
        self.images = []
        for i in range(1, 5):
            img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect = self.image.get_rect()

    def gravity(self):
        if self.is_jumping:
            self.movey += 3.2

    def control(self, x, y):
        """
        control player movement
        """
        self.movex += x

    def jump(self):
        if self.is_jumping is False:
            self.is_falling = False
            self.is_jumping = True

    def update(self):
        """
        Update sprite position
        """

        # moving left
        if self.movex < 0:
            self.is_jumping = True
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = pygame.transform.flip(self.images[self.frame // ani], True, False)

        # moving right
        if self.movex > 0:
            self.is_jumping = True
            self.frame += 1
            if self.frame > 3 * ani:
                self.frame = 0
            self.image = self.images[self.frame // ani]

        # collisions
        enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in enemy_hit_list:
            self.health -= 1
            # print(self.health)

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.movey = 0
            self.rect.bottom = g.rect.top
            self.is_jumping = False  # stop jumping

        # fall off the world
        if self.rect.y > worldy:
            self.health -=1
            print(self.health)
            self.rect.x = tx
            self.rect.y = ty

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.is_jumping = False  # stop jumping
            self.movey = 0
            if self.rect.bottom <= p.rect.bottom:
               self.rect.bottom = p.rect.top
            else:
               self.movey += 3.2

        if self.is_jumping and self.is_falling is False:
            self.is_falling = True
            self.movey -= 33  # how high to jump

        self.rect.x += self.movex
        self.rect.y += self.movey

class Enemy(pygame.sprite.Sprite):
    """
    Spawn an enemy
    """

    def __init__(self, x, y, img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images', img))
        self.image.convert_alpha()
        self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

    def move(self):
        """
        enemy movement
        """
        distance = 80
        speed = 8

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance * 2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1


class Level:
    def ground(lvl, gloc, tx, ty):
        ground_list = pygame.sprite.Group()
        i = 0
        if lvl == 1:
            while i < len(gloc):
                ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png')
                ground_list.add(ground)
                i = i + 1

        if lvl == 2:
            print("Level " + str(lvl))

        return ground_list

    def bad(lvl, eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0], eloc[1], 'enemy.png')
            enemy_list = pygame.sprite.Group()
            enemy_list.add(enemy)
        if lvl == 2:
            print("Level " + str(lvl))

        return enemy_list

    # x location, y location, img width, img height, img file
    def platform(lvl, tx, ty):
        plat_list = pygame.sprite.Group()
        ploc = []
        i = 0
        if lvl == 1:
            ploc.append((200, worldy - ty - 128, 3))
            ploc.append((300, worldy - ty - 256, 3))
            ploc.append((550, worldy - ty - 128, 4))
            while i < len(ploc):
                j = 0
                while j <= ploc[i][2]:
                    plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png')
                    plat_list.add(plat)
                    j = j + 1
                print('run' + str(i) + str(ploc[i]))
                i = i + 1

        if lvl == 2:
            print("Level " + str(lvl))

        return plat_list


'''
Setup
'''

backdrop = pygame.image.load(os.path.join('images', 'stage.png'))
clock = pygame.time.Clock()
pygame.init()
backdropbox = world.get_rect()
main = True

player = Player()  # spawn player
player.rect.x = 0  # go to x
player.rect.y = 30  # go to y
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10

eloc = []
eloc = [300, 0]
enemy_list = Level.bad(1, eloc)

gloc = []
tx = 64
ty = 64

i = 0
while i <= (worldx / tx) + tx:
    gloc.append(i * tx)
    i = i + 1

ground_list = Level.ground(1, gloc, tx, ty)
plat_list = Level.platform(1, tx, ty)

'''
Main Loop
'''

while main:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            try:
                sys.exit()
            finally:
                main = False

        if event.type == pygame.KEYDOWN:
            if event.key == ord('q'):
                pygame.quit()
                try:
                    sys.exit()
                finally:
                    main = False
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(-steps, 0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(steps, 0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                player.jump()

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps, 0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps, 0)

    # scroll the world forward
    if player.rect.x >= forwardx:
        scroll = player.rect.x - forwardx
        player.rect.x = forwardx
        for p in plat_list:
            p.rect.x -= scroll
        for e in enemy_list:  # enemy scroll
            e.rect.x -= scroll  # enemy scroll

    # scroll the world backward
    if player.rect.x <= backwardx:
        scroll = backwardx - player.rect.x
        player.rect.x = backwardx
        for p in plat_list:
            p.rect.x += scroll
        for e in enemy_list:    # enemy scroll
            e.rect.x += scroll  # enemy scroll

    world.blit(backdrop, backdropbox)
    player.update()
    player.gravity()
    player_list.draw(world)
    enemy_list.draw(world)
    ground_list.draw(world)
    plat_list.draw(world)
    for e in enemy_list:
        e.move()
    pygame.display.flip()
    clock.tick(fps)

ترجمة -وبتصرف- للمقال Add jumping to your Python platformer game والمقال Enable your Python game player to run forward and backward لصاحبيه Seth Kenlon و Jess Weichler.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...