У персонажей можно управлять автопилотом.  
Поддерживаются включение/выключение, определение набора действий, их вероятностей и
запуск определённого действия из этого определённого набора.

Эта статья будет идти от простого и более важного до менее востребованного, но более сложного и детализированного.  
Если вы не являетесь программистом, то можно предположить, что вам будут полезны только первые 4 абзаца.

***

## Быстрый старт

Для начала можно просто "включить" автопилот без каких-либо дополнительных настроек,
создав "стандартные действия" и установив их для нужного персонажа (или нескольких):

```python
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`.

Но так уж устроен Питон, что скопируется не сам список, а ссылка на него,
поэтому при изменении этого списка у одного персонажа, он изменится и у всех остальных, и это следует учитывать.  
Например, перед изменением можно делать копию:
```python
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`.  
Далее функция действия возвращает новое состояние, которое будет передано ей при следующем обновлении.  
Если возвращается состояние `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` - состояние действия на текущий момент (об этом говорилось в абзаце "Состояния действий").

Напоминание: получить RPG-действия персонажа `character` можно через `character.get_actions()`.  
Через них можно запускать другие действия или хранить какие-то данные.

Также необходимо помнить, что:
* при `60` кадрах в секунду на обновление `1` кадра даётся `16` мс,
* кроме вашего кода выполняется куча другого кода,
* ваше действие может выполняться на `100` различных персонажах каждый кадр,
* наиболее сложно-вычислимые стадии действий иногда будут произходить сразу у нескольких персонажей в `1` кадре,
* у питона есть сборщик мусора, который тоже иногда будет вносить свой вклад в задержку обновления кадра.
Поэтому вам нужно максимально экономить ресурсы компьютера.

Например, прокладывание пути (тяжёлая операция) в большинстве случаев достаточно сделать `1` раз (на старте),
в то время как бОльшую часть времени будет происходить дешёвая по времени проверка вида
"Ещё идём? Да - продолжаем идти. Нет - завершение".  
Если же путь нужно время от времени обновлять (например, если цель пути - двигающийся персонаж),
то делать это стоит не каждый кадр, а раз в секунду,
а если локации персонажа и цели разные (т. е. расстояние велико) - раз в `3` секунды
(цифры даны в качестве простого и понятного примера).

***

## Работа со временем

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

Нельзя полагаться на то, что следующий кадр будет через примерно `0.016` секунды,
или что текущее время + `3` секунды не будет давно прошедшим для уже следующего кадра.  
Вполне возможно, что в текущем кадре будет сделано сохранение, которое будет загружено через минуту или через месяц
(в любом случае вы рискуете получить неадекватный результат).  

Поэтому рекомендуется вместо `time.time()` использовать специальную функцию `get_game_time()`, которая учитывает лишь нужное время.

***

Может оказаться полезной вспомогательная функция для поиска случайного места, свободного для передвижения:  
`rpg_random_free_point(location_names)`  
Где `location_names` - список локаций (`None` недопустим).  
Возвращает кортёж `(location_name, x, y)` или `None` (если некоторое число случайных проверок не дали результата).

***

[Далее ->](./Пример-автопилота)  
[<- Назад](./Персонажи)
