Пример простейшего визуального редактора

Если вам надоело писать код, чтобы построить уровень для мини-игры, то вам захочется сделать себе визуальную среду, перетаскивать объекты по экрану и сохранять результаты в файл. Здесь вы увидите пример и сможете на его основе создать собственные редакторы уровней, персонажей, чего угодно. Чтобы не заморачиваться с форматами для сохранения данных, я создал модуль, который записывает любые классы, их списки, словари или просто переменные в виде кода на питоне в файл с расширением RPY. Нужно указать имя файла (можно без расширения), имя функции, которая будет отвечать за "загрузку", ее "уровень" (порядок выполнения блока init) и через запятую имена всех переменных, списков и т.д., которые нужно сохранить, то есть превратить в ренпаевский код.



# в папку game проекта нужно закинуть файл rpy_save.rpy
# он отвечает за сохранения любых данных (в виде кода на питоне)
# уровень этого блока должен быть ниже уровня сохраняемого

init python:
    # создадим для примера какой-нибудь класс для объектов на экране
    # у него может быть любая структура, параметры, функции
    # классов может быть несколько
    class TUnit:
        # имена параметров на входе должны совпадать с именами параметров класса!
        def __init__(self, id, sprite=None, x=0, y=0, flip=False):
            # уникальный идентификатор объекта
            self.id = id
            # его изображение (объекты могут выглядеть одинаково)
            if not sprite:
                sprite = id
            self.sprite = sprite
            # координаты объекта
            self.x = x
            self.y = y
            # отзеркаливание
            self.flip = flip

        # функция возвращает зум объекта по горизонтали
        def xzoom(self):
            if self.flip:
                return -1.0
            return 1.0

    # перечень всех объектов на экране
    units = []

    # если флаг установлен, то режим редактора, а не игры
    edit_mode = False

    # выбранный юнит
    current_unit = None

    # список доступных спрайтов
    sprites_list = ["guy", "kolonna", "lenin", "bg"]

    # последний выбранный спрайт
    last_sprite = sprites_list[0]

    # функция возвращает объект с нужным id
    def get_unit(id):
        # перебираем все юниты в поисках нужного
        for i in units:
            if i.id == id:
                return i
        return None

    # функция для перетаскивания объекта мышкой
    def draggetxy(drags, drop):
        global units, current_unit

        # сбрасываем старые координаты объекта
        drags[0].oldposition = None
        # получаем доступ к нужному
        i = get_unit(drags[0].drag_name)
        # если он существует, то
        if i:
            # меняем координаты объекта на текущие
            i.x, i.y = drags[0].x, drags[0].y
            # выбираем текущий объект
            current_unit = i
            # перерисовываем экран
            renpy.restart_interaction()
    
    # сохраняем уровень и закрываем программу,
    # чтобы при следующем запуске ренпай скомпилировал
    # сохраненный код на питоне в файл типа *.rpyc
    def save_quit():
        # сохраняем в файл TEST_DATA_SAVE.rpy
        # сгенерированный блок test_data_init
        # с уровнем 1 (init 1 python:)
        # через запятую указать имена всех переменных,
        # объектов, списков, кортежей и т.д.,
        # текущее состояние которых нужно запомнить
        # в данном случае только список объектов на экране
        # и список доступных спрайтов
        rpy_save("TEST_DATA_SAVE", "test_data_init", 1, "units", "sprites_list")
        # закрываем программу
        Quit(confirm=False)()
    # создаем action для кнопки редактора
    SaveQuit = renpy.curry(save_quit)

    # создать новый уникальный id
    def new_id():
        i = 0
        name = None
        # перебираем номера id, пока не попадется еще несуществующий
        while not name:
            if get_unit("id" + str(i)):
                i += 1
            else:
                name = "id" + str(i)
        return name

    # добавление нового объекта на экран
    def add_unit():
        global units, current_unit
        current_unit = TUnit("id", last_sprite)
        current_unit.id = new_id()
        # добавляем созданный объект в список
        units.append(current_unit)
        renpy.restart_interaction()
    AddUnit = renpy.curry(add_unit)

    # удаление объекта с экрана
    def del_unit():
        global units, current_unit
        if current_unit:
            units.remove(current_unit)
            current_unit = None
            renpy.restart_interaction()
    DelUnit = renpy.curry(del_unit)

    # смена слоя объекта
    def up_unit():
        global units, current_unit
        if current_unit:
            oldindex = units.index(current_unit)
            newindex = oldindex + 1
            if newindex >= len(units):
                newindex = 0
            units.insert(newindex, units.pop(oldindex))
            renpy.restart_interaction()
    UpUnit = renpy.curry(up_unit)

    # отзеркаливание объекта
    def flip_unit():
        global units, current_unit
        if current_unit:
            current_unit.flip = not current_unit.flip
            renpy.restart_interaction()
    FlipUnit = renpy.curry(flip_unit)

    # смена изображения объекта
    def spr_unit():
        global units, current_unit, last_sprite
        if current_unit:
            index = sprites_list.index(current_unit.sprite) + 1
            if index >= len(sprites_list):
                index = 0
            current_unit.sprite = sprites_list[index]
            last_sprite = sprites_list[index]
            renpy.restart_interaction()
    SprUnit = renpy.curry(spr_unit)

