Монстры
Для начала, я прописал референсы для монстры. Они хранятся в папке graphic/monsters
. Также я добавил звуки ударов и закинул их в папку audio/attack
Далее, в setting.py прописаны их входные данные:
monster_data = {
'axalot': {'health': 200, 'exp': 400, 'damage': 40, 'attack_type': 'slash', 'attack_sound': '../audio/attack/slash.wav', 'speed': 3, 'resistance': 3, 'attack_radius': 80, 'notice_radius': 300},
'lizard': {'health': 50, 'exp': 100, 'damage': 15,'attack_type': 'claw', 'attack_sound': '../audio/attack/claw.wav', 'speed': 2, 'resistance': 3, 'attack_radius': 100, 'notice_radius': 400},
'snake': {'health': 100,'exp':100,'damage': 10,'attack_type': 'claw', 'attack_sound': '../audio/attack/claw.wav', 'speed': 4, 'resistance': 3, 'attack_radius': 80, 'notice_radius': 350},
'spirit': {'health': 150,'exp':200,'damage': 15,'attack_type': 'claw', 'attack_sound': '../audio/attack/claw.wav', 'speed': 3, 'resistance': 3, 'attack_radius': 100, 'notice_radius': 400}}
Пробежимся по параметрам:
health
— здоровье монстраexp
— сколько очков за смерть монстраdamage
— какой урон он нанесёт героюattack_type
— тип атакиattack_sound
— звук удараspeed
— скорость зверькаresistance
— на сколько монстр отлетит после нашего удараattack_radius
— радиус, с которого монстр опасен и может ударитьnotice_radius
— радиус зрения монстра
Далее, создадим новый файл сущностей (entity.py
) и добавим туда супер демона с наследованием групп:
import pygame
class Entity(pygame.sprite.Sprite):
def __init__(self, groups):
super().__init__(groups)
Скопируем методы move
и collision
из player.py
. Теперь удаляем из player.py
эти методы и будем ссылаться в классе не на pygame.sprite.Sprite
, а на Entity
(не забывайте импортировать файл). Эти небольшие танцы с бубном нужны для того, чтобы не переписывать каждый раз правила движений для нашего героя и монстров. Все они — одинаковые сущности. Также я перенёс демонов скорости анимации, фрейма и определения вектора скорости. Когда всё сделаете, перепроверьте, что всё работает.
Затем, наконец, создадим файл enemy.py
:
import pygame
from settings import *
from entity import Entity
class Enemy(Entity):
def __init__(self, monster_name, pos, groups):
super().__init__(groups)
self.sprite_type = 'enemy' #новый тип спрайтов — враги
self.image = pygame.Surface((64, 64)) #наш традиционный размер тайла
self.rect = self.image.get_rect(topleft = pos) #традиционная отрисовка
Перейдём к настройке уровня. Для начала, сделаем так, чтобы наш герой спаунился там, где надо (зелёный квадрат на карте). Для этого, нам нужно импортировать новый csv-файл из уже созданной карты 'entities': import_csv_layout('../map/map_Spawn.csv')
. И пропишем в методе creat_map
новый объект:
if style == 'entities':
if col == '8':
self.player = Player((x, y), [self.visible_sprites], self.obstacle_sprites, self.create_attack, self.destroy_weapon)
Результат вас удивит:

Герои Хайрула размножились. Проблема в том, что я прорисовывал карту разными наборами элементов. Не повторяйте моих ошибок и давайте всё исправлять. Дело в том, что номер тайла автоматически рассчитывается программой Tiles, а я указал, разные тайлы и номера задублировались, так как у меня был отдельный файл Bebs.png для спауна врагов и Link_and_block.png для Линка и блоков-стен. Теперь я объединил два набора тайлов и присвоил линку номер 16. Результат:

Линк на своём законном месте. Давайте заспаумим врагов, добавив лишь один else:
else:
Enemy('monster', (x, y), [self.visible_sprites])

Монстры отобразились, но теперь нужно отобразить их верно. Работаем с файлом enemy.py
:
import pygame
from settings import *
from entity import Entity
from support import *
class Enemy(Entity):
def __init__(self, monster_name, pos, groups):
super().__init__(groups)
self.sprite_type = 'enemy' #новый тип спрайтов — враги
self.import_graphics(monster_name) #обращаемся к новой функции перебора картинок
self.status = 'idle' #установим базовый статус
self.image = self.animations[self.status][self.frame_index] #перебираем номер фрейма в папке из функции ниже
self.rect = self.image.get_rect(topleft = pos) #традиционная отрисовка
def import_graphics(self, monster_name):
self.animations = {'idle': [], 'move': [], 'attack': []} #перебираем возможные варианты анимаций в папках
main_path = f'../graphic/monsters/{monster_name}/' #обращаемся к монстру по имени :)
for animation in self.animations.keys(): #перебираем все картинки
self.animations[animation] = import_folder(main_path + animation) #перебор благодаря support-файлу
Тут мы делаем всё ровно также, как и ранее, но если в файле level.py
мы внесём имя любого монстра, то получим картинку монстра на карте:

Осталось только перебрать монстров по их номерам тайлов на карте:
else:
if col == '0':
monster_name = 'axolot'
elif col == '4':
monster_name = 'lizard'
elif col == '8':
monster_name = 'snake'
else:
monster_name = 'spirit'
Enemy(monster_name, (x, y), [self.visible_sprites])

