Часть 2. ДОБАВЛЯЕМ ОБЪЕКТЫ.


В первом уроке мы взяли примитивный рогалик, в котором есть только отрисовка карты, один персонаж и возможность ходить стрелками. Описав работу игры на PyGame вкратце, мы поменяли персонажа, серый пол сменили на траву, отредактировали карту.
В этом уроке мы добавим свои объекты в игру, сделаем для них класс и алгоритмы, чтобы отрисовка объектов и персонажа выглядела правильно - нижние объекты перекрывают верхние.


Начнем урок с добавления в скрипты roguelike.py и models.py строки, которая исключит ошибки из-за кириллицы(русских букв в коде).
В самом начале файла models.py добавляем строку, выделенную черным фоном:
# -*- coding: utf-8 -*-
import pygame
И в roguelike.py так же:
#/usr/bin/env python
# -*- coding: utf-8 -*-
После этого комментарии в коде на русском языке, и вообще символы кириллицы перестанут вызывать любые ошибки и проблемы для любых версий Python.


Для удобства ещё уберем с карты игры все тайлы, кроме травы, чтобы не мешались и не портили вид - просто заменяем все 'W' и 'c' в массивах decorationData и tileData на пустые пробелы ' '. Получаем чистую карту, готовую к добавлению объектов:

 

