В 2024-м я начал разработку приключенческой игры "Азраил, вестник смерти". Будучи инди, я писал сценарий, геймдизайн и код сам. Теперь я хочу поделиться своими наработками, чтобы вам не пришлось писать это заново. В этой заметке выкладываю код системы репутации и гайд, как ей пользоваться.

Сразу обращу внимание, я не являюсь профессиональным программистом, и этот материал написан для начинающих разработчиков на Ren'Py, которые ещё не освоились в Python достаточно, чтобы написать свою механику репутации.
Чем поможет?
Большинство визуальных новелл пишется на движке Ren'Py. Код для него использует либо собственный синтаксис движка, либо реализуется через вставку на python. Это главное, за что я полюбил ренпай: вроде бы и ограниченный простой движок, а вроде и мощный инструмент благодаря наличию питона.
Если вы новичок в работе с игровыми движками, важно понимать, что Ren'py заточен в первую очередь на создание визуальных новелл, и всё остальное на нём писать проблематично. Я выбрал его, т.к. моя игра изначально начиналась как классическая новелла. Со временем в ней появилась экономика, боёвка, ситибилдер, пазлы, герои, коллекции, батлер, инвентарь и репутация. На последней системе я хочу остановиться сегодня.
Возможности
Множество визуальных новелл имеют репутацию с персонажами. Это простая механика, хорошо вписывающаяся в игры, упор которых сделан на сюжет и выбор. Моя система репутации очень проста в использовании. Она умеет:
Задать список субъектов (персонажей, кланов, фракций...), с которыми будем считать репутацию;
Задать дефолтное значение репутации с каждым субъектом;
Задать границы (минимум, максимум) значений репутации;
Увеличить и уменьшить репутацию на заданное значение;
Добавить в обработчик триггер события на изменение репутации (к примеру, если репутация с шефом ниже -5, игрока увольняют).
Установка
Ниже приведён python-код класса репутации.
reputation.rpy
# Список цветов в зависимости от значения репутации
define reputation_colors = {
-5: "#ff566d",
-4: "#ff96a4",
-3: "#ff96a4",
-2: "#ffc5ce",
-1: "#ffc5ce",
0: "#ffffff",
1: "#c1ffd0",
2: "#c1ffd0",
3: "#83ffa2",
4: "#83ffa2",
5: "#2fff63"
}
# Цвет по умолчанию на случай, если не задан в списке цветов или возникла ошибка
define reputation_default_color = "#ffffff"
# Максимальное и минимальное значение репутации
define reputation_minimum = -100
define reputation_maximum = 100
# Список тех, с кем можно иметь репутацию
define reputation_subjects = {
'character1': 0,
'character2': 0
}
# Имена тех, с кем можно иметь репутацию
define reputation_subject_names = {
'character1': _("первым персонажем"),
'character2': _("вторым персонажем")
}
# Локали
define rep_inc_str = _("Репутация с [subject_name] улучшилась на [value].")
define rep_dec_str = _("Репутация с [subject_name] ухудшилась на [value].")
init python:
# Класс репутации
class Reputation:
"""
Класс репутации с другими персонажами.
"""
__reputation = {}
__colors = {}
__events = []
notify_on_change = True
trigger_only_first_event = False
# Инициализирует объект класса репутации
def __init__(self, default_reputation=None):
global reputation_colors
global reputation_default_color
global reputation_subjects
if default_reputation != None:
self.__reputation = default_reputation
else:
self.__reputation = reputation_subjects
for char, value in self.__reputation.items():
if value in reputation_colors:
self.__colors[char] = reputation_colors[value]
else:
self.__colors[char] = reputation_default_color
self.__events = []
# Вернуть репутацию по обозначению персонажа
def get(self, subject):
if subject in self.__reputation:
return self.__reputation[subject]
return None
# Изменить репутацию: кому, на сколько
def change(self, subject, delta, notify=None, mark_met=True):
global reputation_colors
global reputation_minimum
global reputation_maximum
if subject in self.__reputation:
self.__reputation[subject]+= delta
if reputation_maximum != None:
self.__reputation[subject] = min(self.__reputation[subject], reputation_maximum)
if reputation_minimum != None:
self.__reputation[subject] = max(self.__reputation[subject], reputation_minimum)
self.__colors[subject] = reputation_colors[self.__reputation[subject]]
if notify or (self.notify_on_change and notify != False):
global rep_inc_str
global rep_dec_str
global reputation_subject_names
if delta >= 0:
notify_str = __(rep_inc_str).replace("[subject_name]", __(reputation_subject_names[subject])).replace("[value]", str(delta))
else:
notify_str = __(rep_dec_str).replace("[subject_name]", __(reputation_subject_names[subject])).replace("[value]", str(-delta))
renpy.notify(notify_str)
if mark_met and ('char_meet' in globals()):
globals()['char_meet'][subject] = True
self._trigger_events(subject, delta=delta)
return True
return False
# Увеличить репутацию на 1
def inc(self, subject):
return self.change(subject, 1)
# Уменьшить репутацию на 1
def dec(self, subject):
return self.change(subject, -1)
# Цвет текущей репутации
@property
def colors(self):
return self.__colors
# Текущие значения репутаций
@property
def values(self):
return self.__reputation
# Добавить событие в обработчик
def register_event(self, subject: str, value: int, _label="", _screen="", _function=None, compare_method='=', repeat=False, **kwargs):
"""
Регистрирует событие изменения репутации.
:param str subject: Персонаж, репутацию которого отслеживаем
:param int value: Значение репутации, при котором триггерить событие
:param str _label: Какую метку вызывать при возникновении события
:param str compare_method: "=" для точного равенства репутации с персонажем subject значению value
:param bool repeat: Триггерить ли событие при каждом изменении репутации или только единожды
:return: True, если событие было добавлено в обработчик
:rtype: bool
"""
new_event = {}
new_event['subject'] = subject
new_event['value'] = value
new_event['label'] = _label
new_event['screen'] = _screen
new_event['function'] = _function
new_event['compare_method'] = compare_method
new_event['repeat'] = repeat
new_event['count_triggered'] = 0
new_event['kwargs'] = {}
for k, v in kwargs.items():
new_event['kwargs'][k] = v
self.__events.append(new_event)
return True
# Проверить триггеры всех возможных событий при изменении репутации
def _trigger_events(self, subject, delta=None):
for event in self.__events:
if (event['subject'] == subject or event['subject'] == "" or event['subject'] == None) and (event['repeat'] or event['count_triggered'] == 0):
if ((event['compare_method'] == "=" or event['compare_method'] == "==") and (self.__reputation[subject] == event['value'])) \
or ((event['compare_method'] == ">") and (self.__reputation[subject] > event['value'])) \
or ((event['compare_method'] == "<") and (self.__reputation[subject] < event['value'])) \
or ((event['compare_method'] == ">=") and (self.__reputation[subject] >= event['value'])) \
or ((event['compare_method'] == "<=") and (self.__reputation[subject] <= event['value'])) \
or ((event['compare_method'] == "!=") and (self.__reputation[subject] != event['value'])):
event['count_triggered'] += 1
globals()['reputation_subject'], globals()['reputation_value'], globals()['reputation_delta'] = subject, self.__reputation[subject], delta
if callable(event['function']):
event['function'](**event['kwargs'])
if event['screen']:
renpy.show_screen(event['screen'], **event['kwargs'])
if event['label']:
renpy.call(event['label'], from_current=False, **event['kwargs'])
if self.trigger_only_first_event:
return event['count_triggered']
return False
Чтобы добавить его в свой Ren'py проект, просто скачайте reputation.rpy файл и положите его в любую папку внутри вашего проекта. Я держал его просто в корневой папке игры.
Инициализация
Добавьте в свой скрипт создание экземпляра класса репутации. Это желательно сделать до фактического кода игры. Я, к примеру, создал для таких случаев файл init_game.rpy, который исполняется первым после метки start. Однако, в случае системы репутации достаточно будет создать объект класса Reputation в блоке init python или просто объявить соответствующую переменную и скормить ему словарь в формате: субъект репутации - стартовое значение.
# Создание объекта репутации
# Задаём список тех, с кем будет меняться репутация и её начальные значения
define default_reputation = {
'anton': -1,
'stella': 1,
'roman': 0
}
# Создаём переменную reputation - экземпляр класса репутации
# вся работа с системой репутации далее будет идти через неё
default reputation = Reputation(default_reputation)
Вызываемые методы
values - выдаёт словарь с текущими значениями репутаций со всеми субъектами. Его можно использовать, к примеру, чтобы проверить, какая сейчас репутация с тем или иным персонажем, и в зависимости от этого выдать разную реакцию. Вместо полного списка values можно запросить только репутацию с один персонажем с помощью метода get(subject).
# Здесь get('anton') и values['anton'] эквивалентны
if reputation.get('anton') > 10:
anton "Да ты мой братишка! Иди обниму."
elif reputation.values['anton'] < 0:
anton "Сорян, но мы не так хорошо знакомы."
colors - словарь цветов, ассоциированных с текущим значением репутации. В своей игре я использую разные цвета для отображения значения репутации в интерфейсе. Это помогает игроку понять, насколько у него всё хорошо или плохо с каждым из персонажей. Чтобы использовать данный функционал, задайте цвет для каждого значения репутации в константе reputation_colors. Если же этот функционал вам не нужен, просто проигнорируйте его, не заполнение словаря цветов никак не скажется на функционале системы репутации.
text "[charname]: [reputation.value[key]]" color reputation.colors[key]