Теперь, добавим аргумент obstacle_sprites
в нашу конструкцию, чтобы монстры могли взаимодействовать с Линком. Далее, создадим update-метод для файла enemy.py
:
def update(self):
self.move(self.speed)
Далее, нужно прописать несколько статусов для наших плохишей. Добавим их в демонов данного файла:
self.monster_name = monster_name #имя монстра
monster_info = monster_data[self.monster_name] #перехват данных монстра по имени
self.health = monster_info['health']
self.exp = monster_info['exp']
self.speed = monster_info['speed']
self.attack_damage = monster_info['damage']
self.resistance = monster_info['resistance']
self.attack_radius = monster_info['attack_radius']
self.notice_radius = monster_info['notice_radius']
self.attack_type = monster_info['attack_type']
Тут мы ссылаемся на файл settings.py и перехватываем все параметры монстров оттуда. Теперь наша задача прописать метод определения дистанции до объекта. Я думал, что эта задача непроста, так как координаты объекта рассчитываются с верхнего левого угла, у них есть свои векторы (скорости), да ещё и нужна нормализация для предотвращения "диагонального чита" (как это было у Линка). Собственно весь метод:
def get_player_distance_direction(self, player):
enemy_vec = pygame.math.Vector2(self.rect.center) #координата врага
player_vec = pygame.math.Vector2(player.rect.center) #координата Линка
distance = (player_vec - enemy_vec).magnitude() #Евклидова величина
if distance > 0:
direction = (player_vec - enemy_vec).normalize() #вычисление вектора сближения
else:
direction = pygame.math.Vector2() #точка, мы друг в друге
return(distance, direction)
Я искренне не ожидал, что это так просто. По сути, все сложные методы вычисления Евклидовой величины по поиску дистанции мы переложили на функцию magnitude()
, а с читерской функцией normalize()
вы уже знакомы. И зачем я учил математику? Далее, пропишем метод определения статуса монстра по отношению к Линку:
def get_status(self, player):
distance = self.get_player_distance_direction(player)[0]
if distance <= self.attack_radius:
self.status = 'attack'
elif distance <= self.notice_radius:
self.status = 'move'
else:
self.status = 'idle'
Тут мы отсекаем изнутри во вне "окружности" зрения (близко — атака, средняя дистанция — преследование, далеко — idle), но чтобы оно заработало, нам нужно обновлять данные в файле level.py
:
def enemy_update(self, player):
enemy_sprites = [sprite for sprite in self.sprites() if hasattr(sprite,'sprite_type') and sprite.sprite_type == 'enemy']
for enemy in enemy_sprites:
enemy.enemy_update(player)
Тут самая интересная строка — строка прорисовывания спрайтов для врага. Тут можно как в анекдоте: "Потерялся атрибут? Ничего страшного! Всегда есть метод hasattr
". Далее, в run-методе пропишем отрисовку спрайтов врага:
self.visible_sprites.enemy_update(self.player)
Теперь мы сможем замкнуть врага на игрока, а игрока на уровень. Для этого пропишем новый метод в enemy.py
:
def enemy_update(self, player):
self.get_status(player)
Теперь, у нас есть способ получения методов, но мы с ними не взаимодействуем. Исправим это новым методом:
def actions(self, player):
if self.status == 'attack':
print('attack') #тут мы только пишем в терминале атаку
elif self.status == 'move':
self.direction = self.get_player_distance_direction(player)[1] #нанюхивать Линка
else:
self.direction = pygame.math.Vector2() #остановиться по координатам
В этом методе всё так же, как было ранее, но не забудьте закинуть его вызов в enemy_update
-функцию командой self.actions(player)
. Тетерь пропишем анимацию. Она полностью аналогична анимации Линка:
def animate(self):
animation = self.animations[self.status]
self.frame_index += self.animation_speed
if self.frame_index >= len(animation):
self.frame_index = 0
self.image = animation[int(self.frame_index)]
self.rect = self.image.get_rect(center = self.hitbox.center)
Также, добавьте animate в update-функцию. Теперь можно получить ачивку: "Собрал всех чушпанов с района":

Но есть проблема. Они атакуют несчастного Линка толпой без остановки. Это нужно исправить, а значит время нового метода и нового кулдауна. Сначала я добавил нового демона self.can_attack = True
. Это флаг, который будет указывать на то, что монстр может пнуть Линка. Соответственно, нужно подправить условие атаки и помимо дистанции, указать данный флаг. Если вы добавили флаг на True
, то обязательно сразу нужно прописать ситуацию, когда он будет опускаться (положение False
). Запишем этот пункт в методе анимации:
if self.frame_index >= len(animation):
if self.status == 'attack':
self.can_attack = False
Немного объясню происходящее. Анимация атаки не должна прерывать анимацию перехода и если мы завершили весь цикл из переходов от картинки к картинке, то только тогда можно менять флаг на опущенное состояние. Простыми словами, все враги могут ударить нас только 1 раз, так как флаг не поднимается обратно. Поднимать тот самый флаг мы будем по кулдауну через паузу. То есть, я добавлю два демона, которые будут обозначать время атаки и кулдаун после атаки:
self.attack_time = None
self.attack_cooldown = 400
Теперь пропишем сам метод кулдауна по вычислению разницы текущего времени и времени задержки:
def cooldown(self):
if not self.can_attack:
current_time = pygame.time.get_ticks()
if current_time - self.attack_time >= self.attack_cooldown:
self.can_attack = True
Тут самое главное, не забыть про место старта времени, то есть про установку времени на момент атаки:
self.attack_time = pygame.time.get_ticks()
После этого, не забудьте закинуть метод в update-метод. Результат:

Мы не закончили работу с монстрами, но давайте оставлю бэкап проекта сейчас и в следующей части создадим методы взаимодействия нас с монстрами и монстров с нами.
Last updated