# экран редактора (он же экран игры)
screen game:
    modal edit_mode

    # в режиме игры просто выводим объекты на экран
    if not edit_mode:
        for i in units:
            add i.sprite pos (i.x, i.y)
    # в режиме редактора позволяем перемещать и менять объект
    else:
        for i in units:
            drag:
                # имя объекта для функции смены координат
                drag_name i.id
                # координаты объекта
                pos (int(i.x), int(i.y))
                # сам объект
                button:
                    # кнопка без фона и отступов
                    background None
                    xpadding 0 ypadding 0
                    xmargin 0 ymargin 0
                    # изображение объекта
                    add i.sprite xzoom i.xzoom()
                    # чтобы можно было перетаскивать, отключаем действия
                    action []
                    # чтобы перетаскивать только за рисунок
                    focus_mask True
                    # что делать при перетаскивании (функция выше)
                dragged draggetxy
                # выбор предмета кликом, а не перетаскиванием
                clicked SetVariable("current_unit", i)

    if edit_mode:
        # меню редактора
        frame:
            background "#0128"
            align (1.0, 1.0)
            vbox:
                textbutton _("Добавить объект") action AddUnit()
                textbutton _("Сменить спрайт") action [SensitiveIf(current_unit), SprUnit()]
                textbutton _("Поменять слой") action [SensitiveIf(current_unit), UpUnit()]
                textbutton _("Отзеркалить объект") action [SensitiveIf(current_unit), FlipUnit()]
                textbutton _("Удалить объект") action [SensitiveIf(current_unit), DelUnit()]
                textbutton _("Выйти") action Return()
                textbutton _("Сохранить и выйти") action SaveQuit()

        # параметры спрайта
        if current_unit:
            frame:
                background "#0128"
                align (0.0, 1.0)
                vbox:
                    text "id: " + str(current_unit.id)
                    text "sprite: " + str(current_unit.sprite)
                    text "x, y: " + str(current_unit.x) + ", " + str(current_unit.y)
                    text "flip: " + str(current_unit.flip)
                    text "order: " + str(units.index(current_unit))

# выбираем режим
label start:
    $ quick_menu = False
    menu:
        "Режим редактора":
            call edit_mode
        "Режим игры":
            call game_mode
    $ quick_menu = True
    return

# показываем экран в игровом режиме
label game_mode:
    # если сохранения есть, то запускаем загрузку из них
    if "test_data_init" in globals().keys():
        $ test_data_init()

    $ edit_mode = False
    show screen game

    "Показываем результат..."
    return

# показываем экран в режиме редактора
label edit_mode:
    # если сохранения есть, то запускаем загрузку из них
    if "test_data_init" in globals().keys():
        $ test_data_init()

    $ edit_mode = True
    $ current_unit = None
    $ last_sprite = sprites_list[0]
    call screen game
    return

Комментарии