change(subject, value, notify=None, mark_met=True) - изменяет значение текущей репутации с персонажем subject на значение value. Может уведомлять игрока об изменении репутации и отмечать персонажей "встреченными".
$ reputation.change('roman', 2)
roman "Оо, да ты сегодня красавчик! Уважаю."
У объекта репутации есть свойство notify_on_change, по умолчанию заданное в True. В этом случае при изменении репутации будет вызываться стандартное уведомление ренпай и сообщать игроку о том, что репутация с определённым персонажем изменилась.

Если вы хотите, чтобы уведомление не отображалось, установите значение параметра notify_on_change в False.
reputation.notify_on_change = False
Если в методе change указано значение флажка notify, то он имеет более высокий приоритет для текущей команды, чем свойство notify_on_change. Это удобно, если вы обычно показываете изменение репутации автоматически, а в данном случае хотите сделать это вручную или не отображать вообще.
# В данном примере одно действие привело к изменению репутации сразу с двумя персонажами
# Удобно показать это в одном уведомлении вместо двух последовательных
reputation.change('anton', -2, False)
reputation.change('stella', 1, False)
renpy.notify(_("Репутация со Стеллой улучшилась на 1. Репутация с Антоном ухудшилась на 2."))
Фразы уменьшения и увеличения репутации записаны в константах rep_inc_str и rep_dec_str. Ренпай сам создаст для них локализацию, если вы дадите ему такую команду. Имена тех, с кем можно иметь репутацию, хранятся в словаре reputation_subject_names.
# Имена тех, с кем можно иметь репутацию
define reputation_subject_names = {
'anton': _("Антоном"),
'stella': _("Стеллой"),
'roman': _("Романом")
}
Метод change также может принимать флажок mark_met по умолчанию равный True. В Азраиле я отображал репутацию с персонажами только после того, как игрок впервые встретил данного персонажа. По умолчанию класс репутации считает, что, раз ты дал команду изменить репутацию с персонажем, значит ты его встретил, и помечает такого персонажа "встреченным". Чтобы работать с этой системой необходимо объявить переменную char_meet со списком всех субъектов репутации. Игнорирование этой возможности не вызовет ошибки.
default char_meet = {
'anton': False,
'stella': False,
'roman': False
}
inc(subject) - увеличивает репутацию с указанным subject на 1 и уведомляет об этом игрока, если уведомление включено.
# Команды ниже эквивалентны
reputation.change('anton', 1)
reputation.inc('anton')
dec(subject) - уменьшает репутацию с subject на 1 по аналогии с тем, как inc увеличивает.
register_event(subject, value, _label="", _screen="", _function=None, compare_method='=', repeat=False) - даёт системе репутации команду сделать заданное действие при изменении репутации. К примеру, в Азраиле, если репутация с первым иерархом Озимандией станет равна -5, игрок терпит поражение и отправляется в ссылку на 100 лет. Эта механика даёт возможность выдавать награду за получение репутации, одерживать победы или придумать совсем дикие события. Метод register_event принимает следующие аргументы:
subject - с кем отслеживать изменение репутации;
value - значение, с которым идёт сравнение при изменении репутации;
_label - метка, вызываемая методом call, на которую может перейти игра при выполнении условия события;
_screen - экран, который будет будет показан методом show_screen при выполнении условий события;
_function - функция (метод и в принципе любой объект типа callable), которая будет вызвана при срабатывании события;
compare_method - каким методом происходит сравнение текущей репутации с персонажей subject и заданного в value значения. Может принимать значения: =, >, <, >=, <=, !=. Значение по умолчанию - равенство репутации заданному числу;
repeat - если задано False, событие запустится только 1 раз, если True - будет запускаться каждый раз, когда меняется репутация с заданным персонажем и выполняются заданные условия. По умолчанию равно False.

