تجنب الأعداء والركض بعيدًا عنهم شيء والقتال ضدهم شيء آخر تمامًا، فهو يزيد التفاعل بين المستخدم واللعبة، وهو موضوع المقال الختامي من سلسلة بناء لعبة من الصفر باستخدام بايثون، إليك مقالات السلسلة بالترتيب قبل أن نبدأ:
- بناء لعبة نرد بسيطة بلغة بايثون.
- بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame.
- إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame.
- تحريك شخصية اللعبة باستخدام PyGame.
- إضافة شخصية العدو للعبة.
- إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame.
- محاكاة أثر الجاذبية في لعبة بايثون.
- إضافة خاصية القفز والركض إلى لعبة بايثون.
- إضافة الجوائز إلى اللعبة المطورة بلغة بايثون.
- تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة.
- إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون.
لديك تفصيلين تهتم بهما في هذه الحالة، الأول هو إنتاج العنصر الذي سيُرمى والثاني هو المعيار أو الشرط الذي سيبقيه فعالًا فالسهام والكرات النارية لابد ستختفي في النهاية قد تختفي عند حافة الشاشة مثلًا أو تظل صالحة لفترة زمنية محدودة ينتهي مفعولها بعدها وهكذا.
بالإضافة إلى تحديد قدرة اللاعب على القذف فهو يستطيع أن يقذف عنصرًا واحدًا فقط في كل مرة، يرفع هذا الخيار من مستوى التحدي بإعطاء اللاعب فرصةً وحيدة في كل مرة لإصابة العدو، ويساهم من جهةٍ ثانية بتبسيط تعليماتك البرمجية.
يمكنك بعد نهاية القراءة وتعلم الموجود هنا أن تسعى لتطوير البرنامج وتعطي لاعبك القدرة على قذف أكثر من عنصر في وقتٍ واحد.
إنشاء صنف الكائن المقذوف
ستجد الدالة __init__
ضمن تعليمات الصنف المبينة أدناه لإنتاج كائن بايثون للعنصر المقذوف، وهي الدالة نفسها المستخدمة سابقًا لإنتاج البطل و الأعداء.
class Throwable(pygame.sprite.Sprite): """ إنتاج الكائن المقذوف """ def __init__(self, x, y, img, throw): 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.firing = throw
الاختلاف الجوهري بين الدالة __init__
الموجودة في هذا الصنف والدالة نفسها الموجودة في صنف اللاعب Player
وصنف العدو Enemy
هو المتغير self.firing
الذي يتتبع حالة الكائن المقذوف فيما إذا كان لا يزال فعالًا وظاهرًا على الشاشة أم انتهت صلاحيته، وتتغير قيمته تبعًا لذلك فهو يحمل القيمة 1 عندما ينشئ الكائن.
قياس زمن فعالية الكائن المقذوف
تمامًا مثل البطل والعدو يحتاج هذا الكائن إلى الدالة update
لتحديث موقع ظهوره على الشاشة حتى يتحرك في الهواء منطلقًا باتجاه العدو بعد أن رماه البطل.
أسهل طريقة لقياس زمن فعالية الكائن المقذوف هي وصوله إلى حافة الشاشة وخروجه عن مدى الرؤية، لكن عليك أن تحدد أي حافة ستراقب تبعًا لحركة هذا الكائن إن كانت أفقية أو عمودية.
- إذا كان البطل يقذفه أفقيًا مثل السهم أو الطاقة السحرية أو ماشابه فعليك مراقبة حافة الشاشة على المحور الأفقي وتحددها القيمة
worldx
. - أما إذا كان يقذف الأشياء عموديًا أو بكلا الاتجاهين يتعين عليك مراقبة الحافة العمودية للشاشة باستخدام القيمة
worldy
.
يفترض هذا المثال أن الجسم المقذوف يتحرك قليلًا للأمام ثم يسقط على الأرضية دون ارتداد فيتابع سقوطه إلى أسفل الشاشة، اطلع على التعليمات التالية وغيّر القيم الواردة فيها قليلًا لترى ما يناسب لعبتك أكثر:
def update(self,worldy): ''' فيزياء المقذوفات ''' if self.rect.y < worldy: #المحور العمودي self.rect.x += 15 #سرعة حركته إلى الأمام self.rect.y += 5 #سرعة سقوطه إلى أسفل else: self.kill() #انتهاء فاعلية العنصر المقذوف self.firing = 0 #تحرير المتغير أو تصفيره
ارفع قيم self.rect
لتسريع حركة الكائن المقذوف.
عندما يصل المقذوف إلى حافة الشاشة سيختفي أو بالأحرى يُدَمّر، وتتحرر الذاكرة المؤقتة التي أُشغلت بوجوده وتُضبط قيمة المتغير self.firing
إلى الصفر ويتحرر تمهيدًا للرمية التالية.
ضبط إعدادات الكائن المقذوف
أنشئ في مقطع الإعدادات ضمن برنامج مجموعةً خاصة للكائنات التي يمكن أن يقذفها البطل، بصورةٍ مشابهة لمجموعات البطل والأعداء، وجهز كائنًا قابلًا للقذف غير فعال حتى يبدأ به البطل اللعبة، وإلّا فإن قذفته الأولى ستفشل.
يفترض هذا المثال أن سلاح البطل هو الكرة النارية وتحدد مواصفاتها باستعمال المتغير fire
ويمكنك في مستوياتٍ لاحقة تطوير السلاح وتغيير صورته ومواصفاته بالاعتماد على الصنف Throwable
نفسه.
اكتب التعليمات الآتية علمًا أن تعليمات أول سطرين لمعرفة السياق ولا حاجة لكتابتها مجددًا:
player_list = pygame.sprite.Group() #للسياق player_list.add(player) #للسياق fire = Throwable(player.rect.x,player.rect.y,'fire.png',0) firepower = pygame.sprite.Group()
لاحظ أن مكان إنتاج الكائن المقذوف مطابق لمكان إنتاج كائن البطل وهذا يعطي انطباعًا بأنه ينطلق من جعبة البطل، بعد إنتاج كرة النار الأولى يأخذ المتغير self.firing
القيمة صفر ليتيح إمكانية القذف.
إضافة القذف إلى الحلقة الرئيسية
المبدأ نفسه على طول السلسلة اذكر ما تريد تنفيذه في الحلقة الرئيسية، أضف في البداية تعليمات التحكم التي ستُطلق الكرة النارية عندما يستخدم اللاعب الزر المحدد على لوحة المفاتيح.
عند تحريك البطل استخدمنا الضغط على الزر لتبدأ حركة البطل وتحرير الزر أو رفع الضغط عنه ليتوقف البطل عن الحركة، لا يوجد توقف في عملية القذف فلن تحتاج أكثر من إشارة اختر واحدة منهما الضغط أو التحرير.
اكتب التعليمات التالية وأول سطرين لتبيان السياق:
if event.key == pygame.K_UP or event.key == ord('w'): player.jump(platform_list) if event.key == pygame.K_SPACE: if not fire.firing: fire = Throwable(player.rect.x,player.rect.y,'fire.png',1) firepower.add(fire)
تُسند القيمة 1 للمتغير self.firing
على عكس الكرة النارية الأولى غير الفعالة التي كُتبت سابقًا في مقطع الإعدادات.
اكتب التعليمات الخاصة بتحديث الكائن المقذوف ورسمه على الشاشة، واحرص على كتابتها في الموقع الصحيح تمامًا كما هو مبين أدناه:
enemy.move() # للسياق if fire.firing: fire.update(worldy) firepower.draw(world) player_list.draw(screen) # للسياق enemy_list.draw(screen) # للسياق
لاحظ أن تعليمات التحديث تعمل فقط عندما يحمل المتغير self.firing
القيمة 1، أما إذا كانت قيمته 0 فإن الشرط لن يتحقق ويتجاوز البرنامج هذه التعليمات دون أن ينفذها، أما لو كتبت هذه التعليمات دون شروط وحاولت تطبيقها بغض النظر عن قيمة المتغير سيفشل عمل اللعبة لأنها لن تجد كرةً نارية لترسمها أو تحدّث ظهورها على الشاشة.
شغل اللعبة الآن وحاول استخدام السلاح.
اكتشاف التصادم
فور تجريب اللعبة ستلاحظ أن البطل يقذف سلاحه باتجاه العدو وقد يصيبه لكن دون أي تأثير، فالبرنامج لا يملك حتى الآن أي آلية تكتشف تصادم الجسم المقذوف بالعدو.
الآلية المطلوبة مشابهة جدًا للمذكورة في صنف اللاعب Player
، لذا أضف الدالة update
التالية في صنف العدو Enemy
:
def update(self,firepower, enemy_list): """ اكتشاف التصادم مع الكرة النارية """ fire_hit_list = pygame.sprite.spritecollide(self,firepower,False) for fire in fire_hit_list: enemy_list.remove(self)
التعليمات بسيطة فهي تتحقق من حدوث التصادم بين كائن العدو وكل كرة نارية firepower
تنتمي لمجموعة كائنات الكرات النارية التي يقذفها البطل، وفي حال تبين حدوث التصادم يُزال العدو من مجموعة الأعداء ويختفي عن الشاشة.
أضف السطر الأخير من التعليمات الآتية إلى كتلة تعليمات firing
في الحلقة الرئيسية:
if fire.firing: # للسياق fire.update(worldy) # للسياق firepower.draw(screen) # للسياق enemy_list.update(firepower,enemy_list) # تحديث العدو
يتبقى لدينا أمر أخير للتعامل معه وهو اتجاه القذف.
تغيير اتجاه القذف
يقذف بطل اللعبة حاليًا كراته باتجاه اليمين فقط، فعندما ما عرفنا دالة التحديث في صنف القذف جعلناها تزيد عددًا من البكسلات لموقع الكرة وهي تتحرك على المحور X والزيادة على هذا المحور تعني التحرك يمينًا، فماذا لو استدار البطل وأراد القذف نحو اليسار؟
يتمثل الحل المتبع هنا في تعريف متغير جديد يحدد توجه البطل ومن ثم تحديد اتجاه القذف الأنسب وفقًا لقيمته.
عرّف المتغير facing_right
في صنف اللاعب وأعطه القيمة الابتدائية True
إذ إن صورة اللاعب المستخدمة في هذا المثال تتجه افتراضيًا نحو اليمين:
self.score = 0 self.facing_right = True # أضف هذا السطر self.is_jumping = True
وبذلك تكون القيمة True
مؤشرًا على اتجاه البطل إلى اليمين والقيمة False
تشير لاتجاهه يسارًا، وينبغي التبديل بينهما في كل مرة يغير البطل اتجاه حركته، وأنسب مكان ذلك هو التعليمات الخاصة بالتحكم بحركة البطل عبر أزرار لوحة المفاتيح وفق ما يلي:
if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) player.facing_right = False # أضف هذا السطر if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) player.facing_right = True # أضف هذا السطر
عدّل أخيرًا الدالة update
في صنف القذف Throwable
لتجمع أو تطرح عددًا من وحدات البكسل إلى موقع الكرة النارية بناءً على اتجاه حركة البطل:
if self.rect.y < worldy: if player.facing_right: self.rect.x += 15 else: self.rect.x -= 15 self.rect.y += 5
جرب اللعبة الآن وتفقد النتائج.
حاول تطوير اللعبة وامنح البطل بعض النقاط مع كل إصابة موفقة للعدو.
برنامج اللعبة كاملًا
#!/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 pygame.freetype import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 120 BLUE = (80, 80, 155) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) tx = 64 ty = 64 font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts", "amazdoom.ttf") font_size = tx pygame.freetype.init() myfont = pygame.freetype.Font(font_path, font_size) ''' Objects ''' def stats(score, health): myfont.render_to(world, (4, 4), "Score:"+str(score), BLUE, None, size=64) myfont.render_to(world, (4, 72), "Health:"+str(health), BLUE, None, size=64) class Throwable(pygame.sprite.Sprite): """ Spawn a throwable object """ def __init__(self, x, y, img, throw): 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.firing = throw def update(self, worldy): ''' throw physics ''' if self.rect.y < worldy: if player.facing_right: self.rect.x += 15 else: self.rect.x -= 15 self.rect.y += 5 else: self.kill() self.firing = 0 # 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.damage = 0 self.score = 0 self.facing_right = True self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'walk' + 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) if self.damage == 0: for enemy in enemy_hit_list: if not self.rect.contains(enemy): self.damage = self.rect.colliderect(enemy) if self.damage == 1: idx = self.rect.collidelist(enemy_hit_list) if idx == -1: self.damage = 0 # set damage back to 0 self.health -= 1 # subtract 1 hp 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 loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) 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 def update(self, firepower, enemy_list): """ detect firepower collision """ fire_hit_list = pygame.sprite.spritecollide(self, firepower, False) for fire in fire_hit_list: enemy_list.remove(self) 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 def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*5, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_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 fire = Throwable(player.rect.x, player.rect.y, 'fire.png', 0) firepower = pygame.sprite.Group() eloc = [] eloc = [300, worldy-ty-80] enemy_list = Level.bad(1, eloc) gloc = [] 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) enemy_list = Level.bad( 1, eloc ) loot_list = Level.loot(1) ''' 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) player.facing_right = False if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) player.facing_right = True if event.key == pygame.K_SPACE: if not fire.firing: fire = Throwable(player.rect.x, player.rect.y, 'fire.png', 1) firepower.add(fire) # 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: e.rect.x -= scroll for l in loot_list: l.rect.x -= 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: e.rect.x += scroll for l in loot_list: l.rect.x += scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) if fire.firing: fire.update(worldy) firepower.draw(world) enemy_list.draw(world) enemy_list.update(firepower, enemy_list) loot_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() stats(player.score, player.health) pygame.display.flip() clock.tick(fps)
ترجمة -وبتصرف- للمقال Add throwing mechanics to your Python game لصاحبيه Seth Kenlon و Jess Weichler.
اقرأ أيضًا
- المقال السابق: تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة
- النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
تعليقات
إرسال تعليق