Автопилот - TrueCat17/Ren-Engine GitHub Wiki
У персонажей можно управлять автопилотом.
Поддерживаются включение/выключение, определение набора действий, их вероятностей и
запуск определённого действия из этого определённого набора.
Эта статья будет идти от простого и более важного до менее востребованного, но более сложного и детализированного.
Если вы не являетесь программистом, то можно предположить, что вам будут полезны только первые 4 абзаца.
Для начала можно просто "включить" автопилот без каких-либо дополнительных настроек, создав "стандартные действия" и установив их для нужного персонажа (или нескольких):
std_actions = get_std_rpg_actions()
character.set_auto(True)
character_actions = character.set_actions(std_actions)
character_actions.start('spawn')
Здесь character
- RPG-персонаж (персонаж с вызванной make_rpg
), которому устанавливается автопилот, а
character_actions
- копия действий, которая принадлежит именно этому персонажу.
Последняя строка запускает одно из стандартных действий - spawn
(появление; в данном случае - в случайной локации).
Остановить текущее действие для персонажа character
:
character.get_actions().stop()
При включённом автопилоте следующее обновление персонажа запустит случайное действие (см. далее).
Запустить действие name
(не забывать про кавычки):
character.get_actions().start('name')
Запустить случайное действие:
character.get_actions().random()
Обновить действие, перед этим установив ему состояние state
:
character.get_actions().update(state = None)
Если state
не задано (по умолчанию), то оно не меняется.
Также автопилот можно выключить:
character.set_auto(False)
Следует указать, что при запуске нового действия (случайного или нет) или при выключении автопилота
текущее действие автоматически завершается (и сидящий персонаж встанет, а идущий остановится).
Используются при выполнении некоторых действий.
interesting_places
- список названий интересных локаций,
в которые персонаж будет заглядывать чаще, чем в другие.
Например, площадь и пляж: std_actions.interesting_places = ['square', 'beach']
home
- имя локации домика для этого персонажа, либо (если такой локации нет)
кортёж из имени локации и названия места в нём (рядом с какой-либо дверью), в котором этот персонаж будет пропадать во
время "захода" в домик и появляться во время "выхода".
Примеры:
character_actions.home = 'house'
character_actions.home = ('street', 'door')
friends
- список персонажей-друзей (не их имён, важно!) или ещё кого-либо,
с кем данному персонажу будет логично время от времени о чём-то говорить.
Если персонаж находится в списке "друзей" другого, то это не значит,
что он сам обязан содержать его в своём списке, при необходимости этого можно не делать.
Пример: dv.get_actions().friends = [us, mi]
Как уже было сказано выше, функция set_actions
создаёт копию переданных ей действий,
присваивает её в действия персонажа и возвращает их в качестве своего результата.
Свойства тоже копируются, поэтому в примере выше с interesting_places
этот список будет доступен у всех персонажей, для которых установят эти std_actions
.
Но так уж устроен Питон, что скопируется не сам список, а ссылка на него,
поэтому при изменении этого списка у одного персонажа, он изменится и у всех остальных, и это следует учитывать.
Например, перед изменением можно делать копию:
character_actions.interesting_places = list(character_actions.interesting_places)
character_actions.interesting_places.remove('beach')
character_actions.interesting_places.append('park')
Почему set_actions
не делает сразу полную копию списка?
Потому что иногда именно такое поведение может быть удобным.
Список стандартных действий:
-
spawn
- появление в локации из списка интересных мест, если он задан (см. предыдущий абзац), либо просто в случайной локации, -
sit
- попытка посидеть некоторое время на ближайшем объекте-сиденье (если есть на текущей локации), -
other_place
- переход в случайное другое место текущей локации, -
near_location
- переход в случайное место соседней локации, -
random_location
- переход в случайное место случайной локации, -
interesting_place
- переход в случайное место локации из списка интересных (текущей локации, если список не задан), -
look_around
- оглядеться по сторонам, -
home
- пойти домой (если дом указан, см. предыдущий абзац), -
to_friend
- пойти к персонажу из списка друзей (снова предыдущий абзац), одновременно посылая его к себе навстречу, -
follow
- следовать за указанным персонажем (можно обновить с состояниямиrewind_to_min
/rewind_to_max
для "перемотки" движения).
За каждое действие отвечает отдельная python-функция.
У стандартных действий эта функция имеет имя rpg_action_%action_name%
.
Например, rpg_action_spawn
или rpg_action_to_friend
.
Хранятся в свойствах allow
и block
соответственно, содержат названия действий (или сами функции).
Заблокированное действие не может быть выбрано автоматически и не может быть запущено вручную.
Если список разрешённых действий не пуст, то запрещены все действия, которых в нём нет.
Копируются, как и другие свойства.
Каждое действие имеет свою вероятность, с которой оно будет выбрано после выполнения предыдущего действия.
Также кроме обычной вероятности может быть задан диапазон из минимальной и максимальной, тогда
вероятность будет новым случайным числом в этом диапазоне при каждом новом выборе.
Вероятность может быть нулевой, тогда такое действие никогда не будет выбрано автоматически (например, spawn
).
Сумма вероятностей не обязана равняться 100
, это не проценты.
Стандартные действия имеют вероятности от 10
до 50
(у каждого по разному), но здесь не приводится точных цифр, т. к.
они могут быть изменены в будущем, если будет решено, что это улучшит автопилот.
Посмотреть точные цифры на текущий момент можно в файле Ren-Engine/rpy/rpg/actions.rpy
в функции get_std_rpg_actions
.
Там же можно увидеть, как инициализируется объект RpgActions
,
и как можно обойтись без использования вышеупомянутой функции получения стандартных действий.
При запуске устанавливается действие, его состояние start
, а также именованные и неименованные параметры.
По умолчанию действие будет запущено не сразу, а лишь при следующем обновлении персонажа (в ближайшем кадре).
Иногда это не желательно, тогда в метод start
можно передать в явном виде параметр state = 'start'
.
При вызове функция действия возвращает новое состояние, которое будет передано ей при следующем обновлении.
Если возвращается состояние end
, то действие тут же вызывается снова с состоянием end
,
после чего оно считается завершённым, и происходит запуск случайного действия.
Почему при возврате состояния end
функция вызывается с этим состоянием тут же, а не на следующем кадре?
Это сделано для удобства управления передвижением.
Метод Character.move_to_place
пытается проложить дорогу до нужной точки, но если это невозможно,
то двигает персонажа по прямой сквозь все препятствия (если начальная и конечная точка на 1 локации)
или телепортирует в конечную точку (если она в другой локации).
Если это нежелательно, то в случае отсутствия пути для отмены передвижения
следует вызвать этот же метод со значением None
, чтобы остановить персонажа,
после чего обычно следует завершение действия.
И ровно то же самое (остановка на месте) происходит и в состоянии принудительного завершения.
Поэтому, чтобы не дублировать код, состояние end
обновляется сразу же.
Текущее состояние хранится в свойстве state
.
Текущая функция - в свойстве cur_action
.
Хранится именно функция, а не имя действия,
т. к. это позволяет однократно запускать функцию без её регистрации в наборе действий.
Вы можете добавить свои функции для управления персонажами:
actions.set_action("name", function, min_chance, max_chance = None)
Здесь:
-
actions
- RPG-действия, их можно получить, создав экземпляр классаRpgActions
или из функцииget_std_rpg_actions
, -
name
- имя устанавливаемого действия (строка), -
function
- функция, управляющая поведением, -
min_chance
,max_chance
- диапазон вероятности запуска (об этом говорилось в абзаце "Вероятности действий").
Функция должна принимать 2 параметра:
-
character
- персонаж, которым нужно управлять, -
state
- состояние действия на текущий момент (об этом говорилось в абзаце "Состояния действий").
Кроме того, функция может принимать и другие параметры, которые ей можно передать во время запуска:
actions.start(my_super_func, my_param = 'my_value')
Напоминание: получить RPG-действия персонажа character
можно через character.get_actions()
.
Через них можно запускать другие действия или хранить какие-то данные.
Кстати, при запуске другого действия из текущего, необходимо вернуть значение IGNORE_STATE
,
чтобы значение state
не было перезаписано возвращаемым значением (см. rpg_action_near_location
).
Также необходимо помнить, что:
- при
60
кадрах в секунду на обновление1
кадра даётся16
мс, - кроме вашего кода выполняется куча другого кода,
- ваше действие может выполняться на
100
различных персонажах каждый кадр, - наиболее сложно-вычислимые стадии действий иногда будут происходить сразу у нескольких персонажей в
1
кадре, - у питона есть сборщик мусора, который тоже иногда будет вносить свой вклад в задержку обновления кадра. Поэтому вам нужно максимально экономить ресурсы компьютера.
Например, прокладывание пути (тяжёлая операция) в большинстве случаев достаточно сделать 1
раз (на старте),
в то время как бОльшую часть времени будет происходить дешёвая по времени проверка вида
"Ещё идём? Да - продолжаем идти. Нет - завершение".
Если же путь нужно время от времени обновлять (например, если цель пути - двигающийся персонаж),
то делать это стоит не каждый кадр, а раз в секунду,
а если локации персонажа и цели разные (т. е. расстояние велико) - раз в 3
секунды
(цифры даны в качестве простого и понятного примера).
Иногда нужно запретить выбор того же действия рандомом после того, как это действие закончилось.
Например, если персонаж только что сидел и встал, то снова садиться после этого - нелепо.
Для этого есть метод actions.set_cooldown(action, time)
,
который запрещает действию action
повторяться time
секунд после завершения.
Это относится только к случайному выбору действия, при необходимости его всё ещё можно запустить вручную.
Некоторые действия (как, например, sit
- сидеть) имеют длительность своей основной фазы
(которая может быть случайной).
Для более удобной настройки таких параметров имеется метод
actions.set_random_param('your_param', min_value, max_value)
.
Для отключения случайности параметрам min_value
и max_value
можно указать одно значение.
Действие при необходимости вызовет метод actions.get_random_param('your_param')
и
получит случайное число от min_value
до max_value
, указанных ранее.
Добавить действие в очередь (такие действия будут выполняться друг за другом по порядку):
character_actions.add('need_action')
Можно указывать и доп. параметры:
character_actions.add('need_action', need_location)
character_actions.add('need_action', max_dist = 150, state = 'moving')
Метод add
добавляет указанные параметры в свойство queue
и
запускает первое действие из этого массива с нужными параметрами после завершения текущего действия
(после чего удаляет его из очереди).
Каждый элемент "очереди" является массивом со значениями [action_name, args, kwargs]
,
при необходимости его можно менять/смотреть вручную.
Система сохранения и загрузки игры накладывает свой отпечаток на работу со временем.
Нельзя полагаться на то, что следующий кадр будет через примерно 0.016
секунды,
или что текущее время + 3
секунды не будет давно прошедшим для уже следующего кадра.
Вполне возможно, что в текущем кадре будет сделано сохранение, которое будет загружено через минуту или через месяц
(в любом случае вы рискуете получить неадекватный результат).
Поэтому рекомендуется вместо time.time()
использовать специальную функцию get_game_time()
, которая учитывает лишь нужное время.
Может оказаться полезной вспомогательная функция для поиска случайного места, свободного для передвижения:
rpg_random_free_point(location_names)
Где location_names
- список локаций (None
недопустим).
Возвращает кортёж (location_name, x, y)
или None
(если некоторое число случайных проверок не дали результата).