🎮
The Legend of PyZelda: Breath of the Snakes
  • Обложка
  • Введение
  • Подготовка
  • Пишем уровень
  • Создаём игрока
  • Создание камеры
  • Графика
  • Анимация Линка
  • Оружие героя
  • Интерфейс
  • Монстры
  • Драки с монстрами
  • Музыка
  • Деплой
  • Итоги
Powered by GitBook
On this page

Драки с монстрами

PreviousМонстрыNextМузыка

Last updated 1 year ago

Итак, для начала создадим два новых демона для атак в level.py:

self.attack_sprites = pygame.sprite.Group() #атакующий спрайт
self.attackable_sprites = pygame.sprite.Group() #атакуемый спрайт

Думаю, по названиям понятно, что демоны нужны для обозначения процесса атаки. Дополним метод вызова врагов помимо видимых спрайтов, атакуемыми спрайтами (self.attack_sprites):

Enemy(monster_name, (x, y), [self.visible_sprites, self.attackable_sprites], self.obstacle_sprites)

Также, дополним метод create_attack атакующим спрайтом (self.attack_sprites) :

def create_attack(self):
    self.current_attack = Weapon(self.player, [self.visible_sprites, self.attack_sprites])

Далее пропишем новый метод с логикой атаки игрока:

def player_attack_logic(self):
    if self.attack_sprites:
        for attack_sprites in self.attack_sprites:
            collision_sprites = pygame.sprite.spritecollide(attack_sprites, self.attackable_sprites, False)
            if collision_sprites:
                for target_sprite in collision_sprites:
                    target_sprite.kill()

Самая интересная строка тут, это строка с методом пайгейма pygame.sprite.spritecollide. Данный метод позволяет удалять спрайт из группы. Первый аргумент функции — спрайты для атаки, второй — группа спрайтов, из которой мы будем удалять спрайт, третий — DoKill. Если у DoKill установлено значение True, все спрайты, которые сталкиваются, будут удалены из группы. Далее, в run-методе пропишем наш метод. Исход:

Немного улучшим метод player_attack_logic:

if target_sprite.sprite_type == 'enemy':
    target_sprite.get_damage(self.player, attack_sprites.sprite_type)

Мы стали сопоставлять наши спрайты по типам. В моём проекте типов только два (enemy и weapon). Я прописал в файле weapon.py в демоне строку для присваивания ему нового типа:

self.sprite_type = 'weapon'

Далее нам не нужно удалять врага при ударе. Нам нужно прописывать ему урон от нашего оружия. Собственно, теперь нужно в файле enemy.py прописать новый метод — get_damage:

def get_damage(player, attack_type):
    if attack_type == 'weapon':
        self.health -= player.get_full_weapon_damage()

Тут мы прописываем новый метод (get_full_weapon_damage), который должен высчитывать сумму урона от оружия и от силы самого Линка (прямо как в Dark Souls). Пропишем же данный метод в player.py:

def get_full_weapon_damage(self):
    base_damage = self.stats['attack'] #урон самого Линка
    weapon_damage = weapon_data[self.weapon]['damage'] #урон от выбранного оружия
    return base_damage + weapon_damage

Находим и складываем уроны из наших списков. Возможно, тут встанет вопрос: "Зачем так сложно, если у нас только один меч и всё?". Я хотел бы сделать проект так, чтобы вы могли самостоятельно с ним "поиграться". Собственно и цель книги не номинация "Игра года" в The Game Awards, а лишь попытка продемонстрировать работоспособность языка Python как неплохого движка. Ну да вернёмся к коду. Помимо урона, я решил сразу прописать кулдаун оружия и заменил строку:

if current_time - self.attack_time >= self.attack_cooldown

На строку:

if current_time - self.attack_time >= self.attack_cooldown + weapon_data[self.weapon]['cooldown']

Напишем новый метод в enemy.py на проверку смерти монстра:

def check_death(self):
    if self.health <= 0:
        self.kill()

Тут я даже не знаю что ещё подсветить в коде :) Не забудьте добавить данный метод в update-метод. Теперь один удар приводит к смерти врага. Таким образом, PyGame считает, что пока оружие соприкасается с врагом (вызывается метод коллизий), удары наносятся один за другим. Как итог — Линк танк, который уничтожает всё на своём пути. Исправим это. Для начала, создадим новых демонов enemy.py:

self.vulnerable = True #флаг уязвимости
self.hit_time = None #время удара
self.invincibility_duration = 300 #продолжительность неуязвимости

Тут достаточно прозрачные демоны. Важный момент — флаг уязвимости. Если он поднят — враг может получать урон. Теперь встроим их в get_damage:

def get_damage(self, player, attack_type):
    if self.vulnerable: 
        if attack_type == 'weapon':
            self.health -= player.get_full_weapon_damage()
        self.hit_time = pygame.time

Далее необходимо дополнить код cooldown-метода:

if not self.vulnerable:
    if current_time - self.hit_time >= self.invincibility_duration:
        self.vulnerable = True

Тут, как я говорил ранее, сразу добавляем вариацию флага. У нас было место, где флаг опускается и теперь в cooldown-методе он поднимается по истечению указанного времени неуязвимости. Теперь все враги убиваются весьма приятно. Теперь, нужно добавить отбивание врага на дистанцию, которая указана у каждого врага. Создадим ещё метод:

def hit_reaction(self):
    if not self.vulnerable:
        self.direction *= -self.resistance

Осталось вычислить положение в методе get_damage:

self.direction = self.get_player_distance_direction(player)[1]

Теперь добавим мерцание во время удара, чтобы понять что удар был сделан. В PyGame все сигнатуры (синусоидные функции) лежат в диапазоне от -255 до 255, а позиции удобно брать из синусоид, так как интерпретация в Python будет работать на основе степеней (как и любой калькулятор), а затем будет процесс получения точки на синусоиде. Этот процесс я описывал дольше, чем будет писаться метод отображения пульсации:

def wave_value(self):
    value = sin(pygame.time.get_ticks())
    if value >= 0:
        return 255
    else:
        return 0

Данный метод написан в entity.py. Не забудьте импортировать sin-метод из библиотеки math. Теперь вызовем данный метод при ударе по врагу в методе animate:

if not self.vulnerable:
    alpha = self.wave_value()
    self.image.set_alpha(alpha)
else:
    self.image.set_alpha(255)

Сейчас мы бьём врагов абсолютно верно и можем отследить когда монстры атакуют нас (в терминал приходит сообщение "attack"). Осталось сделать метод, который делает урон Линку. Пропишем его в level.py:

def damage_player(self, amount, attack_type):
    if self.player.vulnerable:
        self.player.health -= amount
        self.player.vulnerable = False
        self.player.hurt_time = pygame.time.get_ticks()

Дополним в create_map в строку вызова монстров нанесение урона Линку:

Enemy(monster_name, (x, y), [self.visible_sprites, self.attackable_sprites], self.obstacle_sprites, self.damage_player)

Добавим в Enemy-класс параметр (damage_player) и пропишем нового демона — self.damage_player = damage_player. Теперь вместо простого вывода сообщения "attack" выполним вызов нашего метода:

self.damage_player(amount, attack_type)

Добавим параметры таймеров в демонов нашего player-класса:

self.vulnerable = True
self.hurt_time = None
self.invulnerablity_duration = 500

Они аналогичны демонам в enemy.py. Далее традиционно пропишем смену флага в наш кулдаун-метод:

if not self.vulnerable:
    if current_time - self.hurt_time >= self.invulnerablity_duration:
        self.vulnerable = True

Теперь пропишем наше мерцание. Тут всё также, как и ранее. Прописывать будем в animate-методе:

if not self.vulnerable:
    alpha = self.wave_value()
    self.image.set_alpha(alpha)
else:
    self.image.set_alpha(255)

Теперь нам нужно восстанавливать энергию (повторим механику из Dark Souls). Для этого создадим метод energy_recover:

def energy_recover(self):
    if self.energy < self.stats['energy']:
        self.energy += 0.1
    else:
        self.energy = self.stats['energy']

Далее пропишем, что при ударе у нас теряется 10 очков стамины, а если её не хватает — атака не проходит:

if keys[pygame.K_SPACE]:
    if self.energy >= 10:
        self.energy -= 10
        self.attacking = True
        self.attack_time = pygame.time.get_ticks()
        self.create_attak()
        self.weapon_attack_sound.play()

Последнее, что я хотел бы сделать — добавить экспу за убийство врага в level.py:

def add_xp(self, amount):
    self.player.exp += amount

Добавим этот же метод для create_map в Enemy. Далее повторим всё, что делали ранее с damage_player.

. Приступим к последнему шагу — музыке.

Файлы данного шага тут