وصلنا للمقال الخامس من سلسلة بناء لعبة من الصفر باستخدام بايثون، وقد تعلمنا في أجزائها الأربعة السابقة كيفية بناء لعبة فيديو باستخدام الوحدة Pygame وطورنا اللعبة بوجود بطل متحرك، والآن سنضيف إليها بعض الإثارة بإضافة شخصية شريرة للمشهد أو عدو ليواجه البطل فهذا تقليدي في عالم الألعاب، ومن ثم سنبني إطار عمل خاص بإضافة مستويات اللعبة.
يمكنك مطالعة المقال ضمن السلسلة:
- بناء لعبة نرد بسيطة بلغة بايثون.
- بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame.
- إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame.
- تحريك شخصية اللعبة باستخدام PyGame.
- إضافة شخصية العدو للعبة.
- إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame.
- محاكاة أثر الجاذبية في لعبة بايثون.
- إضافة خاصية القفز والركض إلى لعبة بايثون.
- إضافة الجوائز إلى اللعبة المطورة بلغة بايثون.
- تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة.
- إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون.
بناء شخصية العدو مشابه تمامًا لبناء شخصية البطل سنستخدم الوظائف نفسها، وفي حال لم تُحضّر له صورة حتى الآن يمكنك اختيارها من مجموعة الصور المجانية المتاحة على منصة OpenGameArt.org أو نزلها من الملف المرفق opp2_sprites، وننصحك بتعلم المزيد عن رخصة المشاع الإبداعي حتى لا تنتهك حقوق النشر في الصور التي تستخدمها.
إنشاء شخصية العدو
العملية مشابهة جدًا لعملية إنشاء كائن البطل، فلديك المفاتيح الرئيسة للعمل.
- أنشئ صنفًا خاصًا بشخصية العدو لإنتاج كائن الشخصية.
-
أنشئ دالة
update
لتحديث كائن العدو في الحلقة الرئيسية. -
أنشئ دالة
move
لتمنح شخصية العدو حركة عشوائية.
ابدأ بالصنف بالطريقة نفسها لصنف البطل، ومن ثم حدد الصورة أو مجموعة الصور التي تعتمدها لشخصية العدو واحفظها في المجلد images الخاص بالصور ضمن مجلد مشروعك (المجلد نفسه الذي يتضمن صور البطل)، وأعطها اسمًا دلاليًا مثل enemy.png المستخدم في حالتنا.
بالتأكيد ستبدو اللعبة أفضل لو جعلت كافة العناصر تتحرك، لكننا سنترك شخصية العدو دون حركة في هذه المرحلة حتى نخفف عليك التعقيد، ولو رغبت بتحريكه استذكر خطواتنا في المقال السابق.
اكتب التعليمات التالية لإنشاء صنف العدو في أعلى المقطع الخاص بالكائنات objects
ضمن برنامجك:
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
يمكنك استخدام الصنف نفسه لتوليد عدة أعداء، فكل ما عليك فعله هو استدعاء الصنف وإعطائه صورة لكل شخصية وإحداثيات ظهورها على الشاشة X و Y.
تمامًا مثل صنف اللاعب، اكتب صنف العدو في مقطع الإعدادات setup
ضمن البرنامج:
enemy = Enemy(300,0,'enemy.png') # إنتاج العدو enemy_list = pygame.sprite.Group() # إنشاء مجموعة الأعداء enemy_list.add(enemy) # إضافة عدو للمجموعة
أنتجت التعليمات السابقة كائنًا يسمى enemy
إحداثيات موقعه هي 300 بيكسل على المحور X و 0 بيكسل على المحور Y، ما يعني أن زاويته العلوية اليسرى عند النقطة 0 وتمتد صورته للأسفل تبعًا لمساحتها، لذا تنبه لأحجام صور الأعداء والأبطال واضبط إحداثياتها لتكون مواضعها مناسبة على الشاشة، وولد الأعداء في مكانٍ يمكنك إيصال بطل اللعبة إليه ففي مثالنا وضعنا العدو في النهاية عند النقطة 0 بيكسل والبطل عند النقطة 30 بيكسل على المحور العمودي Y ليظهرا بنفس المستوى (وهذا بسبب افتقار اللعبة إلى الحركة العمودية)، علمًا أن الصفر على المحور Y يقع في الأعلى ما يعني أن الأرقام تزداد كلما اتجهت نحو أسفل الشاشة.
ولاحظ أن صورة البطل مدمجة مع كود الصنف أي ثابتة hard coded في مثالنا، ذلك أن البطل وحيد في اللعبة، أما صور الأعداء متغيرة إذ قد ترغب باستخدام أكثر من نموذج لها، لذا حدد الصورة خلال عملية إنشاء الشخصية، والصورة المستخدمة في مثالنا هي enemy.png
.
رسم شخصية على الشاشة
حاول تشغيل لعبتك الآن، ستعمل، لكن شخصية العدو لن تظهر على الشاشة، اعترضتنا هذه المشكلة قبلًا وبالتأكيد عرفت السبب فتعليمات إنشاء الشخصية لم تكتب ضمن الحلقة الرئيسية، تذكّر دومًا أن أي شيء تريد حدوثه باستمرار لابد من ذكره في حلقة التكرار، أما بدون ذلك فسيظهر العدو على الشاشة لمدة بسيطة جدًا لأجزاء من الثانية ويختفي دون أن تلحظه حتى.
أضف السطر الأوسط من التعليمات التالية إلى الحلقة حتى تُحدّث ظهور مجموعة الأعداء enemy_list
التي تتكون حاليًا من عدو واحد لكن يمكنك توسيعها بإضافة أعداء آخرين وسيظهرون جميعًا مع كل تكرار للحلقة.
player_list.draw(world) enemy_list.draw(world) # تحديث مجموعة الأعداء pygame.display.flip()
شغل اللعبة بعد التعديلات، وستجد شخصية العدو على الشاشة في الموقع الذي تحدده الإحداثيات X و Y.
إضافة مراحل للعبة
يخطط مطور الألعاب لمستويات أو مراحل اللعبة حتى وهي في مهدها، ورغم بساطة لعبتنا وعدم وجود أي مستوى فيها حتى الآن، إلّا أننا سنكتب التعليمات كما لو كنا نخطط لبناء عدة مستويات.
فكر في ماهية المستوى الذي تعّده؟ عادةً عندما تلعب كيف تعرف أنك في مستوى معين أو أنك انتقلت لمستوى جديد؟
يمكننا القول أن المستوى هو مجموعة من العناصر المميزة، وفي ألعاب المنصات مثل لعبتنا يتكون المستوى من مجموعة منصات مرتبة ترتيبًا معينًا، بالإضافة إلى نوع خاص بالمستوى من الأعداء والغنائم وما شابه، أما عن طريقة بناء المستوى سننشئ له صنفًا خاصًا يبني المستوى حول اللاعب، ونعيد استخدامه بعد الانتهاء لبناء المستويات اللاحقة.
انقل الآن التعليمات التي كتبتها سابقًا لبناء شخصية العدو ومجموعة الأعداء، إلى دالة جديدة واستدعيها مع كل بناء لمستوى جديد، ستحتاج بالطبع لبعض التعديلات لإنتاج أنواع مختلفة من الأعداء لكل مستوى.
class Level(): 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
مهمة التعليمة return
السابقة هي إرجاع قائمة تتضمن مجموعة الأعداء enemy_list
التي استخدمتها مع كل تنفيذ للدالة Level.bad
.
نظرًا لأن إنشاء العدو أصبح جزءًا من إنشاء المستوى، سنجري تعديلًا على مقطع الإعدادات setup
، فبدلًا من إنشاء العدو، سنحدد المستوى الذي ينتمي إليه وأين سيتم إنتاجه، وذلك وفق الأوامر التالية:
eloc = [] eloc = [300,0] enemy_list = Level.bad( 1, eloc )
شغل اللعبة مجددًا وتأكد من صحة إنشاء المستوى ومن ظهور البطل والعدو.
التصادم مع العدو
لا تُعدّ شخصية العدو فاعلة ما لم تسبب ضررًا للاعب أو تؤثر عليه، والتصادم هو أشيع أنواع الضرر في الألعاب.
أما بعد التصادم علينا فحص حالة البطل أو صحته وسنجري ذلك في صنف البطل، يمكنك بالطبع فحص صحة العدو بالطريقة نفسها لكننا سنكتفي بتتبع صحة اللاعب، وذلك من خلال تعريف متغير خاص يشير إلى صحة اللاعب وفق السطر الثاني من التعليمات التالية أما السطر الأول فذُكر للإشارة إلى السياق فقط.
self.frame = 0 self.health = 10
وأيضًا أضف التعليمات التالية في الدالة update
ضمن صنف اللاعب.
hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in hit_list: self.health -= 1 print(self.health)
تكشف هذه التعليمات التصادم باستخدام sprite.spritecollide
إحدى دوال Pygame ضمن المتغير hit_list
لحظة التلامس بين صندوق البطل -الذي تفحص صحته- وصندوق أي شخصية تنتمي لمجموعة العدو enemy_list
، ويرسل عندها إشارة إلى الحلقة for
تؤدي إلى خصم نقطة من صحة اللاعب في كل مرة يحدث فيها التصادم.
ولأن هذه التعليمات مكتوبة في الدالة update
لصنف اللاعب التي تُستدعى مع كل تكرار للحلقة الرئيسية، فإن Pygame ستتحق من التصادم مع كل نبضة ساعة.
تحريك العدو
يعدّ نمط العدو الثابت الذي بنيناه هنا مقبولًا في الألعاب مثل المسامير والمصائد التي تعرقل سير اللاعب، إلاّ أن العدو المتحرك يضيف صعوبةً وإثارة للعبة.
سنبدأ بتحريك العدو مع مراعاة أمرين، الأول أنه حركته آلية تُدار من قبل البرنامج ولا يتحكم بها المستخدم مثل حركة البطل، والثاني أن بيئة اللعبة أو عالمها متحرك، فكيف سنجعل العدو يتحرك داخله ذهابًا وإيابًا وهو أساسًا يتحرك؟
لنفترض أنك ترغب ببرمجة حركة العدو ليتحرك 10 خطوات نحو اليمين ومن ثم 10 نحو اليسار، كائن العدو لا يملك في مواصفاته البرمجية ما يمكنه من عدّ الخطوات، لذا ستحتاج إلى تعريف متغير يتتبع خطواته ويعدها، ومن ثم تبرمج عدوك ليتحرك يمينًا أو يسارًا بناءً على عدد الخطوات التي أنجزها.
لنبدأ إذًا بتعريف متغير العداد المسؤول عن عدّ الخطوات، عبر إضافة السطر الأخير من التعليمات التالية إلى صنف العدو:
self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 # متغير العداد
ثم ننشئ دالة الحركة move
في صنف العدو أيضًا، وذلك عبر الحلقة if-else بالشروط الآتية، التي ستجعلها حلقة لا نهائية.
- تحرك يمينًا إذا كانت قيمة العداد بين 0 و 100.
- تحرك يسارًا إذا كانت قيمة العداد بين 100 و 200.
- صفّر العداد وأرجعه للقيمة 0 عندما تتجاوز قيمته العدد 200.
ستحرك هذه الحلقة كائن العدو يمينًا ويسارًا إلى ما لا نهاية فلا يوجود قيمة للمتغير تجعلها تتوقف فهو دائمًا بين 0 و 100 أو بين 100 و 200.
أما عن كيفية اختيار القيم التي ستشكل أقصى مسافة يمكن للكائن الوصول إليها فالموضوع نسبي، يتغير تبعًا لقياس شاشة العرض لديك وقياس منصة اللعبة، لذا ننصحك بالتجريب، ابدأ بأرقام صغيرة ولاحظ النتائج ثم زدها لتصل لغايتك، والأمر نفسه بالنسبة لسرعة الحركة، مثلًا جرب التالي في البداية ويمكنك تعديله حسب رضاك عن النتائج:
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
بعد كتابة هذه التعليمات ستعرض عليك بيئة التطوير Pycharm تحذيرًا يتضمن اقتراحًا لتبسيط موازنة السلاسل بهدف تحسين أسلوب كتابة التعليمات، يمكنك قبول الاقتراح وتعلم المزيد عن الكتابة المتقدمة في بايثون، أو تجاهل التحذير بأمان والاستمرار.
السؤال الآن، هل سيتحرك العدو عند تشغيل اللعبة؟ أم أننا تحتاج لخطوة إضافية؟
الإجابة هي لا بالطبع، لن يتحرك ما لم تستدعِ الدالة move
ضمن حلقة البرنامج الرئيسية، أضف السطرين الأخيرين من التعليمات التالية لتُنجز ذلك:
enemy_list.draw(world) #حدّث العدو for e in enemy_list: e.move()
شغل اللعبة وشاهد ما يحدث عند اصطدام بطل اللعبة بالعدو، ربما ستحتاج لبعض التعديلات مثل تغيير مكان إنتاج العدو لتصنع التصادم، وعندما يحدث ذلك، تفقد النقاط التي خُصمت من رصيد صحة اللاعب من خلال بيئة التطوير IDLE أو Pycharm.
قد تلاحظ أن النقاط تُخصم من صحة اللاعب (أو من أدوار اللاعب) في كل لحظة من الاصطدام، ستتعلم معالجة ذلك مع تطور مهاراتك في بايثون، أما حاليًا جرّب أن تنتج المزيد من الأعداء ووأيًا كانوا لا تنسَ إضافتهم إلى مجموعة الأعداء enemy_list
، ومن ثم حاول تغير طريقة حركتهم استخدم حركة مختلفة لكل عدو.
الشكل النهائي لبرنامج اللعبة
إليك الشكل النهائي لبرنامج اللعبة متضمنًا كامل التعليمات البرمجية التي استخدمناها خلال السلسلة، ليبقى مرجعًا لك:
#!/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 ''' 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.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 control(self, x, y): """ control player movement """ self.movex += x self.movey += y def update(self): """ Update sprite position """ self.rect.x = self.rect.x + self.movex self.rect.y = self.rect.y + self.movey # moving left if self.movex < 0: 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.frame += 1 if self.frame > 3*ani: self.frame = 0 self.image = self.images[self.frame//ani] hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in hit_list: self.health -= 1 print(self.health) 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 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 ''' 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 ) ''' 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'): print('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_list.draw(world) enemy_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps)
ترجمة -وبتصرف- للمقال What's a hero without a villain? How to add one to your Python game لصاحبه Seth Kenlon.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.