Теперь добавим новые tileset-картинки с объектами. (как делать tileset'ы объектов на фотошопе, я опишу в отдельном уроке позже)

ДЕРЕВЬЯ:
Сохраняем обе картинки прямо из этого блога, помещаем их в каталог игры /assets/.

Создадим для своих объектов отдельных класс в файле models.py. В самый конец этого файла добавляем:
class gameObj(object):
    def __init__(self, pos, objImage, objName = 'unnamed', collisionRectSize=15):
        self.pos = pos #позиция объекта на карте x,y
        self.img = objImage #картинка объекта

        objImageRect = objImage.get_rect()
        self.img_rect = objImageRect #получение прямоугольной Rect(x=0,y=0,w,h) картинки        
        colorkey = objImage.get_at((0, 0)) #берет цвет самого верхнего левого пикселя
        self.img.set_colorkey(colorkey) #устанавливает этот цвет как цвет прозрачности
        self.img_mask = pygame.mask.from_surface(objImage) # по colorkey-цвету прозрачности создает маску(непрозрачных пикелей)
        self.img_rect.x = pos[0] #устанавливает в прямоугольнике x-координату объекта 
        self.img_rect.y = pos[1] #и y-координату
При создании объекта указываются:
pos позиция объекта x,y
objImage  картинка объекта   
objName текстовое имя объекта (по-умолчанию безымянное "unnamed") и 
collisionRectSize  высота прямоугольной области коллизий(об этом напишу позже). 
Далее создается прямоугольник img_rect(класс pygame.Rect), в который вписана картинка объекта.
Этот Rect[x,y,w,h] является массивом из 4-х чисел, координат x,y, которые сначала равны нулю, и w,h - ширины и высоты картинки объекта. 
colorkey получает цвет левого верхнего пикселяфункцией get_at(...). Полученный цвет используется, как цвет прозрачности в функциях set_colorkey(...) и mask.from_surface(...)
Создавая self.img_mask по цвету прозрачности(colorkey), мы получаем маску картинки, в которой все прозрачные пиксели установлены в 0, а непрозрачные - 1. Маска даст возможность столкновения с персонажем и обход объектов.
В конце координаты x,y в img_rect устанавливаются позиции объекта на карте pos, которая передана в аргументах конструктора, чтобы прямоугольник img_rect правильно располагался на экране, вписывая в себя картинку объекта.

Добавляем новые объекты на карту.

Сначала в самое начало скрипта roguelike.py добавляем нужные нам функции.
Добавлять то, что выделенно чёрным фоном.
from random import randint, choice, randrange
from utils import loadSpritesheet, loadCharacterSpritesheet
from models import Level, Tile, Entity, gameObj
randchage(...) → генерирует случайное число из указанного диапазона
gameObj → наш класс, который мы сделали для своих объектов в файле models.py

Теперь добавляем создание наших новых объектов в функцию:
def loadTextures():
    global playerTextures, wallTextures, ceilingTextures, floorTextures

    global gameObjectList #глобальная переменная со всеми объектами в игре 
    gameObjectList = []#делаем переменную пустым списком
    
    itemSprites1 = loadSpritesheet('assets/Items1.png', 16, 32, xScale=gameScale, yScale=gameScale) #режем вещи Items1.png на отдельные картинки-объекты
    for i in range(len(itemSprites1)): #для каждой нарезанной картинки-объекта, помещаем его в случайное место на карте
        gameObjectList.append(gameObj([randrange(0, xRes-itemSprites1[i].get_rect().w),randrange(0, yRes - itemSprites1[i].get_rect().h)], itemSprites1[i], 'item' + str(i))) 
    treeSprites = loadSpritesheet('assets/trees1.png', 114, 100, xScale=gameScale, yScale=gameScale) #режем деревья Trees1.png на отдельные картинки-объекты
    for i in range(len(treeSprites)): #деревья также разбрасываем в случайные места на карте
        gameObjectList.append(gameObj([randrange(0, xRes-treeSprites[i].get_rect().w),randrange(0, yRes - treeSprites[i].get_rect().h)], treeSprites[i], 'a tree ' + str(i)))

В этом коде объявляется глобальная переменная global gameObjectList[], доступная во всех остальных функция, в которую мы поместим множество всех созданных объектов. Далее разрезание tileset-картинок на отдельные картинки-объекты и расположение их в случайных местах на карте при помощи функции randrange(...)

Рисуем созданные объекты на игровом экране.

В конце функции def main(): добавляем отрисовку всех наших объектов:
                if ((x, y - 1) == (pX, pY)):
                    screen.blit(player.image, (player.x + player.offsetX, player.y + player.offsetY))

        playerPos = (player.x + player.offsetX, player.y + player.offsetY) #позиция игрока на карте x,y
        for indx,_ in enumerate(gameObjectList): #в массиве всех созданным нами объектов
            curObj = gameObjectList[indx]
            screen.blit(curObj.img, curObj.pos) #рисуем картинку каждого объекта на поверхность игрового экрана

Проверяем работу, и видим, что есть проблема - игрок всегда находится под любым объектом, и сами объекты располагаются поверх друг друга неестественным образом:
Исправим эту проблему, расчитывая нижнюю точку каждого объекта в их массиве gameObjectList[] и сортировку массива по этому признаку.

Рисуем объекты в правильном порядке.

Сначала допишем в класс объектов расчеты и нахождение самого нижнего непрозрачного пикселя объекта, чтобы знать какой объект ниже, а какой выше и отрисовывать объекты в нужном порядке.

В файле models.py, в конец класса gameObj добавляем выделенное черным фоном:
...
        self.img_rect.x = pos[0] #устанавливает в прямоугольнике x-координату объекта 
        self.img_rect.y = pos[1] #и y-координату
        
        
        leftmostX = objImageRect.w 
        rightmostX = 0 
        topY = objImageRect.h 
        lowY = 0 
        for imgY in range(objImageRect.h): #расчет прямоугольника, в который вписана картинка объект без прозрачных областей 
            for imgX in range(objImageRect.w):
                if (self.img_mask.get_at((imgX, imgY))) == 1: 
                    if imgY > lowY: 
                        lowY = imgY 

        self.lowestY = lowY # Y-координата самого нижнего непрозрачного пикселя в картинке объекта
Самая нижняя Y-координата для картинки объекта заносится в LowestY.

Теперь файл roguelike.py, добавляем одну строку в функцию
def loadTextures():
    for i in range(len(treeSprites)): #деревья также разбрасываем в случайные места на карте
        gameObjectList.append(gameObj([randrange(0, xRes-treeSprites[i].get_rect().w),randrange(0, yRes - treeSprites[i].get_rect().h)], treeSprites[i], 'a tree ' + str(i)))   
    
    gameObjectList.sort(key=lambda x: x.pos[1]+x.lowestY) #сортируем массив объектов по ключу самого 

    playerTextures = loadCharacterSpritesheet('assets/' + playerType + '.png', 32, 32, 10, 10, xScale=gameScale, yScale=gameScale, colorkey=-1)
Функция sort(...) расположит элементы в массиве gameObjectList[] в порядке, соответственном координате lowestY самого нижнего непрозрачного пикселя объекта. Таким образом самые нижние объекты будут рисоваться последними и перекрывать верхние объекты.

Еще нужно добавить отрисовку игрока в соответствии с его Y-координатой. Для этого в конец функции def main(): добавляем две строки:
...
            curObj = gameObjectList[indx]
            screen.blit(curObj.img, curObj.pos) #рисуем картинку каждого объекта на поверхность игрового экрана
            if (playerPos[1] + player.rect[3]) >= (curObj.pos[1] + curObj.lowestY): #если самая нижняя точка игрока объекта выше игрока
                screen.blit(player.image, playerPos) #рисуем игрока поверх объекта 

        pygame.display.flip()
...
После этого, игрок и объекты будут правильно отображаться на экране игры, перекрывая друг друга, в соответствии с вертикальным положением на экране:
Рабочие файлы для этого урока: Github-каталог
roguelike.py
models.py
/assets/trees1.png
/assets/Items1.png 

Архив .zip со всеми рабочими файлами для этого урока: LESSON-2.OBJECTS.zip


Если что не ясно, либо есть ошибки в уроке, пишите и спрашивайте в комменатриях или в DISCORD, постараюсь всем ответить и все исправить.

Комментарии

Отправить комментарий