Пример использования:
# Регистрируем событие поражения в случае потери репутации с Романом
reputation.register_event('roman', -10, _label="defeat")
label defeat:
"Антон эпично пробивает вам в челюсть. Глаза застелает пелена мрака."
"Поражение."
$ MainMenu(confirm=False)()
В качестве аргумента subject можно задать None или пустую строку (""). В этом случае событие будет вызываться при изменении репутации у любого персонажа. Пример:
# При каждом изменении репутации с любым персонажем вызывает окошко info_screen
reputation.register_event('', -10, compare_method=">", _screen="info_screen", repeat=True)
В метод register_event можно передать любое число именованных параметров. Когда случается событие, заданное этим методом, он будет передавать все эти параметры вызываемой метке, функции или отображаемому экрану. К примеру, следующий код приведёт к тому, что при изменении репутации со Стеллой до -5 или менее, игра покажет стандартный диалог с кнопками Да/Нет.
reputation.register_event('stella', -5, compare_method="<=", _screen="confirm", repeat=True, \
message="Вы уверены, что хотите устроить апокалипсис? Точно-точно уверены?", \
yes_action=Jump("defeat"), no_action=Jump("start"))

В данном примере мы передали стандартному экрану confirm, который уже есть в Ren'Py три параметра:
message - текст сообщения;
yes_action - screen action, который будет вызван, если игрок нажмёт кнопку "Да", в данном случае это переход к метке defeat;
no_action - screen action, который будет вызван, если игрок нажмёт кнопку "Нет", в данном случае это переход к метке start.
Подсказка. Работая со скринами (screen), не забывайте про Hide(), чтобы прятать окошки после совершения действия. Сделать это можно, задав событие в виде списка: yes_actioin = [Hide(), your_action()].
С помощью register_event вы можете, к примеру, сделать так, чтобы при любом изменении репутации всплывало окошко, в котором будет написано, с кем и на сколько изменилась репутация, вместо стандартного Notify. На этот случай класс репутации записывает в globals следующие параметры:
reputation_subject - с кем изменилась репутация;
reputation_value - текущее значение репутации с reputation_subject;
reputation_delta - на сколько изменилась репутация с reputation_subject.
К ним можно обратиться из вызываемого экрана или метки с помощью функции globals():
globals()['reputation_subject']
Обратите внимание, что reputation_delta содержит не фактическое значение того, на сколько изменилась репутация, а то, на которое была дана команда изменить репутацию. Как же они могут отличаться? Вроде бы я сказал увеличить репутацию с персонажем на 2, значит, я ожидаю, что игра увеличит её на 2. Дело в том, что система репутации подразумевает наличие максимального и минимального значения репутации. Они задаются константами:
# Максимальное и минимальное значение репутации
define reputation_minimum = -5
define reputation_maximum = 5
Если вам не нужен этот функционал, просто установите значения этих констант в None.
А вот пример синтаксиса, вызывающего функцию:
reputation.register_event('stella', 5, _function=renpy.notify, message="Стелла одобряет.")
Присваивая параметру _function функцию, важно указать только её название без скобок и аргументов. Все аргументы указываются через запятую, как в примере выше. Если же вы напишете _function=renpy.notify(), то параметру _function будет присвоена не функция renpy.notify, а результат её выполнения. Он, очевидно, не является функцией, а значит при срабатывании такого события функция renpy.notify вызвана не будет.
Обратите внимание на возможные конфликты при использовании системы вызова событий при изменении репутации. Если у вас зарегистрировано два ивента, которые могут сработать одновременно, то система обработает их в порядке добавления в очередь.
К чему это может привести? Если два события должны вызвать две метки, то игра перейдёт только по одной из них. В случае, если эта метка отработает и закончится возвратом, произойдёт переход по второй. Но в базовом сценарии вторая метка никогда не сработает.
Если же вам нужно показать два экрана, конфликта не возникнет. Исключение - случаи, когда экран сам реагирует на другие действия. К примеру, вы хотите показать всплывающий тултит, который исчезнет при любом действии игрока. Вызов метки или экрана может привести к тому, что игрок никогда не увидит этот тултип. Также стоит помнить о порядке отработки событий, особенно, если часть из них вызывает метку, а другие - показывают экран.
Если же одно событие должно и показать экран, и перейти к метке и выполнить функцию, оно сначала вызовет функцию, затем отобразит экран, а потом вызовет метку.
Я буду рад, если написанная мной система репутации пригодится вам при разработке вашей игры на Ren'Py. Чтобы понять, что вам интересно получить и другие игровые механики, написанные на python для движка Ren'Py, ставьте этой статье палец вверх и добавляйте её в закладки.
Игра, для которой я написал этот код, в этом году выйдет в релиз. А пока можно добавить игру в вишлист в Steam, чтобы не пропустить открытое тестирование и бесплатную демку.
Куда лучше, чем в программировании, я разбираюсь в геймдизайне, продюсировании и игровой сценаристике, делая игры 17 лет, и вот уже 10 из них преподаю в Высшей Школе Бизнеса НИУ ВШЭ на программе:
Я веду дисциплины, посвящённые системному геймдизайну, нарративному дизайну, техническим основам разработки игр, рассказываю про инвестиции в геймдев, продюсирование и управление игровыми студиями.